From 4fe12a21ddf07246986051c18a8ed1e2e8524bb2 Mon Sep 17 00:00:00 2001 From: ata <111813260+atacolak@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:59:52 +1000 Subject: [PATCH] feat(terminal): persist font size per terminal tab --- rust/limux-host-linux/src/layout_state.rs | 51 +++++++++++++++ rust/limux-host-linux/src/pane.rs | 66 ++++++++++++++++++- rust/limux-host-linux/src/terminal.rs | 16 ----- rust/limux-host-linux/src/window.rs | 79 ++++++++++------------- 4 files changed, 148 insertions(+), 64 deletions(-) diff --git a/rust/limux-host-linux/src/layout_state.rs b/rust/limux-host-linux/src/layout_state.rs index a234b7fd..a38424dc 100644 --- a/rust/limux-host-linux/src/layout_state.rs +++ b/rust/limux-host-linux/src/layout_state.rs @@ -191,6 +191,8 @@ pub enum TabContentState { cwd: Option, #[serde(default)] agent: Option, + #[serde(default)] + font_size: Option, }, Browser { #[serde(default)] @@ -260,6 +262,7 @@ impl TabState { content: TabContentState::Terminal { cwd: cwd.map(|value| value.to_string()), agent: None, + font_size: None, }, } } @@ -1379,6 +1382,7 @@ mod tests { launch_command: None, restore_on_startup: true, }), + font_size: None, }, }], }); @@ -1474,6 +1478,7 @@ mod tests { }), restore_on_startup: true, }), + font_size: None, }, }; @@ -1498,6 +1503,52 @@ mod tests { } } + #[test] + fn terminal_tab_state_round_trips_font_size_override() { + let tab = TabState { + id: "tab-a".to_string(), + custom_name: None, + pinned: false, + content: TabContentState::Terminal { + cwd: Some("/tmp/project".to_string()), + agent: None, + font_size: Some(17.5), + }, + }; + + let raw = serde_json::to_string(&tab).expect("encode tab"); + let decoded: TabState = serde_json::from_str(&raw).expect("decode tab"); + + match decoded.content { + TabContentState::Terminal { font_size, .. } => { + assert_eq!(font_size, Some(17.5)); + } + other => panic!("expected terminal tab, got {other:?}"), + } + } + + #[test] + fn terminal_tab_state_defaults_missing_font_size_override() { + let decoded: TabState = serde_json::from_str( + r#"{ + "id": "tab-a", + "custom_name": null, + "pinned": false, + "tab_kind": "terminal", + "cwd": "/tmp/project", + "agent": null + }"#, + ) + .expect("decode legacy tab"); + + match decoded.content { + TabContentState::Terminal { font_size, .. } => { + assert_eq!(font_size, None); + } + other => panic!("expected terminal tab, got {other:?}"), + } + } + #[test] fn restorable_agent_resume_command_runs_from_cwd() { let agent = RestorableAgentState { diff --git a/rust/limux-host-linux/src/pane.rs b/rust/limux-host-linux/src/pane.rs index 00f0753f..945786a7 100644 --- a/rust/limux-host-linux/src/pane.rs +++ b/rust/limux-host-linux/src/pane.rs @@ -230,12 +230,15 @@ pub struct PaneCallbacks { #[derive(Clone)] struct TerminalTabState { cwd: Rc>>, + font_size: Rc>>, handle: terminal::TerminalHandle, } #[derive(Clone)] pub struct TerminalShortcutTarget { handle: terminal::TerminalHandle, + font_size: Rc>>, + callbacks: Rc, } impl TerminalShortcutTarget { @@ -243,6 +246,40 @@ impl TerminalShortcutTarget { self.handle.perform_binding_action(action) } + pub fn font_size_override(&self) -> Option { + *self.font_size.borrow() + } + + pub fn set_font_size_override(&self, size: f32) -> bool { + if !size.is_finite() { + return false; + } + + let size = size.clamp(1.0, 255.0); + let action = format!("set_font_size:{size}"); + if !self.handle.perform_binding_action(&action) { + return false; + } + + *self.font_size.borrow_mut() = Some(size); + (self.callbacks.on_state_changed)(); + true + } + + pub fn reset_font_size_override(&self, default_size: Option) -> bool { + let action = match default_size.filter(|size| size.is_finite()) { + Some(size) => format!("set_font_size:{}", size.clamp(1.0, 255.0)), + None => "reset_font_size".to_string(), + }; + if !self.handle.perform_binding_action(&action) { + return false; + } + + *self.font_size.borrow_mut() = None; + (self.callbacks.on_state_changed)(); + true + } + pub fn show_find(&self) -> bool { self.handle.show_find() } @@ -943,6 +980,7 @@ struct TerminalTabOptions<'a> { pinned: bool, cwd: Option<&'a str>, agent: Option, + font_size: Option, } struct BrowserTabOptions<'a> { @@ -976,7 +1014,11 @@ fn restore_tabs_from_state( for saved_tab in &saved_state.tabs { match &saved_tab.content { - TabContentState::Terminal { cwd, agent } => add_terminal_tab_inner( + TabContentState::Terminal { + cwd, + agent, + font_size, + } => add_terminal_tab_inner( internals, cwd.as_deref().or(working_directory), Some(TerminalTabOptions { @@ -985,6 +1027,7 @@ fn restore_tabs_from_state( pinned: saved_tab.pinned, cwd: cwd.as_deref().or(working_directory), agent: agent.clone(), + font_size: *font_size, }), ), TabContentState::Browser { uri } => add_browser_tab_inner( @@ -1207,6 +1250,13 @@ fn add_terminal_tab_inner( .and_then(|value| value.cwd.map(|cwd| cwd.to_string())) .or_else(|| working_directory.map(|cwd| cwd.to_string())), )); + let term_font_size = Rc::new(RefCell::new( + options + .as_ref() + .and_then(|value| value.font_size) + .filter(|size| size.is_finite()) + .map(|size| size.clamp(1.0, 255.0)), + )); let term_callbacks = make_terminal_callbacks(internals, &tab_id, &title_label, &term_cwd); let hover_focus = { let callbacks = internals.callbacks.clone(); @@ -1249,11 +1299,19 @@ fn add_terminal_tab_inner( ); } + let saved_font_size = *term_font_size.borrow(); + let saved_font_size = saved_font_size.or_else(|| { + let config = (internals.callbacks.current_config)(); + let configured = config.borrow().font_size; + configured + .filter(|size| size.is_finite()) + .map(|size| size.clamp(1.0, 255.0)) + }); let term = terminal::create_terminal( working_directory, terminal::TerminalOptions { hover_focus, - saved_font_size: (internals.callbacks.current_config)().borrow().font_size, + saved_font_size, startup_command, extra_env, }, @@ -1276,6 +1334,7 @@ fn add_terminal_tab_inner( kind: TabKind::Terminal { state: TerminalTabState { cwd: term_cwd.clone(), + font_size: term_font_size.clone(), handle: term.handle.clone(), }, }, @@ -1576,6 +1635,7 @@ pub fn snapshot_pane_state(pane_widget: >k::Widget) -> Option { TabKind::Terminal { state } => TabContentState::Terminal { cwd: state.cwd.borrow().clone(), agent: None, + font_size: *state.font_size.borrow(), }, TabKind::Browser { state } => TabContentState::Browser { uri: state.uri.borrow().clone(), @@ -1839,6 +1899,8 @@ pub fn focused_shortcut_target(pane_widget: >k::Widget) -> FocusedShortcutTarg .. }) => FocusedShortcutTarget::Terminal(TerminalShortcutTarget { handle: state.handle.clone(), + font_size: state.font_size.clone(), + callbacks: internals.callbacks.clone(), }), Some(TabEntry { kind: TabKind::Browser { state }, diff --git a/rust/limux-host-linux/src/terminal.rs b/rust/limux-host-linux/src/terminal.rs index 4bd00101..0e2f6d25 100644 --- a/rust/limux-host-linux/src/terminal.rs +++ b/rust/limux-host-linux/src/terminal.rs @@ -1828,22 +1828,6 @@ pub fn create_terminal( // Context menu // --------------------------------------------------------------------------- -/// Send a binding action to every live surface. -pub(crate) fn broadcast_binding_action(action: &str) { - SURFACE_MAP.with(|map| { - for &key in map.borrow().keys() { - let surface = key as ghostty_surface_t; - unsafe { - ghostty_surface_binding_action( - surface, - action.as_ptr() as *const c_char, - action.len(), - ); - } - } - }); -} - fn surface_action(surface: Option, action: &str) { if let Some(surface) = surface { unsafe { diff --git a/rust/limux-host-linux/src/window.rs b/rust/limux-host-linux/src/window.rs index d089b3b5..4d41e9c3 100644 --- a/rust/limux-host-linux/src/window.rs +++ b/rust/limux-host-linux/src/window.rs @@ -5260,66 +5260,53 @@ fn dispatch_terminal_command(state: &State, command: ShortcutCommand) -> bool { ShortcutCommand::TerminalClearScrollback => target.perform_binding_action("clear_screen"), ShortcutCommand::TerminalCopy => target.perform_binding_action("copy_to_clipboard"), ShortcutCommand::TerminalPaste => target.perform_binding_action("paste_from_clipboard"), - ShortcutCommand::TerminalIncreaseFontSize => persist_font_size_delta(state, 1.0), - ShortcutCommand::TerminalDecreaseFontSize => persist_font_size_delta(state, -1.0), - ShortcutCommand::TerminalResetFontSize => persist_font_size_reset(state), + ShortcutCommand::TerminalIncreaseFontSize => adjust_terminal_font_size(state, &target, 1.0), + ShortcutCommand::TerminalDecreaseFontSize => { + adjust_terminal_font_size(state, &target, -1.0) + } + ShortcutCommand::TerminalResetFontSize => reset_terminal_font_size(state, &target), _ => false, } } -fn persist_font_size_delta(state: &State, delta: f32) -> bool { - let current = { - let s = state.borrow(); - let current = s.config.borrow().font_size; - current - }; - let new_size = font_size_after_delta(current, crate::terminal::default_font_size(), delta); - - if let Err(err) = persist_font_size(state, Some(new_size)) { - show_font_size_save_error(state, err); - return false; - } - - broadcast_font_size(new_size); - true -} - -fn persist_font_size_reset(state: &State) -> bool { - if let Err(err) = persist_font_size(state, None) { - show_font_size_save_error(state, err); - return false; - } - - crate::terminal::broadcast_binding_action("reset_font_size"); - true +fn terminal_default_font_size(state: &State) -> f32 { + terminal_default_font_size_override(state).unwrap_or_else(crate::terminal::default_font_size) } -fn persist_font_size(state: &State, font_size: Option) -> Result<(), String> { - let mut updated = { +fn terminal_default_font_size_override(state: &State) -> Option { + let config = { let s = state.borrow(); - let updated = s.config.borrow().clone(); - updated + s.config.clone() }; - updated.font_size = font_size; - app_config::save(&updated)?; - - state.borrow().config.borrow_mut().font_size = font_size; - Ok(()) + let configured = config.borrow().font_size; + configured + .filter(|size| size.is_finite()) + .map(|size| size.clamp(1.0, 255.0)) } -fn font_size_after_delta(current: Option, default: f32, delta: f32) -> f32 { - (current.unwrap_or(default) + delta).clamp(1.0, 255.0) +fn adjust_terminal_font_size( + state: &State, + target: &pane::TerminalShortcutTarget, + delta: f32, +) -> bool { + let new_size = font_size_after_delta( + target.font_size_override(), + terminal_default_font_size(state), + delta, + ); + target.set_font_size_override(new_size) } -fn show_font_size_save_error(state: &State, err: String) { - let detail = format!("Failed to save Limux settings: {err}"); - eprintln!("limux: {detail}"); - show_runtime_error(state, "Failed to save settings", &detail); +fn reset_terminal_font_size(state: &State, target: &pane::TerminalShortcutTarget) -> bool { + target.reset_font_size_override(terminal_default_font_size_override(state)) } -fn broadcast_font_size(size: f32) { - let action = format!("set_font_size:{size}"); - crate::terminal::broadcast_binding_action(&action); +fn font_size_after_delta(current: Option, default: f32, delta: f32) -> f32 { + let base = current + .filter(|size| size.is_finite()) + .unwrap_or(default) + .clamp(1.0, 255.0); + (base + delta).clamp(1.0, 255.0) } fn dispatch_browser_command(state: &State, command: ShortcutCommand) -> bool {