aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/actions.rs1
-rw-r--r--src/app/agent_resume.rs1
-rw-r--r--src/app/api.rs3
-rw-r--r--src/app/api/layouts.rs6
-rw-r--r--src/app/api/panes.rs3
-rw-r--r--src/app/api/plugins/panes.rs2
-rw-r--r--src/app/api/tabs.rs2
-rw-r--r--src/app/creation.rs2
-rw-r--r--src/app/input/mod.rs1
-rw-r--r--src/app/input/mouse.rs4
-rw-r--r--src/app/input/navigate.rs3
-rw-r--r--src/app/input/terminal.rs1
-rw-r--r--src/app/popup.rs2
-rw-r--r--src/app/theme_sync.rs8
-rw-r--r--src/ghostty/mod.rs45
-rw-r--r--src/pane.rs48
-rw-r--r--src/pane/terminal.rs84
-rw-r--r--src/persist/restore.rs3
-rw-r--r--src/pty/actor.rs73
-rw-r--r--src/pty/actor/unix.rs138
-rw-r--r--src/server/notifications.rs1
-rw-r--r--src/terminal/runtime.rs19
-rw-r--r--src/terminal_theme.rs9
-rw-r--r--src/ui/dialogs.rs1
-rw-r--r--src/ui/mobile.rs1
-rw-r--r--src/ui/sidebar.rs1
-rw-r--r--src/workspace.rs38
-rw-r--r--src/workspace/tab.rs27
28 files changed, 497 insertions, 30 deletions
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<Vec<u8>>,
clipboard_writes: Vec<Vec<u8>>,
size_report: ffi::GhosttySizeReportSize,
+ color_scheme: Option<ColorScheme>,
+}
+
+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::<TerminalCallbackState>() };
+ 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(
@@ -793,6 +828,12 @@ impl Terminal {
.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,
(&glyph_protocol as *const bool).cast(),
)
@@ -913,6 +954,10 @@ impl Terminal {
Ok(())
}
+ pub fn set_color_scheme(&mut self, color_scheme: Option<ColorScheme>) -> Option<ColorScheme> {
+ mem::replace(&mut self.callback_state.color_scheme, color_scheme)
+ }
+
pub fn take_pwd_changes(&mut self) -> Vec<Vec<u8>> {
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<Bytes>) {
+ 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<crate::terminal_theme::HostAppearance>,
+ ) {
+ 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<crate::terminal_theme::HostAppearance>,
shell_config: PaneShellConfig<'_>,
launch_env: &PaneLaunchEnv,
events: mpsc::Sender<AppEvent>,
@@ -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<crate::terminal_theme::HostAppearance>,
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<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
@@ -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<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
@@ -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<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
@@ -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<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
@@ -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<crate::terminal_theme::HostAppearance>,
+ ) -> Option<Bytes> {
+ 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<crate::terminal_theme::HostAppearance>,
+ ) -> Option<Bytes> {
+ 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,
})
);
@@ -5010,6 +5055,45 @@ mod tests {
}
#[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);
let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap();
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<Bytes>,
control_tx: std_mpsc::Sender<PtyIoControlCommand>,
+ write_tx: std_mpsc::Sender<Bytes>,
+ response_order: Arc<Mutex<()>>,
accepting: Arc<Mutex<bool>>,
}
@@ -84,6 +86,16 @@ mod windows {
self.data_tx.try_send(bytes)
}
+ pub(crate) fn write_terminal_response(&self, response: impl FnOnce() -> Option<Bytes>) {
+ 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::<Bytes>(1024);
let (control_tx, control_rx) = std_mpsc::channel::<PtyIoControlCommand>();
+ let (write_tx, write_rx) = std_mpsc::channel::<Bytes>();
+ 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<Mutex<Box<dyn Write + Send>>>,
- 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<PtyResizeRequest>,
nudge: Option<PtyResize>,
+ terminal_responses: Vec<Bytes>,
}
pub(crate) struct PtyIoActorConfig {
@@ -90,6 +91,7 @@ pub(crate) struct PtyIoActorHandle {
wake: fd::WakeWriter,
user_writes: Arc<Mutex<UserWriteGate>>,
controls: Arc<Mutex<SharedPtyControls>>,
+ response_order: Arc<Mutex<()>>,
}
#[derive(Debug)]
@@ -157,6 +159,24 @@ impl PtyIoActorHandle {
}
}
+ pub(crate) fn write_terminal_response(&self, response: impl FnOnce() -> Option<Bytes>) {
+ 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<Mutex<SharedPtyControls>>,
+ response_order: Arc<Mutex<()>>,
on_read: ReadCallback,
on_reader_exit: Option<ReaderExitCallback>,
poll_observer: Option<std_mpsc::Sender<()>>,
@@ -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<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
@@ -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<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
launch_env: &crate::pane::PaneLaunchEnv,
events: mpsc::Sender<AppEvent>,
@@ -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<crate::terminal_theme::HostAppearance>,
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<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
@@ -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<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
@@ -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<crate::terminal_theme::HostAppearance>,
+ ) {
+ 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<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
@@ -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<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
@@ -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<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
@@ -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<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
@@ -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<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
@@ -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<crate::terminal_theme::HostAppearance>,
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<crate::terminal_theme::HostAppearance>,
) -> 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<crate::terminal_theme::HostAppearance>,
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<PathBuf>,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
+ host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
extra_env: Vec<(String, String)>,
) -> std::io::Result<crate::workspace::tab::NewPane> {
@@ -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<crate::terminal_theme::HostAppearance>,
) -> std::io::Result<crate::workspace::tab::NewPane> {
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<PathBuf>,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
+ host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
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<PathBuf>,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
+ host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
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<crate::terminal_theme::HostAppearance>,
focus_new_pane: bool,
) -> Option<std::io::Result<(usize, crate::workspace::tab::NewPane)>> {
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<crate::terminal_theme::HostAppearance>,
focus_new_pane: bool,
) -> Option<std::io::Result<(usize, crate::workspace::tab::NewPane)>> {
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<PathBuf>,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
+ host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
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<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
launch_env: &PaneLaunchEnv,
events: mpsc::Sender<AppEvent>,
@@ -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<crate::terminal_theme::HostAppearance>,
launch_env: &PaneLaunchEnv,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
@@ -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<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
launch_env: &PaneLaunchEnv,
events: mpsc::Sender<AppEvent>,
@@ -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<PathBuf>,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
+ host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
launch_env: &PaneLaunchEnv,
) -> std::io::Result<NewPane> {
@@ -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<PathBuf>,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
+ host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
launch_env: &PaneLaunchEnv,
) -> std::io::Result<NewPane> {
@@ -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<crate::terminal_theme::HostAppearance>,
) -> std::io::Result<NewPane> {
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<crate::terminal_theme::HostAppearance>,
) -> std::io::Result<NewPane> {
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<crate::terminal_theme::HostAppearance>,
) -> std::io::Result<NewPane> {
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<PathBuf>,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
+ host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
launch_env: &PaneLaunchEnv,
command: Option<SplitCommand<'_>>,
@@ -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(),