Problem
A set of small, high-confidence duplicate pairs between the two backends and between a backend and code that already exists in render.rs / Engine. Individually trivial; together ~330 production lines that are stated twice and can drift independently (see the GTK tab-bar issue in this chain for what that drift costs).
1. register_panel_accelerators — byte-identical, 44 lines ×2
src/app.rs:90-133 and src/tui_main/mod.rs:160-211: the same 14-entry table and the same loop. Move to src/render.rs and call it from both.
2. EngineAction dispatch — five copies
src/app.rs:1488-1562 dispatch_engine_action
src/app.rs:4748-4803 handle_menu_action
src/tui_main/shell_app.rs:3808-3867 dispatch_post_key_action
src/tui_main/shell_app.rs:2319-2395 inline MenuEvent::Activated arm
src/tui_main/mod.rs:912-947 handle_action
The two TUI copies are near-identical to each other (OpenTerminal, RunInTerminal, OpenFolderDialog, OpenWorkspaceDialog, SaveWorkspaceAsDialog, OpenRecentDialog, QuitWithUnsaved).
Fix: render::apply_engine_action(engine, action, is_macro, &mut impl EngineActionHost) with four host hooks — open_terminal(cols, rows), folder_dialog, file_dialog, exit. ~150 lines.
3. Gutter action apply — identical 5-arm match
src/tui_main/mouse.rs:2624-2667 vs src/gtk/click.rs:465-509. The router (resolve_gutter_action) is already shared; only the apply is duplicated. Fix: render::apply_gutter_action. ~40 lines.
4. GTK re-implements engine dialog helpers
src/app.rs:5548-5582 show_quit_confirm and :5655-5690 show_close_tab_confirm duplicate Engine::show_quit_confirm / show_close_tab_confirm (src/core/engine/panels.rs:46-100) — same DialogButton literals. The TUI calls the engine (src/tui_main/shell_app.rs:2370, src/tui_main/mouse.rs:2252). Fix: delete GTK's copies. ~70 lines.
5. Session save
src/tui_main/mod.rs:948-972 vs src/app.rs:1446-1487 — same 20 lines plus GTK window size. Fix: Engine::save_session_state(). ~20 lines.
6. Explorer context action table
src/tui_main/mod.rs:690-738 vs src/app.rs:5497-5520 + :4869-4887 — same string → ExplorerAction mapping. ~40 lines.
7. GTK states sidebar_owner three times
src/app.rs:4812-4827 (current_active_panel_id), :2441-2449 (paint_sidebar_panel_rung), and the real render::sidebar_owner call at :5138. Collapse to the shared call.
8. TUI open-codes Engine::clear_sidebar_focus
src/tui_main/mouse.rs:2173-2181 sets 9 flags individually; Engine::clear_sidebar_focus() (src/core/engine/accessors.rs:575) is the same 9 assignments, and GTK calls it at src/app.rs:3855.
Also: the 5-line "collapse sidebar" sequence (hide_sidebar; has_focus = false; clear_sidebar_focus; session.explorer_visible = false; session.save) appears 5× in src/tui_main/shell_app.rs (:2779, :3134, :3298, :3480, :3512) and once at src/app.rs:5450. Extract one helper.
Fix
Take these in order, one commit each, so a failure is bisectable. Re-verify each pair immediately before editing — line numbers were measured at ee26268.
Acceptance
Problem
A set of small, high-confidence duplicate pairs between the two backends and between a backend and code that already exists in
render.rs/Engine. Individually trivial; together ~330 production lines that are stated twice and can drift independently (see the GTK tab-bar issue in this chain for what that drift costs).1.
register_panel_accelerators— byte-identical, 44 lines ×2src/app.rs:90-133andsrc/tui_main/mod.rs:160-211: the same 14-entry table and the same loop. Move tosrc/render.rsand call it from both.2.
EngineActiondispatch — five copiessrc/app.rs:1488-1562dispatch_engine_actionsrc/app.rs:4748-4803handle_menu_actionsrc/tui_main/shell_app.rs:3808-3867dispatch_post_key_actionsrc/tui_main/shell_app.rs:2319-2395inlineMenuEvent::Activatedarmsrc/tui_main/mod.rs:912-947handle_actionThe two TUI copies are near-identical to each other (
OpenTerminal,RunInTerminal,OpenFolderDialog,OpenWorkspaceDialog,SaveWorkspaceAsDialog,OpenRecentDialog,QuitWithUnsaved).Fix:
render::apply_engine_action(engine, action, is_macro, &mut impl EngineActionHost)with four host hooks —open_terminal(cols, rows),folder_dialog,file_dialog,exit. ~150 lines.3. Gutter action apply — identical 5-arm match
src/tui_main/mouse.rs:2624-2667vssrc/gtk/click.rs:465-509. The router (resolve_gutter_action) is already shared; only the apply is duplicated. Fix:render::apply_gutter_action. ~40 lines.4. GTK re-implements engine dialog helpers
src/app.rs:5548-5582show_quit_confirmand:5655-5690show_close_tab_confirmduplicateEngine::show_quit_confirm/show_close_tab_confirm(src/core/engine/panels.rs:46-100) — sameDialogButtonliterals. The TUI calls the engine (src/tui_main/shell_app.rs:2370,src/tui_main/mouse.rs:2252). Fix: delete GTK's copies. ~70 lines.5. Session save
src/tui_main/mod.rs:948-972vssrc/app.rs:1446-1487— same 20 lines plus GTK window size. Fix:Engine::save_session_state(). ~20 lines.6. Explorer context action table
src/tui_main/mod.rs:690-738vssrc/app.rs:5497-5520+:4869-4887— same string →ExplorerActionmapping. ~40 lines.7. GTK states
sidebar_ownerthree timessrc/app.rs:4812-4827(current_active_panel_id),:2441-2449(paint_sidebar_panel_rung), and the realrender::sidebar_ownercall at:5138. Collapse to the shared call.8. TUI open-codes
Engine::clear_sidebar_focussrc/tui_main/mouse.rs:2173-2181sets 9 flags individually;Engine::clear_sidebar_focus()(src/core/engine/accessors.rs:575) is the same 9 assignments, and GTK calls it atsrc/app.rs:3855.Also: the 5-line "collapse sidebar" sequence (
hide_sidebar;has_focus = false;clear_sidebar_focus;session.explorer_visible = false;session.save) appears 5× insrc/tui_main/shell_app.rs(:2779,:3134,:3298,:3480,:3512) and once atsrc/app.rs:5450. Extract one helper.Fix
Take these in order, one commit each, so a failure is bisectable. Re-verify each pair immediately before editing — line numbers were measured at
ee26268.Acceptance
EngineActiondispatch), add one and state that it fails if the collapse is done wrong.cargo test,cargo clippy -- -D warnings,cargo fmtpass.