Skip to content

Skip dwindle togglesplit on scrolling workspaces - #11863

Open
0xPer0 wants to merge 1 commit into
omacom:quattrofrom
0xPer0:fix/super-j-skip-scrolling-layoutmsg
Open

0xPer0 wants to merge 1 commit into
omacom:quattrofrom
0xPer0:fix/super-j-skip-scrolling-layoutmsg

Conversation

@0xPer0

@0xPer0 0xPer0 commented Sep 14, 2026

Copy link
Copy Markdown

Super + J always dispatched dwindle's togglesplit. On a scrolling workspace Hyprland raises no such layoutmsg for scrolling and shows a Lua runtime error overlay.

The bind now checks the active (or special) workspace layout:

  • dwindle: togglesplit (unchanged)
  • scrolling: consume_or_expel next (stack a lone column into its neighbor, or expel a stacked window)
  • one window, or any other layout: no-op

Testing

  • bash test/shell.d/hyprland-window-split-toggle-test.sh: passed
  • bash test/shell.d/hyprland-binding-conflicts-test.sh: passed
  • ./test/cli: passed
  • Live on Hyprland 0.56.2: hyprctl configerrors clean after reload; dispatching the new path on a one-window scrolling workspace no longer raises the Lua runtime error

Super+J sent layoutmsg togglesplit unconditionally, which Hyprland rejects
on a scrolling workspace. Dispatch consume_or_expel on scrolling instead.
@llstrk

llstrk commented Sep 30, 2026

Copy link
Copy Markdown

Automated AI review

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Outcome: SUPER + J does nothing for a lone window in the last scrolling column (per source), and the new test cannot fail.

SUPER + J no-op on a scrolling edge

consume_or_expel next needs a column to the right; for a lone window in the last column Hyprland 0.56.2 logs "no adjacent column" and nothing moves. A new window opens right of the focused one and takes focus, so with two windows the focused one is usually last. Found in source, not run live.

Suggested change: If consume_or_expel next returns ok == false, retry with consume_or_expel prev.

Details

Source: ScrollingAlgorithm.cpp in Hyprland v0.56.2, consume_or_expel handling (lines 1580-1602, "no adjacent column" at 1594-1597) and newTarget column insertion (768-771). Warnings are logged, not shown as an overlay.

Suggested shape (checked only against a stubbed hl in the style of the PR's test, where a last-column failure retries with prev and a window with a next column still sends only next):

local result = hl.dispatch(hl.dsp.layout("consume_or_expel next"))
if result and result.ok == false then
  hl.dispatch(hl.dsp.layout("consume_or_expel prev"))
end

What would settle it live: in a Hyprland 0.56.2 session, open two windows on a scrolling workspace, press SUPER + J on the right-hand one, and check for "no adjacent column" in the log with the layout unchanged.

The new test cannot fail

Line 7 runs lua <<'LUA' || fail. Without a script argument, lua ignores the stdin chunk's result, so a failed assert exits 0. It prints ok at the merge-base and with the scrolling branch broken.

Suggested change: Use lua - <<'LUA', as hyprland-qconsole-test.sh does.

Reproducer (passes at the merge-base and on a broken head)

The new split-toggle test passes even when its assertions fail

test/shell.d/hyprland-window-split-toggle-test.sh runs its Lua with lua <<'LUA' ... LUA || fail .... When lua is given no script argument and stdin is not a terminal, the standalone interpreter runs stdin through dofile(L, NULL) and ignores the result (lua.c, pmain, in both Lua 5.4.7 and 5.5.1). A failed assert prints to stderr, but lua still exits 0, so || fail never runs and the file prints ok - ....

Run with Lua 5.5.1 and bash, from the repository root of a clean checkout. Commits: PR head b22820a1a9cb041433b16d13c6e61324fa261e0e, merge-base b679363bed05415771a1b1dc92c6899a908236f7, current quattro 8b4eae66da2938ba9559f103b18dbf85cdf28a70.

1. Minimal interpreter behaviour

lua <<'LUA'
assert(false, "assertion inside lua <<'LUA'")
LUA
echo "lua <<'LUA'   exit=$?"
lua - <<'LUA'
assert(false, "assertion inside lua - <<'LUA'")
LUA
echo "lua - <<'LUA' exit=$?"

stdout:

lua <<'LUA'   exit=0
lua - <<'LUA' exit=1

stderr:

lua: stdin:1: assertion inside lua <<'LUA'
stack traceback:
	[C]: in global 'assert'
	stdin:1: in main chunk
	[C]: in ?
lua: stdin:1: assertion inside lua - <<'LUA'
stack traceback:
	[C]: in global 'assert'
	stdin:1: in main chunk
	[C]: in ?

2. Break SUPER + J at the PR head: the test still passes

At the PR head:

sed -i 's/consume_or_expel next/consume_or_expel prev/; s/(workspace.windows or 0) < 2/(workspace.windows or 0) < 0/' \
  default/hypr/bindings/tiling.lua
bash test/shell.d/hyprland-window-split-toggle-test.sh; echo "exit=$?"

stdout:

ok - SUPER + J dispatches togglesplit on dwindle and consume_or_expel on scrolling
exit=0

stderr (stdin:65 is test line 72, assert(dispatched[1].msg == "consume_or_expel next")):

lua: stdin:65: assertion failed!
stack traceback:
	[C]: in global 'assert'
	stdin:65: in main chunk
	[C]: in ?

Same at the merge-base with only the new test file copied in (SUPER + J there is still hl.dsp.layout("togglesplit")):

bash test/shell.d/hyprland-window-split-toggle-test.sh; echo "exit=$?"

stdout:

ok - SUPER + J dispatches togglesplit on dwindle and consume_or_expel on scrolling
exit=0

stderr (stdin:51 is test line 58, "SUPER + J binds a layout-aware function"):

lua: stdin:51: SUPER + J binds a layout-aware function
stack traceback:
	[C]: in global 'assert'
	stdin:51: in main chunk
	[C]: in ?

3. With lua - the test catches both and still passes on the PR

test/shell.d/hyprland-qconsole-test.sh already uses lua - <<'LUA' || fail, with a comment saying the assertions have to fail the file themselves. Applying the same to line 7:

sed -i "7s/ lua <<'LUA' || fail/ lua - <<'LUA' || fail/" test/shell.d/hyprland-window-split-toggle-test.sh
sed -n 7p test/shell.d/hyprland-window-split-toggle-test.sh
bash test/shell.d/hyprland-window-split-toggle-test.sh; echo "exit=$?"

Mutated PR head (from step 2), stdout:

HOME="$(mktemp -d)" OMARCHY_PATH="$ROOT" lua - <<'LUA' || fail "SUPER + J split toggle loads under a Hyprland stub"
exit=1

stderr:

lua: stdin:65: assertion failed!
stack traceback:
	[C]: in global 'assert'
	stdin:65: in main chunk
	[C]: in ?
not ok - SUPER + J split toggle loads under a Hyprland stub

Merge-base plus the new test, stdout:

HOME="$(mktemp -d)" OMARCHY_PATH="$ROOT" lua - <<'LUA' || fail "SUPER + J split toggle loads under a Hyprland stub"
exit=1

stderr:

lua: stdin:51: SUPER + J binds a layout-aware function
stack traceback:
	[C]: in global 'assert'
	stdin:51: in main chunk
	[C]: in ?
not ok - SUPER + J split toggle loads under a Hyprland stub

Unmodified PR head, and current quattro with the PR commit cherry-picked, stdout (stderr empty):

HOME="$(mktemp -d)" OMARCHY_PATH="$ROOT" lua - <<'LUA' || fail "SUPER + J split toggle loads under a Hyprland stub"
ok - SUPER + J dispatches togglesplit on dwindle and consume_or_expel on scrolling
exit=0
More findings

Keybindings menu entry for SUPER + J no longer runs anything

bin/omarchy-menu-keybindings replays binds from the Lua source, and a plain Lua function leaves nothing to replay: the "Toggle window split" row has an empty dispatcher and picking it exits 1 (before, it ran togglesplit). Open PR #11851 fixes this for function binds generally by calling the ref Hyprland reports; with #11851 merged and a fresh menu cache, picking the entry runs this function (checked against a stand-in built from Hyprland source, not live Hyprland).

Suggested change: If this lands before #11851, name the function and register it the way quattro registers the terminal bind (#13521): o.bind_commands[o.toggle_window_split] = "hyprctl eval 'o.toggle_window_split()'", which needs a rebase onto quattro (live hyprctl eval untested).

Picking "Toggle window split" from the keybindings menu does nothing after the PR

bin/omarchy-menu-keybindings (SUPER + K) runs the entry a user picks. Hyprland reports every Lua bind as dispatcher __lua, with the bind's registry ref as its arg, so the menu re-reads the Lua config with a stub hl and records what each bind runs.

  • Before the PR, SUPER + J was hl.dsp.layout("togglesplit"). The stub records that as a Lua dispatcher, and picking the entry runs hyprctl dispatch 'hl.dsp.layout("togglesplit")'.
  • With the PR, SUPER + J is a plain Lua function. The stub records an empty dispatcher, and dispatch_binding returns 1 without running anything.
  • On current quattro, Hand the terminal launcher the focused window's pid from Lua #13521 added o.bind_commands for exactly this case (the terminal bind), with a test "picking the terminal bind from the menu launches a terminal".

Run with Lua 5.5.1, GNU Awk 5.4.1 and bash. Commits:

  • PR head b22820a1a9cb041433b16d13c6e61324fa261e0e
  • merge-base b679363bed05415771a1b1dc92c6899a908236f7
  • current quattro 8b4eae66da2938ba9559f103b18dbf85cdf28a70, with the PR commit cherry-picked

Test in the repository's style

Save as test/shell.d/keybindings-menu-split-toggle-test.sh:

#!/bin/bash

source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"

require_command lua

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

home="$tmpdir/home"
stub_bin="$tmpdir/bin"
log="$tmpdir/hyprctl.log"
mkdir -p "$home/.config" "$stub_bin"
cp -r "$ROOT/config/hypr" "$home/.config/hypr"

# Hyprland reports every Lua bind as dispatcher __lua with its registry ref as
# arg, so the menu has to recover what SUPER + J runs from the Lua source.
cat >"$stub_bin/hyprctl" <<'EOF'
#!/bin/bash
case "$1" in
  binds) printf 'bind\n\tmodmask: 64\n\tsubmap: \n\tkey: SUPER + J\n\tkeycode: 0\n\tcatchall: false\n\tdescription: Toggle window split\n\tdispatcher: __lua\n\targ: 135\n' ;;
  devices) echo "active keymap: English (US)" ;;
  dispatch) shift; printf '%s\n' "$*" >>"$HYPRCTL_LOG" ;;
esac
EOF

# Pick the "Toggle window split" entry, as a user would in the menu.
cat >"$stub_bin/omarchy-menu-select" <<'EOF'
#!/bin/bash
grep -m1 'Toggle window split'
EOF
chmod +x "$stub_bin/hyprctl" "$stub_bin/omarchy-menu-select"

env -i PATH="$stub_bin:$ROOT/bin:$PATH" HOME="$home" XDG_CACHE_HOME="$tmpdir/cache" \
  OMARCHY_PATH="$ROOT" HYPRCTL_LOG="$log" bash "$ROOT/bin/omarchy-menu-keybindings"

[[ -s $log ]] ||
  fail "picking Toggle window split from the keybindings menu dispatches something" \
    "$(cat "$tmpdir"/cache/omarchy/keybindings-*.records 2>/dev/null | grep 'Toggle window split' | sed 's/\t/<TAB>/g')"
pass "picking Toggle window split from the keybindings menu dispatches: $(cat "$log")"

Run from the repository root:

bash test/shell.d/keybindings-menu-split-toggle-test.sh; echo "exit=$?"

Actual output

Merge-base (SUPER + J is hl.dsp.layout("togglesplit")), stdout (stderr empty):

ok - picking Toggle window split from the keybindings menu dispatches: hl.dsp.layout("togglesplit")
exit=0

PR head, stdout:

exit=1

stderr (<TAB> marks the empty dispatcher and argument fields of the cached menu record):

SUPER + J                           → Toggle window split<TAB><TAB>
not ok - picking Toggle window split from the keybindings menu dispatches something

Current quattro with the PR commit cherry-picked: identical to the PR head (exit 1, same stderr).

Verified and notes

Verified against Hyprland 0.56.2 source and sandbox tests:

  • togglesplit on scrolling raises the "no such layoutmsg for scrolling" runtime error overlay; the new bind avoids it.
  • hl.get_active_special_workspace, hl.get_active_workspace, workspace.windows, workspace.tiled_layout and consume_or_expel next exist with the assumed names and types; preferring the special workspace matches where Hyprland sends layout messages.
  • Dwindle behaviour is unchanged for two or more windows; with one window the old togglesplit did nothing anyway (at most a warning).
  • The PR applies cleanly to current quattro, and the new test (also with lua -), the binding conflict test and the keybindings menu test pass there.

Related open PRs: #6924 and #12851 make SUPER + J dwindle-only instead and conflict with this change in default/hypr/bindings/tiling.lua. #10529 binds consume_or_expel next to SUPER + BACKSLASH and prev to SUPER + SHIFT + BACKSLASH, and merges cleanly.

Optional notes:

  • HOME="$(mktemp -d)" leaves one directory per run; docs/testing.md asks for trap ... EXIT cleanup, and this test does not need a separate HOME.
  • manual/07-hotkeys.md still describes SUPER + J as a horizontal/vertical toggle, with no scrolling behaviour.

Review information

Test scope: Source and sandbox tests at b22820a and current quattro, against Hyprland 0.56.2 source; not run live.

AI process: Opus 5.5 Medium coordination and synthesis, Opus 5.5 Xhigh technical review and final fact check, GPT 6 Sol Xhigh search for related issues, Opus 5.5 Medium editorial check.

Opt out: To stop receiving these reviews, reply to this comment saying so.

@omarchybot omarchybot added the bug Something isn't working label Oct 3, 2026
@omarchybot

Copy link
Copy Markdown
Collaborator

Thanks for this. It fixes the same bug as #6924, which is older: both stop SUPER + J from sending dwindle's togglesplit on a scrolling workspace, checking the special workspace first. #12851 is a third fix for it. When competing fixes are similar the oldest is kept, and #6924 has no defect that this one avoids, so #6924 is the one going to the maintainer. This pull request is waiting on the maintainer's decision.

What this one adds beyond the fix is new behaviour: on scrolling, SUPER + J now runs consume_or_expel next. That is a feature, and whether SUPER + J should do it is the maintainer's call. #10529 also binds consume_or_expel to its own keys. If you want to propose it, a separate pull request after #6924 lands would be the way, with two things fixed:

  • test/shell.d/hyprland-window-split-toggle-test.sh:7 runs lua <<'LUA' || fail. With no script argument, lua exits 0 even when an assert fails, so this test passes whatever the binding does. I confirmed that with Lua 5.5.1. lua - <<'LUA' makes a failed assertion fail the test.
  • consume_or_expel next has no column to the right when the focused window is alone in the last column, which is where a new window opens. The workspace-wide windows >= 2 check doesn't catch this, so the key does nothing there and Hyprland reports "no adjacent column".

How this was checked: I read the diffs of #6924, #11863 and #12851 against quattro and compared them. Codex Medium compared the three on its own and also picked #6924. It agreed on both findings above, but it may have read my reasoning, so its agreement isn't independent confirmation. Nothing was run on a worker for this pull request, since it is not the fix going forward.

@jandrusk

Copy link
Copy Markdown

Nice layout-aware SUPER+J for #7556. Overlaps #6924 — this version's consume_or_expel for scrolling looks more complete; I'd close #6924 as dup once one lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants