diff --git a/default/hypr/bindings/utilities.lua b/default/hypr/bindings/utilities.lua index ee5b9900..820e7824 100644 --- a/default/hypr/bindings/utilities.lua +++ b/default/hypr/bindings/utilities.lua @@ -102,6 +102,18 @@ o.bind("SUPER + CTRL + W", "Network", "omarchy-shell shell toggle omarchy.networ o.bind("SUPER + CTRL + P", "Power", "omarchy-shell shell toggle omarchy.power") o.bind("SUPER + CTRL + T", "Activity", { tui = "btop" }) +-- The letters above name a panel; the numbers count them. 1 is the leftmost +-- panel in the bar's right section, and a widget with no panel of its own (the +-- tray) is not counted, so the number matches the icon a user would point at. +-- A bar with fewer panels than this leaves the tail of the range doing nothing. +for panel = 1, 9 do + o.bind( + "SUPER + CTRL + code:" .. tostring(panel + 9), + "Bar panel " .. panel, + "omarchy-shell -q shell togglePanelAt right " .. panel + ) +end + o.bind("SUPER + CTRL + Z", "Zoom in", function() local zoom = hl.get_config("cursor.zoom_factor") or 1 hl.config({ cursor = { zoom_factor = zoom + 1 } }) diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index 12b950b5..5c0daa46 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -417,6 +417,21 @@ Item { return slots } + // The Nth panel in a bar region, counted the way the bar reads: layout order, + // and only the panels actually on screen. A widget with no panel (the tray) + // and one that is hiding itself are passed over, so the number lands on the + // Nth panel icon the user can see rather than the Nth layout entry. + // One-based, because it exists for hotkeys; anything else lands on no slot. + // + // Counting any bar surface is enough: every monitor lays its bar out from the + // one layout, and summoning the id routes through pickPanelSlot, which opens + // the focused monitor's copy whichever surface was counted. + function panelWidgetIdAt(region, index) { + var slots = panelNavigationSlots(String(region || ""), null) + var slot = slots[Math.round(Number(index)) - 1] + return slot ? String(slot.moduleName || "") : "" + } + function switchPanelFrom(owner, direction) { if (!owner) return false diff --git a/shell/shell.qml b/shell/shell.qml index 1b8ae9c9..71a9834f 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -1011,6 +1011,19 @@ ShellRoot { shell.toggle(id, payloadJson) } + // A bar section's panels answer to their position as well as their id, so a + // hotkey can mean "the third panel in the right section" and keep meaning + // it after the bar is rearranged. Returns the id it acted on, or "unknown" + // when the section holds no panel at that position. + function togglePanelAt(section: string, index: string): string { + var id = shell.bar && typeof shell.bar.panelWidgetIdAt === "function" + ? shell.bar.panelWidgetIdAt(section, index) + : "" + if (!id) return "unknown" + shell.toggle(id, "{}") + return id + } + function call(id: string, method: string, arg: string): string { return shell.callIfLoaded(id, method, arg) } diff --git a/test/shell.d/bar-test.sh b/test/shell.d/bar-test.sh index 304998cc..7d52fc49 100644 --- a/test/shell.d/bar-test.sh +++ b/test/shell.d/bar-test.sh @@ -183,6 +183,23 @@ assert( 'bar tabs between panels within one bar surface' ) +// A positional hotkey means "the third panel in this section", so it counts the +// panels the bar actually draws. Reusing the tab-order walk is what keeps the +// count honest: a widget with no panel and a hidden one are already passed over +// there, and reading the layout config a second time would count both. +assert( + /function panelWidgetIdAt\(region, index\) \{[\s\S]*?panelNavigationSlots\(String\(region \|\| ""\), null\)/.test(barSource), + 'bar counts positional panels off the drawn tab order' +) +assert( + /var slot = slots\[Math\.round\(Number\(index\)\) - 1\]/.test(barSource), + 'positional panels are one-based, and anything off the end lands on no slot' +) +assert( + /function togglePanelAt\(section: string, index: string\): string \{[\s\S]*?shell\.bar\.panelWidgetIdAt\(section, index\)[\s\S]*?shell\.toggle\(id, "\{\}"\)/.test(shellSource), + 'shell toggles a bar panel by its position over IPC' +) + const clockSlot = { id: 'clock' } const traySlot = { id: 'tray' } const horizontalTargets = [ diff --git a/test/shell.d/hyprland-default-config-test.sh b/test/shell.d/hyprland-default-config-test.sh index e9cabd17..e37ef931 100644 --- a/test/shell.d/hyprland-default-config-test.sh +++ b/test/shell.d/hyprland-default-config-test.sh @@ -175,6 +175,21 @@ if grep -Fq $'SUPER + CTRL + X Toggle dictation' <<<"$missing_voxtype_output"; t fi pass "missing Voxtype skips dictation bindings" +# The panel hotkeys claim a row of keys that workspace switching already uses +# under other modifiers, so the count matters as much as the bindings: a tenth +# claim on SUPER + CTRL + a number is a collision with one of these. +panels_home="$tmpdir/panels-home" +mkdir -p "$panels_home" +panels_output=$(run_omarchy_bindings "$panels_home") +for panel in 1 2 3 4 5 6 7 8 9; do + grep -Fqx "SUPER + CTRL + code:$((panel + 9))"$'\t'"Bar panel $panel" <<<"$panels_output" || + fail "bar panel hotkeys count the right section" "$panel" +done +number_claims=$(cut -f1 <<<"$panels_output" | grep -cE '^SUPER \+ CTRL \+ code:1[0-9]$' || true) +(( number_claims == 9 )) || + fail "only the bar panel hotkeys bind SUPER + CTRL + a number" "$number_claims" +pass "bar panel hotkeys bind SUPER + CTRL + a number without a collision" + migration=$(grep -rl 'Move stock Hyprland user overrides into package defaults' "$ROOT/migrations" | head -n 1 || true) [[ -n $migration ]] || fail "Hyprland default config migration exists"