You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
So the only record of what these two handlers are supposed to have in common is a set
of comments that no longer resolve. A divergence introduced today is invisible: there is
no test, no type, and now not even a working pointer that relates the two.
What is already shared
The actions are engine-side and correct — Engine::handle_key (core/engine/keys.rs:9)
plus 15 focused handlers: handle_picker_key, handle_quickfix_key, handle_terminal_key, handle_ext_panel_key, handle_ext_panel_input_key, handle_editor_hover_key, handle_settings_key, handle_ai_panel_key, handle_search_input_key, handle_context_menu_key, handle_spell_suggestion_key, handle_sc_key, handle_sc_commit_input_key, handle_explorer_rename_key, handle_explorer_new_entry_key.
What is duplicated is the precedence ladder that decides which one to call — dialog
before folder picker before activity bar before sidebar before terminal before the
Alt block before the editor. That ladder is pure state inspection over Engine. It has
no backend-specific content, which is exactly why the TUI side could be written by
transcribing the GTK side in the first place.
Proposed shape
One shared render::route_key(&Engine, KeyInput) -> KeyRoute, mirroring the route_mouse_event proposal in #733 — an enum naming the resolved consumer plus its
payload. Each backend converts its native key event into KeyInput, calls the router,
matches the KeyRoute, and calls the engine method. The ladder is then stated once and
both backends inherit changes to it.
Check quadraui first.MenuSystem::handle already owns the menu-bar intercept for
both backends and is the pattern to follow; if quadraui's ShellApp/AppShell seam can
own more of this ladder, that is where it belongs and this becomes an adoption issue
rather than a render.rs build. Resolve before writing code (CLAUDE.md
Platform-Neutrality Rule).
Do not simply update the 19 stale comments. Re-pointing them buys another six months
of the same drift; they exist because there was no shared artefact to point at.
Staging
Slice by rung, top of the ladder down, each slice deleting both implementations in the
same PR:
Delete the two entry points' remains and the 19 mirrors comments
Acceptance criteria
Per slice:
The rung is implemented once, in shared code, called from both backends.
Both bespoke arms are deleted in the same PR — a migration that leaves both paths
is not done (GOALS.md).
Black-box coverage on both backends — TuiDriver via driver_with_shell(TuiShellApp, …)
and GtkDriver (src/gtk/testing.rs). Assert on rendered output after the keypress,
never on state being populated (CLAUDE.md rule 1).
The PR states the new tests were verified RED against unfixed develop (rule 2).
Combined production line count of the five entry points strictly decreases; PR states
before/after.
After #732 (Msg bus) — GTK's key rungs currently terminate in Msg variants, so
untangling those first means this issue migrates a direct call rather than a bus hop.
Runs alongside or after #733, which does the same job for mouse (and before #735, which composes what both produce); the two share the route_* shape, so whichever lands first sets the pattern and the second should
follow it rather than invent a second one.
⚠️ Line numbers in this body drift — locate by symbol
Every line number above was measured on develop @ 4872ab9 (2026-09-01). They are
already wrong.#727 landed src/gtk/mod.rs +435/−44 within hours and moved enum Msg 1096→1131, fn dispatch 1799→1896, sync_scrollbar 2657→2754. Seven more
issues sit ahead of this one in the chain, each deleting or moving thousands of lines.
Treat the numbers as provenance — proof the claim was verified once — never as
coordinates. Locate by these anchors instead:
grep -nE "fn (handle_key_press|dispatch_gtk_panel_accelerator)" src/gtk/mod.rs
grep -nE "fn (handle_key_pressed|handle_sidebar_focused_key|dispatch_panel_accelerator_sizeless)" src/tui_main/shell_app.rs
grep -rn "mirrors mod.rs" src/tui_main/ # the 19 stale pointers; must be 0 at the end
This is the same defect #734 exists to fix: src/tui_main/ carries 19 mirrors mod.rs:NNNN comments whose targets all drifted after #540, leaving the only
record of a cross-backend contract pointing at unrelated code. Do not re-point the
numbers when they rot — the anchors are the durable form.
Summary
Keyboard dispatch is duplicated across the backends, ~2,000 production lines, and the TUI
half documents itself as a transcription of the GTK half:
handle_key_press(src/gtk/mod.rs:2884)dispatch_gtk_panel_accelerator(:398)handle_key_pressed(src/tui_main/shell_app.rs:3526)handle_sidebar_focused_key(:3027)dispatch_panel_accelerator_sizeless(:2811)The evidence is written into the code
src/tui_main/carries 19mirrors mod.rs:NNNNcomments, twelve of them onhandle_key_pressed's own precedence rungs:Every one of those line references is now wrong. The #540 Relm4→ShellApp cutover
renumbered
mod.rsand nothing updated them:mod.rs:1629debug_sidebar_lh: Rc::new(Cell::new(20.0)),— a field initialisermod.rs:1653char_width_cell: Rc::new(Cell::new(9.0)),— a field initialisermod.rs:2439| Msg::OpenWorkspaceDialog— a match arm indispatchmod.rs:2703engine.calculate_group_window_rects(...)— window layoutSo the only record of what these two handlers are supposed to have in common is a set
of comments that no longer resolve. A divergence introduced today is invisible: there is
no test, no type, and now not even a working pointer that relates the two.
What is already shared
The actions are engine-side and correct —
Engine::handle_key(core/engine/keys.rs:9)plus 15 focused handlers:
handle_picker_key,handle_quickfix_key,handle_terminal_key,handle_ext_panel_key,handle_ext_panel_input_key,handle_editor_hover_key,handle_settings_key,handle_ai_panel_key,handle_search_input_key,handle_context_menu_key,handle_spell_suggestion_key,handle_sc_key,handle_sc_commit_input_key,handle_explorer_rename_key,handle_explorer_new_entry_key.What is duplicated is the precedence ladder that decides which one to call — dialog
before folder picker before activity bar before sidebar before terminal before the
Alt block before the editor. That ladder is pure state inspection over
Engine. It hasno backend-specific content, which is exactly why the TUI side could be written by
transcribing the GTK side in the first place.
Proposed shape
One shared
render::route_key(&Engine, KeyInput) -> KeyRoute, mirroring theroute_mouse_eventproposal in #733 — an enum naming the resolved consumer plus itspayload. Each backend converts its native key event into
KeyInput, calls the router,matches the
KeyRoute, and calls the engine method. The ladder is then stated once andboth backends inherit changes to it.
Check quadraui first.
MenuSystem::handlealready owns the menu-bar intercept forboth backends and is the pattern to follow; if quadraui's
ShellApp/AppShellseam canown more of this ladder, that is where it belongs and this becomes an adoption issue
rather than a
render.rsbuild. Resolve before writing code (CLAUDE.mdPlatform-Neutrality Rule).
Do not simply update the 19 stale comments. Re-pointing them buys another six months
of the same drift; they exist because there was no shared artefact to point at.
Staging
Slice by rung, top of the ladder down, each slice deleting both implementations in the
same PR:
handle_sidebar_focused_keyis the 467-lineone; expect this slice to be the largest)
Ctrl+Shift+V— coordinate with Ctrl+V paste silently does nothing on GTK — quadraui swallows the key, vimcode has no ClipboardPaste handler #593, which touchesthe same rung on the GTK side
dispatch_gtk_panel_accelerator↔dispatch_panel_accelerator_sizeless)mirrorscommentsAcceptance criteria
Per slice:
is not done (
GOALS.md).TuiDriverviadriver_with_shell(TuiShellApp, …)and
GtkDriver(src/gtk/testing.rs). Assert on rendered output after the keypress,never on state being populated (
CLAUDE.mdrule 1).develop(rule 2).before/after.
cargo build && cargo test && cargo clippy -- -D warnings && cargo fmt --checkEXIT=0.Final slice:
grep -rn 'mirrors mod\.rs' src/tui_main/returns nothing.Sequencing
After #732 (
Msgbus) — GTK's key rungs currently terminate inMsgvariants, sountangling those first means this issue migrates a direct call rather than a bus hop.
Runs alongside or after #733, which does the same job for mouse (and before #735, which composes what both produce); the two share the
route_*shape, so whichever lands first sets the pattern and the second shouldfollow it rather than invent a second one.
Files
src/render.rssrc/gtk/mod.rssrc/tui_main/shell_app.rssrc/gtk/testing.rsMilestone
#7 Platform-Neutral
Every line number above was measured on
develop @ 4872ab9(2026-09-01). They arealready wrong. #727 landed
src/gtk/mod.rs+435/−44 within hours and movedenum Msg1096→1131,fn dispatch1799→1896,sync_scrollbar2657→2754. Seven moreissues sit ahead of this one in the chain, each deleting or moving thousands of lines.
Treat the numbers as provenance — proof the claim was verified once — never as
coordinates. Locate by these anchors instead:
This is the same defect #734 exists to fix:
src/tui_main/carries 19mirrors mod.rs:NNNNcomments whose targets all drifted after #540, leaving the onlyrecord of a cross-backend contract pointing at unrelated code. Do not re-point the
numbers when they rot — the anchors are the durable form.