From 10eb0bc744165ae91dfce9fd731fe170410b34ec Mon Sep 17 00:00:00 2001 From: Can Celik Date: Mon, 3 Aug 2026 02:05:27 +0300 Subject: [PATCH] fix: propagate host color scheme to pane apps (#2214) refs #714 --- docs/next/CHANGELOG.md | 1 + src/app/actions.rs | 1 + src/app/agent_resume.rs | 1 + src/app/api.rs | 3 + src/app/api/layouts.rs | 6 ++ src/app/api/panes.rs | 3 + src/app/api/plugins/panes.rs | 2 + src/app/api/tabs.rs | 2 + src/app/creation.rs | 2 + src/app/input/mod.rs | 1 + src/app/input/mouse.rs | 4 + src/app/input/navigate.rs | 3 + src/app/input/terminal.rs | 1 + src/app/popup.rs | 2 + src/app/theme_sync.rs | 8 ++ src/ghostty/mod.rs | 45 ++++++++++++ src/pane.rs | 48 +++++++++++- src/pane/terminal.rs | 84 +++++++++++++++++++++ src/persist/restore.rs | 3 + src/pty/actor.rs | 73 +++++++++++------- src/pty/actor/unix.rs | 138 ++++++++++++++++++++++++++++++++++- src/server/notifications.rs | 1 + src/terminal/runtime.rs | 19 +++++ src/terminal_theme.rs | 9 +++ src/ui/dialogs.rs | 1 + src/ui/mobile.rs | 1 + src/ui/sidebar.rs | 1 + src/workspace.rs | 38 ++++++++++ src/workspace/tab.rs | 27 +++++++ 29 files changed, 498 insertions(+), 30 deletions(-) diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 4779d9f9..57205475 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -12,6 +12,7 @@ - Relicensed Herdr from AGPL-3.0-or-later to Apache-2.0. ### Fixed +- Pane applications now receive semantic light/dark query responses and live Mode 2031 updates when the host appearance changes. (#714) - Worktree CLI help and docs no longer advertise the redundant `--json` flag; worktree commands remain JSON-only and continue accepting the flag for compatibility. (#2171) - OpenCode 2 preview panes now appear as OpenCode agents and use the existing OpenCode status detection. (#2169) - Pane text copied through VS Code Remote Tunnels now reaches the viewing machine's clipboard instead of overwriting the remote host clipboard. (#2015) diff --git a/src/app/actions.rs b/src/app/actions.rs index 9ef4af68..51f71877 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -3704,6 +3704,7 @@ mod tests { live_cwd.clone(), 0, crate::terminal_theme::TerminalTheme::default(), + None, crate::pane::PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::NonLogin), &crate::pane::PaneLaunchEnv::default(), events, diff --git a/src/app/agent_resume.rs b/src/app/agent_resume.rs index 45a2e3fa..1b459628 100644 --- a/src/app/agent_resume.rs +++ b/src/app/agent_resume.rs @@ -239,6 +239,7 @@ impl App { cwd, self.state.pane_scrollback_limit_bytes, host_terminal_theme, + self.state.host_terminal_appearance, crate::pane::PaneShellConfig::new(&self.state.default_shell, self.state.shell_mode), &launch_env, self.event_tx.clone(), diff --git a/src/app/api.rs b/src/app/api.rs index 76087e2d..5f81a5cb 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -545,6 +545,7 @@ impl App { cwd, self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, + self.state.host_terminal_appearance, crate::pane::PaneShellConfig::new(&self.state.default_shell, self.state.shell_mode), &launch_env, self.event_tx.clone(), @@ -1712,6 +1713,7 @@ mod tests { live_cwd.clone(), 0, crate::terminal_theme::TerminalTheme::default(), + None, crate::pane::PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::NonLogin), &crate::pane::PaneLaunchEnv::default(), events, @@ -1804,6 +1806,7 @@ mod tests { live_cwd.clone(), 0, crate::terminal_theme::TerminalTheme::default(), + None, crate::pane::PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::NonLogin), &crate::pane::PaneLaunchEnv::default(), events, diff --git a/src/app/api/layouts.rs b/src/app/api/layouts.rs index 34795474..9d2756b8 100644 --- a/src/app/api/layouts.rs +++ b/src/app/api/layouts.rs @@ -93,6 +93,7 @@ impl App { let default_shell = self.state.default_shell.clone(); let scrollback_limit_bytes = self.state.pane_scrollback_limit_bytes; let host_terminal_theme = self.state.host_terminal_theme; + let host_terminal_appearance = self.state.host_terminal_appearance; let extra_env = match super::env::normalize_launch_env(root_leaf.env.clone()) { Ok(env) => env, Err((code, message)) => return encode_error(id, &code, message), @@ -115,6 +116,7 @@ impl App { extra_env, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, ) } else { ws.create_tab( @@ -123,6 +125,7 @@ impl App { first_cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, crate::pane::PaneShellConfig::new(&default_shell, self.state.shell_mode), extra_env, ) @@ -397,6 +400,7 @@ impl App { let default_shell = self.state.default_shell.clone(); let scrollback_limit_bytes = self.state.pane_scrollback_limit_bytes; let host_terminal_theme = self.state.host_terminal_theme; + let host_terminal_appearance = self.state.host_terminal_appearance; let cwd = pane .cwd .as_ref() @@ -425,6 +429,7 @@ impl App { extra_env, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, false, ) } else { @@ -437,6 +442,7 @@ impl App { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, crate::pane::PaneShellConfig::new(&default_shell, self.state.shell_mode), extra_env, false, diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs index 86e019ac..72798a1b 100644 --- a/src/app/api/panes.rs +++ b/src/app/api/panes.rs @@ -57,6 +57,7 @@ impl App { let default_shell = self.state.default_shell.clone(); let scrollback_limit_bytes = self.state.pane_scrollback_limit_bytes; let host_terminal_theme = self.state.host_terminal_theme; + let host_terminal_appearance = self.state.host_terminal_appearance; let previous_focus = self.state.current_pane_focus_target(); let Some(ws) = self.state.workspaces.get_mut(ws_idx) else { return encode_error(id, "pane_not_found", "pane not found"); @@ -76,6 +77,7 @@ impl App { split_cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, extra_env, params.focus, @@ -88,6 +90,7 @@ impl App { split_cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, extra_env, params.focus, diff --git a/src/app/api/plugins/panes.rs b/src/app/api/plugins/panes.rs index 74a5df23..8b0749e3 100644 --- a/src/app/api/plugins/panes.rs +++ b/src/app/api/plugins/panes.rs @@ -126,6 +126,7 @@ impl App { extra_env, self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, + self.state.host_terminal_appearance, params.focus || placement == PluginPanePlacement::Zoomed, ); let (tab_idx, new_pane) = match result { @@ -202,6 +203,7 @@ impl App { extra_env, self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, + self.state.host_terminal_appearance, ) { Ok(result) => result, Err(err) => return encode_error(id, "plugin_pane_open_failed", err.to_string()), diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs index 2a2f3cdb..42d2b029 100644 --- a/src/app/api/tabs.rs +++ b/src/app/api/tabs.rs @@ -69,6 +69,7 @@ impl App { let default_shell = self.state.default_shell.clone(); let scrollback_limit_bytes = self.state.pane_scrollback_limit_bytes; let host_terminal_theme = self.state.host_terminal_theme; + let host_terminal_appearance = self.state.host_terminal_appearance; let extra_env = match super::env::normalize_launch_env(env) { Ok(env) => env, Err((code, message)) => return encode_error(id, &code, message), @@ -85,6 +86,7 @@ impl App { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, crate::pane::PaneShellConfig::new(&default_shell, self.state.shell_mode), extra_env, ) diff --git a/src/app/creation.rs b/src/app/creation.rs index fc01710e..d531e733 100644 --- a/src/app/creation.rs +++ b/src/app/creation.rs @@ -196,6 +196,7 @@ impl App { initial_cwd, self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, + self.state.host_terminal_appearance, crate::pane::PaneShellConfig::new(&self.state.default_shell, self.state.shell_mode), Vec::new(), )?; @@ -249,6 +250,7 @@ impl App { cols, self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, + self.state.host_terminal_appearance, crate::pane::PaneShellConfig::new(&self.state.default_shell, self.state.shell_mode), self.event_tx.clone(), self.render_notify.clone(), diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index f22c7994..7eca40b6 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -772,6 +772,7 @@ impl AppState { cwd, self.pane_scrollback_limit_bytes, self.host_terminal_theme, + self.host_terminal_appearance, crate::pane::PaneShellConfig::new(&self.default_shell, self.shell_mode), Vec::new(), ) { diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs index 2c32c95a..d43dcfc4 100644 --- a/src/app/input/mouse.rs +++ b/src/app/input/mouse.rs @@ -2945,6 +2945,7 @@ mod tests { 80, app.state.pane_scrollback_limit_bytes, app.state.host_terminal_theme, + app.state.host_terminal_appearance, crate::pane::PaneShellConfig::new(&app.state.default_shell, app.state.shell_mode), app.event_tx.clone(), app.render_notify.clone(), @@ -3373,6 +3374,7 @@ mod tests { mouse_protocol_encoding: crate::input::MouseProtocolEncoding::Sgr, mouse_alternate_scroll: true, modify_other_keys: false, + color_scheme_reporting: false, }; assert_eq!(wheel_routing(input_state), WheelRouting::MouseReport); @@ -4049,6 +4051,7 @@ mod tests { mouse_protocol_encoding: crate::input::MouseProtocolEncoding::Default, mouse_alternate_scroll: true, modify_other_keys: false, + color_scheme_reporting: false, }; assert_eq!(wheel_routing(input_state), WheelRouting::AlternateScroll); @@ -4065,6 +4068,7 @@ mod tests { mouse_protocol_encoding: crate::input::MouseProtocolEncoding::Default, mouse_alternate_scroll: true, modify_other_keys: false, + color_scheme_reporting: false, }; assert_eq!(wheel_routing(input_state), WheelRouting::HostScroll); diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index 6d0f7f22..68941e66 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -1005,6 +1005,7 @@ impl App { env, self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, + self.state.host_terminal_appearance, )?; let new_pane_id = new_pane.pane_id; self.terminal_runtimes @@ -1091,6 +1092,7 @@ impl App { extra_env, self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, + self.state.host_terminal_appearance, true, ); let (tab_idx, new_pane) = match result { @@ -3282,6 +3284,7 @@ navigate_pane_down = "ctrl+j" 80, app.state.pane_scrollback_limit_bytes, app.state.host_terminal_theme, + app.state.host_terminal_appearance, crate::pane::PaneShellConfig::new(&app.state.default_shell, app.state.shell_mode), app.event_tx.clone(), app.render_notify.clone(), diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs index f0e10d9c..36d60e41 100644 --- a/src/app/input/terminal.rs +++ b/src/app/input/terminal.rs @@ -425,6 +425,7 @@ mod tests { 80, app.state.pane_scrollback_limit_bytes, app.state.host_terminal_theme, + app.state.host_terminal_appearance, crate::pane::PaneShellConfig::new(&app.state.default_shell, app.state.shell_mode), app.event_tx.clone(), app.render_notify.clone(), diff --git a/src/app/popup.rs b/src/app/popup.rs index f3d2e459..c5877987 100644 --- a/src/app/popup.rs +++ b/src/app/popup.rs @@ -71,6 +71,7 @@ impl App { crate::pane::AgentDetection::Disabled, app.state.pane_scrollback_limit_bytes, app.state.host_terminal_theme, + app.state.host_terminal_appearance, app.event_tx.clone(), app.render_notify.clone(), app.render_dirty.clone(), @@ -102,6 +103,7 @@ impl App { crate::pane::AgentDetection::Disabled, app.state.pane_scrollback_limit_bytes, app.state.host_terminal_theme, + app.state.host_terminal_appearance, app.event_tx.clone(), app.render_notify.clone(), app.render_dirty.clone(), diff --git a/src/app/theme_sync.rs b/src/app/theme_sync.rs index e4284f69..32716ced 100644 --- a/src/app/theme_sync.rs +++ b/src/app/theme_sync.rs @@ -50,6 +50,7 @@ impl App { } self.state.host_terminal_appearance = Some(appearance); self.state.host_terminal_appearance_explicit = explicit; + self.apply_host_terminal_appearance_to_panes(); self.refresh_effective_app_theme() } @@ -65,6 +66,7 @@ impl App { } self.state.host_terminal_appearance = appearance; self.state.host_terminal_appearance_explicit = explicit; + self.apply_host_terminal_appearance_to_panes(); self.refresh_effective_app_theme() } @@ -95,6 +97,12 @@ impl App { true } + fn apply_host_terminal_appearance_to_panes(&self) { + for runtime in self.terminal_runtimes.values() { + runtime.apply_host_terminal_appearance(self.state.host_terminal_appearance); + } + } + fn apply_host_terminal_theme_to_panes(&self) { for runtime in self.terminal_runtimes.values() { runtime.apply_host_terminal_theme(self.state.host_terminal_theme); diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index c4541948..685c0735 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -119,6 +119,21 @@ impl FocusEvent { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColorScheme { + Light, + Dark, +} + +impl ColorScheme { + fn as_raw(self) -> ffi::GhosttyColorScheme { + match self { + Self::Light => ffi::GhosttyColorScheme_GHOSTTY_COLOR_SCHEME_LIGHT, + Self::Dark => ffi::GhosttyColorScheme_GHOSTTY_COLOR_SCHEME_DARK, + } + } +} + pub const MOD_SHIFT: u16 = ffi::GHOSTTY_MODS_SHIFT as u16; pub const MOD_CTRL: u16 = ffi::GHOSTTY_MODS_CTRL as u16; pub const MOD_ALT: u16 = ffi::GHOSTTY_MODS_ALT as u16; @@ -163,6 +178,7 @@ pub const MODE_MOUSE_SGR_PIXELS: u16 = 1016; pub const MODE_BRACKETED_PASTE: u16 = 2004; pub const MODE_SYNCHRONIZED_OUTPUT: u16 = 2026; pub const MODE_GRAPHEME_CLUSTER: u16 = 2027; +pub const MODE_COLOR_SCHEME_REPORT: u16 = 2031; // These are documented in vendor/libghostty-vt/include/ghostty/vt/terminal.h, // but the generated bindings do not currently expose named constants for them. const TERMINAL_DATA_COLOR_FOREGROUND: ffi::GhosttyTerminalData = 18; @@ -457,6 +473,25 @@ struct TerminalCallbackState { pwd_changes: Vec>, clipboard_writes: Vec>, size_report: ffi::GhosttySizeReportSize, + color_scheme: Option, +} + +unsafe extern "C" fn color_scheme_trampoline( + _terminal: ffi::GhosttyTerminal, + userdata: *mut c_void, + out_scheme: *mut ffi::GhosttyColorScheme, +) -> bool { + if userdata.is_null() || out_scheme.is_null() { + return false; + } + let state = unsafe { &*userdata.cast::() }; + let Some(color_scheme) = state.color_scheme else { + return false; + }; + unsafe { + out_scheme.write(color_scheme.as_raw()); + } + true } unsafe extern "C" fn size_trampoline( @@ -791,6 +826,12 @@ impl Terminal { (clipboard_write_trampoline as *const ()).cast(), ) .into_result()?; + ffi::ghostty_terminal_set( + terminal.raw, + ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_COLOR_SCHEME, + (color_scheme_trampoline as *const ()).cast(), + ) + .into_result()?; ffi::ghostty_terminal_set( terminal.raw, ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_GLYPH_PROTOCOL, @@ -913,6 +954,10 @@ impl Terminal { Ok(()) } + pub fn set_color_scheme(&mut self, color_scheme: Option) -> Option { + mem::replace(&mut self.callback_state.color_scheme, color_scheme) + } + pub fn take_pwd_changes(&mut self) -> Vec> { mem::take(&mut self.callback_state.pwd_changes) } diff --git a/src/pane.rs b/src/pane.rs index 4ffd0097..7669fc13 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -1127,6 +1127,18 @@ impl PaneRuntimeIo { } } + fn write_terminal_response(&self, response: impl FnOnce() -> Option) { + match self { + PaneRuntimeIo::Actor(actor) => actor.write_terminal_response(response), + #[cfg(test)] + PaneRuntimeIo::TestChannel { sender, .. } => { + if let Some(bytes) = response() { + let _ = sender.try_send(bytes); + } + } + } + } + fn send_bytes_after(&self, bytes: Bytes, delay: std::time::Duration) { match self { PaneRuntimeIo::Actor(actor) => { @@ -1610,6 +1622,16 @@ impl PaneRuntime { self.terminal.apply_host_terminal_theme(theme); } + pub fn apply_host_terminal_appearance( + &self, + appearance: Option, + ) { + self.io + .write_terminal_response(|| self.terminal.apply_host_terminal_appearance(appearance)); + } + + // Runtime construction threads PTY geometry, host context, launch policy, and render hooks. + #[allow(clippy::too_many_arguments)] pub fn spawn( pane_id: PaneId, rows: u16, @@ -1617,6 +1639,7 @@ impl PaneRuntime { cwd: std::path::PathBuf, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: PaneShellConfig<'_>, launch_env: &PaneLaunchEnv, events: mpsc::Sender, @@ -1630,6 +1653,7 @@ impl PaneRuntime { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, launch_env, None, @@ -1648,6 +1672,7 @@ impl PaneRuntime { cwd: std::path::PathBuf, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: PaneShellConfig<'_>, launch_env: &PaneLaunchEnv, initial_history_ansi: Option<&str>, @@ -1667,6 +1692,7 @@ impl PaneRuntime { cols, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, events, render_notify, render_dirty, @@ -1693,6 +1719,7 @@ impl PaneRuntime { agent_detection: AgentDetection, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, events: mpsc::Sender, render_notify: Arc, render_dirty: Arc, @@ -1707,6 +1734,7 @@ impl PaneRuntime { cols, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, events, render_notify, render_dirty, @@ -1729,6 +1757,7 @@ impl PaneRuntime { agent_detection: AgentDetection, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, events: mpsc::Sender, render_notify: Arc, render_dirty: Arc, @@ -1752,6 +1781,7 @@ impl PaneRuntime { cols, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, events, render_notify, render_dirty, @@ -1767,6 +1797,7 @@ impl PaneRuntime { import: crate::handoff_runtime::ImportedHandoffRuntime, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, events: mpsc::Sender, render_notify: Arc, render_dirty: Arc, @@ -1803,6 +1834,7 @@ impl PaneRuntime { } let pane_terminal = GhosttyPaneTerminal::new(terminal, response_tx.clone())?; pane_terminal.apply_host_terminal_theme(host_terminal_theme); + let _ = pane_terminal.apply_host_terminal_appearance(host_terminal_appearance); pane_terminal.seed_terminal_title(terminal_title); if let Some(input_state) = input_state { pane_terminal.seed_handoff_input_state(input_state); @@ -1916,6 +1948,7 @@ impl PaneRuntime { cols: u16, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, events: mpsc::Sender, render_notify: Arc, render_dirty: Arc, @@ -1936,6 +1969,7 @@ impl PaneRuntime { } let pane_terminal = GhosttyPaneTerminal::new(terminal, response_tx.clone())?; pane_terminal.apply_host_terminal_theme(host_terminal_theme); + let _ = pane_terminal.apply_host_terminal_appearance(host_terminal_appearance); pane_terminal.set_windows_powershell_prompt_cwd_reporting( initial_state.windows_powershell_prompt_cwd_reporting, ); @@ -3393,7 +3427,7 @@ mod tests { let runtime = PaneRuntime::test_with_screen_bytes( 80, 24, - b"\x1b[>5u\x1b[>4;2m\x1b[?1h\x1b[?2004h\x1b[?1004h\x1b[?1002h\x1b[?1006h", + b"\x1b[>5u\x1b[>4;2m\x1b[?1h\x1b[?2004h\x1b[?1004h\x1b[?1002h\x1b[?1006h\x1b[?2031h", ); runtime.test_process_pty_bytes("\x1b]2;✳ 修复🙂标题\x1b\\".as_bytes()); @@ -3414,6 +3448,7 @@ mod tests { mouse_protocol_encoding: crate::input::MouseProtocolEncoding::Sgr, mouse_alternate_scroll: true, modify_other_keys: true, + color_scheme_reporting: true, }) ); } @@ -3508,6 +3543,17 @@ mod tests { ); } + #[tokio::test] + async fn subscribed_idle_child_receives_color_scheme_transition() { + let (runtime, mut rx) = PaneRuntime::test_with_channel(80, 24); + runtime.apply_host_terminal_appearance(Some(crate::terminal_theme::HostAppearance::Dark)); + runtime.test_process_pty_bytes(b"\x1b[?2031h"); + + runtime.apply_host_terminal_appearance(Some(crate::terminal_theme::HostAppearance::Light)); + + assert_eq!(rx.recv().await, Some(Bytes::from_static(b"\x1b[?997;2n"))); + } + #[test] fn foreground_shell_reports_process_exit_before_clearing_agent() { assert_eq!( diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index e464554c..438c3037 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -120,6 +120,8 @@ pub struct InputState { pub mouse_alternate_scroll: bool, #[serde(default)] pub modify_other_keys: bool, + #[serde(default)] + pub color_scheme_reporting: bool, } impl InputState { @@ -458,6 +460,13 @@ impl PaneTerminal { self.ghostty.apply_host_terminal_theme(theme); } + pub fn apply_host_terminal_appearance( + &self, + appearance: Option, + ) -> Option { + self.ghostty.apply_host_terminal_appearance(appearance) + } + pub fn has_transient_default_color_override(&self) -> bool { self.ghostty.has_transient_default_color_override() } @@ -988,6 +997,32 @@ impl GhosttyPaneTerminal { } } + pub fn apply_host_terminal_appearance( + &self, + appearance: Option, + ) -> Option { + let mut core = self.core.lock().ok()?; + let color_scheme = appearance.map(|appearance| match appearance { + crate::terminal_theme::HostAppearance::Dark => crate::ghostty::ColorScheme::Dark, + crate::terminal_theme::HostAppearance::Light => crate::ghostty::ColorScheme::Light, + }); + let previous = core.terminal.set_color_scheme(color_scheme); + + let transitioned = matches!( + (previous, color_scheme), + (Some(previous), Some(current)) if previous != current + ); + if !transitioned + || !core + .terminal + .mode_get(crate::ghostty::MODE_COLOR_SCHEME_REPORT) + .unwrap_or(false) + { + return None; + } + appearance.map(|appearance| Bytes::from_static(appearance.color_scheme_report())) + } + pub fn has_transient_default_color_override(&self) -> bool { self.core .lock() @@ -1331,6 +1366,10 @@ impl GhosttyPaneTerminal { crate::ghostty::MODE_MOUSE_ALTERNATE_SCROLL, input_state.mouse_alternate_scroll, ); + let _ = core.terminal.mode_set( + crate::ghostty::MODE_COLOR_SCHEME_REPORT, + input_state.color_scheme_reporting, + ); for mode in [ MODE_MOUSE_X10, @@ -1585,6 +1624,10 @@ impl GhosttyPaneTerminal { .keyboard_state_ansi() .ok() .is_some_and(|ansi| !ansi.is_empty()), + color_scheme_reporting: core + .terminal + .mode_get(crate::ghostty::MODE_COLOR_SCHEME_REPORT) + .ok()?, }) } @@ -3907,6 +3950,7 @@ mod tests { mouse_protocol_encoding: crate::input::MouseProtocolEncoding::Sgr, mouse_alternate_scroll: true, modify_other_keys: true, + color_scheme_reporting: true, }); assert_eq!( @@ -3920,6 +3964,7 @@ mod tests { mouse_protocol_encoding: crate::input::MouseProtocolEncoding::Sgr, mouse_alternate_scroll: true, modify_other_keys: true, + color_scheme_reporting: true, }) ); @@ -5009,6 +5054,45 @@ mod tests { assert!(rx.try_recv().is_err()); } + #[test] + fn color_scheme_queries_and_live_updates_follow_terminal_mode() { + let (tx, mut rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + + assert!(pane + .apply_host_terminal_appearance(Some(crate::terminal_theme::HostAppearance::Dark)) + .is_none()); + let query = pane.process_pty_bytes(pane_id, 0, b"\x1b[?996n", &tx); + assert_eq!( + query.terminal_responses, + vec![Bytes::from_static(b"\x1b[?997;1n")] + ); + + pane.process_pty_bytes(pane_id, 0, b"\x1b[?2031h", &tx); + assert!(pane + .apply_host_terminal_appearance(Some(crate::terminal_theme::HostAppearance::Dark)) + .is_none()); + assert_eq!( + pane.apply_host_terminal_appearance(Some(crate::terminal_theme::HostAppearance::Light)), + Some(Bytes::from_static(b"\x1b[?997;2n")) + ); + + assert!(pane.apply_host_terminal_appearance(None).is_none()); + let unknown_query = pane.process_pty_bytes(pane_id, 0, b"\x1b[?996n", &tx); + assert!(unknown_query.terminal_responses.is_empty()); + assert!(pane + .apply_host_terminal_appearance(Some(crate::terminal_theme::HostAppearance::Dark)) + .is_none()); + + pane.process_pty_bytes(pane_id, 0, b"\x1bc", &tx); + assert!(pane + .apply_host_terminal_appearance(Some(crate::terminal_theme::HostAppearance::Light)) + .is_none()); + assert!(rx.try_recv().is_err()); + } + #[test] fn process_pty_bytes_returns_xtgettcap_truecolor_query_responses_without_queuing_input() { let (tx, mut rx) = mpsc::channel(8); diff --git a/src/persist/restore.rs b/src/persist/restore.rs index 99b9a64c..3c5775c9 100644 --- a/src/persist/restore.rs +++ b/src/persist/restore.rs @@ -582,6 +582,7 @@ fn restore_tab( }, runtime_context.scrollback_limit_bytes, crate::terminal_theme::TerminalTheme::default(), + None, runtime_context.events.clone(), runtime_context.render_notify.clone(), runtime_context.render_dirty.clone(), @@ -594,6 +595,7 @@ fn restore_tab( cwd.clone(), runtime_context.scrollback_limit_bytes, crate::terminal_theme::TerminalTheme::default(), + None, runtime_context.shell_config, &launch_env, startup.initial_history_ansi, @@ -612,6 +614,7 @@ fn restore_tab( cwd.clone(), runtime_context.scrollback_limit_bytes, crate::terminal_theme::TerminalTheme::default(), + None, runtime_context.shell_config, &launch_env, startup.initial_history_ansi, diff --git a/src/pty/actor.rs b/src/pty/actor.rs index 8c3a6b52..6fde5051 100644 --- a/src/pty/actor.rs +++ b/src/pty/actor.rs @@ -52,6 +52,8 @@ mod windows { pub(crate) struct PtyIoActorHandle { data_tx: mpsc::Sender, control_tx: std_mpsc::Sender, + write_tx: std_mpsc::Sender, + response_order: Arc>, accepting: Arc>, } @@ -84,6 +86,16 @@ mod windows { self.data_tx.try_send(bytes) } + pub(crate) fn write_terminal_response(&self, response: impl FnOnce() -> Option) { + let _order = self + .response_order + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if let Some(bytes) = response().filter(|bytes| !bytes.is_empty()) { + let _ = self.write_tx.send(bytes); + } + } + pub(crate) fn resize( &self, rows: u16, @@ -128,39 +140,55 @@ mod windows { let mut reader = master .try_clone_reader() .map_err(|err| std::io::Error::other(err.to_string()))?; - let writer = master + let mut writer = master .take_writer() .map_err(|err| std::io::Error::other(err.to_string()))?; - let writer = Arc::new(Mutex::new(writer)); let (data_tx, mut data_rx) = mpsc::channel::(1024); let (control_tx, control_rx) = std_mpsc::channel::(); + let (write_tx, write_rx) = std_mpsc::channel::(); + let response_order = Arc::new(Mutex::new(())); let accepting = Arc::new(Mutex::new(!initially_quiesced)); + std::thread::spawn(move || { + for bytes in write_rx { + if writer.write_all(&bytes).is_err() || writer.flush().is_err() { + break; + } + } + debug!(pane_id, "windows pty writer thread exiting"); + }); + { - let writer = Arc::clone(&writer); + let write_tx = write_tx.clone(); std::thread::spawn(move || { while let Some(bytes) = data_rx.blocking_recv() { - if write_all_locked(&writer, &bytes).is_err() { + if write_tx.send(bytes).is_err() { break; } } - debug!(pane_id, "windows pty writer thread exiting"); + debug!(pane_id, "windows pty input thread exiting"); }); } { - let writer = Arc::clone(&writer); + let write_tx = write_tx.clone(); + let response_order = Arc::clone(&response_order); std::thread::spawn(move || { let mut buf = [0u8; 8192]; loop { match reader.read(&mut buf) { Ok(0) => break, Ok(n) => { + let _order = response_order + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); let result = on_read(&buf[..n]); - for response in result.terminal_responses { - if write_all_locked(&writer, &response).is_err() { - break; - } + if result + .terminal_responses + .into_iter() + .any(|response| write_tx.send(response).is_err()) + { + break; } } Err(err) => { @@ -177,7 +205,7 @@ mod windows { } { - let writer = Arc::clone(&writer); + let write_tx = write_tx.clone(); std::thread::spawn(move || { for command in control_rx { match command { @@ -191,10 +219,12 @@ mod windows { }) { warn!(pane_id, err = %err, "windows pty resize failed"); } - for response in request.terminal_responses { - if write_all_locked(&writer, &response).is_err() { - break; - } + if request + .terminal_responses + .into_iter() + .any(|response| write_tx.send(response).is_err()) + { + break; } } PtyIoControlCommand::Shutdown => break, @@ -207,22 +237,13 @@ mod windows { Ok(PtyIoActorHandle { data_tx, control_tx, + write_tx, + response_order, accepting, }) } } - fn write_all_locked( - writer: &Arc>>, - bytes: &[u8], - ) -> std::io::Result<()> { - let mut writer = writer - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - writer.write_all(bytes)?; - writer.flush() - } - #[allow(dead_code)] fn _assert_duration_send(_: Duration) {} } diff --git a/src/pty/actor/unix.rs b/src/pty/actor/unix.rs index 426c3327..599f02cb 100644 --- a/src/pty/actor/unix.rs +++ b/src/pty/actor/unix.rs @@ -60,6 +60,7 @@ struct PtyResizeRequest { struct SharedPtyControls { resize: Option, nudge: Option, + terminal_responses: Vec, } pub(crate) struct PtyIoActorConfig { @@ -90,6 +91,7 @@ pub(crate) struct PtyIoActorHandle { wake: fd::WakeWriter, user_writes: Arc>, controls: Arc>, + response_order: Arc>, } #[derive(Debug)] @@ -157,6 +159,24 @@ impl PtyIoActorHandle { } } + pub(crate) fn write_terminal_response(&self, response: impl FnOnce() -> Option) { + let _order = self + .response_order + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let Some(bytes) = response() else { + return; + }; + if !bytes.is_empty() { + self.controls + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .terminal_responses + .push(bytes); + self.wake_actor(); + } + } + pub(crate) fn resize( &self, rows: u16, @@ -349,12 +369,14 @@ impl PtyIoActor { accepting: !config.initially_quiesced, })); let controls = Arc::new(Mutex::new(SharedPtyControls::default())); + let response_order = Arc::new(Mutex::new(())); let handle = PtyIoActorHandle { data_tx, control_tx, wake: wake_pipe.writer, user_writes, controls: Arc::clone(&controls), + response_order: Arc::clone(&response_order), }; let mut runner = PtyIoActorRunner { @@ -371,6 +393,7 @@ impl PtyIoActor { current_write_offset: 0, wake_read_fd: wake_pipe.read_fd, controls, + response_order, on_read: config.on_read, on_reader_exit: config.on_reader_exit, poll_observer, @@ -402,6 +425,7 @@ struct PtyIoActorRunner { current_write_offset: usize, wake_read_fd: OwnedFd, controls: Arc>, + response_order: Arc>, on_read: ReadCallback, on_reader_exit: Option, poll_observer: Option>, @@ -625,12 +649,16 @@ impl PtyIoActorRunner { } fn apply_pending_controls(&mut self) { - let (resize, nudge) = { + let (resize, nudge, terminal_responses) = { let mut controls = self .controls .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); - (controls.resize.take(), controls.nudge.take()) + ( + controls.resize.take(), + controls.nudge.take(), + std::mem::take(&mut controls.terminal_responses), + ) }; if self.state == ActorState::Released { return; @@ -642,6 +670,7 @@ impl PtyIoActorRunner { if let Some(nudge) = nudge { self.nudge(nudge); } + self.enqueue_terminal_responses(terminal_responses); } fn read_once(&mut self) -> bool { @@ -655,8 +684,25 @@ impl PtyIoActorRunner { false } Ok(n) => { + let response_order = Arc::clone(&self.response_order); + let _order = response_order + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); let result = (self.on_read)(&buf[..n]); - self.enqueue_terminal_responses(result.terminal_responses); + self.controls + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .terminal_responses + .extend(result.terminal_responses); + drop(_order); + let terminal_responses = std::mem::take( + &mut self + .controls + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .terminal_responses, + ); + self.enqueue_terminal_responses(terminal_responses); true } } @@ -769,6 +815,7 @@ mod tests { io::{Read, Write}, os::fd::{AsRawFd, FromRawFd, IntoRawFd}, os::unix::net::UnixStream, + sync::atomic::{AtomicBool, Ordering}, }; fn test_wake_pair() -> (fd::WakeWriter, OwnedFd) { @@ -834,6 +881,7 @@ mod tests { current_write_offset: 0, wake_read_fd: wake_pipe.read_fd, controls: Arc::new(Mutex::new(SharedPtyControls::default())), + response_order: Arc::new(Mutex::new(())), on_read: Box::new(|_| PtyReadResult::empty()), on_reader_exit: None, poll_observer: None, @@ -1099,11 +1147,13 @@ mod tests { wake, user_writes: Arc::new(Mutex::new(UserWriteGate { accepting: true })), controls: Arc::clone(&controls), + response_order: Arc::new(Mutex::new(())), }; handle.resize(20, 80, 8, 16, vec![Bytes::from_static(b"old")]); handle.resize(40, 120, 9, 18, vec![Bytes::from_static(b"new")]); handle.nudge_child_redraw_after_handoff(41, 121, 10, 20); + handle.write_terminal_response(|| Some(Bytes::from_static(b"response"))); let controls = controls.lock().expect("controls lock"); assert_eq!( @@ -1127,6 +1177,84 @@ mod tests { cell_height_px: 20, }) ); + assert_eq!( + controls.terminal_responses, + vec![Bytes::from_static(b"response")] + ); + } + + #[test] + fn appearance_transition_report_precedes_query_of_new_scheme() { + let (actor_socket, mut peer) = UnixStream::pair().expect("socket pair"); + actor_socket + .set_nonblocking(true) + .expect("actor socket nonblocking"); + let owned = unsafe { OwnedFd::from_raw_fd(actor_socket.into_raw_fd()) }; + let (data_tx, data_rx) = mpsc::channel(ACTOR_COMMAND_BUFFER); + let (control_tx, control_rx) = std_mpsc::channel(); + let wake_pipe = fd::create_wake_pipe().expect("wake pipe"); + let controls = Arc::new(Mutex::new(SharedPtyControls::default())); + let response_order = Arc::new(Mutex::new(())); + let light = Arc::new(AtomicBool::new(false)); + let query_light = Arc::clone(&light); + let runner = PtyIoActorRunner { + pane_id: 1, + file: std::fs::File::from(owned), + data_rx, + control_rx, + state: ActorState::Running, + pending_writes: VecDeque::new(), + current_write_offset: 0, + wake_read_fd: wake_pipe.read_fd, + controls: Arc::clone(&controls), + response_order: Arc::clone(&response_order), + on_read: Box::new(move |_| PtyReadResult { + terminal_responses: vec![if query_light.load(Ordering::Acquire) { + Bytes::from_static(b"query-light") + } else { + Bytes::from_static(b"query-dark") + }], + }), + on_reader_exit: None, + poll_observer: None, + }; + let handle = PtyIoActorHandle { + data_tx, + control_tx, + wake: wake_pipe.writer, + user_writes: Arc::new(Mutex::new(UserWriteGate { accepting: true })), + controls, + response_order, + }; + let (changed_tx, changed_rx) = std_mpsc::channel(); + let (continue_tx, continue_rx) = std_mpsc::channel(); + + let appearance = std::thread::spawn(move || { + handle.write_terminal_response(|| { + light.store(true, Ordering::Release); + changed_tx.send(()).expect("notify appearance change"); + continue_rx.recv().expect("continue appearance report"); + Some(Bytes::from_static(b"live-light")) + }); + }); + changed_rx.recv().expect("appearance changed"); + peer.write_all(b"query").expect("write query"); + let reader = std::thread::spawn(move || { + let mut runner = runner; + assert!(runner.read_once()); + runner + }); + continue_tx.send(()).expect("release appearance report"); + appearance.join().expect("appearance thread joins"); + let runner = reader.join().expect("reader thread joins"); + + assert_eq!( + runner.pending_writes, + VecDeque::from([ + Bytes::from_static(b"live-light"), + Bytes::from_static(b"query-light"), + ]) + ); } #[test] @@ -1159,6 +1287,7 @@ mod tests { wake, user_writes: Arc::new(Mutex::new(UserWriteGate { accepting: true })), controls: Arc::new(Mutex::new(SharedPtyControls::default())), + response_order: Arc::new(Mutex::new(())), }; let write = tokio::spawn(async move { @@ -1204,6 +1333,7 @@ mod tests { wake, user_writes: Arc::new(Mutex::new(UserWriteGate { accepting: true })), controls: Arc::new(Mutex::new(SharedPtyControls::default())), + response_order: Arc::new(Mutex::new(())), }; let write_handle = handle.clone(); let write = tokio::spawn(async move { @@ -1258,6 +1388,7 @@ mod tests { wake, user_writes: Arc::new(Mutex::new(UserWriteGate { accepting: true })), controls: Arc::new(Mutex::new(SharedPtyControls::default())), + response_order: Arc::new(Mutex::new(())), }; let handoff = std::thread::spawn(move || handle.begin_handoff(Duration::from_secs(1))); @@ -1302,6 +1433,7 @@ mod tests { current_write_offset: 0, wake_read_fd: fd::create_wake_pipe().expect("wake pipe").read_fd, controls: Arc::new(Mutex::new(SharedPtyControls::default())), + response_order: Arc::new(Mutex::new(())), on_read: Box::new(|_| PtyReadResult::empty()), on_reader_exit: None, poll_observer: None, diff --git a/src/server/notifications.rs b/src/server/notifications.rs index b78cb6df..4ce33a24 100644 --- a/src/server/notifications.rs +++ b/src/server/notifications.rs @@ -120,6 +120,7 @@ mod tests { live_cwd.clone(), 0, crate::terminal_theme::TerminalTheme::default(), + None, crate::pane::PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::NonLogin), &crate::pane::PaneLaunchEnv::default(), events, diff --git a/src/terminal/runtime.rs b/src/terminal/runtime.rs index 38b36942..8a3da876 100644 --- a/src/terminal/runtime.rs +++ b/src/terminal/runtime.rs @@ -64,6 +64,7 @@ impl TerminalRuntime { import: crate::handoff_runtime::ImportedHandoffRuntime, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, events: mpsc::Sender, render_notify: Arc, render_dirty: Arc, @@ -72,6 +73,7 @@ impl TerminalRuntime { import, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, events, render_notify, render_dirty, @@ -79,6 +81,8 @@ impl TerminalRuntime { .map(Self) } + // Wrapper mirrors pane runtime construction arguments. + #[allow(clippy::too_many_arguments)] pub fn spawn( pane_id: PaneId, rows: u16, @@ -86,6 +90,7 @@ impl TerminalRuntime { cwd: std::path::PathBuf, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, launch_env: &crate::pane::PaneLaunchEnv, events: mpsc::Sender, @@ -99,6 +104,7 @@ impl TerminalRuntime { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, launch_env, events, @@ -117,6 +123,7 @@ impl TerminalRuntime { cwd: std::path::PathBuf, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, launch_env: &crate::pane::PaneLaunchEnv, initial_history_ansi: Option<&str>, @@ -131,6 +138,7 @@ impl TerminalRuntime { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, launch_env, initial_history_ansi, @@ -153,6 +161,7 @@ impl TerminalRuntime { agent_detection: crate::pane::AgentDetection, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, events: mpsc::Sender, render_notify: Arc, render_dirty: Arc, @@ -167,6 +176,7 @@ impl TerminalRuntime { agent_detection, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, events, render_notify, render_dirty, @@ -186,6 +196,7 @@ impl TerminalRuntime { agent_detection: crate::pane::AgentDetection, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, events: mpsc::Sender, render_notify: Arc, render_dirty: Arc, @@ -200,6 +211,7 @@ impl TerminalRuntime { agent_detection, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, events, render_notify, render_dirty, @@ -211,6 +223,13 @@ impl TerminalRuntime { self.0.apply_host_terminal_theme(theme); } + pub fn apply_host_terminal_appearance( + &self, + appearance: Option, + ) { + self.0.apply_host_terminal_appearance(appearance); + } + pub fn begin_graceful_release(&self, agent: crate::detect::Agent) { self.0.begin_graceful_release(agent); } diff --git a/src/terminal_theme.rs b/src/terminal_theme.rs index 154b36fe..ffe78a95 100644 --- a/src/terminal_theme.rs +++ b/src/terminal_theme.rs @@ -11,6 +11,15 @@ pub enum HostAppearance { Light, } +impl HostAppearance { + pub const fn color_scheme_report(self) -> &'static [u8] { + match self { + Self::Dark => b"\x1b[?997;1n", + Self::Light => b"\x1b[?997;2n", + } + } +} + impl RgbColor { pub fn inferred_appearance(self) -> HostAppearance { let luminance = u32::from(self.r) * 299 + u32::from(self.g) * 587 + u32::from(self.b) * 114; diff --git a/src/ui/dialogs.rs b/src/ui/dialogs.rs index 84ce1d35..dae1c67a 100644 --- a/src/ui/dialogs.rs +++ b/src/ui/dialogs.rs @@ -827,6 +827,7 @@ mod tests { live_cwd, 0, crate::terminal_theme::TerminalTheme::default(), + None, crate::pane::PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::NonLogin), &crate::pane::PaneLaunchEnv::default(), events, diff --git a/src/ui/mobile.rs b/src/ui/mobile.rs index b7738992..2e6c97ad 100644 --- a/src/ui/mobile.rs +++ b/src/ui/mobile.rs @@ -1473,6 +1473,7 @@ mod tests { live_cwd.clone(), 0, crate::terminal_theme::TerminalTheme::default(), + None, crate::pane::PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::NonLogin), &crate::pane::PaneLaunchEnv::default(), events, diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 395bb2c3..d9f6197f 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -2247,6 +2247,7 @@ rows = [[{ token = "git_status", fg = "#123456" }]] live_cwd.clone(), 0, crate::terminal_theme::TerminalTheme::default(), + None, crate::pane::PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::NonLogin), &crate::pane::PaneLaunchEnv::default(), events, diff --git a/src/workspace.rs b/src/workspace.rs index 23e35322..0d0e35bd 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -276,6 +276,7 @@ impl Workspace { cols: u16, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, events: mpsc::Sender, render_notify: Arc, @@ -287,6 +288,7 @@ impl Workspace { cols, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, events, render_notify, @@ -302,6 +304,7 @@ impl Workspace { cols: u16, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, events: mpsc::Sender, render_notify: Arc, @@ -314,6 +317,7 @@ impl Workspace { cols, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, events, render_notify, @@ -332,6 +336,7 @@ impl Workspace { argv: &[String], scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, events: mpsc::Sender, render_notify: Arc, render_dirty: Arc, @@ -343,6 +348,7 @@ impl Workspace { argv, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, events, render_notify, render_dirty, @@ -358,6 +364,7 @@ impl Workspace { argv: &[String], scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, events: mpsc::Sender, render_notify: Arc, render_dirty: Arc, @@ -369,6 +376,7 @@ impl Workspace { cols, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, crate::pane::PaneShellConfig::new("", crate::config::ShellModeConfig::NonLogin), events, render_notify, @@ -385,6 +393,7 @@ impl Workspace { cols: u16, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, events: mpsc::Sender, render_notify: Arc, @@ -407,6 +416,7 @@ impl Workspace { argv, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, &launch_env, events, render_notify, @@ -420,6 +430,7 @@ impl Workspace { cols, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, &launch_env, events, @@ -501,6 +512,7 @@ impl Workspace { cwd: PathBuf, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, extra_env: Vec<(String, String)>, ) -> std::io::Result<(usize, TerminalState, TerminalRuntime)> { @@ -510,6 +522,7 @@ impl Workspace { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, None, extra_env, @@ -525,6 +538,7 @@ impl Workspace { extra_env: Vec<(String, String)>, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, ) -> std::io::Result<(usize, TerminalState, TerminalRuntime)> { self.create_tab_with_runtime( rows, @@ -532,6 +546,7 @@ impl Workspace { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, crate::pane::PaneShellConfig::new("", crate::config::ShellModeConfig::NonLogin), Some(argv), extra_env, @@ -545,6 +560,7 @@ impl Workspace { cwd: PathBuf, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, argv: Option<&[String]>, extra_env: Vec<(String, String)>, @@ -575,6 +591,7 @@ impl Workspace { argv, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, &launch_env, events, render_notify, @@ -588,6 +605,7 @@ impl Workspace { cols, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, &launch_env, events, @@ -655,6 +673,7 @@ impl Workspace { cwd: Option, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, extra_env: Vec<(String, String)>, ) -> std::io::Result { @@ -674,6 +693,7 @@ impl Workspace { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, &launch_env, )?; @@ -692,6 +712,7 @@ impl Workspace { extra_env: Vec<(String, String)>, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, ) -> std::io::Result { let pane_number = self.next_public_pane_number; let tab_number = self @@ -711,11 +732,14 @@ impl Workspace { &launch_env, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, )?; self.register_new_pane_with_number(new_pane.pane_id, pane_number); Ok(new_pane) } + // Workspace split routing carries pane identity, geometry, host context, and focus policy. + #[allow(clippy::too_many_arguments)] pub fn split_pane( &mut self, pane_id: PaneId, @@ -725,6 +749,7 @@ impl Workspace { cwd: Option, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, extra_env: Vec<(String, String)>, focus_new_pane: bool, @@ -738,6 +763,7 @@ impl Workspace { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, extra_env, focus_new_pane, @@ -756,6 +782,7 @@ impl Workspace { cwd: Option, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, extra_env: Vec<(String, String)>, focus_new_pane: bool, @@ -769,6 +796,7 @@ impl Workspace { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, extra_env, focus_new_pane, @@ -788,6 +816,7 @@ impl Workspace { extra_env: Vec<(String, String)>, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, focus_new_pane: bool, ) -> Option> { self.split_pane_with_runtime( @@ -799,6 +828,7 @@ impl Workspace { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, crate::pane::PaneShellConfig::new("", crate::config::ShellModeConfig::NonLogin), extra_env, focus_new_pane, @@ -819,6 +849,7 @@ impl Workspace { extra_env: Vec<(String, String)>, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, focus_new_pane: bool, ) -> Option> { self.split_pane_with_runtime( @@ -830,6 +861,7 @@ impl Workspace { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, crate::pane::PaneShellConfig::new("", crate::config::ShellModeConfig::NonLogin), extra_env, focus_new_pane, @@ -848,6 +880,7 @@ impl Workspace { cwd: Option, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, extra_env: Vec<(String, String)>, focus_new_pane: bool, @@ -872,6 +905,7 @@ impl Workspace { &launch_env, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, ), None => tab.split_focused_argv_command( direction, @@ -882,6 +916,7 @@ impl Workspace { &launch_env, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, ), } } else { @@ -894,6 +929,7 @@ impl Workspace { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, &launch_env, ), @@ -904,6 +940,7 @@ impl Workspace { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, &launch_env, ), @@ -1663,6 +1700,7 @@ mod tests { &argv, 1024, crate::terminal_theme::TerminalTheme::default(), + None, events, render_notify, render_dirty, diff --git a/src/workspace/tab.rs b/src/workspace/tab.rs index 55fa4c96..5cc35b68 100644 --- a/src/workspace/tab.rs +++ b/src/workspace/tab.rs @@ -52,6 +52,8 @@ pub struct Tab { } impl Tab { + // Tab construction threads pane runtime geometry, host context, and render hooks. + #[allow(clippy::too_many_arguments)] pub fn new( number: usize, initial_cwd: PathBuf, @@ -59,6 +61,7 @@ impl Tab { cols: u16, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, launch_env: &PaneLaunchEnv, events: mpsc::Sender, @@ -72,6 +75,7 @@ impl Tab { cols, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, launch_env, events, @@ -81,6 +85,8 @@ impl Tab { ) } + // Command tab construction mirrors the shell tab runtime arguments. + #[allow(clippy::too_many_arguments)] pub fn new_argv_command( number: usize, initial_cwd: PathBuf, @@ -89,6 +95,7 @@ impl Tab { argv: &[String], scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, launch_env: &PaneLaunchEnv, events: mpsc::Sender, render_notify: Arc, @@ -101,6 +108,7 @@ impl Tab { cols, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, crate::pane::PaneShellConfig::new("", crate::config::ShellModeConfig::NonLogin), launch_env, events, @@ -118,6 +126,7 @@ impl Tab { cols: u16, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, launch_env: &PaneLaunchEnv, events: mpsc::Sender, @@ -137,6 +146,7 @@ impl Tab { crate::pane::AgentDetection::Enabled, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, events.clone(), render_notify.clone(), render_dirty.clone(), @@ -149,6 +159,7 @@ impl Tab { initial_cwd.clone(), scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, launch_env, events.clone(), @@ -202,6 +213,7 @@ impl Tab { cwd: Option, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, launch_env: &PaneLaunchEnv, ) -> std::io::Result { @@ -213,6 +225,7 @@ impl Tab { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, launch_env, None, @@ -228,6 +241,7 @@ impl Tab { cwd: Option, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, launch_env: &PaneLaunchEnv, ) -> std::io::Result { @@ -239,6 +253,7 @@ impl Tab { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, launch_env, None, @@ -255,6 +270,7 @@ impl Tab { launch_env: &PaneLaunchEnv, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, ) -> std::io::Result { self.split_focused_with_runtime( direction, @@ -264,6 +280,7 @@ impl Tab { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, crate::pane::PaneShellConfig::new("", crate::config::ShellModeConfig::NonLogin), launch_env, Some(SplitCommand::Shell { @@ -283,6 +300,7 @@ impl Tab { launch_env: &PaneLaunchEnv, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, ) -> std::io::Result { self.split_focused_with_runtime( direction, @@ -292,6 +310,7 @@ impl Tab { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, crate::pane::PaneShellConfig::new("", crate::config::ShellModeConfig::NonLogin), launch_env, Some(SplitCommand::Argv { argv, launch_env }), @@ -309,6 +328,7 @@ impl Tab { launch_env: &PaneLaunchEnv, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, ) -> std::io::Result { self.split_focused_with_runtime( direction, @@ -318,12 +338,15 @@ impl Tab { cwd, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, crate::pane::PaneShellConfig::new("", crate::config::ShellModeConfig::NonLogin), launch_env, Some(SplitCommand::Argv { argv, launch_env }), ) } + // Split construction threads geometry, host context, launch policy, and command state. + #[allow(clippy::too_many_arguments)] fn split_focused_with_runtime( &mut self, direction: Direction, @@ -333,6 +356,7 @@ impl Tab { cwd: Option, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, + host_terminal_appearance: Option, shell_config: crate::pane::PaneShellConfig<'_>, launch_env: &PaneLaunchEnv, command: Option>, @@ -363,6 +387,7 @@ impl Tab { crate::pane::AgentDetection::Enabled, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, self.events.clone(), self.render_notify.clone(), self.render_dirty.clone(), @@ -377,6 +402,7 @@ impl Tab { crate::pane::AgentDetection::Enabled, scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, self.events.clone(), self.render_notify.clone(), self.render_dirty.clone(), @@ -388,6 +414,7 @@ impl Tab { actual_cwd.clone(), scrollback_limit_bytes, host_terminal_theme, + host_terminal_appearance, shell_config, launch_env, self.events.clone(),