aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-07-24 01:28:35 +0300
committerGitHub <noreply@github.com>2026-07-24 01:28:35 +0300
commit503613c8ecbea752a234daf30cb0ad5a8bf93e4e (patch)
tree34b71e4c7055244ed191c043457c572e3ccfc935 /src
parent58ad2c6438cf25cdf7e95528c7114d3bf12626d2 (diff)
fix: inherit host palette in pane queries (#1759)
refs #1752 Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/app/agent_resume.rs6
-rw-r--r--src/app/mod.rs35
-rw-r--r--src/app/runtime.rs4
-rw-r--r--src/app/theme_sync.rs15
-rw-r--r--src/client/input.rs117
-rw-r--r--src/client/mod.rs5
-rw-r--r--src/ghostty/mod.rs22
-rw-r--r--src/pane/osc.rs2
-rw-r--r--src/pane/terminal.rs128
-rw-r--r--src/raw_input.rs103
-rw-r--r--src/server/clients.rs5
-rw-r--r--src/server/headless.rs17
-rw-r--r--src/terminal_theme.rs58
-rw-r--r--src/ui/panes.rs2
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()
},
);