diff --git a/dotfiles/config/hypr/hyprland.conf b/dotfiles/config/hypr/hyprland.conf index a1d38281..a414f67a 100644 --- a/dotfiles/config/hypr/hyprland.conf +++ b/dotfiles/config/hypr/hyprland.conf @@ -182,6 +182,8 @@ plugin { workspace_method = first 1 # Only show workspaces with windows skip_empty = true + # Show numeric workspace labels in the expo grid + show_workspace_numbers = true # 3 columns -> 3x3 grid when 9 workspaces are visible columns = 3 } @@ -250,6 +252,7 @@ bind = $mainMod SHIFT, Return, exec, $terminal # Overview (Hyprexpo) # ----------------------------------------------------------------------------- bind = $mainMod, TAB, hyprexpo:expo, toggle +bind = $mainMod SHIFT, TAB, hyprexpo:expo, bring bind = $mainMod, Q, killactive, bind = $mainMod SHIFT, C, killactive, bind = $mainMod SHIFT, Q, exit, diff --git a/nixos/hyprland.nix b/nixos/hyprland.nix index cc94f9bd..2efc1c47 100644 --- a/nixos/hyprland.nix +++ b/nixos/hyprland.nix @@ -1,4 +1,13 @@ { config, pkgs, makeEnable, inputs, ... }: +let + system = pkgs.stdenv.hostPlatform.system; + hyprexpoPatched = inputs.hyprland-plugins.packages.${system}.hyprexpo.overrideAttrs (old: { + patches = (old.patches or [ ]) ++ [ + ./patches/hyprexpo-pr-612-workspace-numbers.patch + ./patches/hyprexpo-pr-616-bring-mode.patch + ]; + }); +in makeEnable config "myModules.hyprland" true { myModules.taffybar.enable = true; @@ -97,7 +106,7 @@ makeEnable config "myModules.hyprland" true { inputs.hy3.packages.${pkgs.stdenv.hostPlatform.system}.hy3 # Hyprexpo plugin from hyprland-plugins (workspace overview) - inputs.hyprland-plugins.packages.${pkgs.stdenv.hostPlatform.system}.hyprexpo + hyprexpoPatched # For scripts jq diff --git a/nixos/patches/hyprexpo-pr-612-workspace-numbers.patch b/nixos/patches/hyprexpo-pr-612-workspace-numbers.patch new file mode 100644 index 00000000..68d80f05 --- /dev/null +++ b/nixos/patches/hyprexpo-pr-612-workspace-numbers.patch @@ -0,0 +1,228 @@ +From aaefc0ff0bc4348de04f311ad0101da44c62ae94 Mon Sep 17 00:00:00 2001 +From: Ivan Malison +Date: Wed, 4 Feb 2026 00:54:52 -0800 +Subject: [PATCH 1/2] hyprexpo: optionally render workspace numbers + +--- + hyprexpo/README.md | 3 +- + hyprexpo/main.cpp | 2 + + hyprexpo/overview.cpp | 109 ++++++++++++++++++++++++++++++++++++++++++ + hyprexpo/overview.hpp | 4 ++ + 4 files changed, 117 insertions(+), 1 deletion(-) + +diff --git a/README.md b/README.md +index 97bd1d4..aac2e97 100644 +--- a/README.md ++++ b/README.md +@@ -28,6 +28,8 @@ gap_size | number | gap between desktops | `5` + bg_col | color | color in gaps (between desktops) | `rgb(000000)` + 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` ++show_workspace_numbers | boolean | show numeric labels for workspaces | `false` ++workspace_number_color | color | color of workspace number labels | `rgb(ffffff)` + gesture_distance | number | how far is the max for the gesture | `300` + + ### Keywords +@@ -57,4 +59,3 @@ off | hides the overview + disable | same as `off` + on | displays the overview + enable | same as `on` +- +diff --git a/main.cpp b/main.cpp +index 883fd82..ff9f380 100644 +--- a/main.cpp ++++ b/main.cpp +@@ -239,6 +239,8 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { + HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:bg_col", Hyprlang::INT{0xFF111111}); + HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:workspace_method", Hyprlang::STRING{"center current"}); + HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:skip_empty", Hyprlang::INT{0}); ++ HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:show_workspace_numbers", Hyprlang::INT{0}); ++ HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:workspace_number_color", Hyprlang::INT{0xFFFFFFFF}); + + HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:gesture_distance", Hyprlang::INT{200}); + +diff --git a/overview.cpp b/overview.cpp +index 5721948..926a9f8 100644 +--- a/overview.cpp ++++ b/overview.cpp +@@ -1,5 +1,8 @@ + #include "overview.hpp" + #include ++#include ++#include ++#include + #define private public + #include + #include +@@ -15,6 +18,86 @@ + #undef private + #include "OverviewPassElement.hpp" + ++static Vector2D renderLabelTexture(SP out, const std::string& text, const CHyprColor& color, int fontSizePx) { ++ if (!out || text.empty() || fontSizePx <= 0) ++ return {}; ++ ++ auto measureSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); ++ auto measureCairo = cairo_create(measureSurface); ++ ++ PangoLayout* measureLayout = pango_cairo_create_layout(measureCairo); ++ pango_layout_set_text(measureLayout, text.c_str(), -1); ++ auto* fontDesc = pango_font_description_from_string("Sans Bold"); ++ pango_font_description_set_size(fontDesc, fontSizePx * PANGO_SCALE); ++ pango_layout_set_font_description(measureLayout, fontDesc); ++ pango_font_description_free(fontDesc); ++ ++ PangoRectangle inkRect, logicalRect; ++ pango_layout_get_extents(measureLayout, &inkRect, &logicalRect); ++ ++ const int textW = std::max(1, (int)std::ceil(logicalRect.width / (double)PANGO_SCALE)); ++ const int textH = std::max(1, (int)std::ceil(logicalRect.height / (double)PANGO_SCALE)); ++ ++ g_object_unref(measureLayout); ++ cairo_destroy(measureCairo); ++ cairo_surface_destroy(measureSurface); ++ ++ const int pad = std::max(4, (int)std::round(fontSizePx * 0.35)); ++ const int width = textW + pad * 2; ++ const int height = textH + pad * 2; ++ ++ auto surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); ++ auto cairo = cairo_create(surface); ++ ++ // Clear the pixmap ++ cairo_save(cairo); ++ cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR); ++ cairo_paint(cairo); ++ cairo_restore(cairo); ++ ++ // Background for legibility ++ cairo_set_source_rgba(cairo, 0.0, 0.0, 0.0, 0.55); ++ cairo_rectangle(cairo, 0, 0, width, height); ++ cairo_fill(cairo); ++ ++ PangoLayout* layout = pango_cairo_create_layout(cairo); ++ pango_layout_set_text(layout, text.c_str(), -1); ++ fontDesc = pango_font_description_from_string("Sans Bold"); ++ pango_font_description_set_size(fontDesc, fontSizePx * PANGO_SCALE); ++ pango_layout_set_font_description(layout, fontDesc); ++ pango_font_description_free(fontDesc); ++ ++ pango_layout_get_extents(layout, &inkRect, &logicalRect); ++ const double xOffset = (width - logicalRect.width / (double)PANGO_SCALE) / 2.0; ++ const double yOffset = (height - logicalRect.height / (double)PANGO_SCALE) / 2.0; ++ ++ cairo_set_source_rgba(cairo, color.r, color.g, color.b, color.a); ++ cairo_move_to(cairo, xOffset, yOffset); ++ pango_cairo_show_layout(cairo, layout); ++ ++ g_object_unref(layout); ++ ++ cairo_surface_flush(surface); ++ ++ const auto DATA = cairo_image_surface_get_data(surface); ++ out->allocate(); ++ glBindTexture(GL_TEXTURE_2D, out->m_texID); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); ++ ++#ifndef GLES2 ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED); ++#endif ++ ++ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA); ++ ++ cairo_destroy(cairo); ++ cairo_surface_destroy(surface); ++ ++ return {width, height}; ++} ++ + static void damageMonitor(WP thisptr) { + g_pOverview->damage(); + } +@@ -34,11 +117,14 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn + static auto* const* PGAPS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:gap_size")->getDataStaticPtr(); + static auto* const* PCOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:bg_col")->getDataStaticPtr(); + static auto* const* PSKIP = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:skip_empty")->getDataStaticPtr(); ++ static auto* const* PSHOWNUM = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:show_workspace_numbers")->getDataStaticPtr(); ++ static auto* const* PNUMCOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:workspace_number_color")->getDataStaticPtr(); + static auto const* PMETHOD = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:workspace_method")->getDataStaticPtr(); + + SIDE_LENGTH = **PCOLUMNS; + GAP_WIDTH = **PGAPS; + BG_COLOR = **PCOL; ++ showWorkspaceNumbers = **PSHOWNUM; + + // process the method + bool methodCenter = true; +@@ -126,6 +212,17 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn + Vector2D tileRenderSize = (pMonitor->m_size - Vector2D{GAP_WIDTH * pMonitor->m_scale, GAP_WIDTH * pMonitor->m_scale} * (SIDE_LENGTH - 1)) / SIDE_LENGTH; + CBox monbox{0, 0, tileSize.x * 2, tileSize.y * 2}; + ++ if (showWorkspaceNumbers) { ++ const CHyprColor numberColor = **PNUMCOL; ++ const int fontSizePx = std::max(12, (int)std::round(tileRenderSize.y * pMonitor->m_scale * 0.22)); ++ for (auto& image : images) { ++ if (image.workspaceID == WORKSPACE_INVALID) ++ continue; ++ image.labelTex = makeShared(); ++ image.labelSizePx = renderLabelTexture(image.labelTex, std::to_string(image.workspaceID), numberColor, fontSizePx); ++ } ++ } ++ + if (!ENABLE_LOWRES) + monbox = {{0, 0}, pMonitor->m_pixelSize}; + +@@ -452,6 +549,18 @@ void COverview::fullRender() { + texbox.round(); + CRegion damage{0, 0, INT16_MAX, INT16_MAX}; + g_pHyprOpenGL->renderTextureInternal(images[x + y * SIDE_LENGTH].fb.getTexture(), texbox, {.damage = &damage, .a = 1.0}); ++ ++ if (showWorkspaceNumbers) { ++ auto& image = images[x + y * SIDE_LENGTH]; ++ if (image.workspaceID != WORKSPACE_INVALID && image.labelTex && image.labelTex->m_texID != 0 && image.labelSizePx.x > 0 && image.labelSizePx.y > 0) { ++ const Vector2D labelSize = image.labelSizePx / pMonitor->m_scale; ++ const float margin = std::max(4.0, tileRenderSize.y * 0.05); ++ CBox labelBox = {x * tileRenderSize.x + x * GAPSIZE + margin, y * tileRenderSize.y + y * GAPSIZE + margin, labelSize.x, labelSize.y}; ++ labelBox.scale(pMonitor->m_scale).translate(pos->value()); ++ labelBox.round(); ++ g_pHyprOpenGL->renderTexture(image.labelTex, labelBox, {.a = 1.0}); ++ } ++ } + } + } + } +diff --git a/overview.hpp b/overview.hpp +index 4b02400..1f6bf3c 100644 +--- a/overview.hpp ++++ b/overview.hpp +@@ -8,6 +8,7 @@ + #include + #include + #include ++class CTexture; + + // saves on resources, but is a bit broken rn with blur. + // hyprland's fault, but cba to fix. +@@ -58,6 +59,8 @@ class COverview { + int64_t workspaceID = -1; + PHLWORKSPACE pWorkspace; + CBox box; ++ SP labelTex; ++ Vector2D labelSizePx; + }; + + Vector2D lastMousePosLocal = Vector2D{}; +@@ -81,6 +84,7 @@ class COverview { + + bool swipe = false; + bool swipeWasCommenced = false; ++ bool showWorkspaceNumbers = false; + + friend class COverviewPassElement; + }; +-- +2.52.0 + + diff --git a/nixos/patches/hyprexpo-pr-616-bring-mode.patch b/nixos/patches/hyprexpo-pr-616-bring-mode.patch new file mode 100644 index 00000000..58760a2c --- /dev/null +++ b/nixos/patches/hyprexpo-pr-616-bring-mode.patch @@ -0,0 +1,147 @@ +From edc05ce88f79ceda0cdcb9aa68ec371b1af323de Mon Sep 17 00:00:00 2001 +From: Ivan Malison +Date: Mon, 16 Feb 2026 21:50:16 -0800 +Subject: [PATCH 2/2] hyprexpo: add bring selection mode + +--- + hyprexpo/README.md | 1 + + hyprexpo/main.cpp | 50 +++++++++++++++++++++++++++++++++++++++++++ + hyprexpo/overview.cpp | 12 +++++++++-- + hyprexpo/overview.hpp | 3 ++- + 4 files changed, 63 insertions(+), 3 deletions(-) + +diff --git a/README.md b/README.md +index aac2e97..084f02b 100644 +--- a/README.md ++++ b/README.md +@@ -55,6 +55,7 @@ Here are a list of options you can use: + | --- | --- | + toggle | displays if hidden, hide if displayed + select | selects the hovered desktop ++bring | brings a window from the hovered desktop to the current desktop + off | hides the overview + disable | same as `off` + on | displays the overview +diff --git a/main.cpp b/main.cpp +index ff9f380..78bac24 100644 +--- a/main.cpp ++++ b/main.cpp +@@ -65,6 +65,47 @@ static void hkAddDamageB(void* thisptr, const pixman_region32_t* rg) { + g_pOverview->onDamageReported(); + } + ++static PHLWINDOW windowToBringFromWorkspace(const PHLWORKSPACE& workspace) { ++ if (!workspace) ++ return nullptr; ++ ++ for (auto it = g_pCompositor->m_windows.rbegin(); it != g_pCompositor->m_windows.rend(); ++it) { ++ const auto& w = *it; ++ if (!w || w->m_workspace != workspace || !w->m_isMapped || w->isHidden()) ++ continue; ++ ++ return w; ++ } ++ ++ return nullptr; ++} ++ ++static SDispatchResult bringWindowFromWorkspace(int64_t sourceWorkspaceID) { ++ if (sourceWorkspaceID == WORKSPACE_INVALID) ++ return {.success = false, .error = "selected workspace is empty"}; ++ ++ const auto FOCUSSTATE = Desktop::focusState(); ++ const auto MONITOR = FOCUSSTATE->monitor(); ++ if (!MONITOR || !MONITOR->m_activeWorkspace) ++ return {.success = false, .error = "no active monitor/workspace"}; ++ ++ if (sourceWorkspaceID == MONITOR->activeWorkspaceID()) ++ return {}; ++ ++ const auto SOURCEWORKSPACE = g_pCompositor->getWorkspaceByID(sourceWorkspaceID); ++ if (!SOURCEWORKSPACE) ++ return {.success = false, .error = "selected workspace is not open"}; ++ ++ const auto WINDOW = windowToBringFromWorkspace(SOURCEWORKSPACE); ++ if (!WINDOW) ++ return {.success = false, .error = "selected workspace has no mapped windows"}; ++ ++ g_pCompositor->moveWindowToWorkspaceSafe(WINDOW, MONITOR->m_activeWorkspace); ++ FOCUSSTATE->fullWindowFocus(WINDOW); ++ g_pCompositor->warpCursorTo(WINDOW->middle()); ++ return {}; ++} ++ + static SDispatchResult onExpoDispatcher(std::string arg) { + + if (g_pOverview && g_pOverview->m_isSwiping) +@@ -77,6 +118,15 @@ static SDispatchResult onExpoDispatcher(std::string arg) { + } + return {}; + } ++ if (arg == "bring") { ++ if (g_pOverview) { ++ g_pOverview->selectHoveredWorkspace(); ++ const auto BRINGRESULT = bringWindowFromWorkspace(g_pOverview->selectedWorkspaceID()); ++ g_pOverview->close(false); ++ return BRINGRESULT; ++ } ++ return {}; ++ } + if (arg == "toggle") { + if (g_pOverview) + g_pOverview->close(); +diff --git a/overview.cpp b/overview.cpp +index 926a9f8..45ee982 100644 +--- a/overview.cpp ++++ b/overview.cpp +@@ -343,6 +343,14 @@ void COverview::selectHoveredWorkspace() { + closeOnID = x + y * SIDE_LENGTH; + } + ++int64_t COverview::selectedWorkspaceID() const { ++ const int ID = closeOnID == -1 ? openedID : closeOnID; ++ if (ID < 0 || ID >= (int)images.size()) ++ return WORKSPACE_INVALID; ++ ++ return images[ID].workspaceID; ++} ++ + void COverview::redrawID(int id, bool forcelowres) { + if (!pMonitor) + return; +@@ -451,7 +459,7 @@ void COverview::onDamageReported() { + g_pCompositor->scheduleFrameForMonitor(pMonitor.lock()); + } + +-void COverview::close() { ++void COverview::close(bool switchToSelection) { + if (closing) + return; + +@@ -471,7 +479,7 @@ void COverview::close() { + + redrawAll(); + +- if (TILE.workspaceID != pMonitor->activeWorkspaceID()) { ++ if (switchToSelection && TILE.workspaceID != pMonitor->activeWorkspaceID()) { + pMonitor->setSpecialWorkspace(0); + + // If this tile's workspace was WORKSPACE_INVALID, move to the next +diff --git a/overview.hpp b/overview.hpp +index 1f6bf3c..ca59f32 100644 +--- a/overview.hpp ++++ b/overview.hpp +@@ -33,8 +33,9 @@ class COverview { + void onSwipeEnd(); + + // close without a selection +- void close(); ++ void close(bool switchToSelection = true); + void selectHoveredWorkspace(); ++ int64_t selectedWorkspaceID() const; + + bool blockOverviewRendering = false; + bool blockDamageReporting = false; +-- +2.52.0 +