From b57b660c5584607c53065f7236ad1b488b51b42d Mon Sep 17 00:00:00 2001 From: Igor Warzocha Date: Thu, 16 Apr 2026 15:22:25 +0100 Subject: [PATCH 1/2] Add terminal scrollbar support --- rust/limux-ghostty-sys/src/lib.rs | 10 ++++ rust/limux-host-linux/src/pane.rs | 2 +- rust/limux-host-linux/src/terminal.rs | 83 ++++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/rust/limux-ghostty-sys/src/lib.rs b/rust/limux-ghostty-sys/src/lib.rs index 6b3a1a10..a9454608 100644 --- a/rust/limux-ghostty-sys/src/lib.rs +++ b/rust/limux-ghostty-sys/src/lib.rs @@ -62,6 +62,7 @@ pub const GHOSTTY_ACTION_NEW_WINDOW: c_int = 1; pub const GHOSTTY_ACTION_NEW_TAB: c_int = 2; pub const GHOSTTY_ACTION_CLOSE_TAB: c_int = 3; pub const GHOSTTY_ACTION_NEW_SPLIT: c_int = 4; +pub const GHOSTTY_ACTION_SCROLLBAR: c_int = 26; pub const GHOSTTY_ACTION_RENDER: c_int = 27; pub const GHOSTTY_ACTION_DESKTOP_NOTIFICATION: c_int = 31; pub const GHOSTTY_ACTION_SET_TITLE: c_int = 32; @@ -293,6 +294,7 @@ pub struct ghostty_action_s { // Must be exactly 24 bytes to match the C union. #[repr(C)] pub union ghostty_action_u { + pub scrollbar: ghostty_action_scrollbar_s, pub desktop_notification: ghostty_action_desktop_notification_s, pub set_title: ghostty_action_set_title_s, pub pwd: ghostty_action_pwd_s, @@ -300,6 +302,14 @@ pub union ghostty_action_u { _padding: [u8; 24], } +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ghostty_action_scrollbar_s { + pub total: u64, + pub offset: u64, + pub len: u64, +} + #[repr(C)] #[derive(Clone, Copy)] pub struct ghostty_action_desktop_notification_s { diff --git a/rust/limux-host-linux/src/pane.rs b/rust/limux-host-linux/src/pane.rs index 45d83fd9..20f5c476 100644 --- a/rust/limux-host-linux/src/pane.rs +++ b/rust/limux-host-linux/src/pane.rs @@ -1070,7 +1070,7 @@ fn add_terminal_tab_inner( }, term_callbacks, ); - let widget: gtk::Widget = term.overlay.clone().upcast(); + let widget = term.root.clone(); internals.content_stack.add_named(&widget, Some(&tab_id)); { diff --git a/rust/limux-host-linux/src/terminal.rs b/rust/limux-host-linux/src/terminal.rs index ef07bcd9..596199cd 100644 --- a/rust/limux-host-linux/src/terminal.rs +++ b/rust/limux-host-linux/src/terminal.rs @@ -31,6 +31,7 @@ unsafe impl Sync for GhosttyState {} static GHOSTTY: OnceLock = OnceLock::new(); static CURRENT_COLOR_SCHEME: AtomicI32 = AtomicI32::new(GHOSTTY_COLOR_SCHEME_LIGHT); +static CURRENT_SCROLLBAR_ENABLED: AtomicBool = AtomicBool::new(true); static WAKEUP_IDLE_QUEUED: AtomicBool = AtomicBool::new(false); type TitleChangedCallback = dyn Fn(&str); @@ -43,6 +44,9 @@ type WidgetCallback = dyn Fn(>k::Widget); struct SurfaceEntry { gl_area: gtk::GLArea, toast_overlay: gtk::Overlay, + scrollbar: gtk::Scrollbar, + scrollbar_adjustment: gtk::Adjustment, + scrollbar_syncing: Rc>, on_title_changed: Option>, on_pwd_changed: Option>, on_desktop_notification: Option>, @@ -262,7 +266,7 @@ impl TerminalHandle { } pub struct TerminalWidget { - pub overlay: gtk::Overlay, + pub root: gtk::Widget, pub handle: TerminalHandle, } @@ -355,6 +359,7 @@ pub fn init_ghostty() { let config = load_ghostty_config(); let background_opacity = load_background_opacity(config); + CURRENT_SCROLLBAR_ENABLED.store(load_scrollbar_enabled(config), Ordering::Relaxed); let runtime_config = ghostty_runtime_config_s { userdata: ptr::null_mut(), @@ -418,6 +423,21 @@ fn load_background_opacity(config: ghostty_config_t) -> f64 { } } +fn load_scrollbar_enabled(config: ghostty_config_t) -> bool { + let mut value: *const c_char = ptr::null(); + let key = b"scrollbar"; + let loaded = unsafe { + ghostty_config_get( + config, + (&mut value as *mut *const c_char).cast::(), + key.as_ptr().cast::(), + key.len(), + ) + }; + + !loaded || value.is_null() || unsafe { std::ffi::CStr::from_ptr(value) }.to_bytes() != b"never" +} + fn ghostty_color_scheme_for_dark_mode(dark: bool) -> c_int { if dark { GHOSTTY_COLOR_SCHEME_DARK @@ -482,6 +502,31 @@ unsafe extern "C" fn ghostty_action_cb( let tag = action.tag; match tag { + GHOSTTY_ACTION_SCROLLBAR => { + if target.tag == GHOSTTY_TARGET_SURFACE { + let surface_key = unsafe { target.target.surface } as usize; + let scrollbar = unsafe { action.action.scrollbar }; + SURFACE_MAP.with(|map| { + if let Some(entry) = map.borrow().get(&surface_key) { + entry.scrollbar_syncing.set(true); + entry.scrollbar_adjustment.configure( + scrollbar.offset as f64, + 0.0, + scrollbar.total as f64, + 1.0, + scrollbar.len as f64, + scrollbar.len as f64, + ); + entry.scrollbar_syncing.set(false); + entry.scrollbar.set_visible( + CURRENT_SCROLLBAR_ENABLED.load(Ordering::Relaxed) + && scrollbar.total > scrollbar.len, + ); + } + }); + } + true + } GHOSTTY_ACTION_RENDER => { if target.tag == GHOSTTY_TARGET_SURFACE { let surface_key = unsafe { target.target.surface } as usize; @@ -594,6 +639,7 @@ unsafe extern "C" fn ghostty_action_cb( } GHOSTTY_ACTION_RELOAD_CONFIG => { let config = load_ghostty_config(); + CURRENT_SCROLLBAR_ENABLED.store(load_scrollbar_enabled(config), Ordering::Relaxed); match target.tag { GHOSTTY_TARGET_APP => unsafe { ghostty_app_update_config(app, config); @@ -851,6 +897,7 @@ pub fn create_terminal( let callbacks = Rc::new(RefCell::new(callbacks)); let surface_cell: Rc>> = Rc::new(RefCell::new(None)); let had_focus = Rc::new(Cell::new(false)); + let scrollbar_syncing = Rc::new(Cell::new(false)); let clipboard_context_cell: Rc> = Rc::new(Cell::new(ptr::null_mut())); @@ -860,6 +907,17 @@ pub fn create_terminal( overlay.set_hexpand(true); overlay.set_vexpand(true); + let scrollbar_adjustment = gtk::Adjustment::new(0.0, 0.0, 0.0, 1.0, 0.0, 0.0); + let scrollbar = gtk::Scrollbar::new(gtk::Orientation::Vertical, Some(&scrollbar_adjustment)); + scrollbar.set_visible(false); + scrollbar.set_vexpand(true); + + let root = gtk::Box::new(gtk::Orientation::Horizontal, 0); + root.set_hexpand(true); + root.set_vexpand(true); + root.append(&overlay); + root.append(&scrollbar); + let search_entry = gtk::SearchEntry::builder() .hexpand(true) .placeholder_text("Find in terminal") @@ -938,15 +996,30 @@ pub fn create_terminal( } }); } + { + let surface_cell = surface_cell.clone(); + let scrollbar_syncing = scrollbar_syncing.clone(); + scrollbar_adjustment.connect_value_changed(move |adj| { + if scrollbar_syncing.get() { + return; + } + + let row = adj.value().round() as usize; + surface_action(*surface_cell.borrow(), &format!("scroll_to_row:{row}")); + }); + } // On realize: create the Ghostty surface { let gl = gl_area.clone(); let overlay_for_map = overlay.clone(); + let scrollbar_for_map = scrollbar.clone(); + let scrollbar_adjustment_for_map = scrollbar_adjustment.clone(); let surface_cell = surface_cell.clone(); let callbacks = callbacks.clone(); let had_focus = had_focus.clone(); let clipboard_context_cell = clipboard_context_cell.clone(); + let scrollbar_syncing = scrollbar_syncing.clone(); gl_area.connect_realize(move |gl_area| { gl_area.make_current(); if let Some(err) = gl_area.error() { @@ -1030,6 +1103,9 @@ pub fn create_terminal( SurfaceEntry { gl_area: gl.clone(), toast_overlay: overlay_for_map.clone(), + scrollbar: scrollbar_for_map.clone(), + scrollbar_adjustment: scrollbar_adjustment_for_map.clone(), + scrollbar_syncing: scrollbar_syncing.clone(), on_title_changed: Some(Box::new({ let cb = callbacks.clone(); move |title| { @@ -1439,7 +1515,10 @@ pub fn create_terminal( }); } - TerminalWidget { overlay, handle } + TerminalWidget { + root: root.upcast(), + handle, + } } // --------------------------------------------------------------------------- From 150c3127a8c7596d49972adc36a0d7114d7caeab Mon Sep 17 00:00:00 2001 From: Igor Warzocha Date: Thu, 16 Apr 2026 15:31:53 +0100 Subject: [PATCH 2/2] Respect Ghostty terminal scrollbar policy --- rust/limux-ghostty-sys/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/limux-ghostty-sys/src/lib.rs b/rust/limux-ghostty-sys/src/lib.rs index a9454608..2489e57e 100644 --- a/rust/limux-ghostty-sys/src/lib.rs +++ b/rust/limux-ghostty-sys/src/lib.rs @@ -378,15 +378,15 @@ extern "C" { // Config pub fn ghostty_config_new() -> ghostty_config_t; pub fn ghostty_config_free(config: ghostty_config_t); - pub fn ghostty_config_load_default_files(config: ghostty_config_t); - pub fn ghostty_config_load_recursive_files(config: ghostty_config_t); - pub fn ghostty_config_finalize(config: ghostty_config_t); pub fn ghostty_config_get( config: ghostty_config_t, out: *mut c_void, key: *const c_char, key_len: usize, ) -> bool; + pub fn ghostty_config_load_default_files(config: ghostty_config_t); + pub fn ghostty_config_load_recursive_files(config: ghostty_config_t); + pub fn ghostty_config_finalize(config: ghostty_config_t); // App pub fn ghostty_app_new(