Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 1 addition & 86 deletions cmux-tui/crates/cmux-tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6664,7 +6664,6 @@ pub struct App {
#[cfg(test)]
config_reload_applications: usize,
pub chrome: ChromeTheme,
default_colors: cmux_tui_core::DefaultColors,
pub tree: TreeView,
tab_locations: HashMap<SurfaceId, [usize; 4]>,
pub render_states: HashMap<SurfaceId, RenderState>,
Expand Down Expand Up @@ -8015,7 +8014,6 @@ enum MachineControllerCommand {

struct MachineSessionPreparation {
initial_size: Option<(u16, u16)>,
default_colors: cmux_tui_core::DefaultColors,
generation: u64,
pty_input: PtyInputSender,
surface_filter: Option<SurfaceId>,
Expand All @@ -8030,7 +8028,6 @@ struct PreparedMachineSession {
tree: TreeView,
label: String,
session_available: bool,
color_error: Option<String>,
machine: Option<MachineKey>,
}

Expand Down Expand Up @@ -8377,15 +8374,7 @@ fn prepare_machine_session(
preparation.initial_size,
Some(machine_ui),
)?;
let color_error = if replacement.reused {
None
} else {
replacement
.session
.set_default_colors(preparation.default_colors)
.err()
.map(|error| error.to_string())
};

let session_available = machine_ui.session_available;
let (session, event_worker, mux_titles, mux_recovery_generation) = prepare_ordered_session(
replacement.session,
Expand All @@ -8404,7 +8393,6 @@ fn prepare_machine_session(
tree,
label: replacement.label,
session_available,
color_error,
machine: replacement.machine,
})
}
Expand Down Expand Up @@ -8881,7 +8869,6 @@ fn run_with_machine_updates_inner(
#[cfg(test)]
config_reload_applications: 0,
chrome,
default_colors,
tree: TreeView::default(),
tab_locations: HashMap::new(),
render_states: HashMap::new(),
Expand Down Expand Up @@ -10379,7 +10366,6 @@ impl App {
self.config.scrollbar.position,
self.config.pane.padding,
),
default_colors: self.default_colors,
generation: self.session_generation.wrapping_add(1).max(1),
pty_input: self.pty_input.sender(),
surface_filter: self.surface_only,
Expand Down Expand Up @@ -11081,7 +11067,6 @@ impl App {
tree,
label,
session_available,
color_error,
machine: _,
} = prepared;
self.pty_input.activate_session_generation(generation);
Expand All @@ -11096,12 +11081,6 @@ impl App {
if let Some(worker) = self.session_event_worker.as_ref() {
worker.activate();
}
if let Some(error) = color_error {
self.status_message = Some(format!(
"{}: {error}",
localization::catalog().sidebar.machine_terminal_colors_failed
));
}
if session_available {
if publishes_global_cell_metrics(self.surface_only) {
self.session.set_cell_pixel_size(self.cell_pixels.0, self.cell_pixels.1);
Expand Down Expand Up @@ -39941,7 +39920,6 @@ mod tests {
let dispatcher = PtyInputDispatcher::spawn(|_| {}).unwrap();
super::MachineSessionPreparation {
initial_size: None,
default_colors: cmux_tui_core::DefaultColors::default(),
generation: 2,
pty_input: dispatcher.sender(),
surface_filter: None,
Expand Down Expand Up @@ -39981,7 +39959,6 @@ mod tests {
tree,
label: label.into(),
session_available: true,
color_error: None,
machine: None,
},
},
Expand Down Expand Up @@ -40332,7 +40309,6 @@ mod tests {
tree,
label: "second".into(),
session_available: true,
color_error: None,
machine: None,
},
true,
Expand Down Expand Up @@ -40410,7 +40386,6 @@ mod tests {
tree,
label: "second".into(),
session_available: true,
color_error: None,
machine: None,
},
true,
Expand Down Expand Up @@ -40459,62 +40434,6 @@ mod tests {
}

#[test]
fn machine_color_failure_status_uses_the_selected_locale() {
const CHILD_ENV: &str = "CMUX_MACHINE_COLOR_FAILURE_LOCALE_CHILD";
if std::env::var_os(CHILD_ENV).is_none() {
let output = std::process::Command::new(std::env::current_exe().unwrap())
.arg("app::tests::machine_color_failure_status_uses_the_selected_locale")
.arg("--exact")
.arg("--nocapture")
.env(CHILD_ENV, "1")
.env("LC_ALL", "ja_JP.UTF-8")
.output()
.unwrap();
assert!(
output.status.success(),
"Japanese machine color failure child failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
return;
}

let first = Mux::new("machine-color-locale-first", SurfaceOptions::default());
let second = Mux::new("machine-color-locale-second", SurfaceOptions::default());
let (mut app, _events) = test_app_with_events(Session::Local(first));
let pty_input = PtyInputDispatcher::spawn(|_| {}).unwrap();
let (session, event_worker, mux_titles, mux_recovery_generation) = prepare_ordered_session(
Session::Local(second),
pty_input.sender(),
app.app_events.clone(),
2,
None,
)
.unwrap();
let tree = session.tree();

app.install_prepared_machine_session(
super::PreparedMachineSession {
session,
event_worker,
generation: 2,
mux_titles,
mux_recovery_generation,
tree,
label: "second".into(),
session_available: false,
color_error: Some("offline".into()),
machine: None,
},
true,
);

assert_eq!(
app.status_message.as_deref(),
Some("ターミナルの色を適用できませんでした: offline")
);
}

#[test]
fn replaced_session_ignores_old_surface_lane_completion() {
let first = Mux::new("surface-lane-generation-first", SurfaceOptions::default());
Expand Down Expand Up @@ -40561,7 +40480,6 @@ mod tests {
tree,
label: "second".into(),
session_available: true,
color_error: None,
machine: None,
},
true,
Expand Down Expand Up @@ -40898,7 +40816,6 @@ mod tests {
tree,
label: "second".into(),
session_available: true,
color_error: None,
machine: None,
},
true,
Expand Down Expand Up @@ -41408,7 +41325,6 @@ mod tests {
tree,
label: "replacement".into(),
session_available: true,
color_error: None,
machine: None,
},
true,
Expand Down Expand Up @@ -41489,7 +41405,6 @@ mod tests {
config: Config::default(),
config_reload_applications: 0,
chrome: ChromeTheme::dark(),
default_colors: cmux_tui_core::DefaultColors::default(),
tree: TreeView::default(),
tab_locations: HashMap::new(),
render_states: HashMap::<u64, RenderState>::new(),
Expand Down
3 changes: 0 additions & 3 deletions cmux-tui/crates/cmux-tui/src/localization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,6 @@ pub(crate) struct SidebarMessages {
pub machine_provider_lifecycle_update_failed: &'static str,
pub machine_provider_workspace_update_failed: &'static str,
pub machine_reconnect_failed: &'static str,
pub machine_terminal_colors_failed: &'static str,
pub machine_provider_external_connect_unsupported: &'static str,
pub machine_provider_external_connect_ambiguous: &'static str,
pub machine_not_ready_to_connect: &'static str,
Expand Down Expand Up @@ -1599,7 +1598,6 @@ OPTIONS:
machine_provider_lifecycle_update_failed: "Machine provider lifecycle update failed",
machine_provider_workspace_update_failed: "Machine provider workspace update failed",
machine_reconnect_failed: "Could not reconnect machine",
machine_terminal_colors_failed: "Could not apply terminal colors",
machine_provider_external_connect_unsupported: "This machine provider cannot connect external machines",
machine_provider_external_connect_ambiguous: "The previous connection attempt may have succeeded; reconnect the provider and retry with the same pairing code",
machine_not_ready_to_connect: "Selected machine is not ready to connect",
Expand Down Expand Up @@ -2188,7 +2186,6 @@ ID とセッション:
machine_provider_lifecycle_update_failed: "マシンプロバイダーのライフサイクル更新に失敗しました",
machine_provider_workspace_update_failed: "マシンプロバイダーのワークスペース更新に失敗しました",
machine_reconnect_failed: "マシンに再接続できませんでした",
machine_terminal_colors_failed: "ターミナルの色を適用できませんでした",
machine_provider_external_connect_unsupported: "このマシンプロバイダーは外部マシンに接続できません",
machine_provider_external_connect_ambiguous: "前回の接続処理が完了している可能性があります。プロバイダーを再接続し、同じペアリングコードで再試行してください",
machine_not_ready_to_connect: "選択したマシンは接続準備ができていません",
Expand Down
121 changes: 78 additions & 43 deletions cmux-tui/crates/cmux-tui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2421,24 +2421,19 @@ fn initial_provider_connection_notice(
format!("{}: {error}", messages.initial_machine_connection_failed)
}

fn publish_session_default_colors(
session: &Session,
colors: cmux_tui_core::DefaultColors,
surface_only: Option<cmux_tui_core::SurfaceId>,
) -> anyhow::Result<()> {
// A scoped attach receives the target terminal's resolved colors through
// vt-state. Publishing this client's host colors would recolor sibling
// surfaces and change the session defaults for future terminals.
if surface_only.is_some() {
return Ok(());
fn frontend_default_colors(
mut configured: cmux_tui_core::DefaultColors,
host: cmux_tui_core::DefaultColors,
) -> cmux_tui_core::DefaultColors {
// Host OSC 10/11 replies describe this frontend. They may select
// compatible local chrome, but never become shared session defaults.
if host.fg.is_some() {
configured.fg = host.fg;
}
match session {
Session::Local(mux) => {
mux.seed_default_colors_if_no_durable_override(colors);
Ok(())
}
Session::Remote(remote) => remote.set_default_colors(colors),
if host.bg.is_some() {
configured.bg = host.bg;
}
Comment on lines +2433 to 2435

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing OSC background selects light

When theme.chrome is auto, the host supplies no OSC 11 background, and Ghostty config specifies a light background, frontend_default_colors retains that configured background and selects light chrome instead of the documented dark fallback.

configured
}

fn run_tui_once(
Expand All @@ -2451,20 +2446,9 @@ fn run_tui_once(
) -> anyhow::Result<app::RunOutcome> {
crossterm::terminal::enable_raw_mode()?;
let config = config::load();
let mut colors = config.terminal_defaults;
let host_colors = host_colors::probe_default_colors();
if host_colors.fg.is_some() {
colors.fg = host_colors.fg;
}
if host_colors.bg.is_some() {
colors.bg = host_colors.bg;
}
let color_result = publish_session_default_colors(&session, colors, surface_only);
let raw_result = crossterm::terminal::disable_raw_mode();
if let Err(err) = color_result {
crate::client_log::stderr_log!("startup", "cmux-tui: failed to set default colors: {err}");
}
raw_result?;
let colors =
frontend_default_colors(config.terminal_defaults, host_colors::probe_default_colors());
crossterm::terminal::disable_raw_mode()?;
app::run_with_machine_updates(
session,
session_label,
Expand Down Expand Up @@ -2831,6 +2815,7 @@ mod tests {
assert_eq!(shell_quote(r"C:\future session.sock"), r"'C:\future session.sock'");
}

#[cfg(unix)]
#[test]
fn absent_socket_recovery_only_shows_reset_when_supported() {
let messages = &localization::catalog_for_locale("en_US.UTF-8").startup;
Expand Down Expand Up @@ -2867,29 +2852,79 @@ mod tests {
assert!(!unsupported.contains("reset-state"), "{unsupported}");
}

#[cfg(unix)]
#[test]
fn scoped_terminal_attach_does_not_publish_session_default_colors() {
let mux = Mux::new("scoped-terminal-color-test", SurfaceOptions::default());
let original = cmux_tui_core::DefaultColors {
fg: Some(cmux_tui_core::Rgb { r: 1, g: 2, b: 3 }),
fn remote_host_colors_stay_client_local_across_concurrent_attaches() {
let dark = cmux_tui_core::DefaultColors {
fg: Some(cmux_tui_core::Rgb { r: 0xee, g: 0xee, b: 0xee }),
bg: Some(cmux_tui_core::Rgb { r: 0x11, g: 0x11, b: 0x11 }),
..Default::default()
};
let client = cmux_tui_core::DefaultColors {
fg: Some(cmux_tui_core::Rgb { r: 4, g: 5, b: 6 }),
let light = cmux_tui_core::DefaultColors {
fg: Some(cmux_tui_core::Rgb { r: 0x22, g: 0x22, b: 0x22 }),
bg: Some(cmux_tui_core::Rgb { r: 0xee, g: 0xee, b: 0xee }),
..Default::default()
};
mux.set_default_colors(original);
let session = Session::Local(mux.clone());
let mux = Mux::new(
format!("remote-host-color-test-{}", std::process::id()),
SurfaceOptions { command: Some(vec!["/bin/cat".to_string()]), ..Default::default() },
);
mux.set_default_colors(dark);
let authoritative = mux.new_workspace(None, Some((12, 4))).unwrap();
let socket = cmux_tui_core::server::serve(mux.clone(), None).unwrap();

let existing = Session::Remote(RemoteSession::connect(&socket).unwrap());
let session::SurfaceAttach::Attached(existing_surface) =
existing.try_surface_sized(authoritative.id, Some((12, 4))).unwrap()
else {
panic!("existing client did not attach");
};
let light_client = Session::Remote(RemoteSession::connect(&socket).unwrap());
let session::SurfaceAttach::Attached(light_surface) =
light_client.try_surface_sized(authoritative.id, Some((12, 4))).unwrap()
else {
panic!("light client did not attach");
};

publish_session_default_colors(&session, client, Some(7)).unwrap();
let light_projection = frontend_default_colors(dark, light);
assert_eq!(
mux.default_colors(),
original,
"scoped terminal attach must retain the session and sibling tabs' colors"
dark,
"a second client's host colors must not mutate the shared session"
);
let mut existing_render = ghostty_vt::RenderState::new().unwrap();
assert_eq!(
existing_surface.render_frame(&mut existing_render).unwrap().frame.default_colors.0,
dark.bg.unwrap(),
"the already-attached dark client must stay dark"
);
assert_eq!(
config::ChromeTheme::for_defaults(config::ChromeMode::Auto, light_projection),
config::ChromeTheme::light(),
"the light client may still project compatible local chrome"
);

publish_session_default_colors(&session, client, None).unwrap();
assert_eq!(mux.default_colors(), client, "full-session clients still publish their colors");
let application_background = cmux_tui_core::Rgb { r: 0x17, g: 0x1b, b: 0x2e };
authoritative.write_bytes(b"\x1b]11;#171b2e\x1b\\\n").unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
let mut existing_render = ghostty_vt::RenderState::new().unwrap();
let existing_background =
existing_surface.render_frame(&mut existing_render).unwrap().frame.default_colors.0;
let mut light_render = ghostty_vt::RenderState::new().unwrap();
let light_background =
light_surface.render_frame(&mut light_render).unwrap().frame.default_colors.0;
if existing_background == application_background
&& light_background == application_background
{
break;
}
assert!(
std::time::Instant::now() < deadline,
"application-authored OSC defaults did not reach both client projections"
);
std::thread::sleep(std::time::Duration::from_millis(10));
}
}

#[test]
Expand Down
Loading