diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:28:05 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:29:28 +0300 |
| commit | 4a3302d19a2baa3a33e910e0088c24e780905b90 (patch) | |
| tree | dcaf76fae1c82102d933c9225b17ca7ac5d3a847 /src/pane | |
| parent | 550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (diff) | |
chore: update vendored libghostty-vt
refs #276
Diffstat (limited to 'src/pane')
| -rw-r--r-- | src/pane/osc.rs | 90 | ||||
| -rw-r--r-- | src/pane/terminal.rs | 252 |
2 files changed, 295 insertions, 47 deletions
diff --git a/src/pane/osc.rs b/src/pane/osc.rs index 838515e0..f7cf9eb9 100644 --- a/src/pane/osc.rs +++ b/src/pane/osc.rs @@ -144,10 +144,9 @@ impl DefaultColorOscTracker { } fn is_default_color_set_osc(body: &[u8]) -> bool { - matches!( - parse_default_color_event(body), - Some(DefaultColorEvent::Set(_)) - ) + parse_default_color_events(body) + .iter() + .any(|event| matches!(event, DefaultColorEvent::Set(_))) } #[derive(Debug, Default)] @@ -233,27 +232,43 @@ impl DefaultColorEventTracker { } fn finalize(&mut self, end_offset: usize) { - if let Some(event) = parse_default_color_event(&self.body) { - self.pending - .push(DefaultColorTrackedEvent { end_offset, event }); - } + self.pending.extend( + parse_default_color_events(&self.body) + .into_iter() + .map(|event| DefaultColorTrackedEvent { end_offset, event }), + ); self.body.clear(); } + pub(super) fn in_progress_event(&self) -> Option<DefaultColorEvent> { + if !matches!( + self.state, + DefaultColorOscTrackerState::OscBody | DefaultColorOscTrackerState::OscEscape + ) { + return None; + } + let mut events = parse_default_color_events(&self.body); + (events.len() == 1).then(|| events.remove(0)) + } + pub(super) fn drain_pending(&mut self) -> Vec<DefaultColorTrackedEvent> { std::mem::take(&mut self.pending) } } -fn parse_default_color_event(body: &[u8]) -> Option<DefaultColorEvent> { - match body { +fn parse_default_color_events(body: &[u8]) -> Vec<DefaultColorEvent> { + let single = match body { b"10;?" => Some(DefaultColorEvent::Query(DefaultColorQuery::Foreground)), b"11;?" => Some(DefaultColorEvent::Query(DefaultColorQuery::Background)), b"12;?" => Some(DefaultColorEvent::Query(DefaultColorQuery::Cursor)), b"110" | b"110;" => Some(DefaultColorEvent::Reset(DefaultColorQuery::Foreground)), b"111" | b"111;" => Some(DefaultColorEvent::Reset(DefaultColorQuery::Background)), - _ => parse_palette_color_query(body).or_else(|| parse_default_color_set_event(body)), + _ => parse_palette_color_query(body), + }; + if let Some(event) = single { + return vec![event]; } + parse_default_color_set_events(body) } fn parse_palette_color_query(body: &[u8]) -> Option<DefaultColorEvent> { @@ -270,15 +285,33 @@ fn parse_palette_color_query(body: &[u8]) -> Option<DefaultColorEvent> { .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] { - b"10" => DefaultColorQuery::Foreground, - b"11" => DefaultColorQuery::Background, - _ => return None, +fn parse_default_color_set_events(body: &[u8]) -> Vec<DefaultColorEvent> { + let Some(separator) = body.iter().position(|byte| *byte == b';') else { + return Vec::new(); + }; + let start = match &body[..separator] { + b"10" => 10, + b"11" => 11, + b"12" => 12, + _ => return Vec::new(), }; - let value = &body[separator + 1..]; - (!value.is_empty() && value != b"?").then_some(DefaultColorEvent::Set(query)) + body[separator + 1..] + .split(|byte| *byte == b';') + .filter(|value| !value.is_empty()) + .enumerate() + .filter_map(|(offset, value)| { + if value == b"?" { + return None; + } + let query = match start + offset { + 10 => DefaultColorQuery::Foreground, + 11 => DefaultColorQuery::Background, + 12 => DefaultColorQuery::Cursor, + _ => return None, + }; + Some(DefaultColorEvent::Set(query)) + }) + .collect() } /// 256 KiB of base64 ≈ 192 KiB of text — enough for real source-file copies @@ -1359,6 +1392,25 @@ mod tests { } #[test] + fn default_color_event_tracker_tracks_each_multi_value_set() { + let mut tracker = DefaultColorEventTracker::default(); + + tracker.observe( + b"\x1b]10;rgb:11/22/33;rgb:44/55/66\x1b\\\x1b]10;?;rgb:77/88/99\x1b\\\x1b]10;;rgb:aa/bb/cc\x1b\\", + ); + + assert_eq!( + tracked_default_color_events(tracker.drain_pending()), + vec![ + DefaultColorEvent::Set(DefaultColorQuery::Foreground), + DefaultColorEvent::Set(DefaultColorQuery::Background), + DefaultColorEvent::Set(DefaultColorQuery::Background), + DefaultColorEvent::Set(DefaultColorQuery::Foreground), + ] + ); + } + + #[test] fn default_color_event_tracker_handles_split_default_color_queries() { let mut tracker = DefaultColorEventTracker::default(); diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 06cdea35..cf7a82f4 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -1107,6 +1107,7 @@ impl GhosttyPaneTerminal { core.xtgettcap_query_tracker .observe(filtered_bytes.as_ref()); core.decscusr_tracker.observe(filtered_bytes.as_ref()); + let in_progress_default_color_event = core.default_color_event_tracker.in_progress_event(); let default_color_events = core.default_color_event_tracker.drain_pending(); let xtgettcap_responses = core.xtgettcap_query_tracker.drain_pending(); let write_started = crate::render_prof::timer(); @@ -1114,6 +1115,7 @@ impl GhosttyPaneTerminal { &mut core, filtered_bytes.as_ref(), default_color_events, + in_progress_default_color_event, xtgettcap_responses, &mut terminal_responses, ); @@ -1177,6 +1179,7 @@ impl GhosttyPaneTerminal { core: &mut GhosttyPaneCore, bytes: &[u8], default_color_events: Vec<DefaultColorTrackedEvent>, + in_progress_default_color_event: Option<DefaultColorEvent>, xtgettcap_responses: Vec<XtgettcapResponse>, terminal_responses: &mut Vec<Bytes>, ) { @@ -1196,16 +1199,26 @@ impl GhosttyPaneTerminal { let mut written = 0; for event in events { let end_offset = event.end_offset().min(bytes.len()); + let mut libghostty_responses = Vec::new(); if end_offset > written { core.terminal.write(&bytes[written..end_offset]); - terminal_responses.extend(self.drain_pending_pty_responses()); + libghostty_responses = self.drain_pending_pty_responses(); written = end_offset; } match event { OrderedPtyResponseEvent::DefaultColor(event) => { - respond_to_default_color_event(core, terminal_responses, event.event); + let replacement = respond_to_default_color_event(core, event.event); + if replacement.is_some() { + remove_last_matching_libghostty_color_reply( + &mut libghostty_responses, + event.event, + ); + } + terminal_responses.extend(libghostty_responses); + terminal_responses.extend(replacement); } OrderedPtyResponseEvent::Xtgettcap(response) => { + terminal_responses.extend(libghostty_responses); terminal_responses.push(response.bytes); } } @@ -1213,7 +1226,13 @@ impl GhosttyPaneTerminal { if written < bytes.len() { core.terminal.write(&bytes[written..]); - terminal_responses.extend(self.drain_pending_pty_responses()); + let mut libghostty_responses = self.drain_pending_pty_responses(); + if let Some(event) = in_progress_default_color_event { + if default_color_event_response(core, event).is_some() { + remove_last_matching_libghostty_color_reply(&mut libghostty_responses, event); + } + } + terminal_responses.extend(libghostty_responses); } if !core.child_default_foreground_changed && !core.child_default_background_changed { @@ -1777,7 +1796,7 @@ impl GhosttyPaneTerminal { Ok(rows) => rows, Err(_) => return, }; - let mut grapheme_codepoints = Vec::new(); + let mut grapheme_bytes = Vec::new(); let mut symbol_scratch = String::new(); let mut y = 0u16; while y < area.height && rows.next() { @@ -1800,7 +1819,7 @@ impl GhosttyPaneTerminal { &cells, basic.wide, hide_kitty_placeholders, - &mut grapheme_codepoints, + &mut grapheme_bytes, &mut symbol_scratch, ) { Ok(symbol) => symbol, @@ -2031,7 +2050,7 @@ fn ghostty_collect_dirty_patch( let Ok(mut rows) = render_state.populate_row_iterator(&mut row_iterator) else { fallback!("populate_rows_error"); }; - let mut grapheme_codepoints = Vec::new(); + let mut grapheme_bytes = Vec::new(); let mut symbol_scratch = String::new(); let mut patch_rows = Vec::new(); let mut y = 0u16; @@ -2069,7 +2088,7 @@ fn ghostty_collect_dirty_patch( &cells, basic.wide, hide_kitty_placeholders, - &mut grapheme_codepoints, + &mut grapheme_bytes, &mut symbol_scratch, ) { Ok(symbol) => symbol.to_owned(), @@ -2475,7 +2494,7 @@ fn ghostty_buffer_symbol_into<'a>( cells: &crate::ghostty::RowCellIter<'_>, wide: crate::ghostty::CellWide, hide_kitty_placeholders: bool, - grapheme_codepoints: &mut Vec<u32>, + grapheme_bytes: &mut Vec<u8>, symbol_scratch: &'a mut String, ) -> Result<&'a str, crate::ghostty::Error> { symbol_scratch.clear(); @@ -2483,7 +2502,7 @@ fn ghostty_buffer_symbol_into<'a>( crate::ghostty::CellWide::SpacerTail => {} crate::ghostty::CellWide::SpacerHead => symbol_scratch.push(' '), crate::ghostty::CellWide::Narrow | crate::ghostty::CellWide::Wide => { - cells.grapheme_text_into(grapheme_codepoints, symbol_scratch)?; + cells.grapheme_text_into(grapheme_bytes, symbol_scratch)?; let hidden_kitty_placeholder = hide_kitty_placeholders && symbol_scratch.chars().next().map(u32::from) == Some(crate::ghostty::KITTY_UNICODE_PLACEHOLDER); @@ -2640,30 +2659,59 @@ impl OrderedPtyResponseEvent { } } +fn remove_last_matching_libghostty_color_reply( + responses: &mut Vec<Bytes>, + event: DefaultColorEvent, +) { + if let Some(index) = responses + .iter() + .rposition(|response| is_matching_libghostty_color_reply(response, event)) + { + responses.remove(index); + } +} + +fn is_matching_libghostty_color_reply(response: &Bytes, event: DefaultColorEvent) -> bool { + let prefix = match event { + DefaultColorEvent::Query(query) => format!("\x1b]{};rgb:", query.osc_number()), + DefaultColorEvent::PaletteQuery(index) => format!("\x1b]4;{index};rgb:"), + DefaultColorEvent::Set(_) | DefaultColorEvent::Reset(_) => return false, + }; + response.starts_with(prefix.as_bytes()) + && (response.ends_with(b"\x07") || response.ends_with(b"\x1b\\")) +} + fn respond_to_default_color_event( core: &mut GhosttyPaneCore, - terminal_responses: &mut Vec<Bytes>, event: DefaultColorEvent, -) { +) -> Option<Bytes> { match event { - DefaultColorEvent::Query(query) => { - if let Some(response) = default_color_query_response(query, core) { - terminal_responses.push(response); - } + DefaultColorEvent::Query(_) | DefaultColorEvent::PaletteQuery(_) => { + default_color_event_response(core, event) } - 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); + None } - DefaultColorEvent::Set(query) => mark_child_default_color_changed(core, query, true), DefaultColorEvent::Reset(query) => { mark_child_default_color_changed(core, query, false); apply_cached_host_default_color(core, query); + None } } } +fn default_color_event_response( + core: &mut GhosttyPaneCore, + event: DefaultColorEvent, +) -> Option<Bytes> { + match event { + DefaultColorEvent::Query(query) => default_color_query_response(query, core), + DefaultColorEvent::PaletteQuery(index) => palette_color_query_response(index, core), + DefaultColorEvent::Set(_) | DefaultColorEvent::Reset(_) => None, + } +} + fn default_color_query_response( query: DefaultColorQuery, core: &mut GhosttyPaneCore, @@ -4021,7 +4069,7 @@ mod tests { .populate_row_iterator(&mut row_iterator) .unwrap(); let mut row_cells = crate::ghostty::RowCells::new().unwrap(); - let mut grapheme_codepoints = Vec::new(); + let mut grapheme_bytes = Vec::new(); let mut symbol_scratch = String::new(); let mut out = Vec::new(); @@ -4033,7 +4081,7 @@ mod tests { &cells, wide, false, - &mut grapheme_codepoints, + &mut grapheme_bytes, &mut symbol_scratch, ) .unwrap() @@ -4048,7 +4096,6 @@ mod tests { #[test] fn grapheme_cluster_mode_renders_flag_emoji_in_single_wide_cell() { let mut terminal = crate::ghostty::Terminal::new(40, 1, 0).unwrap(); - terminal.enable_grapheme_cluster_mode().unwrap(); terminal.write("🇧🇷".as_bytes()); let cells = render_cells_to_symbols(&mut terminal); @@ -4064,7 +4111,6 @@ mod tests { #[test] fn grapheme_cluster_mode_renders_zwj_family_in_single_wide_cell() { let mut terminal = crate::ghostty::Terminal::new(40, 1, 0).unwrap(); - terminal.enable_grapheme_cluster_mode().unwrap(); terminal.write("👨\u{200d}👩\u{200d}👧".as_bytes()); let cells = render_cells_to_symbols(&mut terminal); @@ -4188,6 +4234,27 @@ mod tests { } #[test] + fn resize_shrinks_both_axes_with_cursor_at_old_bottom() { + let (tx, _rx) = mpsc::channel(4); + let mut terminal = crate::ghostty::Terminal::new(8, 4, 10_000).unwrap(); + terminal.write(b"alpha\r\nbeta\r\ngamma\r\ndelta"); + let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); + + pane.resize(3, 7, 8, 16); + + assert_eq!(pane.visible_text(), "beta\ngamma\ndelta\n"); + assert_eq!(pane.detection_text(), "beta\ngamma\ndelta\n"); + assert_eq!( + pane.scroll_metrics(), + Some(ScrollMetrics { + offset_from_bottom: 0, + max_offset_from_bottom: 1, + viewport_rows: 3, + }) + ); + } + + #[test] fn resize_reflow_keeps_scrolled_viewport_and_bottom_detection_sane() { let (tx, _rx) = mpsc::channel(4); let mut terminal = crate::ghostty::Terminal::new(12, 4, 10_000).unwrap(); @@ -4508,6 +4575,19 @@ mod tests { } #[test] + fn process_pty_bytes_does_not_advertise_unsupported_glyph_protocol() { + 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_25a1;s\x1b\\", &tx); + + assert!(result.terminal_responses.is_empty()); + assert!(rx.try_recv().is_err()); + } + + #[test] fn process_pty_bytes_returns_libghostty_query_responses_without_queuing_input() { let (tx, mut rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); @@ -4655,7 +4735,10 @@ mod tests { }); let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]11;?\x07", &tx); - assert!(result.terminal_responses.is_empty()); + assert_eq!( + result.terminal_responses, + vec![Bytes::from_static(b"\x1b]11;rgb:1111/2222/3333\x07")] + ); assert!(rx.try_recv().is_err()); } @@ -4884,7 +4967,7 @@ mod tests { } #[test] - fn process_pty_bytes_ignores_malformed_palette_color_queries() { + fn process_pty_bytes_ignores_malformed_and_preserves_multi_palette_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(); @@ -4897,7 +4980,15 @@ mod tests { &tx, ); - assert!(result.terminal_responses.is_empty()); + assert_eq!(result.terminal_responses.len(), 1); + assert!(result.terminal_responses[0].starts_with(b"\x1b]4;0;rgb:")); + assert_eq!( + result.terminal_responses[0] + .windows(4) + .filter(|window| *window == b"rgb:") + .count(), + 2 + ); assert!(rx.try_recv().is_err()); } @@ -4957,6 +5048,108 @@ mod tests { } #[test] + fn process_pty_bytes_preserves_untracked_multi_color_query_responses() { + 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); + pane.apply_host_terminal_theme(crate::terminal_theme::TerminalTheme { + foreground: Some(crate::terminal_theme::RgbColor { + r: 0x65, + g: 0x7b, + b: 0x83, + }), + background: Some(crate::terminal_theme::RgbColor { + r: 0xfd, + g: 0xf6, + b: 0xe3, + }), + }); + + let palette = pane.process_pty_bytes(pane_id, 0, b"\x1b]4;0;?;1;?\x1b\\", &tx); + let palette_response = palette.terminal_responses.concat(); + assert!(palette_response.starts_with(b"\x1b]4;0;rgb:")); + assert_eq!( + palette_response + .windows(4) + .filter(|window| *window == b"rgb:") + .count(), + 2 + ); + + let defaults = pane.process_pty_bytes(pane_id, 0, b"\x1b]10;?;?;?\x1b\\", &tx); + let default_response = defaults.terminal_responses.concat(); + assert!( + default_response.starts_with(b"\x1b]10;rgb:"), + "unexpected default-color report: {:?}", + String::from_utf8_lossy(&default_response) + ); + assert_eq!( + default_response + .windows(4) + .filter(|window| *window == b"rgb:") + .count(), + 3 + ); + let core = pane.core.lock().unwrap(); + assert!(!core.child_default_foreground_changed); + assert!(!core.child_default_background_changed); + drop(core); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn process_pty_bytes_preserves_earlier_aggregate_palette_reply() { + 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;0;?;1;?\x1b\\\x1b]4;0;?\x1b\\", &tx); + + assert_eq!(result.terminal_responses.len(), 2); + assert_eq!( + result.terminal_responses[0] + .windows(4) + .filter(|window| *window == b"rgb:") + .count(), + 2 + ); + assert!(result.terminal_responses[1].starts_with(b"\x1b]4;0;rgb:")); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn process_pty_bytes_preserves_libghostty_reply_for_child_color_override() { + 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); + + pane.process_pty_bytes(pane_id, 0, b"\x1b]10;rgb:11/22/33\x07", &tx); + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]10;?\x1b\\", &tx); + + assert_eq!(result.terminal_responses.len(), 1); + assert!(result.terminal_responses[0].starts_with(b"\x1b]10;rgb:1111/2222/3333")); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn process_pty_bytes_tracks_later_multi_value_color_set() { + 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.process_pty_bytes(pane_id, 0, b"\x1b]10;?;rgb:44/55/66\x1b\\", &tx); + + let core = pane.core.lock().unwrap(); + assert!(!core.child_default_foreground_changed); + assert!(core.child_default_background_changed); + } + + #[test] fn process_pty_bytes_returns_cursor_color_query_response_from_foreground_fallback() { let (tx, mut rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); @@ -5140,7 +5333,10 @@ mod tests { let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]11;rgb:11/22/33\x07\x1b]11;?\x07", &tx); - assert!(result.terminal_responses.is_empty()); + assert_eq!( + result.terminal_responses, + vec![Bytes::from_static(b"\x1b]11;rgb:1111/2222/3333\x07")] + ); assert!(rx.try_recv().is_err()); let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]111\x07\x1b]11;?\x07", &tx); |
