fix: propagate host color scheme to pane apps (#2214)

refs #714
This commit is contained in:
Can Celik
2026-08-03 02:05:27 +03:00
committed by GitHub
parent cbc31d34a1
commit 10eb0bc744
29 changed files with 498 additions and 30 deletions
+1
View File
@@ -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)
+1
View File
@@ -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,
+1
View File
@@ -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(),
+3
View File
@@ -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,
+6
View File
@@ -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,
+3
View File
@@ -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,
+2
View File
@@ -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()),
+2
View File
@@ -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,
)
+2
View File
@@ -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(),
+1
View File
@@ -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(),
) {
+4
View File
@@ -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);
+3
View File
@@ -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(),
+1
View File
@@ -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(),
+2
View File
@@ -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(),
+8
View File
@@ -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);
+45
View File
@@ -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(
@@ -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<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)
}
+47 -1
View File
@@ -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!(
+84
View File
@@ -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,
})
);
@@ -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);
+3
View File
@@ -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,
+47 -26
View File
@@ -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) {}
}
+135 -3
View File
@@ -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,
+1
View File
@@ -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,
+19
View File
@@ -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);
}
+9
View File
@@ -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;
+1
View File
@@ -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,
+1
View File
@@ -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,
+1
View File
@@ -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,
+38
View File
@@ -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,
+27
View File
@@ -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(),