Problem
src/app.rs:1 describes itself as "struct App — the backend-neutral editor shell application". It is not neutral: src/app.rs:592 types the backend field as the concrete GTK struct.
pub(crate) backend: Rc<RefCell<backend::GtkBackend>>,
That single line is what keeps src/lib.rs:45-46 gating the whole 7,131-line module behind #[cfg(feature = "gui")], and it is the stated Stage-1 blocker on #47 (native macOS GUI).
The blocker is gone upstream. quadraui#699 (filed 2026-09-03 16:38Z, closed 17:11Z, PR#700 / 88345fb) put both handle accessors on the Backend trait, and that commit is already inside vimcode's pinned rev:
|
Location |
Backend trait |
quadraui/src/backend.rs:745 fn modal_stack_handle(&self) -> Rc<RefCell<ModalStack>>, :750 fn drag_state_handle(&self) -> Rc<RefCell<DragState>> |
GtkBackend |
gtk/backend.rs:1439, :1447 |
TuiBackend |
tui/backend.rs:967, :971 |
MacBackend |
macos/backend.rs:695, :699 |
WinBackend |
win/backend.rs:762, :766 |
App's 19 calls (modal_stack_handle ×12 at src/app.rs:1144, 1315, 2166, 2214, 2226, 3594, 3621, 3639, 3774, 4319, 4342, 4606; drag_state_handle ×7 at :2115, 3605, 3775, 3788, 3972, 4341, 4603) currently resolve to GtkBackend's inherent copies (gtk/backend.rs:560, :582) only because the field names the concrete type. Retype the field and all 19 compile unchanged.
Correction to the record: PLAN.md and GOALS.md both state "44 call sites" for this blocker. That number came from grep -n 'self\.backend\.' src/gtk/mod.rs — every use of the field, not these two methods. Today src/app.rs has 17 self.backend. lines. Do not size this work from the 44.
Scope
Retype App::backend and remove the GTK-concrete coupling that follows from it. Out of scope: any macOS code. #47's stages 2+ cannot be built or verified on any machine in this fleet (no Apple SDK / objc2 toolchain) — this issue is the buildable, testable part.
The residual GTK coupling in src/app.rs is 35 code lines (python3 scripts/native_lines.py gtk src/app.rs reports 64, but ~39 of those are doc-comments that merely mention GTK). Beyond the backend field:
Platform-typed fields — :281 settings_monitor: Option<gio::FileMonitor>, :425 window: Option<gtk4::Window>, :580 css_provider: Option<gtk4::CssProvider>. Also in the new_headless signature at :896-900.
Hook sites, with upstream status:
| Hook |
Site |
Status |
| Exit |
:1481, :1544 glib::idle_add_local_once(process::exit) |
quadraui already has Reaction::Exit (quadraui/src/runtime.rs; GTK's ReactionSink::request_exit at gtk/run.rs:1074). app.rs uses it 0 times; TUI returns it at 7 sites. Pure adoption. |
| 200 ms yank highlight |
:1921 glib::timeout_add_local_once |
No toolkit needed. TUI does it portably with yank_hl_deadline: Cell<Option<Instant>> polled in tick (src/tui_main/shell_app.rs:486, :2938). |
| Colorscheme / dark mode |
:1934-1940, :5836-5839 |
Trait has set_theme (already called once by App). |
| Window maximize |
:5521-5546 |
Trait has toggle_window_maximize — and App already calls it while also driving gtk4::Window::maximize directly at :5530-5536. Drop the direct call. |
| Window title / minimize / close |
:2043-2044, :5521-5546 |
Small genuine gap — no set_title/minimize/close on the trait. File upstream if needed. |
| CSD capture |
:4999-5027 (list_toplevels, set_decorated(false)) |
Needs a window-chrome flag in ShellConfig, not a toolkit call. |
| Settings-file monitor |
:856-868 |
Needs a file watcher, not GTK (notify crate or mtime poll in tick). Parity note: the TUI has no settings hot-reload at all — zero hits. |
| Icon theme path / bundled icon font |
:821-825 |
Only matters if native icon rendering remains. |
| Folder picker |
:5592-5613 gtk4::FileDialog::select_folder |
Genuine gap in PlatformServices (quadraui/src/backend.rs:2136 has file open/save only) — handled by the FolderPickerController adoption issue, not here. |
| Text measurement |
:5971-5972 click::build_editor_click_context → set_pango_context |
Genuinely toolkit-bound. Leave it; this plus folder-select are the only two real native needs in the file. |
crate::gtk::{click, css, util} is less coupled than the module doc claims: of 19 helpers used, only click::build_editor_click_context (Pango), css::load_css (returns CssProvider), and util::install_bundled_icon_font/app_icon_image are toolkit-bound. pixel_to_click_target (11 uses), handle_mouse_click/double/drag, resolve_tab_right_click, make_theme_css, open_url and all 10 gtk/mod.rs helpers have no GTK types in their signatures.
Fix
- Retype
App::backend to a dyn Backend handle. Verify the 19 handle calls compile untouched.
- Adopt
Reaction::Exit; delete the two glib::idle_add_local_once exits.
- Port the yank-highlight timer to the TUI's deadline-in-
tick pattern.
- Drop the direct
gtk4::Window::maximize call in favour of the trait method already being called beside it.
- Isolate what genuinely remains (Pango measurement context, window title/minimize/close, CSD, icon theme, settings watcher) behind a narrow trait or
PlatformServices extension, so the gui gate covers only that.
- Update the
src/app.rs module doc — it currently lists blockers that no longer exist.
Do not close #47; it stays open for the macOS stages. Note in the PR which of the module doc's blockers are now retired.
Acceptance
cargo test (GUI lane) green; cargo clippy -- -D warnings, cargo fmt pass.
python3 scripts/native_lines.py gtk src/app.rs drops materially from 64.
- Behaviour-preserving refactor: exit, yank highlight, maximize and theme switching must be covered by existing or new
GtkDriver black-box tests. If you add one, state in the PR that it fails against unfixed develop per CLAUDE.md.
Problem
src/app.rs:1describes itself as "struct App— the backend-neutral editor shell application". It is not neutral:src/app.rs:592types the backend field as the concrete GTK struct.That single line is what keeps
src/lib.rs:45-46gating the whole 7,131-line module behind#[cfg(feature = "gui")], and it is the stated Stage-1 blocker on #47 (native macOS GUI).The blocker is gone upstream. quadraui#699 (filed 2026-09-03 16:38Z, closed 17:11Z, PR#700 /
88345fb) put both handle accessors on theBackendtrait, and that commit is already inside vimcode's pinned rev:Backendtraitquadraui/src/backend.rs:745fn modal_stack_handle(&self) -> Rc<RefCell<ModalStack>>,:750fn drag_state_handle(&self) -> Rc<RefCell<DragState>>GtkBackendgtk/backend.rs:1439,:1447TuiBackendtui/backend.rs:967,:971MacBackendmacos/backend.rs:695,:699WinBackendwin/backend.rs:762,:766App's 19 calls (
modal_stack_handle×12 atsrc/app.rs:1144, 1315, 2166, 2214, 2226, 3594, 3621, 3639, 3774, 4319, 4342, 4606;drag_state_handle×7 at:2115, 3605, 3775, 3788, 3972, 4341, 4603) currently resolve toGtkBackend's inherent copies (gtk/backend.rs:560,:582) only because the field names the concrete type. Retype the field and all 19 compile unchanged.Scope
Retype
App::backendand remove the GTK-concrete coupling that follows from it. Out of scope: any macOS code. #47's stages 2+ cannot be built or verified on any machine in this fleet (no Apple SDK /objc2toolchain) — this issue is the buildable, testable part.The residual GTK coupling in
src/app.rsis 35 code lines (python3 scripts/native_lines.py gtk src/app.rsreports 64, but ~39 of those are doc-comments that merely mention GTK). Beyond the backend field:Platform-typed fields —
:281 settings_monitor: Option<gio::FileMonitor>,:425 window: Option<gtk4::Window>,:580 css_provider: Option<gtk4::CssProvider>. Also in thenew_headlesssignature at:896-900.Hook sites, with upstream status:
:1481,:1544glib::idle_add_local_once(process::exit)Reaction::Exit(quadraui/src/runtime.rs; GTK'sReactionSink::request_exitatgtk/run.rs:1074).app.rsuses it 0 times; TUI returns it at 7 sites. Pure adoption.:1921glib::timeout_add_local_onceyank_hl_deadline: Cell<Option<Instant>>polled intick(src/tui_main/shell_app.rs:486,:2938).:1934-1940,:5836-5839set_theme(already called once by App).:5521-5546toggle_window_maximize— and App already calls it while also drivinggtk4::Window::maximizedirectly at:5530-5536. Drop the direct call.:2043-2044,:5521-5546set_title/minimize/closeon the trait. File upstream if needed.:4999-5027(list_toplevels,set_decorated(false))ShellConfig, not a toolkit call.:856-868notifycrate or mtime poll intick). Parity note: the TUI has no settings hot-reload at all — zero hits.:821-825:5592-5613gtk4::FileDialog::select_folderPlatformServices(quadraui/src/backend.rs:2136has file open/save only) — handled by the FolderPickerController adoption issue, not here.:5971-5972click::build_editor_click_context→set_pango_contextcrate::gtk::{click, css, util}is less coupled than the module doc claims: of 19 helpers used, onlyclick::build_editor_click_context(Pango),css::load_css(returnsCssProvider), andutil::install_bundled_icon_font/app_icon_imageare toolkit-bound.pixel_to_click_target(11 uses),handle_mouse_click/double/drag,resolve_tab_right_click,make_theme_css,open_urland all 10gtk/mod.rshelpers have no GTK types in their signatures.Fix
App::backendto adyn Backendhandle. Verify the 19 handle calls compile untouched.Reaction::Exit; delete the twoglib::idle_add_local_onceexits.tickpattern.gtk4::Window::maximizecall in favour of the trait method already being called beside it.PlatformServicesextension, so theguigate covers only that.src/app.rsmodule doc — it currently lists blockers that no longer exist.Do not close #47; it stays open for the macOS stages. Note in the PR which of the module doc's blockers are now retired.
Acceptance
cargo test(GUI lane) green;cargo clippy -- -D warnings,cargo fmtpass.python3 scripts/native_lines.py gtk src/app.rsdrops materially from 64.GtkDriverblack-box tests. If you add one, state in the PR that it fails against unfixeddevelopper CLAUDE.md.