[hyprexpo] enhancement: outer gaps, workspace selection, workspace enumeration, tile rounding, multi-monitor
Resolves #123
Summary
This PR adds comprehensive keyboard-driven workspace selection to Hyprexpo, mouse hover feedback, per-monitor workspace configuration, numbered labels with rich styling, configurable inner/outer gaps, native Hyprland-style gradient borders with auto-detection, and rounded tiles with per-state overrides.
Highlights
- Keyboard Navigation: Arrow key movement, number/letter quick-select, auto-submap integration
- Mouse Hover: Visual feedback with configurable hover borders and tile rounding
-
Multi-Monitor Support: Per-monitor workspace method configuration via
hyprexpo_workspace_methodkeyword - Visual Feedback: Current workspace highlight + separate keyboard focus highlight + mouse hover highlight
- Rich Labels: Per-tile labels with background bubbles, font controls, pixel-perfect centering
-
Flexible Spacing:
gaps_inandgaps_outfor clean spacing -
Auto-Detecting Borders: Unified
border_color_*config supports both solid colors (rgb(66ccff)) and gradients (rgba(33ccffee) rgba(00ff99ee) 45deg) - Rounded Tiles: Per-state rounding overrides (focus/current/hover) with matching rounded borders
Why
- Make Hyprexpo fully navigable from keyboard and responsive to mouse, quickly selecting workspaces without mode switching
- Support multi-monitor setups with different workspace layouts per display
- Improve visual clarity: show numbers, highlight focus/current/hover states, provide consistent spacing
- Simplify configuration by auto-detecting border format (solid vs gradient)
- Align look & feel with Hyprland themes via native gradient borders
Key Features
Keyboard Navigation
-
Dispatchers:
-
hyprexpo:kb_focus <left|right|up|down>: Move keyboard focus -
hyprexpo:kb_confirm: Select focused tile -
hyprexpo:kb_selecti <index>: Select by 1-based visual index (recommended; enables 1-key selection) -
hyprexpo:kb_selectn <id>: Select by workspace ID (legacy; 0→10) -
hyprexpo:kb_select <token>: Single-character token (1-9, 0, a-z)
-
-
Auto-entered submap
hyprexpowhile overview is open (configurable viakeynav_enable) - Movement modes: Spatial (default) or reading-order horizontal moves
-
Wrap behavior: Configurable per axis (
keynav_wrap_h,keynav_wrap_v)
Mouse Hover
- Real-time hover detection with visual feedback
- Configurable hover borders (
border_color_hover) - Per-state tile rounding including hover (
tile_rounding_hover) - Priority system: focus > current > hover
Multi-Monitor Support
-
Global keyword
workspace_methodfor per-monitor configuration -
Repeatable syntax:
workspace_method = MONITOR_NAME <center|first> <workspace> -
Backwards compatible: Falls back to
plugin:hyprexpo:workspace_methodfor monitors without specific config -
Example:
# Global default (inside plugin block) plugin { hyprexpo { workspace_method = center current } } # Per-monitor overrides (outside plugin block, at top level) workspace_method = DP-1 first 1 workspace_method = HDMI-A-1 center 5
Labels (Numbers)
- Positioning: Centered within label container using ink-bounds for true visual centering
- Background bubble: circle/square/rounded, custom padding, color, rounding
- Per-state styling: default/hover/focus/current colors and scale multipliers
- Font styling: family, bold, italic, underline, strikethrough
- Pixel snap: On by default to avoid blurry edges
- Visibility modes: always, hover, focus, hover+focus, current+focus, never
-
Content modes:
token(default) |index|id -
Token overrides:
label_token_mapup to 50 comma-separated tokens (empty entries skip labels)
Borders (Auto-Detecting)
-
Unified config:
border_color_*auto-detects format:- Solid:
rgb(66ccff)or0xFF66CCFF - Gradient:
rgba(33ccffee) rgba(00ff99ee) 45deg
- Solid:
-
Native rendering: Uses
CGradientValueData+renderBorderwith proper rounded corner support - Per-state: Separate configs for current, focus, and hover
Gaps
-
gaps_in: Inner spacing between tiles -
gaps_out: Outer margin around the grid (animated during open/close)
Config Changes (Breaking / Migration)
Removed/Deprecated
- ~~
gap_size~~ →gaps_in
Border Format
Supports solid and hyprland-style gradient borders (via native api)
border_color_current = rgba(33ccffee) rgba(00ff99ee) 45deg # Auto-detects gradient
border_color_focus = rgb(ffcc66) # Auto-detects solid
border_color_hover = rgb(aabbcc) # Auto-detects solid
Minimal Migration Example
plugin {
hyprexpo {
# Spacing
gaps_in = 5
gaps_out = 0
# Borders (auto-detecting format)
border_width = 2
border_color_current = rgba(33ccffee) rgba(00ff99ee) 45deg # Gradient
border_color_focus = rgb(ffcc66) # Solid
border_color_hover = rgb(aabbcc) # Solid
# Tiles (rounded corners with per-state overrides)
tile_rounding = 12
tile_rounding_power = 2.0
tile_rounding_focus = -1 # -1 = inherit from tile_rounding
tile_rounding_current = -1
tile_rounding_hover = -1
# Keyboard navigation
keynav_enable = 1
keynav_wrap_h = 1
keynav_wrap_v = 1
# Labels (defaults to centered with background bubble)
label_enable = 1
label_text_mode = token # token | index | id
label_bg_enable = 1
label_bg_shape = circle
label_padding = 8
label_pixel_snap = 1
}
}
# Per-monitor workspace methods (optional, at top level)
workspace_method = DP-1 first 1
workspace_method = HDMI-A-1 center current
New Options (Grouped)
Keyboard Navigation
-
plugin:hyprexpo:keynav_enable(int/bool): default1 -
plugin:hyprexpo:keynav_wrap_h,plugin:hyprexpo:keynav_wrap_v: default1 -
plugin:hyprexpo:keynav_reading_order: default0(spatial)
Borders (Auto-Detecting)
-
plugin:hyprexpo:border_width(int): default2 -
plugin:hyprexpo:border_color(string): default border for unused tiles -
plugin:hyprexpo:border_color_current(string): defaultrgb(66ccff) -
plugin:hyprexpo:border_color_focus(string): defaultrgb(ffcc66) -
plugin:hyprexpo:border_color_hover(string): defaultrgb(aabbcc)
Tile Rounding
-
plugin:hyprexpo:tile_rounding(int): default0 -
plugin:hyprexpo:tile_rounding_power(float): default2.0 -
plugin:hyprexpo:tile_rounding_focus(int): default-1(inherit) -
plugin:hyprexpo:tile_rounding_current(int): default-1(inherit) -
plugin:hyprexpo:tile_rounding_hover(int): default-1(inherit)
Gaps
-
plugin:hyprexpo:gaps_in(int): default5 -
plugin:hyprexpo:gaps_out(int): default0
Labels: Placement and Visibility
-
plugin:hyprexpo:label_enable(int/bool): default1 -
plugin:hyprexpo:label_position:top-left|top-right|bottom-left|bottom-right|center -
plugin:hyprexpo:label_offset_x,label_offset_y(int): default0 -
plugin:hyprexpo:label_show:always|hover|focus|hover+focus|current+focus|never
Labels: Per-State and Font
-
plugin:hyprexpo:label_color_default|hover|focus|current(int colors) -
plugin:hyprexpo:label_scale_hover,label_scale_focus(float): default1.0 -
plugin:hyprexpo:label_font_family(string): defaultSans -
plugin:hyprexpo:label_font_size(int): default16 -
plugin:hyprexpo:label_font_bold,label_font_italic(int): default0 -
plugin:hyprexpo:label_text_underline,label_text_strikethrough(int): default0
Labels: Container and Precision
-
plugin:hyprexpo:label_bg_enable(int/bool): default1 -
plugin:hyprexpo:label_bg_shape:circle|square|rounded -
plugin:hyprexpo:label_bg_color(int): default0x88000000 -
plugin:hyprexpo:label_bg_rounding(int): default8 -
plugin:hyprexpo:label_padding(int): default8 -
plugin:hyprexpo:label_pixel_snap(int): default1 -
plugin:hyprexpo:label_center_adjust_x,label_center_adjust_y(int): optical nudge -
plugin:hyprexpo:label_text_mode:token|index|id -
plugin:hyprexpo:label_token_map(string): up to 50 comma-separated tokens
Usage (Bindings)
Recommended submap (auto-entered if keynav_enable = 1):
submap = hyprexpo
# Arrow key navigation
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
# Direct selection via numbers (1-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
# Extended selection via SHIFT+numbers (11-20)
bind = SHIFT, 1, hyprexpo:kb_selecti, 11
bind = SHIFT, 2, hyprexpo:kb_selecti, 12
# ... (continue for 3-9, 0)
# Alphabetic selection (21-46)
bind = , a, hyprexpo:kb_selecti, 21
bind = , b, hyprexpo:kb_selecti, 22
# ... (continue for c-z)
submap = reset
Testing
# Build
make clean && make
# Load in current session (requires hyprpm or manual plugin loading)
hyprpm reload
# Or add to hyprland.conf:
# exec-once = hyprpm reload -n
Implementation Notes
-
Keyboard nav state: Lives in
COverviewand integrates with render pass to highlight focus/current/hover - Mouse hover: Real-time tracking with damage updates on hover state changes
-
Multi-monitor: Uses global map
g_monitorWorkspaceMethodswith monitor name keys, falls back to plugin config - Labels: Pango+Cairo rasterization to OpenGL textures; centering uses ink bounds for optical alignment with pixel snapping
-
Auto-detecting borders:
isGradientBorderSpec()checks for dualrgba()pattern; parsesrgb()or hex for solid colors -
Gradient borders:
CGradientValueData+CHyprOpenGLImpl::renderBorderwith round/roundingPower for proper rounded corners - Outer gap animation: Tied to overview animation to avoid visual jumps
Backward Compatibility
Breaking Changes
-
gap_sizerenamed togaps_in -
outer_gapremoved (usegaps_out) -
border_color_current/focuschanged from INT to STRING
Migration Path
# Old config
gap_size = 5
border_style = hyprland
border_color_current = 0xFF66CCFF
border_grad_current = rgba(33ccffee) rgba(00ff99ee) 45deg
# New config
gaps_in = 5
border_color_current = rgba(33ccffee) rgba(00ff99ee) 45deg # Gradient auto-detected
Deprecated (Still Supported)
-
border_style: Ignored, kept for backwards compatibility -
border_grad_current/focus/hover: Used as fallback if newborder_color_*is empty
Potential Follow-ups
- Auto-inherit gradients from Hyprland's
col.active_border/col.inactive_borderwhen plugin gradient strings not set - Multi-digit quick-select with key buffer + timeout
- Additional label content modes and formatting placeholders
- Optional shader-based gradients if public API added upstream
Note to maintainers: Feedback welcome on API shape and config naming; happy to adjust for consistency with core. If you prefer not to maintain these features, no worries—I'm already maintaining a fork at hyprexpo-plus.
this MR will have to wait a moment for https://github.com/hyprwm/hyprland-plugins/pull/496 to be merged first as it changes the structure a bit
I found an issue related to multi-monitor support anyways, need to refactor some things.
minor refactor to support future upstream and rebased feature/better-hyprexpo onto scroll-overview. Not yet fully tested.
Notes:
- Current state of this branch "works" with hyprscrolling but is unstable, still needs work.
- Attempted to implement as many config values into new overview with mixed success. Was able to get keybinds and tile rounding working, kind of. The issue I encountered was state drift annd unpredictability when using both cursor and keybinds. Faced way too many issues and hit my timebox.
- I will wait for
scroll-overviewto be merged and then take another stab at first making the modifications stable without changing any of the scrolling logic, and only then attempt adding keybinds, multi-monitor support and styling to the nnewscroll-overviewfiles.