From 46ca043fbe45644d2049521876a9ac85e19575ff Mon Sep 17 00:00:00 2001 From: sandwich Date: Fri, 10 Oct 2025 14:59:05 +0200 Subject: [PATCH] add kbselecti and update README --- hyprexpo/README.md | 116 ++++++++++++++++++++++++++++++++---------- hyprexpo/main.cpp | 23 ++++++++- hyprexpo/overview.cpp | 45 +++++++++++++--- 3 files changed, 150 insertions(+), 34 deletions(-) diff --git a/hyprexpo/README.md b/hyprexpo/README.md index 7627e83..217a9d1 100644 --- a/hyprexpo/README.md +++ b/hyprexpo/README.md @@ -14,7 +14,7 @@ plugin { bg_col = rgb(111111) workspace_method = center current # [center/first] [workspace] e.g. first 1 or center m+1 - gesture_distance = 300 # how far is the "max" for the gesture + gesture_distance = 200 # how far is the "max" for the gesture gaps_out = 0 # outer margin (px) } } @@ -39,6 +39,8 @@ plugin { # numbers (labels) label_enable = 1 label_font_size = 16 + # content mode: token (default) | id | index + label_text_mode = token # positioning label_position = top-left # top-left|top-right|bottom-left|bottom-right|center label_offset_x = 6 @@ -54,10 +56,20 @@ plugin { label_scale_hover = 1.0 label_scale_focus = 1.0 # label background bubble - label_bg_enable = 0 + label_bg_enable = 1 label_bg_color = rgba(00000088) - label_bg_rounding = 4 + label_bg_shape = circle # circle|square|rounded + label_bg_rounding = 8 # used for rounded label_padding = 4 + # font + precision + label_font_family = Sans + label_font_bold = 0 + label_font_italic = 0 + label_text_underline = 0 + label_text_strikethrough = 0 + label_pixel_snap = 1 + # optional: override tokens (up to 50, comma-separated). Empty entries allowed. + # label_token_map = "1,2,3,4,5,6,7,8,9,0,!,@,#,$,%,^,&,*,(,),a,b,c,..." } } ``` @@ -69,10 +81,10 @@ plugin { | columns | number | how many desktops are displayed on one line | `3` | | gaps_in | number | inner gaps between tiles | `5` | | gaps_out | number | outer margin around the grid | `0` | -| bg_col | color | color in gaps (between desktops) | `rgb(000000)` | +| bg_col | color | color in gaps (between desktops) | `rgb(111111)` | | workspace_method | [center/first] [workspace] | position of the desktops | `center current` | | skip_empty | boolean | whether the grid displays workspaces sequentially by id using selector "r" (`false`) or skips empty workspaces using selector "m" (`true`) | `false` | -| gesture_distance | number | how far is the max for the gesture | `300` | +| gesture_distance | number | how far is the max for the gesture | `200` | #### Subcategory `scrolling` @@ -94,29 +106,70 @@ Applies to the scrolling layout overview bind = MODIFIER, KEY, hyprexpo:expo, OPTION ``` -Example: +Example: ```bash # This will toggle HyprExpo when SUPER+g is pressed bind = SUPER, g, hyprexpo:expo, toggle -# Optional submap for keyboard selection when Hyprexpo is active +# Optional submap for keyboard selection when Hyprexpo is active (index-based, 1..46) submap = hyprexpo - binde = , left, hyprexpo:kb_focus, left - binde = , right, hyprexpo:kb_focus, right - binde = , up, hyprexpo:kb_focus, up - binde = , down, hyprexpo:kb_focus, down - binde = , return, hyprexpo:kb_confirm - # number selection by workspace id - binde = , 1, hyprexpo:kb_selectn, 1 - binde = , 2, hyprexpo:kb_selectn, 2 - binde = , 3, hyprexpo:kb_selectn, 3 - binde = , 4, hyprexpo:kb_selectn, 4 - binde = , 5, hyprexpo:kb_selectn, 5 - binde = , 6, hyprexpo:kb_selectn, 6 - binde = , 7, hyprexpo:kb_selectn, 7 - binde = , 8, hyprexpo:kb_selectn, 8 - binde = , 9, hyprexpo:kb_selectn, 9 - binde = , 0, hyprexpo:kb_selectn, 0 # 0 -> 10 + bind = , left, hyprexpo:kb_focus, left + bind = , right, hyprexpo:kb_focus, right + bind = , up, hyprexpo:kb_focus, up + bind = , down, hyprexpo:kb_focus, down + bind = , return, hyprexpo:kb_confirm + + # 1..10 via digits (0 -> 10) + bind = , 1, hyprexpo:kb_selecti, 1 + bind = , 2, hyprexpo:kb_selecti, 2 + bind = , 3, hyprexpo:kb_selecti, 3 + bind = , 4, hyprexpo:kb_selecti, 4 + bind = , 5, hyprexpo:kb_selecti, 5 + bind = , 6, hyprexpo:kb_selecti, 6 + bind = , 7, hyprexpo:kb_selecti, 7 + bind = , 8, hyprexpo:kb_selecti, 8 + bind = , 9, hyprexpo:kb_selecti, 9 + bind = , 0, hyprexpo:kb_selecti, 10 + + # 11..20 via SHIFT+digits + bind = SHIFT, 1, hyprexpo:kb_selecti, 11 + bind = SHIFT, 2, hyprexpo:kb_selecti, 12 + bind = SHIFT, 3, hyprexpo:kb_selecti, 13 + bind = SHIFT, 4, hyprexpo:kb_selecti, 14 + bind = SHIFT, 5, hyprexpo:kb_selecti, 15 + bind = SHIFT, 6, hyprexpo:kb_selecti, 16 + bind = SHIFT, 7, hyprexpo:kb_selecti, 17 + bind = SHIFT, 8, hyprexpo:kb_selecti, 18 + bind = SHIFT, 9, hyprexpo:kb_selecti, 19 + bind = SHIFT, 0, hyprexpo:kb_selecti, 20 + + # 21..46 via alpha + bind = , a, hyprexpo:kb_selecti, 21 + bind = , b, hyprexpo:kb_selecti, 22 + bind = , c, hyprexpo:kb_selecti, 23 + bind = , d, hyprexpo:kb_selecti, 24 + bind = , e, hyprexpo:kb_selecti, 25 + bind = , f, hyprexpo:kb_selecti, 26 + bind = , g, hyprexpo:kb_selecti, 27 + bind = , h, hyprexpo:kb_selecti, 28 + bind = , i, hyprexpo:kb_selecti, 29 + bind = , j, hyprexpo:kb_selecti, 30 + bind = , k, hyprexpo:kb_selecti, 31 + bind = , l, hyprexpo:kb_selecti, 32 + bind = , m, hyprexpo:kb_selecti, 33 + bind = , n, hyprexpo:kb_selecti, 34 + bind = , o, hyprexpo:kb_selecti, 35 + bind = , p, hyprexpo:kb_selecti, 36 + bind = , q, hyprexpo:kb_selecti, 37 + bind = , r, hyprexpo:kb_selecti, 38 + bind = , s, hyprexpo:kb_selecti, 39 + bind = , t, hyprexpo:kb_selecti, 40 + bind = , u, hyprexpo:kb_selecti, 41 + bind = , v, hyprexpo:kb_selecti, 42 + bind = , w, hyprexpo:kb_selecti, 43 + bind = , x, hyprexpo:kb_selecti, 44 + bind = , y, hyprexpo:kb_selecti, 45 + bind = , z, hyprexpo:kb_selecti, 46 submap = reset ``` @@ -135,7 +188,9 @@ Keyboard navigation dispatchers (when overview is active): - Wrapping can be configured with `keynav_wrap_h` and `keynav_wrap_v`. - Reading order (row-major) for horizontal movement can be enabled with `keynav_reading_order`. At grid ends it will wrap to start/end only if both `keynav_wrap_h` and `keynav_wrap_v` are enabled. - `hyprexpo:kb_confirm`: selects the focused tile. -- `hyprexpo:kb_selectn, `: selects the workspace with that id if visible in the grid (0 → 10). +- `hyprexpo:kb_selectn, `: selects by workspace id (legacy; `0` → `10`). +- `hyprexpo:kb_selecti, `: selects by 1-based visual index (recommended for single-keystroke mapping). +- `hyprexpo:kb_select, `: selects by a single token (1..9, 0, a..z), mainly for symbol-based configs. Border styles: - `simple`: single-color border using `border_width`, `border_color_current`, `border_color_focus`. @@ -145,8 +200,8 @@ Border styles: - `plugin:hyprexpo:border_grad_focus = rgba(ffdd44ee) rgba(22aaffee) 30deg` Gaps: -- `gaps_in` controls the spacing between tiles. -- `gaps_out` adds the same spacing around the outer edge of the grid. +- `gaps_in` controls the inner spacing between tiles. +- `gaps_out` adds an outer margin (px) around the grid (animated during open/close). Labels (numbers): - `label_position`, `label_offset_x`, `label_offset_y` control placement per tile. @@ -154,3 +209,12 @@ Labels (numbers): - Per-state colors: `label_color_default|hover|focus|current`. - Per-state scale multipliers: `label_scale_hover|focus`. - Optional background bubble behind text: `label_bg_*`, `label_padding`. +- Content: + - `label_text_mode = token` (default) uses the token sequence (1..9,0,!,@,#,…,a..z) or `label_token_map` if provided. + - `label_text_mode = index` shows the 1-based visual index. + - `label_text_mode = id` shows the workspace ID. +- Token overrides: + - `label_token_map` accepts up to 50 comma-separated entries to override tokens. Empty entries allowed (skip rendering). +- Font & precision: + - `label_font_family`, `label_font_bold`, `label_font_italic`, `label_text_underline`, `label_text_strikethrough` + - `label_pixel_snap` (default 1) rounds positions to integer pixels for crisper text. diff --git a/hyprexpo/main.cpp b/hyprexpo/main.cpp index 15d94f3..033f814 100644 --- a/hyprexpo/main.cpp +++ b/hyprexpo/main.cpp @@ -44,6 +44,7 @@ static SDispatchResult onKbFocusDispatcher(std::string arg); static SDispatchResult onKbConfirmDispatcher(std::string arg); static SDispatchResult onKbSelectNumberDispatcher(std::string arg); static SDispatchResult onKbSelectTokenDispatcher(std::string arg); +static SDispatchResult onKbSelectIndexDispatcher(std::string arg); // static void hkRenderWorkspace(void* thisptr, PHLMONITOR pMonitor, PHLWORKSPACE pWorkspace, timespec* now, const CBox& geometry) { @@ -254,6 +255,7 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { HyprlandAPI::addDispatcherV2(PHANDLE, "hyprexpo:kb_confirm", ::onKbConfirmDispatcher); HyprlandAPI::addDispatcherV2(PHANDLE, "hyprexpo:kb_selectn", ::onKbSelectNumberDispatcher); HyprlandAPI::addDispatcherV2(PHANDLE, "hyprexpo:kb_select", ::onKbSelectTokenDispatcher); + HyprlandAPI::addDispatcherV2(PHANDLE, "hyprexpo:kb_selecti", ::onKbSelectIndexDispatcher); HyprlandAPI::addConfigKeyword(PHANDLE, "hyprexpo-gesture", ::expoGestureKeyword, {}); @@ -276,7 +278,11 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_enable", Hyprlang::INT{1}); HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_color", Hyprlang::INT{0xFFFFFFFF}); HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_font_size", Hyprlang::INT{16}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_text_mode", Hyprlang::STRING{"id"}); + // label_text_mode: token (default) | id | index + HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_text_mode", Hyprlang::STRING{"token"}); + // Optional override map for up to 50 tokens, comma-separated. Empty entries allowed. + // Example: "1,2,3,4,5,6,7,8,9,0,!,@,#,$,%,^,&,*,(,),a,..." + HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_token_map", Hyprlang::STRING{""}); // defaults: center/middle within the label container HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_position", Hyprlang::STRING{"center"}); HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_offset_x", Hyprlang::INT{0}); @@ -399,3 +405,18 @@ static SDispatchResult onKbSelectTokenDispatcher(std::string arg) { g_pOverview->onKbSelectToken(*idx); return {}; } + +static SDispatchResult onKbSelectIndexDispatcher(std::string arg) { + if (!g_pOverview) + return {}; + // trim + while (!arg.empty() && std::isspace(arg.front())) arg.erase(arg.begin()); + while (!arg.empty() && std::isspace(arg.back())) arg.pop_back(); + int idx = -1; + try { idx = std::stoi(arg); } catch (...) { idx = -1; } + if (idx <= 0) + return {.success = false, .error = "invalid index (expected >= 1)"}; + // convert to 0-based visible index + g_pOverview->onKbSelectToken(idx - 1); + return {}; +} diff --git a/hyprexpo/overview.cpp b/hyprexpo/overview.cpp index 77bd606..e093413 100644 --- a/hyprexpo/overview.cpp +++ b/hyprexpo/overview.cpp @@ -840,6 +840,7 @@ void COverview::fullRender() { static auto* const* PLABELSIZE = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_font_size")->getDataStaticPtr(); static auto const* PLABELPOS = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_position")->getDataStaticPtr(); static auto const* PLABELMODE = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_text_mode")->getDataStaticPtr(); + static auto const* PTOKENMAP = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_token_map")->getDataStaticPtr(); static auto* const* PLABELOX = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_offset_x")->getDataStaticPtr(); static auto* const* PLABELOY = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_offset_y")->getDataStaticPtr(); static auto const* PLABELSHOW = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_show")->getDataStaticPtr(); @@ -933,11 +934,41 @@ void COverview::fullRender() { tile.round(); std::string label; - if (std::string{*PLABELMODE} == "token") { - int k = tokenCounter; - if (k <= 8) label = std::to_string(k + 1); - else if (k == 9) label = "0"; - else label = std::string(1, char('a' + (k - 10))); + const std::string mode{*PLABELMODE}; + if (mode == "token") { + // override map (comma-separated) up to 50 + std::vector tokens; + if (!std::string{*PTOKENMAP}.empty()) { + const std::string mapStr{*PTOKENMAP}; + size_t start = 0; + for (size_t cur = 0; cur <= mapStr.size(); ++cur) { + if (cur == mapStr.size() || mapStr[cur] == ',') { + std::string t = mapStr.substr(start, cur - start); + while (!t.empty() && std::isspace((unsigned char)t.front())) t.erase(t.begin()); + while (!t.empty() && std::isspace((unsigned char)t.back())) t.pop_back(); + tokens.push_back(t); + start = cur + 1; + } + } + } + + const int k = tokenCounter; // 0-based visible index + if (k < (int)tokens.size() && !tokens[k].empty()) + label = tokens[k]; + else { + if (k <= 8) label = std::to_string(k + 1); // 1..9 + else if (k == 9) label = "0"; // 10 -> 0 + else if (k >= 10 && k <= 19) { // 11..20 + static const char* SYM[10] = {"!","@","#","$","%","^","&","*","(",")"}; + label = SYM[k - 10]; + } else if (k >= 20 && k <= 45) { + label = std::string(1, char('a' + (k - 20))); // 21..46 -> a..z + } else { + label = ""; // 47..50 blank by default + } + } + } else if (mode == "index") { + label = std::to_string(tokenCounter + 1); } else { label = std::to_string(images[id].workspaceID); } @@ -955,8 +986,8 @@ void COverview::fullRender() { } }; - // if label isn't shown per mode, still advance token index - if (!shouldShow(id)) { tokenCounter++; continue; } + // if label isn't shown per mode or label is empty, still advance token index + if (!shouldShow(id) || label.empty()) { tokenCounter++; continue; } CHyprColor col = CHyprColor{(uint64_t)**PLCOLDEF}; float scl = 1.0f;