mirror of
https://github.com/herdrdev/herdr.git
synced 2026-11-06 00:06:18 +00:00
fix: send pane appearance changes only after the matching background
refs #4183
This commit is contained in:
@@ -270,7 +270,7 @@ impl App {
|
||||
cwd,
|
||||
self.state.pane_scrollback_limit_bytes,
|
||||
host_terminal_theme,
|
||||
self.state.host_terminal_appearance,
|
||||
self.pane_host_terminal_appearance(),
|
||||
crate::pane::PaneShellConfig::new(&self.state.default_shell, self.state.shell_mode),
|
||||
&launch_env,
|
||||
self.event_tx.clone(),
|
||||
|
||||
+1
-1
@@ -614,7 +614,7 @@ impl App {
|
||||
cwd,
|
||||
self.state.pane_scrollback_limit_bytes,
|
||||
self.state.host_terminal_theme,
|
||||
self.state.host_terminal_appearance,
|
||||
self.pane_host_terminal_appearance(),
|
||||
crate::pane::PaneShellConfig::new(&self.state.default_shell, self.state.shell_mode),
|
||||
&launch_env,
|
||||
self.event_tx.clone(),
|
||||
|
||||
@@ -99,7 +99,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 host_terminal_appearance = self.pane_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),
|
||||
@@ -415,7 +415,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 host_terminal_appearance = self.pane_host_terminal_appearance();
|
||||
let cwd = pane
|
||||
.cwd
|
||||
.as_ref()
|
||||
|
||||
@@ -71,7 +71,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 host_terminal_appearance = self.pane_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");
|
||||
|
||||
@@ -127,6 +127,7 @@ impl App {
|
||||
};
|
||||
let (rows, cols) = self.state.new_pane_size(new_pane_placement);
|
||||
let previous_focus = self.state.current_pane_focus_target();
|
||||
let host_terminal_appearance = self.pane_host_terminal_appearance();
|
||||
let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
|
||||
return encode_error(id, "workspace_not_found", "workspace not found");
|
||||
};
|
||||
@@ -140,7 +141,7 @@ impl App {
|
||||
extra_env,
|
||||
self.state.pane_scrollback_limit_bytes,
|
||||
self.state.host_terminal_theme,
|
||||
self.state.host_terminal_appearance,
|
||||
host_terminal_appearance,
|
||||
params.focus || placement == PluginPanePlacement::Zoomed,
|
||||
);
|
||||
let (tab_idx, new_pane) = match result {
|
||||
@@ -206,6 +207,7 @@ impl App {
|
||||
Err((code, message)) => return encode_error(id, &code, message),
|
||||
};
|
||||
let (rows, cols) = self.state.new_pane_size(crate::ui::NewPanePlacement::Alone);
|
||||
let host_terminal_appearance = self.pane_host_terminal_appearance();
|
||||
let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
|
||||
return encode_error(id, "workspace_not_found", "workspace not found");
|
||||
};
|
||||
@@ -217,7 +219,7 @@ impl App {
|
||||
extra_env,
|
||||
self.state.pane_scrollback_limit_bytes,
|
||||
self.state.host_terminal_theme,
|
||||
self.state.host_terminal_appearance,
|
||||
host_terminal_appearance,
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(err) => return encode_error(id, "plugin_pane_open_failed", err.to_string()),
|
||||
|
||||
+1
-1
@@ -69,7 +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 host_terminal_appearance = self.pane_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),
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ impl App {
|
||||
cols,
|
||||
self.state.pane_scrollback_limit_bytes,
|
||||
self.state.host_terminal_theme,
|
||||
self.state.host_terminal_appearance,
|
||||
self.pane_host_terminal_appearance(),
|
||||
crate::pane::PaneShellConfig::new(&self.state.default_shell, self.state.shell_mode),
|
||||
self.event_tx.clone(),
|
||||
self.render_notify.clone(),
|
||||
|
||||
@@ -373,6 +373,7 @@ impl App {
|
||||
.state
|
||||
.new_pane_size(crate::ui::NewPanePlacement::ZoomedOverlay);
|
||||
let (env, _) = self.custom_command_env();
|
||||
let host_terminal_appearance = self.pane_host_terminal_appearance();
|
||||
|
||||
let ws = self
|
||||
.state
|
||||
@@ -400,7 +401,7 @@ impl App {
|
||||
env,
|
||||
self.state.pane_scrollback_limit_bytes,
|
||||
self.state.host_terminal_theme,
|
||||
self.state.host_terminal_appearance,
|
||||
host_terminal_appearance,
|
||||
)?;
|
||||
let new_pane_id = new_pane.pane_id;
|
||||
self.terminal_runtimes
|
||||
@@ -470,6 +471,7 @@ impl App {
|
||||
})
|
||||
});
|
||||
|
||||
let host_terminal_appearance = self.pane_host_terminal_appearance();
|
||||
let (tab_idx, new_pane, workspace_id) = {
|
||||
let ws = self
|
||||
.state
|
||||
@@ -487,7 +489,7 @@ impl App {
|
||||
extra_env,
|
||||
self.state.pane_scrollback_limit_bytes,
|
||||
self.state.host_terminal_theme,
|
||||
self.state.host_terminal_appearance,
|
||||
host_terminal_appearance,
|
||||
true,
|
||||
);
|
||||
let (tab_idx, new_pane) = match result {
|
||||
|
||||
@@ -140,6 +140,7 @@ pub struct App {
|
||||
pub(crate) loaded_host_cursor: crate::config::HostCursorModeConfig,
|
||||
pub(crate) agent_metadata_deadline: Option<Instant>,
|
||||
pub(crate) pending_agent_resume_deadline: Option<Instant>,
|
||||
pub(crate) pending_host_appearance_deadline: Option<Instant>,
|
||||
startup_per_agent_delay: Duration,
|
||||
next_agent_resume_at: Option<Instant>,
|
||||
pub(crate) session_save_deadline: Option<Instant>,
|
||||
@@ -652,6 +653,7 @@ impl App {
|
||||
loaded_host_cursor: config.ui.host_cursor,
|
||||
agent_metadata_deadline: None,
|
||||
pending_agent_resume_deadline: None,
|
||||
pending_host_appearance_deadline: None,
|
||||
startup_per_agent_delay: Duration::from_millis(
|
||||
config.session.startup_per_agent_delay_ms.into(),
|
||||
),
|
||||
|
||||
+2
-2
@@ -54,7 +54,7 @@ impl App {
|
||||
crate::pane::AgentDetection::Disabled,
|
||||
app.state.pane_scrollback_limit_bytes,
|
||||
app.state.host_terminal_theme,
|
||||
app.state.host_terminal_appearance,
|
||||
app.pane_host_terminal_appearance(),
|
||||
app.event_tx.clone(),
|
||||
app.render_notify.clone(),
|
||||
app.render_dirty.clone(),
|
||||
@@ -86,7 +86,7 @@ impl App {
|
||||
crate::pane::AgentDetection::Disabled,
|
||||
app.state.pane_scrollback_limit_bytes,
|
||||
app.state.host_terminal_theme,
|
||||
app.state.host_terminal_appearance,
|
||||
app.pane_host_terminal_appearance(),
|
||||
app.event_tx.clone(),
|
||||
app.render_notify.clone(),
|
||||
app.render_dirty.clone(),
|
||||
|
||||
@@ -167,6 +167,7 @@ impl App {
|
||||
self.next_agent_manifest_update_check,
|
||||
self.agent_metadata_deadline,
|
||||
self.pending_agent_resume_deadline,
|
||||
self.pending_host_appearance_deadline,
|
||||
self.session_save_deadline,
|
||||
self.next_tab_bar_status_deadline(),
|
||||
render_deadline,
|
||||
|
||||
+79
-13
@@ -1,31 +1,97 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::App;
|
||||
use crate::terminal_theme::HostAppearance;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
impl App {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_host_terminal_appearance_state(
|
||||
&mut self,
|
||||
appearance: Option<crate::terminal_theme::HostAppearance>,
|
||||
appearance: Option<HostAppearance>,
|
||||
explicit: bool,
|
||||
) -> bool {
|
||||
if self.state.host_terminal_appearance == appearance
|
||||
&& self.state.host_terminal_appearance_explicit == explicit
|
||||
{
|
||||
return false;
|
||||
}
|
||||
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()
|
||||
self.set_host_terminal_state(self.state.host_terminal_theme, appearance, explicit)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_host_terminal_theme(
|
||||
&mut self,
|
||||
theme: crate::terminal_theme::TerminalTheme,
|
||||
) -> bool {
|
||||
if theme == self.state.host_terminal_theme {
|
||||
self.set_host_terminal_state(
|
||||
theme,
|
||||
self.state.host_terminal_appearance,
|
||||
self.state.host_terminal_appearance_explicit,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn set_host_terminal_state(
|
||||
&mut self,
|
||||
theme: crate::terminal_theme::TerminalTheme,
|
||||
appearance: Option<HostAppearance>,
|
||||
explicit: bool,
|
||||
) -> bool {
|
||||
let theme_changed = theme != self.state.host_terminal_theme;
|
||||
if theme_changed {
|
||||
self.state.host_terminal_theme = theme;
|
||||
self.apply_host_terminal_theme_to_panes();
|
||||
}
|
||||
|
||||
let previous = self.state.host_terminal_appearance;
|
||||
let appearance_changed =
|
||||
previous != appearance || self.state.host_terminal_appearance_explicit != explicit;
|
||||
if appearance_changed {
|
||||
self.state.host_terminal_appearance = appearance;
|
||||
self.state.host_terminal_appearance_explicit = explicit;
|
||||
if previous.is_some() && appearance.is_some() {
|
||||
if previous != appearance {
|
||||
// The matching OSC 11 arrives after the appearance report's round trip.
|
||||
self.pending_host_appearance_deadline
|
||||
.get_or_insert(Instant::now() + Duration::from_millis(750));
|
||||
}
|
||||
} else {
|
||||
self.pending_host_appearance_deadline = None;
|
||||
self.apply_host_terminal_appearance_to_panes();
|
||||
}
|
||||
}
|
||||
|
||||
// A foreground switch must install both facts before releasing the outgoing hold.
|
||||
let released = self.release_host_appearance_if_ready(Instant::now());
|
||||
let repainted = appearance_changed && self.refresh_effective_app_theme();
|
||||
theme_changed || released || repainted
|
||||
}
|
||||
|
||||
pub(crate) fn pane_host_terminal_appearance(&self) -> Option<HostAppearance> {
|
||||
if self.pending_host_appearance_deadline.is_some() {
|
||||
// New panes must describe the background available during the hold.
|
||||
self.state
|
||||
.host_terminal_theme
|
||||
.background
|
||||
.map(|color| color.inferred_appearance())
|
||||
} else {
|
||||
self.state.host_terminal_appearance
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn release_host_appearance_if_ready(&mut self, now: Instant) -> bool {
|
||||
let Some(deadline) = self.pending_host_appearance_deadline else {
|
||||
return false;
|
||||
};
|
||||
let matches = self
|
||||
.state
|
||||
.host_terminal_theme
|
||||
.background
|
||||
.is_some_and(|color| {
|
||||
Some(color.inferred_appearance()) == self.state.host_terminal_appearance
|
||||
});
|
||||
if !matches && now < deadline {
|
||||
return false;
|
||||
}
|
||||
self.state.host_terminal_theme = theme;
|
||||
self.apply_host_terminal_theme_to_panes();
|
||||
self.pending_host_appearance_deadline = None;
|
||||
self.apply_host_terminal_appearance_to_panes();
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
use super::*;
|
||||
use crate::terminal::{TerminalId, TerminalRuntime};
|
||||
use crate::terminal_theme::{RgbColor, TerminalTheme};
|
||||
use bytes::Bytes;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
const DARK: RgbColor = RgbColor {
|
||||
r: 17,
|
||||
g: 17,
|
||||
b: 17,
|
||||
};
|
||||
const LIGHT: RgbColor = RgbColor {
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 255,
|
||||
};
|
||||
|
||||
fn app() -> App {
|
||||
let (_, api_rx) = mpsc::unbounded_channel();
|
||||
App::new(
|
||||
&crate::config::Config::default(),
|
||||
crate::app::AppPolicy::TEST,
|
||||
None,
|
||||
api_rx,
|
||||
crate::api::EventHub::default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn child(app: &mut App) -> (TerminalId, mpsc::Receiver<Bytes>) {
|
||||
let (runtime, rx) = TerminalRuntime::test_with_channel(80, 24);
|
||||
runtime.apply_host_terminal_theme(app.state.host_terminal_theme);
|
||||
runtime.apply_host_terminal_appearance(app.pane_host_terminal_appearance());
|
||||
runtime.test_process_pty_bytes(b"\x1b[?2031h");
|
||||
let id = TerminalId::alloc();
|
||||
app.terminal_runtimes.insert(id.clone(), runtime);
|
||||
(id, rx)
|
||||
}
|
||||
|
||||
fn background(app: &mut App, color: RgbColor) {
|
||||
app.set_host_terminal_theme(TerminalTheme {
|
||||
background: Some(color),
|
||||
..app.state.host_terminal_theme
|
||||
});
|
||||
}
|
||||
|
||||
fn query(app: &App, id: &TerminalId, request: &[u8]) -> Vec<Bytes> {
|
||||
app.terminal_runtimes
|
||||
.get(id)
|
||||
.unwrap()
|
||||
.test_process_pty_bytes(request)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn appearance_waits_for_background_not_foreground_or_palette() {
|
||||
let mut app = app();
|
||||
background(&mut app, DARK);
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Dark), true);
|
||||
let (id, mut rx) = child(&mut app);
|
||||
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Light), true);
|
||||
assert!(rx.try_recv().is_err(), "notification must wait for OSC 11");
|
||||
let mut theme = app.state.host_terminal_theme;
|
||||
theme.foreground = Some(DARK);
|
||||
theme.palette[1] = Some(LIGHT);
|
||||
app.set_host_terminal_theme(theme);
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"OSC 10/palette must not release the hold"
|
||||
);
|
||||
assert_eq!(
|
||||
query(&app, &id, b"\x1b[?996n"),
|
||||
[Bytes::from_static(b"\x1b[?997;1n")]
|
||||
);
|
||||
background(&mut app, LIGHT);
|
||||
assert_eq!(rx.try_recv().unwrap(), b"\x1b[?997;2n"[..]);
|
||||
assert_eq!(
|
||||
query(&app, &id, b"\x1b]11;?\x07"),
|
||||
[Bytes::from_static(b"\x1b]11;rgb:ffff/ffff/ffff\x1b\\")]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn appearance_with_matching_background_is_immediate() {
|
||||
let mut app = app();
|
||||
background(&mut app, DARK);
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Dark), true);
|
||||
let (_, mut rx) = child(&mut app);
|
||||
background(&mut app, LIGHT);
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Light), true);
|
||||
assert_eq!(rx.try_recv().unwrap(), b"\x1b[?997;2n"[..]);
|
||||
assert!(app.pending_host_appearance_deadline.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn appearance_deadline_bounds_missing_or_inconsistent_background() {
|
||||
for missing in [false, true] {
|
||||
let mut app = app();
|
||||
background(&mut app, DARK);
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Dark), true);
|
||||
let (_, mut rx) = child(&mut app);
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Light), true);
|
||||
if missing {
|
||||
app.set_host_terminal_theme(TerminalTheme::default());
|
||||
}
|
||||
let deadline = app.pending_host_appearance_deadline.unwrap();
|
||||
assert!(deadline <= Instant::now() + Duration::from_millis(750));
|
||||
assert!(!app.release_host_appearance_if_ready(deadline - Duration::from_nanos(1)));
|
||||
assert!(rx.try_recv().is_err());
|
||||
assert!(app.release_host_appearance_if_ready(deadline));
|
||||
assert_eq!(rx.try_recv().unwrap(), b"\x1b[?997;2n"[..]);
|
||||
assert!(!app.release_host_appearance_if_ready(deadline));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn appearance_coalesces_without_extending_deadline() {
|
||||
let mut app = app();
|
||||
background(&mut app, DARK);
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Dark), true);
|
||||
let (_, mut rx) = child(&mut app);
|
||||
app.set_host_terminal_theme(TerminalTheme::default());
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Light), true);
|
||||
let deadline = app.pending_host_appearance_deadline;
|
||||
// A provenance-only update must not cancel a pending flip either.
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Light), false);
|
||||
assert_eq!(app.pending_host_appearance_deadline, deadline);
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Dark), true);
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Light), true);
|
||||
assert_eq!(app.pending_host_appearance_deadline, deadline);
|
||||
assert!(rx.try_recv().is_err());
|
||||
background(&mut app, LIGHT);
|
||||
assert_eq!(rx.try_recv().unwrap(), b"\x1b[?997;2n"[..]);
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn appearance_baseline_establish_and_clear_are_immediate() {
|
||||
let mut app = app();
|
||||
let (id, mut rx) = child(&mut app);
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Dark), true);
|
||||
assert_eq!(
|
||||
query(&app, &id, b"\x1b[?996n"),
|
||||
[Bytes::from_static(b"\x1b[?997;1n")]
|
||||
);
|
||||
assert!(app.pending_host_appearance_deadline.is_none());
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Light), true);
|
||||
app.set_host_terminal_appearance_state(None, false);
|
||||
assert!(app.pending_host_appearance_deadline.is_none());
|
||||
assert!(query(&app, &id, b"\x1b[?996n").is_empty());
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn pane_created_during_appearance_hold_gets_consistent_pair() {
|
||||
let mut app = app();
|
||||
app.state.default_shell = "/bin/sh".into();
|
||||
background(&mut app, DARK);
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Dark), true);
|
||||
app.set_host_terminal_appearance_state(Some(HostAppearance::Light), true);
|
||||
let index = app
|
||||
.create_workspace_with_options(std::env::temp_dir(), false)
|
||||
.unwrap();
|
||||
let ws = &app.state.workspaces[index];
|
||||
let id = ws.terminal_id(ws.tabs[0].root_pane).unwrap().clone();
|
||||
assert_eq!(
|
||||
query(&app, &id, b"\x1b[?996n"),
|
||||
[Bytes::from_static(b"\x1b[?997;1n")]
|
||||
);
|
||||
assert_eq!(
|
||||
query(&app, &id, b"\x1b]11;?\x07"),
|
||||
[Bytes::from_static(b"\x1b]11;rgb:1111/1111/1111\x1b\\")]
|
||||
);
|
||||
background(&mut app, LIGHT);
|
||||
assert_eq!(
|
||||
query(&app, &id, b"\x1b[?996n"),
|
||||
[Bytes::from_static(b"\x1b[?997;2n")]
|
||||
);
|
||||
assert_eq!(
|
||||
query(&app, &id, b"\x1b]11;?\x07"),
|
||||
[Bytes::from_static(b"\x1b]11;rgb:ffff/ffff/ffff\x1b\\")]
|
||||
);
|
||||
for (_, runtime) in app.terminal_runtimes.drain() {
|
||||
runtime.shutdown();
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -4019,16 +4019,17 @@ impl PaneRuntime {
|
||||
(release_tx, writer)
|
||||
}
|
||||
|
||||
pub(crate) fn test_process_pty_bytes(&self, bytes: &[u8]) {
|
||||
pub(crate) fn test_process_pty_bytes(&self, bytes: &[u8]) -> Vec<Bytes> {
|
||||
let _content_write_guard = match self.content_write_lock.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
self.content_seq.fetch_add(1, Ordering::AcqRel);
|
||||
let (tx, _rx) = mpsc::channel(1);
|
||||
let _ = self.terminal.process_pty_bytes(self.pane_id, 0, bytes, &tx);
|
||||
let result = self.terminal.process_pty_bytes(self.pane_id, 0, bytes, &tx);
|
||||
self.content_seq.fetch_add(1, Ordering::Release);
|
||||
self.compression.wake();
|
||||
result.terminal_responses
|
||||
}
|
||||
|
||||
pub(crate) fn test_with_scrollback_bytes(
|
||||
|
||||
@@ -845,11 +845,11 @@ impl HeadlessServer {
|
||||
if outer_terminal_focus == Some(true) {
|
||||
self.app.state.mark_active_tab_seen();
|
||||
}
|
||||
self.app.set_host_terminal_appearance_state(
|
||||
self.app.set_host_terminal_state(
|
||||
host_terminal_theme,
|
||||
host_terminal_appearance,
|
||||
host_terminal_appearance_explicit,
|
||||
);
|
||||
self.app.set_host_terminal_theme(host_terminal_theme);
|
||||
}
|
||||
|
||||
fn sync_visible_server_config_diagnostic(&mut self, uses_local_keybindings: bool) {
|
||||
@@ -2270,11 +2270,11 @@ impl HeadlessServer {
|
||||
if !client.shell_surface_active || self.foreground_client_id != Some(client_id) {
|
||||
return false;
|
||||
}
|
||||
let mut changed = self.app.set_host_terminal_appearance_state(
|
||||
let changed = self.app.set_host_terminal_state(
|
||||
client.host_terminal_theme,
|
||||
client.host_terminal_appearance,
|
||||
client.host_terminal_appearance_explicit,
|
||||
);
|
||||
changed |= self.app.set_host_terminal_theme(client.host_terminal_theme);
|
||||
if changed {
|
||||
self.resize_shared_runtime_to_effective_size_before_input();
|
||||
}
|
||||
@@ -3365,6 +3365,7 @@ impl HeadlessServer {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
changed |= self.app.release_host_appearance_if_ready(now);
|
||||
changed |= self.app.handle_tab_bar_status_tasks(now);
|
||||
|
||||
if geometry_dirty {
|
||||
|
||||
@@ -4019,6 +4019,101 @@ fn client_shell_host_theme_follows_foreground_client() {
|
||||
assert!(!server.app.state.host_terminal_appearance_explicit);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn foreground_switch_coalesces_pending_appearance_before_releasing_colors() {
|
||||
use crate::terminal::{TerminalId, TerminalRuntime};
|
||||
use crate::terminal_theme::{HostAppearance, RgbColor, TerminalTheme};
|
||||
|
||||
for incoming in [Some(HostAppearance::Dark), None] {
|
||||
let mut server = test_headless_server();
|
||||
server.app.set_host_terminal_theme(TerminalTheme {
|
||||
background: Some(RgbColor {
|
||||
r: 17,
|
||||
g: 17,
|
||||
b: 17,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
server
|
||||
.app
|
||||
.set_host_terminal_appearance_state(Some(HostAppearance::Dark), true);
|
||||
let (runtime, mut rx) = TerminalRuntime::test_with_channel(80, 24);
|
||||
runtime.apply_host_terminal_theme(server.app.state.host_terminal_theme);
|
||||
runtime.apply_host_terminal_appearance(Some(HostAppearance::Dark));
|
||||
runtime.test_process_pty_bytes(b"\x1b[?2031h");
|
||||
let id = TerminalId::alloc();
|
||||
server.app.terminal_runtimes.insert(id.clone(), runtime);
|
||||
server
|
||||
.app
|
||||
.set_host_terminal_appearance_state(Some(HostAppearance::Light), true);
|
||||
assert!(rx.try_recv().is_err());
|
||||
|
||||
let mut client = ClientConnection::new(
|
||||
(80, 24),
|
||||
crate::kitty_graphics::HostCellSize::default(),
|
||||
2,
|
||||
RenderEncoding::SemanticFrame,
|
||||
None,
|
||||
);
|
||||
client.host_terminal_theme.background = Some(RgbColor {
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 255,
|
||||
});
|
||||
client.host_terminal_appearance = incoming;
|
||||
client.host_terminal_appearance_explicit = incoming.is_some();
|
||||
server.clients.insert(2, client);
|
||||
server.foreground_client_id = Some(2);
|
||||
server.sync_foreground_client_state();
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"must not announce outgoing client A Light"
|
||||
);
|
||||
let expected =
|
||||
incoming.map(|appearance| bytes::Bytes::from_static(appearance.color_scheme_report()));
|
||||
assert_eq!(
|
||||
server
|
||||
.app
|
||||
.terminal_runtimes
|
||||
.get(&id)
|
||||
.unwrap()
|
||||
.test_process_pty_bytes(b"\x1b[?996n"),
|
||||
expected.into_iter().collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_host_appearance_is_released_by_headless_deadline() {
|
||||
use crate::terminal_theme::{HostAppearance, RgbColor, TerminalTheme};
|
||||
|
||||
let mut server = test_headless_server();
|
||||
server.app.set_host_terminal_theme(TerminalTheme {
|
||||
background: Some(RgbColor {
|
||||
r: 17,
|
||||
g: 17,
|
||||
b: 17,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
server
|
||||
.app
|
||||
.set_host_terminal_appearance_state(Some(HostAppearance::Dark), true);
|
||||
server
|
||||
.app
|
||||
.set_host_terminal_appearance_state(Some(HostAppearance::Light), true);
|
||||
let deadline = server.app.pending_host_appearance_deadline.unwrap();
|
||||
assert!(
|
||||
server
|
||||
.app
|
||||
.next_headless_loop_deadline_with_git_refresh(Instant::now(), false, false)
|
||||
.unwrap()
|
||||
<= deadline
|
||||
);
|
||||
server.handle_scheduled_tasks_headless(deadline, false);
|
||||
assert!(server.app.pending_host_appearance_deadline.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_clients_store_known_cell_geometry_independently_of_pixel_mouse() {
|
||||
let mut server = test_headless_server();
|
||||
|
||||
@@ -657,8 +657,8 @@ impl TerminalRuntime {
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn test_process_pty_bytes(&self, bytes: &[u8]) {
|
||||
self.0.test_process_pty_bytes(bytes);
|
||||
pub(crate) fn test_process_pty_bytes(&self, bytes: &[u8]) -> Vec<Bytes> {
|
||||
self.0.test_process_pty_bytes(bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn test_with_scrollback_bytes(
|
||||
|
||||
Reference in New Issue
Block a user