Files
ghostty-rs/docs/rewrite/notes/ghostty-apprt-gtk.md
T
2026-10-11 22:39:16 +02:00

14 KiB

F3: dialogs + config object (class/{dialog,config,config_errors_dialog,close_confirmation_dialog,clipboard_confirmation_dialog,title_dialog,debug_warning}.rs)

Libadwaita >= 1.5 is assumed, so Dialog is always an adw::AlertDialog subclass (no MessageDialog fallback) and DebugWarning uses the adw::Banner (1.3) template; the clipboard dialog always uses the 1.4 template (remember switch). GType names, property names, signals and template children match the Zig classes.

Public API (all &self methods on glib::wrapper! types; signals via connect_*):

  • config::Config (GhosttyConfig; props diagnostics-buffer, has-diagnostics -> generated getters diagnostics_buffer() -> Option<gtk::TextBuffer>, has_diagnostics() -> bool): new(&CoreConfig) (clones), from_arc(Arc<CoreConfig>), new_default(), get() -> Arc<CoreConfig>, get_mut(|&mut CoreConfig| ..) (Arc::make_mut), clone_config() -> CoreConfig. Test ghostty_config ported.
  • dialog::Dialog (GhosttyDialog, parent adw::AlertDialog): present(Option<&impl IsA<Widget>>), close() (force_close); subclass via DialogImpl: AdwAlertDialogImpl (unsafe IsSubclassable impl, SAFETY-commented). Override response through AdwAlertDialogImpl.
  • config_errors_dialog::ConfigErrorsDialog: new(&Config), prop config, present(Option<&W>), close(), connect_reload_config(Fn(&Self)).
  • close_confirmation_dialog::{CloseConfirmationDialog, Target{App,Tab,Window,Surface}}: new(Target), prop target, present(Option<&W>) (sets heading/body), close(), connect_close_request(Fn(&Self)), connect_cancel(Fn(&Self)). Target::title()/body() use ghostty_os::i18n::tr.
  • clipboard_confirmation_dialog::{ClipboardConfirmationDialog, ClipboardRequestBoxed(pub ClipboardRequest)}: new(), props request (boxed, nullable), clipboard-contents (TextBuffer), blur, can-remember (generated getters/setters: set_request(Option<ClipboardRequestBoxed>), set_clipboard_contents(Option<TextBuffer>), set_blur(bool), set_can_remember(bool)), helpers set_request_value(ClipboardRequest), get_request() -> Option<ClipboardRequest>, get_clipboard_contents(), present(Option<&W>), connect_confirm(Fn(&Self, remember: bool)), connect_deny(Fn(&Self, bool)).
  • title_dialog::{TitleDialog, Target{Surface,Tab}} (parent adw::AlertDialog directly, as in Zig): new(Target, Option<&str>), props target, initial-value, present(&W) (anchors to ancestor adw::ApplicationWindow/Window, async choose), connect_set(Fn(&Self, &str)).
  • debug_warning::DebugWarning: new().

Status: all type-check and are clippy-clean; runtime (template instantiation, notify callbacks) NOT yet exercised because the crate did not fully compile when F3 finished. Also added #![allow(clippy::unwrap_used)] to build.rs.

F4: overlays, child-exited banner, scrolled window, css (done)

Files: src/css.rs, src/class/{resize_overlay,key_state_overlay,search_overlay,surface_child_exited,surface_scrolled_window}.rs. Verified in an isolated scratch crate (compiles, clippy -D warnings, instantiated on a live display with no GTK criticals). All GType names match Zig (note GhostttySurfaceScrolledWindow keeps Zig's 3-t typo); templates from the bundled gresource.

  • ResizeOverlay (GhosttyResizeOverlay): props duration(u32, 750), first-delay(250), label(Option), overlay-halign/valign. API: new(), set_label(Option<&str>), label(), schedule().
  • KeyStateOverlay: props tables/sequence are GStrv (Vec<String>) instead of ext.StringList; read-only has-tables/has-sequence; valign-target. API: set_tables(Vec<String>), set_sequence(Vec<String>).
  • SearchOverlay: props active (rw), search-total/search-selected (u64 ro), has-search-total/selected, halign-target, valign-target. API: set_search_total(Option<usize>), set_search_selected(Option<usize>), set_search_contents(&str), grab_focus_entry() (Zig grabFocus; renamed to avoid clashing with Widget::grab_focus), set_active(bool), connect_{stop_search,search_changed(Fn(&Self, Option<String>)),next_match,previous_match}.
  • SurfaceChildExited: prop data (boxed ChildExitedBoxed), signal close-request. API: set_child_exited(Option<&ChildExited>), connect_close_request. The Zig Noop variant (adw < 1.3) is omitted (crate requires adw 1.5).
  • SurfaceScrolledWindow: SurfaceScrolledWindow::new(Option<&impl IsA<Widget>>), props surface (typed GtkWidget, accepts the Surface class), config (typed GObject; the scrollbar-policy closure downcasts to class::config::Config and calls .get().scrollbar - needs Config::get() -> impl Deref<Target = ghostty_config::config::Config>). Binds surface.config -> own config, so Surface must expose a config property holding a class::config::Config.
  • css::CssProviders: new(&Display) (installs runtime provider at APPLICATION+3), load_all(&Config), load_runtime, load_custom (gtk-custom-css, 5MB limit, USER priority), set_color_scheme(dark) (sets prefers-color-scheme on providers, GTK >= 4.20, dynamic property). css::runtime_css(&Config, gtk_416) -> String is pure and tested. Static style*.css come from the gresource: the Application must set resource_base_path = /com/mitchellh/ghostty so AdwApplication loads them automatically.
  • Template signal handlers receive the emitter (not the template) in gtk-rs, so shared handlers locate the overlay with ancestor().
  • build.rs fix: blueprint .ui files are bundled with alias="<ver>/<name>.ui" so resource paths are /com/mitchellh/ghostty/ui/<ver>/<name>.ui (previously doubled ui/ui/).

F1: utilities, key, ipc (files: key, gtk_version, adw_version, weak_ref, ext/, gsettings, cgroup, flatpak, pre_exec, post_fork, ipc/)

Ported (Zig -> Rust): key.zig -> key.rs (accel_from_trigger, xdg_shortcut_from_trigger, label_from_trigger return Option<String>; translate_mods, keyval_unicode_unshifted(&Widget, &gdk::KeyEvent, keycode) -> u32, event_mods(&gdk::Event, Key, ModifierType, Action, winproto_event_mods: impl FnOnce(Option<&gdk::Device>, ModifierType)->Mods) -> Mods, key_from_keyval(u32), keyval_from_key(Key); 132-entry KEYMAP). gtk_version/adw_version -> get_runtime_version() -> (u32,u32,u32), at_least/runtime_at_least/runtime_until, log_version, adw supports_*. "Comptime" version is the cargo feature level (4.14 / 1.5). weak_ref.zig -> thin WeakRef<T> over glib::WeakRef. ext -> ext::actions::{Action, add, add_to_map, add_as_group}, ext::StringList (glib Boxed GhosttyStringList), get_ancestor, g_value_holds (boxedCopy/boxedFree/listFind not needed). gsettings -> get_bool/get_int/get_string(Key). cgroup -> fmt_scope(pid), create_scope(&gio::DBusConnection, pid, Options). flatpak -> resources_dir(). pre_exec/post_fork -> PreExecInfo::new(&Config)/pre_exec(&info) -> Option<u8>; PostForkInfo::new(&Config)/post_fork(&info, Option<pid>) -> Result<(), PostForkError> (uses gio::Application::default() dbus connection; ghostty-termio's Command also has its own copy of the cgroup wait loop). ipc -> ipc::perform_ipc(&Target, &Action) -> Result<bool, ipc::Error> (org.gtk.Actions.Activate, new-window / new-window-command), ipc::dbus::DBus.

Tests: Zig ported 11 (key 3, gtk_version 2, adw_version 1, weak_ref 1, gsettings 4 (the comptime ones became 2), ext/actions 2, ext/slice 3); extra 8. The ext/actions GTK-display test and adw test are skipped at runtime when GTK cannot init (not main thread / no display). Deviations: only unsafe is FromGlib for gdk::Key in key.rs (annotated). many keycode media keys remain a TODO as upstream.

winproto (F2) — src/winproto/{mod,noop,x11,wayland}.rs

Ported winproto.zig, noop.zig, x11.zig, wayland.zig. Deps added: gdk4-x11 (x11, xlib; Xlib via its re-exported dlopen x11 = x11-dl), gdk4-wayland (wayland_crate), wayland-client/-backend(client_system)/-protocols(client,staging)/-protocols-plasma(client).

Public API (consumed by class/window.rs, class/application.rs)

pub trait WindowHost {            // implemented by window.rs using a *weak* ref to the Window gobject
    fn gtk_window(&self) -> Option<gtk::Window>;   // as(gtk.Window)/Native
    fn is_quick_terminal(&self) -> bool;
    fn window_decoration(&self) -> ghostty_config::types_b::WindowDecoration;
    fn config(&self) -> Option<Arc<Config>>;       // getConfig()?.get()
}
pub enum App { None(noop::App), Wayland(wayland::App), X11(x11::App) }
App::init(&gdk::Display, app_id:&str, &Config) -> Result<App>     // wayland, then x11, else None
App::protocol() -> Protocol; App::event_mods(Option<&gdk::Device>, gdk::ModifierType) -> Mods  // falls back to crate::key::translate_mods
App::supports_quick_terminal() -> bool
App::init_quick_terminal(&mut self, &gtk::Window) -> Result<()>   // call BEFORE the window is realized (layer-shell init)
pub enum Window { None, Wayland, X11 }
Window::init(&App, Rc<dyn WindowHost>) -> Result<Window>          // window must be realized (needs gdk::Surface)
Window::{resize_event, sync_appearance, set_urgent(bool)}(&mut self) -> Result<()>
Window::client_side_decoration_enabled(&self) -> bool
Window::add_subprocess_env(&mut self, &mut Vec<(String,String)>) -> Result<()>   // X11: WINDOWID

Zig deinit = Drop (Wayland releases blur/decoration/slide/token objects, disconnects enter-monitor). noop::Window::update_config_event (unused in Zig union) was not ported.

Behaviour notes / deviations

  • Wayland uses gdk4-wayland's shared wayland_client::Connection (no unsafe). We keep a private event queue; blocking roundtrips at init; while an xdg-activation token is outstanding a 50 ms glib timeout dispatches the queue. KDE blur / server-decoration / slide / xdg-activation implemented; xdg_wm_dialog_v1 presence + layer-shell < 1.0.4 check ported. Registry changes after init (global_remove) are not tracked.
  • Quick terminal via gtk4-layer-shell crate (LayerShell trait). Upstream requires libgtk4-layer-shell to be loaded before libwayland-client; Rust link order is not controllable, so the final binary should link it first or run with LD_PRELOAD=libgtk4-layer-shell.so (else is_supported() is false and the quick terminal is disabled).
  • X11: Xlib (dlopen'd via x11-dl) for XGet/Change/DeleteWindowProperty, XEventsQueued/XPeekEvent; XkbQueryExtension/XkbSelectEventDetails declared in a small extern "C" block (#[link(name="X11")]). unsafe is confined to x11.rs (#![allow(unsafe_code)], SAFETY comments).
  • Tests: Zig has none for winproto; 2 added (noop defaults, X11 property struct layouts). Not runtime-tested on a live X11/Wayland compositor in this fork.
  • Depends on crate::key::translate_mods(gdk::ModifierType) -> Mods (F1).

Application / App / GlobalShortcuts / demo (F8)

Files: src/app.rs (App.zig), src/class/application.rs (application.zig), src/class/global_shortcuts.rs (global_shortcuts.zig), src/gl.rs (GL proc loader), examples/demo.rs.

Public API

  • app::App { pub app: Application }: App::new(Rc<dyn CoreApp>), run() (-> Result<(), RunError>), terminate(), wakeup(), perform_action(Target<SurfaceId>, Action) -> bool, App::perform_ipc(&ipc::Target, &ipc::Action) (static). app::waker() -> Arc<dyn Waker> (thread-safe, MainContext::wakeup) - create it before the core app and give it to core. app::MUST_DRAW_FROM_APP_THREAD = true.
  • class::application::Application (GType GhosttyApplication, extends AdwApplication, property config (read-only Config object, notify on change)): Application::default(), new(core_app), core() -> Rc<dyn CoreApp>, rt() -> Rc<dyn RtApp>, get_config() -> class::config::Config (object), core_config() -> Arc<ghostty_config::Config>, winproto() -> Ref<winproto::App> / winproto_mut(), saved_language(), surfaces() -> Vec<Surface>, perform_action(...), new_window(parent: Option<&Surface>, WindowOverrides), quit(), wakeup(), run_loop(), deinit(). AppRt is the RtApp impl (forwards to the default app; main thread only).
  • The giant action switch is Application::perform_action. Surface targets are resolved with Surface::from_id(id) (F5); actions use the Zig mechanisms (activate_action("tab.close"/"win.close"/ "split-tree.*"), ancestor::<SplitTree/Window/Tab>). inspector actions go to Surface::control_inspector (inspector deferred; no-op). Unimplemented actions (secure_input, close_all_windows, ...) log + return false.
  • Startup mirrors Zig: log writer funnel (glib::log_set_writer_func -> log), version logging, Config::load (errors become a diagnostic), LANG/language, i18n, GDK_DEBUG/GDK_DISABLE env (set_gtk_env), adw::init, single-instance flag, app-id (class), winproto init (noop fallback), CSS via css::CssProviders, setlocale(LC_ALL,"") (init_locale, closes the INTEGRATION_TODO item for locale), style-manager <-> core color scheme sync (color_scheme_event + each CoreSurface::color_scheme_callback), app.* GActions (new-window, new-window-command (as: -e, --command=, --working-directory=, --title=), open-config, present-surface (u64 surface id), quit, reload-config), accelerator sync, config errors dialog, quit timer, run_loop (own MainContext iteration + CoreApp::tick + quit conditions), quit() with close confirmation dialog.
  • SIGUSR2 reload: glib-rs has no g_unix_signal_add; a helper thread (signal-hook) hops to the main context.
  • Global shortcuts: portal CreateSession/BindShortcuts over gio D-Bus (deprecated signal_subscribe allowed); GlobalShortcuts signal trigger(String) carries the action in keybind syntax; connect_trigger parses it.
  • gl::proc_address(name): dlsym on libepoxy (works for EGL and GLX); use as the loader for glow / ghostty-renderer::prepare_context.

Changes to core.rs (trait spec)

Added AppMessage::NewWindow, CoreSurface::color_scheme_callback, CoreSurface::update_config; CoreApp::perform_all_action takes a ghostty_input::binding::Action (as in Zig).

Demo

cargo run -p ghostty-apprt-gtk --example demo (env: DEMO_QUIT_AFTER_MS, DEMO_CONFIRM=1). Dummy core: keybindings from the user config drive window/tab/split/palette actions; draw_frame clears with background.

Known gaps

  • toggle_visibility/float_window/secure_input/close_all_windows etc.: unimplemented like upstream.
  • Open URL uses xdg-open (OpenURI portal is a TODO upstream too).
  • async-backend (libxev) is meaningless in Rust and ignored.