diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/agent_resume.rs | 6 | ||||
| -rw-r--r-- | src/app/mod.rs | 35 | ||||
| -rw-r--r-- | src/app/runtime.rs | 4 | ||||
| -rw-r--r-- | src/app/theme_sync.rs | 15 | ||||
| -rw-r--r-- | src/client/input.rs | 117 | ||||
| -rw-r--r-- | src/client/mod.rs | 5 | ||||
| -rw-r--r-- | src/ghostty/mod.rs | 22 | ||||
| -rw-r--r-- | src/pane/osc.rs | 2 | ||||
| -rw-r--r-- | src/pane/terminal.rs | 128 | ||||
| -rw-r--r-- | src/raw_input.rs | 103 | ||||
| -rw-r--r-- | src/server/clients.rs | 5 | ||||
| -rw-r--r-- | src/server/headless.rs | 17 | ||||
| -rw-r--r-- | src/terminal_theme.rs | 58 | ||||
| -rw-r--r-- | src/ui/panes.rs | 2 |
14 files changed, 471 insertions, 48 deletions
diff --git a/src/app/agent_resume.rs b/src/app/agent_resume.rs index 408f5983..45a2e3fa 100644 --- a/src/app/agent_resume.rs +++ b/src/app/agent_resume.rs @@ -410,6 +410,7 @@ mod tests { g: 20, b: 20, }), + ..Default::default() }; assert!(app.start_pending_agent_resumes(false)); @@ -511,6 +512,7 @@ mod tests { g: 20, b: 20, }), + ..Default::default() }; for terminal_id in [&active_terminal, &hidden_terminal] { app.state @@ -575,6 +577,7 @@ mod tests { g: 20, b: 20, }), + ..Default::default() }; app.state .terminals @@ -635,6 +638,7 @@ mod tests { g: 20, b: 20, }), + ..Default::default() }; app.state .terminals @@ -692,6 +696,7 @@ mod tests { g: 20, b: 20, }), + ..Default::default() }; app.state .terminals @@ -752,6 +757,7 @@ mod tests { g: 20, b: 20, }), + ..Default::default() }; app.state .terminals diff --git a/src/app/mod.rs b/src/app/mod.rs index b56c128c..f6dbe261 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1702,6 +1702,11 @@ impl App { self.update_host_terminal_theme(kind, color); } } + crate::raw_input::RawInputEvent::HostPaletteColors { colors } => { + if apply_host_terminal_theme { + self.update_host_terminal_palette_colors(&colors); + } + } crate::raw_input::RawInputEvent::HostColorSchemeChanged(appearance) => { if apply_host_terminal_theme { self.set_host_terminal_appearance(appearance, true); @@ -5797,7 +5802,7 @@ last_pane = "prefix+tab" fn route_client_input_updates_host_terminal_theme_from_osc_response() { let mut app = test_app(); - app.route_client_input(b"\x1b]11;#123456\x07".to_vec()); + app.route_client_input(b"\x1b]11;#123456\x07\x1b]4;7;rgb:aaaa/bbbb/cccc\x1b\\".to_vec()); assert_eq!( app.state.host_terminal_theme.background, @@ -5807,6 +5812,34 @@ last_pane = "prefix+tab" b: 0x56, }) ); + assert_eq!( + app.state.host_terminal_theme.palette[7], + Some(crate::terminal_theme::RgbColor { + r: 0xaa, + g: 0xbb, + b: 0xcc, + }) + ); + + app.route_client_input(crate::raw_input::GHOSTTY_COLOR_SCHEME_DARK_REPORT.to_vec()); + assert_eq!( + app.state.host_terminal_theme.palette[7], + Some(crate::terminal_theme::RgbColor { + r: 0xaa, + g: 0xbb, + b: 0xcc, + }) + ); + + app.route_client_input(b"\x1b]4;7;rgb:dddd/eeee/ffff\x1b\\".to_vec()); + assert_eq!( + app.state.host_terminal_theme.palette[7], + Some(crate::terminal_theme::RgbColor { + r: 0xdd, + g: 0xee, + b: 0xff, + }) + ); } #[tokio::test] diff --git a/src/app/runtime.rs b/src/app/runtime.rs index 269de37d..97054f4b 100644 --- a/src/app/runtime.rs +++ b/src/app/runtime.rs @@ -224,6 +224,9 @@ impl App { crate::raw_input::RawInputEvent::HostDefaultColor { kind, color } => { self.update_host_terminal_theme(kind, color) } + crate::raw_input::RawInputEvent::HostPaletteColors { colors } => { + self.update_host_terminal_palette_colors(&colors) + } crate::raw_input::RawInputEvent::HostColorSchemeChanged(appearance) => { self.query_host_terminal_theme(); self.set_host_terminal_appearance(appearance, true) @@ -1131,6 +1134,7 @@ mod tests { g: 20, b: 20, }), + ..Default::default() }; let terminal_id = app.state.workspaces[0] .terminal_id(pane_id) diff --git a/src/app/theme_sync.rs b/src/app/theme_sync.rs index 392d6f59..c41ca24c 100644 --- a/src/app/theme_sync.rs +++ b/src/app/theme_sync.rs @@ -6,8 +6,8 @@ impl App { pub(super) fn query_host_terminal_theme(&self) { use std::io::Write; - let _ = std::io::stdout() - .write_all(crate::terminal_theme::HOST_COLOR_QUERY_SEQUENCE.as_bytes()); + let query = crate::terminal_theme::host_terminal_theme_query_sequence(); + let _ = std::io::stdout().write_all(query.as_bytes()); let _ = std::io::stdout().flush(); } @@ -26,6 +26,17 @@ impl App { changed | self.set_host_terminal_theme(next_theme) } + pub(super) fn update_host_terminal_palette_colors( + &mut self, + colors: &[(u8, crate::terminal_theme::RgbColor)], + ) -> bool { + let mut next_theme = self.state.host_terminal_theme; + for &(index, color) in colors { + next_theme = next_theme.with_palette_color(index, color); + } + self.set_host_terminal_theme(next_theme) + } + pub(super) fn set_host_terminal_appearance( &mut self, appearance: crate::terminal_theme::HostAppearance, diff --git a/src/client/input.rs b/src/client/input.rs index c37a15c7..28d38bda 100644 --- a/src/client/input.rs +++ b/src/client/input.rs @@ -71,18 +71,18 @@ fn unix_stdin_reader_loop( framer.host_color_query_sent(); framer.enable_host_color_scheme_change_tracking(); } + let mut pending_palette = Vec::new(); while !should_quit.load(Ordering::Acquire) { match reader.read(&mut scratch) { Ok(0) => break, Ok(n) => { - for data in framer.push(&scratch[..n]) { - if event_tx - .blocking_send(ClientLoopEvent::StdinInput(data)) - .is_err() - { - return; - } + if !send_unix_input_chunks( + framer.push(&scratch[..n]), + &event_tx, + &mut pending_palette, + ) { + return; } let timeout_ms = idle_flush_timeout_ms( @@ -93,28 +93,23 @@ fn unix_stdin_reader_loop( let had_pending = framer.has_pending_input(); let chunks = framer.flush_timeout(); let held_escape = had_pending && chunks.is_empty(); - for data in chunks { - if event_tx - .blocking_send(ClientLoopEvent::StdinInput(data)) - .is_err() - { - return; - } + if !send_unix_input_chunks(chunks, &event_tx, &mut pending_palette) + || !flush_unix_palette_input(&event_tx, &mut pending_palette) + { + return; } if held_escape && stdin_read_ready( &reader, crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS, ) == Some(false) + && !send_unix_input_chunks( + framer.flush_timeout(), + &event_tx, + &mut pending_palette, + ) { - for data in framer.flush_timeout() { - if event_tx - .blocking_send(ClientLoopEvent::StdinInput(data)) - .is_err() - { - return; - } - } + return; } } } @@ -129,6 +124,56 @@ fn unix_stdin_reader_loop( } #[cfg(unix)] +fn send_unix_input_chunks( + chunks: Vec<Vec<u8>>, + event_tx: &mpsc::Sender<ClientLoopEvent>, + pending_palette: &mut Vec<Vec<u8>>, +) -> bool { + for data in chunks { + let palette_response = std::str::from_utf8(&data) + .ok() + .and_then(crate::terminal_theme::parse_palette_color_response) + .is_some(); + if palette_response { + pending_palette.push(data); + if pending_palette.len() == 256 && !flush_unix_palette_input(event_tx, pending_palette) + { + return false; + } + continue; + } + let default_color_response = std::str::from_utf8(&data) + .ok() + .and_then(crate::terminal_theme::parse_default_color_response) + .is_some(); + if !default_color_response && !flush_unix_palette_input(event_tx, pending_palette) { + return false; + } + if event_tx + .blocking_send(ClientLoopEvent::StdinInput(data)) + .is_err() + { + return false; + } + } + true +} + +#[cfg(unix)] +fn flush_unix_palette_input( + event_tx: &mpsc::Sender<ClientLoopEvent>, + pending_palette: &mut Vec<Vec<u8>>, +) -> bool { + if pending_palette.is_empty() { + return true; + } + let data = std::mem::take(pending_palette).concat(); + event_tx + .blocking_send(ClientLoopEvent::StdinInput(data)) + .is_ok() +} + +#[cfg(unix)] fn idle_flush_timeout_ms( framer: &crate::raw_input::RawInputByteFramer, host_mouse_capture_active: bool, @@ -357,6 +402,7 @@ fn windows_client_input_event_from_raw( Some(crate::protocol::ClientInputEvent::FocusLost) } crate::raw_input::RawInputEvent::HostDefaultColor { .. } + | crate::raw_input::RawInputEvent::HostPaletteColors { .. } | crate::raw_input::RawInputEvent::HostColorSchemeChanged(_) | crate::raw_input::RawInputEvent::Unsupported => None, } @@ -420,6 +466,33 @@ mod tests { } #[test] + fn palette_replies_are_forwarded_as_one_input_batch() { + let (tx, mut rx) = mpsc::channel(4); + let mut pending = Vec::new(); + assert!(send_unix_input_chunks( + vec![ + b"\x1b]4;0;rgb:1111/2222/3333\x1b\\".to_vec(), + b"\x1b]4;1;rgb:4444/5555/6666\x1b\\".to_vec(), + ], + &tx, + &mut pending, + )); + assert!(rx.try_recv().is_err()); + + assert!(flush_unix_palette_input(&tx, &mut pending)); + let ClientLoopEvent::StdinInput(data) = rx.try_recv().unwrap() else { + panic!("expected palette input batch"); + }; + assert_eq!( + data.windows(4) + .filter(|window| *window == b"\x1b]4;") + .count(), + 2 + ); + assert!(pending.is_empty()); + } + + #[test] fn raw_input_idle_flush_timeout_keeps_escape_responsive() { let timeout_ms = std::hint::black_box(crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS); assert!(timeout_ms <= 20); diff --git a/src/client/mod.rs b/src/client/mod.rs index a22fd227..b1c363f2 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -2135,7 +2135,8 @@ fn should_query_host_terminal_theme() -> bool { } fn write_host_terminal_theme_query(mut writer: impl io::Write) -> io::Result<()> { - writer.write_all(crate::terminal_theme::HOST_COLOR_QUERY_SEQUENCE.as_bytes())?; + let query = crate::terminal_theme::host_terminal_theme_query_sequence(); + writer.write_all(query.as_bytes())?; writer.flush() } @@ -2412,7 +2413,7 @@ mod tests { write_host_terminal_theme_query(&mut output).unwrap(); assert_eq!( output, - crate::terminal_theme::HOST_COLOR_QUERY_SEQUENCE.as_bytes() + crate::terminal_theme::host_terminal_theme_query_sequence().as_bytes() ); } diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index 3a3a0791..1a2a8cbb 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -337,6 +337,12 @@ impl From<ffi::GhosttyColorRgb> for RgbColor { } } +pub fn default_palette() -> [RgbColor; 256] { + let mut palette = [ffi::GhosttyColorRgb::default(); 256]; + unsafe { ffi::ghostty_color_palette_default(palette.as_mut_ptr()) }; + palette.map(Into::into) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CellColor { Palette(u8), @@ -770,6 +776,22 @@ impl Terminal { } } + pub fn set_default_palette(&mut self, palette: &[RgbColor; 256]) -> Result<(), Error> { + let palette = palette.map(|color| ffi::GhosttyColorRgb { + r: color.r, + g: color.g, + b: color.b, + }); + unsafe { + ffi::ghostty_terminal_set( + self.raw, + ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_COLOR_PALETTE, + palette.as_ptr().cast(), + ) + .into_result() + } + } + pub fn resize( &mut self, cols: u16, diff --git a/src/pane/osc.rs b/src/pane/osc.rs index 812c8edd..16ee8fee 100644 --- a/src/pane/osc.rs +++ b/src/pane/osc.rs @@ -891,6 +891,7 @@ mod tests { g: colors.background.g, b: colors.background.b, }), + ..Default::default() } } @@ -1486,6 +1487,7 @@ mod tests { g: 0x22, b: 0x33, }), + ..Default::default() }; pane.apply_host_terminal_theme(host_theme); diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 261f7025..23dcffff 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -940,6 +940,21 @@ impl GhosttyPaneTerminal { if foreground_unowned && background_unowned { core.transient_default_color_owner_pgid = None; } + + let mut palette = crate::ghostty::default_palette(); + for (index, color) in theme.palette.iter().enumerate() { + if let Some(color) = color { + palette[index] = crate::ghostty::RgbColor { + r: color.r, + g: color.g, + b: color.b, + }; + } + } + if let Err(err) = core.terminal.set_default_palette(&palette) { + debug!(err = %err, "failed to apply host terminal palette"); + } + write_host_terminal_theme_selective( &mut core.terminal, theme, @@ -3559,6 +3574,7 @@ mod tests { g: 0x22, b: 0x33, }), + ..Default::default() }; } let core = pane.core.lock().unwrap(); @@ -3585,6 +3601,7 @@ mod tests { g: 0x22, b: 0x33, }), + ..Default::default() }; } let core = pane.core.lock().unwrap(); @@ -4837,6 +4854,7 @@ mod tests { g: 0x2b, b: 0x36, }), + ..Default::default() }); let result = pane.process_pty_bytes(pane_id, 0, b"\x1bP+q5463\x1b\\\x1b]11;?\x07", &tx); @@ -4868,6 +4886,7 @@ mod tests { g: 0xbb, b: 0xcc, }), + ..Default::default() }); let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]11;?\x07", &tx); @@ -4893,6 +4912,7 @@ mod tests { g: 0xbb, b: 0xcc, }), + ..Default::default() }); pane.process_pty_bytes(pane_id, 0, b"\x1b]111\x07", &tx); assert!(!pane.has_transient_default_color_override()); @@ -5049,6 +5069,7 @@ mod tests { g: 0x2b, b: 0x36, }), + ..Default::default() }); let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]11;?\x07\x1b[c", &tx); @@ -5063,23 +5084,110 @@ mod tests { } #[test] - fn process_pty_bytes_returns_palette_color_query_response_without_queuing_input() { + fn process_pty_bytes_returns_host_palette_color_without_queuing_input() { 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); - let color = current_palette_color(&pane, 0); + pane.apply_host_terminal_theme( + crate::terminal_theme::TerminalTheme::default().with_palette_color( + 0, + crate::terminal_theme::RgbColor { + r: 0x11, + g: 0x22, + b: 0x33, + }, + ), + ); let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]4;0;?\x07", &tx); assert_eq!( result.terminal_responses, - vec![expected_osc_rgb_response("4;0", color)] + vec![Bytes::from_static(b"\x1b]4;0;rgb:1111/2222/3333\x1b\\")] ); assert!(rx.try_recv().is_err()); } #[test] + fn opentui_256_palette_query_burst_uses_host_snapshot() { + use std::fmt::Write as _; + + let (tx, _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); + let mut theme = crate::terminal_theme::TerminalTheme::default(); + let mut queries = String::new(); + for index in 0..=u8::MAX { + theme = theme.with_palette_color( + index, + crate::terminal_theme::RgbColor { + r: index, + g: 0x22, + b: 0x33, + }, + ); + let _ = write!(queries, "\x1b]4;{index};?\x07"); + } + pane.apply_host_terminal_theme(theme); + + let result = pane.process_pty_bytes(pane_id, 0, queries.as_bytes(), &tx); + + assert_eq!(result.terminal_responses.len(), 256); + assert_eq!( + result.terminal_responses[0], + Bytes::from_static(b"\x1b]4;0;rgb:0000/2222/3333\x1b\\") + ); + assert_eq!( + result.terminal_responses[255], + Bytes::from_static(b"\x1b]4;255;rgb:ffff/2222/3333\x1b\\") + ); + } + + #[test] + fn child_palette_override_survives_host_refresh_until_reset() { + let (tx, _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); + pane.apply_host_terminal_theme( + crate::terminal_theme::TerminalTheme::default().with_palette_color( + 7, + crate::terminal_theme::RgbColor { + r: 0x11, + g: 0x22, + b: 0x33, + }, + ), + ); + pane.process_pty_bytes(pane_id, 0, b"\x1b]4;7;rgb:aa/bb/cc\x1b\\", &tx); + + pane.apply_host_terminal_theme( + crate::terminal_theme::TerminalTheme::default().with_palette_color( + 7, + crate::terminal_theme::RgbColor { + r: 0x44, + g: 0x55, + b: 0x66, + }, + ), + ); + let overridden = pane.process_pty_bytes(pane_id, 0, b"\x1b]4;7;?\x1b\\", &tx); + assert_eq!( + overridden.terminal_responses, + vec![Bytes::from_static(b"\x1b]4;7;rgb:aaaa/bbbb/cccc\x1b\\")] + ); + + pane.process_pty_bytes(pane_id, 0, b"\x1b]104;7\x1b\\", &tx); + let reset = pane.process_pty_bytes(pane_id, 0, b"\x1b]4;7;?\x1b\\", &tx); + assert_eq!( + reset.terminal_responses, + vec![Bytes::from_static(b"\x1b]4;7;rgb:4444/5555/6666\x1b\\")] + ); + } + + #[test] fn process_pty_bytes_returns_split_palette_color_query_response() { let (tx, mut rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); @@ -5142,6 +5250,7 @@ mod tests { g: 0x2b, b: 0x36, }), + ..Default::default() }); let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]4;0;?\x07\x1b]11;?\x07\x1b[c", &tx); @@ -5172,6 +5281,7 @@ mod tests { g: 0x2b, b: 0x36, }), + ..Default::default() }); let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]11;?\x07", &tx); @@ -5200,6 +5310,7 @@ mod tests { g: 0xf6, b: 0xe3, }), + ..Default::default() }); let palette = pane.process_pty_bytes(pane_id, 0, b"\x1b]4;0;?;1;?\x1b\\", &tx); @@ -5298,6 +5409,7 @@ mod tests { b: 0x83, }), background: None, + ..Default::default() }); let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]12;?\x07", &tx); @@ -5322,6 +5434,7 @@ mod tests { b: 0x83, }), background: None, + ..Default::default() }); pane.process_pty_bytes(pane_id, 0, b"\x1b]10;rgb:11/22/33\x07", &tx); @@ -5347,6 +5460,7 @@ mod tests { b: 0x83, }), background: None, + ..Default::default() }); pane.process_pty_bytes(pane_id, 0, b"\x1b]12;rgb:11/22/33\x07", &tx); @@ -5376,6 +5490,7 @@ mod tests { g: 0xf6, b: 0xe3, }), + ..Default::default() }); let result = @@ -5405,6 +5520,7 @@ mod tests { g: 0xf6, b: 0xe3, }), + ..Default::default() }); let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]11", &tx); @@ -5435,6 +5551,7 @@ mod tests { b: 0xe3, }), background: None, + ..Default::default() }); let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]12", &tx); @@ -5465,6 +5582,7 @@ mod tests { g: 0xf6, b: 0xe3, }), + ..Default::default() }); let result = @@ -5499,6 +5617,7 @@ mod tests { g: 0x22, b: 0x33, }), + ..Default::default() }; pane.apply_host_terminal_theme(host_theme); { @@ -5537,6 +5656,7 @@ mod tests { g: 0x22, b: 0x33, }), + ..Default::default() }; pane.apply_host_terminal_theme(host_theme); { @@ -5574,6 +5694,7 @@ mod tests { g: 0x22, b: 0x33, }), + ..Default::default() }; pane.apply_host_terminal_theme(host_theme); { @@ -5611,6 +5732,7 @@ mod tests { g: 0x22, b: 0x33, }), + ..Default::default() }; pane.apply_host_terminal_theme(host_theme); { diff --git a/src/raw_input.rs b/src/raw_input.rs index 575b3ff6..6b59b392 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -96,7 +96,8 @@ use tokio::sync::mpsc; use crate::input::{parse_terminal_key_sequence, TerminalKey}; use crate::terminal_theme::{ - parse_default_color_response, DefaultColorKind, HostAppearance, RgbColor, + parse_default_color_response, parse_palette_color_response, DefaultColorKind, HostAppearance, + RgbColor, }; const ESC: u8 = 0x1b; @@ -133,6 +134,9 @@ pub enum RawInputEvent { kind: DefaultColorKind, color: RgbColor, }, + HostPaletteColors { + colors: Vec<(u8, RgbColor)>, + }, HostColorSchemeChanged(HostAppearance), Unsupported, } @@ -203,13 +207,13 @@ pub(crate) struct RawInputByteFramer { discard_until: Option<ControlStringFamily>, discarded_tail_bytes: usize, lone_escape_recently_flushed: bool, - host_color_replies_awaited: u8, + host_color_replies_awaited: u16, held_pending_color_esc: bool, host_color_scheme_change_tracking: bool, split_coalesced_escape: bool, } -const HOST_COLOR_QUERY_REPLIES: u8 = 2; +const HOST_COLOR_QUERY_REPLIES: u16 = 258; const MAX_ORPHANED_SGR_MOUSE_TAIL_BYTES: usize = 32; impl RawInputByteFramer { @@ -449,7 +453,10 @@ impl RawInputByteFramer { let Some((event, consumed)) = extract_one_event(&self.buffer) else { break; }; - if matches!(event, RawInputEvent::HostDefaultColor { .. }) { + if matches!( + event, + RawInputEvent::HostDefaultColor { .. } | RawInputEvent::HostPaletteColors { .. } + ) { self.host_color_replies_awaited = self.host_color_replies_awaited.saturating_sub(1); } else if self.host_color_scheme_change_tracking && matches!(event, RawInputEvent::HostColorSchemeChanged(_)) @@ -535,23 +542,30 @@ pub fn spawn_input_reader() -> mpsc::Receiver<RawInputEvent> { let mut framer = RawInputFramer::for_host_input(); framer.host_color_query_sent(); framer.enable_host_color_scheme_change_tracking(); + let mut pending_palette = Vec::new(); loop { match reader.read(&mut scratch) { Ok(0) => break, Ok(n) => { - send_raw_input_events(framer.push(&scratch[..n]), &tx); + send_raw_input_events(framer.push(&scratch[..n]), &tx, &mut pending_palette); if stdin_read_ready(&reader, input_flush_timeout_ms(&framer)) == Some(false) { let had_pending = framer.has_pending_input(); let events = framer.flush_timeout(); let held_escape = had_pending && events.is_empty(); - send_raw_input_events(events, &tx); + send_raw_input_events(events, &tx, &mut pending_palette); + flush_host_palette_events(&tx, &mut pending_palette); if held_escape && stdin_read_ready(&reader, RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS) == Some(false) { - send_raw_input_events(framer.flush_timeout(), &tx); + send_raw_input_events( + framer.flush_timeout(), + &tx, + &mut pending_palette, + ); + flush_host_palette_events(&tx, &mut pending_palette); } } } @@ -563,10 +577,39 @@ pub fn spawn_input_reader() -> mpsc::Receiver<RawInputEvent> { rx } -fn send_raw_input_events(events: Vec<RawInputEvent>, tx: &mpsc::Sender<RawInputEvent>) { +fn send_raw_input_events( + events: Vec<RawInputEvent>, + tx: &mpsc::Sender<RawInputEvent>, + pending_palette: &mut Vec<(u8, RgbColor)>, +) { for event in events { - let _ = tx.blocking_send(event); + match event { + RawInputEvent::HostPaletteColors { colors } => { + pending_palette.extend(colors); + if pending_palette.len() == 256 { + flush_host_palette_events(tx, pending_palette); + } + } + event @ RawInputEvent::HostDefaultColor { .. } => { + let _ = tx.blocking_send(event); + } + event => { + flush_host_palette_events(tx, pending_palette); + let _ = tx.blocking_send(event); + } + } + } +} + +fn flush_host_palette_events( + tx: &mpsc::Sender<RawInputEvent>, + pending_palette: &mut Vec<(u8, RgbColor)>, +) { + if pending_palette.is_empty() { + return; } + let colors = std::mem::take(pending_palette); + let _ = tx.blocking_send(RawInputEvent::HostPaletteColors { colors }); } #[cfg(test)] @@ -685,6 +728,14 @@ fn extract_one_event(buffer: &[u8]) -> Option<(RawInputEvent, usize)> { if let Some((kind, color)) = parse_default_color_response(seq) { return Some((RawInputEvent::HostDefaultColor { kind, color }, seq_len)); } + if let Some((index, color)) = parse_palette_color_response(seq) { + return Some(( + RawInputEvent::HostPaletteColors { + colors: vec![(index, color)], + }, + seq_len, + )); + } match seq { "\x1b[I" => return Some((RawInputEvent::OuterFocusGained, seq_len)), @@ -1234,6 +1285,27 @@ mod tests { } #[test] + fn parses_host_palette_color_response() { + let (RawInputEvent::HostPaletteColors { colors }, consumed) = + extract_one_event(b"\x1b]4;7;rgb:1111/2222/3333\x1b\\").unwrap() + else { + panic!("expected host palette response"); + }; + assert_eq!(consumed, 26); + assert_eq!( + colors, + vec![( + 7, + RgbColor { + r: 0x11, + g: 0x22, + b: 0x33, + } + )] + ); + } + + #[test] fn parses_legacy_up_arrow() { let (RawInputEvent::Key(key), consumed) = extract_one_event(b"\x1b[A").unwrap() else { panic!("expected key"); @@ -2459,6 +2531,7 @@ mod tests { )); assert!(framer.push(b"\x1b").is_empty()); + assert!(framer.flush_timeout().is_empty()); assert_eq!(framer.flush_timeout(), vec![b"\x1b".to_vec()]); } @@ -2484,12 +2557,18 @@ mod tests { #[test] fn stops_holding_lone_escape_after_host_color_reply_completes() { + use std::fmt::Write as _; + let mut framer = RawInputByteFramer::default(); framer.host_color_query_sent(); + let mut replies = + String::from("\x1b]10;rgb:6565/7b7b/8383\x1b\\\x1b]11;rgb:2424/2727/3a3a\x1b\\"); + for index in 0..=u8::MAX { + let _ = write!(replies, "\x1b]4;{index};rgb:1111/2222/3333\x1b\\"); + } - let chunks = - framer.push(b"\x1b]10;rgb:6565/7b7b/8383\x1b\\\x1b]11;rgb:2424/2727/3a3a\x1b\\"); - assert_eq!(chunks.len(), 2); + let chunks = framer.push(replies.as_bytes()); + assert_eq!(chunks.len(), 258); // Window closed: a later lone Escape flushes immediately. assert!(framer.push(b"\x1b").is_empty()); diff --git a/src/server/clients.rs b/src/server/clients.rs index b1a56f0e..3782bd15 100644 --- a/src/server/clients.rs +++ b/src/server/clients.rs @@ -198,6 +198,11 @@ impl ClientConnection { self.set_host_appearance(Some(color.inferred_appearance()), false); } } + crate::raw_input::RawInputEvent::HostPaletteColors { colors } => { + for &(index, color) in colors { + next_theme = next_theme.with_palette_color(index, color); + } + } crate::raw_input::RawInputEvent::HostColorSchemeChanged(appearance) => { changed |= self.set_host_appearance(Some(*appearance), true); } diff --git a/src/server/headless.rs b/src/server/headless.rs index 6817e2ad..f330fbec 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -2615,11 +2615,9 @@ impl HeadlessServer { self.resize_shared_runtime_to_effective_size_before_input(); } let theme_changed = self.update_client_host_theme_from_events(client_id, &events); - self.app.route_client_events_from( - client_id, - events, - self.foreground_client_id == Some(client_id), - ); + // Client-local theme reports were applied above; routing them again would update every + // pane once per palette entry instead of once per captured batch. + self.app.route_client_events_from(client_id, events, false); if self.app.take_config_reloaded_from_disk() { self.reload_server_config(false); } else { @@ -5965,6 +5963,7 @@ next_tab = "" g: 20, b: 20, }), + ..Default::default() }; server .app @@ -6018,6 +6017,7 @@ next_tab = "" g: 20, b: 20, }), + ..Default::default() }; server .app @@ -6082,6 +6082,7 @@ next_tab = "" g: 20, b: 20, }), + ..Default::default() }; server .app @@ -6517,6 +6518,7 @@ next_tab = "" g: 0x22, b: 0x33, }), + ..Default::default() }, None, 1, @@ -6540,6 +6542,7 @@ next_tab = "" g: 0xee, b: 0xff, }), + ..Default::default() }, None, 2, @@ -6587,6 +6590,7 @@ next_tab = "" g: 0x50, b: 0x60, }), + ..Default::default() }; server.clients.insert( 1, @@ -6637,6 +6641,7 @@ next_tab = "" crate::terminal_theme::TerminalTheme { foreground: None, background: Some(crate::terminal_theme::RgbColor { r: 0, g: 0, b: 0 }), + ..Default::default() }, None, 1, @@ -6656,6 +6661,7 @@ next_tab = "" g: 255, b: 255, }), + ..Default::default() }, None, 2, @@ -6685,6 +6691,7 @@ next_tab = "" g: 0x50, b: 0x60, }), + ..Default::default() }; server.app.state.host_terminal_theme = initial_theme; server.clients.insert( diff --git a/src/terminal_theme.rs b/src/terminal_theme.rs index 5627ab1b..154b36fe 100644 --- a/src/terminal_theme.rs +++ b/src/terminal_theme.rs @@ -22,10 +22,21 @@ impl RgbColor { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TerminalTheme { pub foreground: Option<RgbColor>, pub background: Option<RgbColor>, + pub palette: [Option<RgbColor>; 256], +} + +impl Default for TerminalTheme { + fn default() -> Self { + Self { + foreground: None, + background: None, + palette: [None; 256], + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -47,11 +58,26 @@ impl TerminalTheme { self } + pub fn with_palette_color(mut self, index: u8, color: RgbColor) -> Self { + self.palette[usize::from(index)] = Some(color); + self + } + pub fn is_empty(self) -> bool { self.foreground.is_none() && self.background.is_none() } } +pub fn host_terminal_theme_query_sequence() -> String { + use std::fmt::Write as _; + + let mut sequence = String::from(HOST_COLOR_QUERY_SEQUENCE); + for index in 0..=u8::MAX { + let _ = write!(sequence, "\x1b]4;{index};?\x1b\\"); + } + sequence +} + pub fn parse_default_color_response(sequence: &str) -> Option<(DefaultColorKind, RgbColor)> { let body = sequence.strip_prefix("\x1b]")?; let body = body @@ -66,6 +92,15 @@ pub fn parse_default_color_response(sequence: &str) -> Option<(DefaultColorKind, Some((kind, parse_rgb_color(value)?)) } +pub fn parse_palette_color_response(sequence: &str) -> Option<(u8, RgbColor)> { + let body = sequence.strip_prefix("\x1b]4;")?; + let body = body + .strip_suffix("\x1b\\") + .or_else(|| body.strip_suffix('\u{7}'))?; + let (index, value) = body.split_once(';')?; + Some((index.parse().ok()?, parse_rgb_color(value)?)) +} + pub fn osc_set_default_color_sequence(kind: DefaultColorKind, color: RgbColor) -> String { let command = match kind { DefaultColorKind::Foreground => 10, @@ -159,6 +194,27 @@ mod tests { } #[test] + fn parses_palette_responses_and_builds_full_query() { + assert_eq!( + parse_palette_color_response("\x1b]4;255;rgb:1111/2222/3333\x1b\\"), + Some(( + 255, + RgbColor { + r: 0x11, + g: 0x22, + b: 0x33, + } + )) + ); + + let query = host_terminal_theme_query_sequence(); + assert!(query.starts_with(HOST_COLOR_QUERY_SEQUENCE)); + assert!(query.contains("\x1b]4;0;?\x1b\\")); + assert!(query.ends_with("\x1b]4;255;?\x1b\\")); + assert_eq!(query.matches("\x1b]4;").count(), 256); + } + + #[test] fn default_color_reset_sequences_use_xterm_osc_numbers() { assert_eq!( osc_reset_default_color_sequence(DefaultColorKind::Foreground), diff --git a/src/ui/panes.rs b/src/ui/panes.rs index 981f3246..5233060d 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -1400,6 +1400,7 @@ mod tests { g: 14, b: 16, }), + ..Default::default() }; let expected_style = automatic_selection_style(&palette, host_theme); let selection = Some(Selection::range(PaneId::from_raw(1), 0, 0, 2, None)); @@ -1465,6 +1466,7 @@ mod tests { g: 14, b: 16, }), + ..Default::default() }, ); |
