parent
748c3fbb9a
commit
53e4b27165
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
### Fixed
|
||||
- Pane input no longer waits behind the PTY actor's idle read poll, restoring responsive typing at quiet shell prompts. (#379)
|
||||
- Pane apps that query OSC 4 ANSI palette colors now receive the active terminal palette response, so OpenCode and similar TUIs can enable system-theme behavior inside Herdr. (#387)
|
||||
|
||||
## [0.6.6] - 2026-05-31
|
||||
|
||||
|
|
|
|||
|
|
@ -372,6 +372,7 @@ fn cell_color_from_style_color(color: ffi::GhosttyStyleColor) -> Option<CellColo
|
|||
pub struct RenderColors {
|
||||
pub background: RgbColor,
|
||||
pub foreground: RgbColor,
|
||||
pub palette: [RgbColor; 256],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -1870,6 +1871,7 @@ impl RenderState {
|
|||
Ok(RenderColors {
|
||||
background: colors.background.into(),
|
||||
foreground: colors.foreground.into(),
|
||||
palette: colors.palette.map(Into::into),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ pub(super) enum DefaultColorEvent {
|
|||
Query(DefaultColorQuery),
|
||||
Set(DefaultColorQuery),
|
||||
Reset(DefaultColorQuery),
|
||||
PaletteQuery(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
@ -240,10 +241,24 @@ fn parse_default_color_event(body: &[u8]) -> Option<DefaultColorEvent> {
|
|||
b"11;?" => Some(DefaultColorEvent::Query(DefaultColorQuery::Background)),
|
||||
b"110" | b"110;" => Some(DefaultColorEvent::Reset(DefaultColorQuery::Foreground)),
|
||||
b"111" | b"111;" => Some(DefaultColorEvent::Reset(DefaultColorQuery::Background)),
|
||||
_ => parse_default_color_set_event(body),
|
||||
_ => parse_palette_color_query(body).or_else(|| parse_default_color_set_event(body)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_palette_color_query(body: &[u8]) -> Option<DefaultColorEvent> {
|
||||
let index = body.strip_prefix(b"4;")?.strip_suffix(b";?")?;
|
||||
if index.is_empty() || index.len() > 3 || !index.iter().all(|byte| byte.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
let mut value: u16 = 0;
|
||||
for &digit in index {
|
||||
value = value * 10 + u16::from(digit - b'0');
|
||||
}
|
||||
u8::try_from(value)
|
||||
.ok()
|
||||
.map(DefaultColorEvent::PaletteQuery)
|
||||
}
|
||||
|
||||
fn parse_default_color_set_event(body: &[u8]) -> Option<DefaultColorEvent> {
|
||||
let separator = body.iter().position(|byte| *byte == b';')?;
|
||||
let query = match &body[..separator] {
|
||||
|
|
@ -576,13 +591,16 @@ mod tests {
|
|||
fn default_color_event_tracker_detects_queries_sets_and_resets() {
|
||||
let mut tracker = DefaultColorEventTracker::default();
|
||||
|
||||
tracker.observe(b"\x1b]10;?\x07\x1b]11;?\x1b\\\x1b]10;rgb:11/22/33\x07\x1b]111\x07");
|
||||
tracker.observe(
|
||||
b"\x1b]10;?\x07\x1b]11;?\x1b\\\x1b]4;0;?\x07\x1b]10;rgb:11/22/33\x07\x1b]111\x07",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
tracker.drain_pending(),
|
||||
vec![
|
||||
DefaultColorEvent::Query(DefaultColorQuery::Foreground),
|
||||
DefaultColorEvent::Query(DefaultColorQuery::Background),
|
||||
DefaultColorEvent::PaletteQuery(0),
|
||||
DefaultColorEvent::Set(DefaultColorQuery::Foreground),
|
||||
DefaultColorEvent::Reset(DefaultColorQuery::Background),
|
||||
]
|
||||
|
|
@ -590,7 +608,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn default_color_event_tracker_handles_split_queries() {
|
||||
fn default_color_event_tracker_handles_split_default_color_queries() {
|
||||
let mut tracker = DefaultColorEventTracker::default();
|
||||
|
||||
tracker.observe(b"\x1b]11");
|
||||
|
|
@ -605,6 +623,39 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_color_event_tracker_handles_split_palette_color_queries() {
|
||||
let mut tracker = DefaultColorEventTracker::default();
|
||||
|
||||
tracker.observe(b"\x1b]4;25");
|
||||
assert!(tracker.drain_pending().is_empty());
|
||||
tracker.observe(b"5;?\x1b");
|
||||
assert!(tracker.drain_pending().is_empty());
|
||||
tracker.observe(b"\\");
|
||||
|
||||
assert_eq!(
|
||||
tracker.drain_pending(),
|
||||
vec![DefaultColorEvent::PaletteQuery(255)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_color_event_tracker_rejects_malformed_palette_color_queries() {
|
||||
let mut tracker = DefaultColorEventTracker::default();
|
||||
|
||||
tracker.observe(b"\x1b]4;;?\x07");
|
||||
tracker.observe(b"\x1b]4;-1;?\x07");
|
||||
tracker.observe(b"\x1b]4;256;?\x07");
|
||||
tracker.observe(b"\x1b]4;0;?;1;?\x07");
|
||||
tracker.observe(b"\x1b]4;0;rgb:1111/2222/3333\x07");
|
||||
tracker.observe(b"\x1b]4;0;?\x07");
|
||||
|
||||
assert_eq!(
|
||||
tracker.drain_pending(),
|
||||
vec![DefaultColorEvent::PaletteQuery(0)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_color_event_tracker_ignores_other_osc_and_dcs_payloads() {
|
||||
let mut tracker = DefaultColorEventTracker::default();
|
||||
|
|
|
|||
|
|
@ -1305,6 +1305,11 @@ fn respond_to_default_color_events(
|
|||
terminal_responses.push(response);
|
||||
}
|
||||
}
|
||||
DefaultColorEvent::PaletteQuery(index) => {
|
||||
if let Some(response) = palette_color_query_response(index, core) {
|
||||
terminal_responses.push(response);
|
||||
}
|
||||
}
|
||||
DefaultColorEvent::Set(query) => mark_child_default_color_changed(core, query, true),
|
||||
DefaultColorEvent::Reset(query) => mark_child_default_color_changed(core, query, false),
|
||||
}
|
||||
|
|
@ -1321,13 +1326,36 @@ fn default_color_query_response(query: DefaultColorQuery, core: &GhosttyPaneCore
|
|||
}
|
||||
_ => None,
|
||||
}?;
|
||||
let r = u16::from(color.r) * 257;
|
||||
let g = u16::from(color.g) * 257;
|
||||
let b = u16::from(color.b) * 257;
|
||||
Some(Bytes::from(format!(
|
||||
"\x1b]{};rgb:{r:04x}/{g:04x}/{b:04x}\x1b\\",
|
||||
query.osc_number()
|
||||
)))
|
||||
Some(osc_rgb_response(
|
||||
&query.osc_number().to_string(),
|
||||
color.r,
|
||||
color.g,
|
||||
color.b,
|
||||
))
|
||||
}
|
||||
|
||||
fn palette_color_query_response(index: u8, core: &mut GhosttyPaneCore) -> Option<Bytes> {
|
||||
let GhosttyPaneCore {
|
||||
terminal,
|
||||
render_state,
|
||||
..
|
||||
} = core;
|
||||
render_state.update(terminal).ok()?;
|
||||
let colors = render_state.colors().ok()?;
|
||||
let color = colors.palette[usize::from(index)];
|
||||
Some(osc_rgb_response(
|
||||
&format!("4;{index}"),
|
||||
color.r,
|
||||
color.g,
|
||||
color.b,
|
||||
))
|
||||
}
|
||||
|
||||
fn osc_rgb_response(command: &str, r: u8, g: u8, b: u8) -> Bytes {
|
||||
let r = u16::from(r) * 257;
|
||||
let g = u16::from(g) * 257;
|
||||
let b = u16::from(b) * 257;
|
||||
Bytes::from(format!("\x1b]{command};rgb:{r:04x}/{g:04x}/{b:04x}\x1b\\"))
|
||||
}
|
||||
|
||||
fn mark_child_default_color_changed(
|
||||
|
|
@ -1456,6 +1484,24 @@ mod tests {
|
|||
terminal.write(b"END");
|
||||
}
|
||||
|
||||
fn current_palette_color(pane: &GhosttyPaneTerminal, index: u8) -> crate::ghostty::RgbColor {
|
||||
let mut core = pane.core.lock().unwrap();
|
||||
let GhosttyPaneCore {
|
||||
terminal,
|
||||
render_state,
|
||||
..
|
||||
} = &mut *core;
|
||||
render_state.update(terminal).unwrap();
|
||||
render_state.colors().unwrap().palette[usize::from(index)]
|
||||
}
|
||||
|
||||
fn expected_osc_rgb_response(command: &str, color: crate::ghostty::RgbColor) -> Bytes {
|
||||
let r = u16::from(color.r) * 257;
|
||||
let g = u16::from(color.g) * 257;
|
||||
let b = u16::from(color.b) * 257;
|
||||
Bytes::from(format!("\x1b]{command};rgb:{r:04x}/{g:04x}/{b:04x}\x1b\\"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decscusr_cursor_shape_preserves_blinking_variants() {
|
||||
assert_eq!(
|
||||
|
|
@ -2317,6 +2363,95 @@ mod tests {
|
|||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pty_bytes_returns_palette_color_query_response_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);
|
||||
|
||||
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)]
|
||||
);
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[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();
|
||||
let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap();
|
||||
let pane_id = PaneId::from_raw(1);
|
||||
let color = current_palette_color(&pane, 255);
|
||||
|
||||
let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]4;25", &tx);
|
||||
assert!(result.terminal_responses.is_empty());
|
||||
assert!(rx.try_recv().is_err());
|
||||
let result = pane.process_pty_bytes(pane_id, 0, b"5;?\x1b", &tx);
|
||||
assert!(result.terminal_responses.is_empty());
|
||||
assert!(rx.try_recv().is_err());
|
||||
let result = pane.process_pty_bytes(pane_id, 0, b"\\", &tx);
|
||||
|
||||
assert_eq!(
|
||||
result.terminal_responses,
|
||||
vec![expected_osc_rgb_response("4;255", color)]
|
||||
);
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pty_bytes_ignores_malformed_palette_color_queries() {
|
||||
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 result = pane.process_pty_bytes(
|
||||
pane_id,
|
||||
0,
|
||||
b"\x1b]4;;?\x07\x1b]4;-1;?\x07\x1b]4;256;?\x07\x1b]4;0;?;1;?\x07\x1b]4;0;rgb:1111/2222/3333\x07",
|
||||
&tx,
|
||||
);
|
||||
|
||||
assert!(result.terminal_responses.is_empty());
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pty_bytes_orders_palette_reply_before_following_terminal_replies() {
|
||||
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 {
|
||||
foreground: None,
|
||||
background: Some(crate::terminal_theme::RgbColor {
|
||||
r: 0x00,
|
||||
g: 0x2b,
|
||||
b: 0x36,
|
||||
}),
|
||||
});
|
||||
|
||||
let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]4;0;?\x07\x1b]11;?\x07\x1b[c", &tx);
|
||||
|
||||
assert_eq!(result.terminal_responses.len(), 3);
|
||||
assert_eq!(
|
||||
result.terminal_responses[0],
|
||||
expected_osc_rgb_response("4;0", color)
|
||||
);
|
||||
assert_eq!(
|
||||
result.terminal_responses[1],
|
||||
Bytes::from_static(b"\x1b]11;rgb:0000/2b2b/3636\x1b\\")
|
||||
);
|
||||
assert!(String::from_utf8_lossy(&result.terminal_responses[2]).contains('c'));
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pty_bytes_returns_default_color_query_responses_without_queuing_input() {
|
||||
let (tx, mut rx) = mpsc::channel(4);
|
||||
|
|
|
|||
Loading…
Reference in New Issue