diff options
Diffstat (limited to 'src/ghostty/mod.rs')
| -rw-r--r-- | src/ghostty/mod.rs | 274 |
1 files changed, 165 insertions, 109 deletions
diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index a65272c6..d2213c37 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -217,9 +217,7 @@ pub struct KittyImageDescriptor { #[derive(Debug, Clone, Copy)] struct KittyImageFingerprintEntry { - transmit_time_ns: u64, - data_ptr: usize, - data_len: usize, + generation: u64, fingerprint: u64, } @@ -449,7 +447,7 @@ struct WritePtyCallbackState { } unsafe extern "C" fn write_pty_trampoline( - _terminal: ffi::GhosttyTerminal_ptr, + _terminal: ffi::GhosttyTerminal, userdata: *mut c_void, data: *const u8, len: usize, @@ -583,7 +581,7 @@ pub fn encode_focus(event: FocusEvent) -> Result<Vec<u8>, Error> { } pub struct Terminal { - raw: ffi::GhosttyTerminal_ptr, + raw: ffi::GhosttyTerminal, write_pty_callback: Option<Box<WritePtyCallbackState>>, kitty_fingerprints: Mutex<HashMap<u32, KittyImageFingerprintEntry>>, } @@ -600,11 +598,22 @@ impl Terminal { unsafe { ffi::ghostty_terminal_new(ptr::null(), &mut raw, options).into_result()?; } - Ok(Self { + + let terminal = Self { raw, write_pty_callback: None, kitty_fingerprints: Mutex::new(HashMap::new()), - }) + }; + let glyph_protocol = false; + unsafe { + ffi::ghostty_terminal_set( + terminal.raw, + ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_GLYPH_PROTOCOL, + (&glyph_protocol as *const bool).cast(), + ) + .into_result()?; + } + Ok(terminal) } pub fn write(&mut self, bytes: &[u8]) { @@ -711,10 +720,6 @@ impl Terminal { unsafe { ffi::ghostty_terminal_mode_set(self.raw, mode, value).into_result() } } - pub fn enable_grapheme_cluster_mode(&mut self) -> Result<(), Error> { - self.mode_set(MODE_GRAPHEME_CLUSTER, true) - } - pub fn kitty_keyboard_flags(&self) -> Result<u8, Error> { let mut out = 0u8; unsafe { @@ -921,7 +926,7 @@ impl Terminal { } fn format_keyboard_state_ansi(&self, kitty_keyboard: bool) -> Result<String, Error> { - let mut formatter: ffi::GhosttyFormatter_ptr = ptr::null_mut(); + let mut formatter: ffi::GhosttyFormatter = ptr::null_mut(); let options = ffi::GhosttyFormatterTerminalOptions { size: mem::size_of::<ffi::GhosttyFormatterTerminalOptions>(), emit: FormatterFormat::Vt.as_raw(), @@ -998,7 +1003,7 @@ impl Terminal { end: end_ref, rectangle, }; - let mut formatter: ffi::GhosttyFormatter_ptr = ptr::null_mut(); + let mut formatter: ffi::GhosttyFormatter = ptr::null_mut(); let options = ffi::GhosttyFormatterTerminalOptions { size: mem::size_of::<ffi::GhosttyFormatterTerminalOptions>(), emit: format.as_raw(), @@ -1200,7 +1205,7 @@ impl Terminal { } /// Fingerprint for `image`, cached per image id and recomputed only when - /// the image's transmit time (or data identity) changes. + /// the image's generation changes. fn kitty_image_fingerprint_cached( &self, image: ffi::GhosttyKittyGraphicsImage, @@ -1211,19 +1216,16 @@ impl Terminal { format: KittyImageFormat, ) -> u64 { let (data_ptr, data_len) = data; - let Ok(transmit_time_ns) = kitty_image_u64( + let Ok(generation) = kitty_image_u64( image, - ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_TRANSMIT_TIME_NS, + ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_GENERATION, ) else { return kitty_image_fingerprint(data_ptr, data_len, image_width, image_height, format); }; if let Ok(cache) = self.kitty_fingerprints.lock() { if let Some(entry) = cache.get(&image_id) { - if entry.transmit_time_ns == transmit_time_ns - && entry.data_ptr == data_ptr as usize - && entry.data_len == data_len - { + if entry.generation == generation { return entry.fingerprint; } } @@ -1235,9 +1237,7 @@ impl Terminal { cache.insert( image_id, KittyImageFingerprintEntry { - transmit_time_ns, - data_ptr: data_ptr as usize, - data_len, + generation, fingerprint, }, ); @@ -1499,7 +1499,7 @@ impl Terminal { Ok(placements) } - fn raw(&self) -> ffi::GhosttyTerminal_ptr { + fn raw(&self) -> ffi::GhosttyTerminal { self.raw } } @@ -2024,7 +2024,7 @@ fn grid_ref_hyperlink_uri(grid_ref: &ffi::GhosttyGridRef) -> Result<Option<Strin } pub struct RenderState { - raw: ffi::GhosttyRenderState_ptr, + raw: ffi::GhosttyRenderState, } impl RenderState { @@ -2140,7 +2140,7 @@ impl RenderState { ffi::ghostty_render_state_get( self.raw, ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, - (&mut iterator.raw as *mut ffi::GhosttyRenderStateRowIterator_ptr).cast(), + (&mut iterator.raw as *mut ffi::GhosttyRenderStateRowIterator).cast(), ) .into_result()?; } @@ -2183,7 +2183,7 @@ impl Drop for RenderState { } pub struct KeyEvent { - raw: ffi::GhosttyKeyEvent_ptr, + raw: ffi::GhosttyKeyEvent, } impl KeyEvent { @@ -2223,7 +2223,7 @@ impl Drop for KeyEvent { } pub struct KeyEncoder { - raw: ffi::GhosttyKeyEncoder_ptr, + raw: ffi::GhosttyKeyEncoder, } impl KeyEncoder { @@ -2254,7 +2254,7 @@ impl Drop for KeyEncoder { } pub struct MouseEvent { - raw: ffi::GhosttyMouseEvent_ptr, + raw: ffi::GhosttyMouseEvent, } impl MouseEvent { @@ -2294,7 +2294,7 @@ impl Drop for MouseEvent { } pub struct MouseEncoder { - raw: ffi::GhosttyMouseEncoder_ptr, + raw: ffi::GhosttyMouseEncoder, } impl MouseEncoder { @@ -2379,7 +2379,7 @@ fn encode_with_retry( } pub struct RowIterator { - raw: ffi::GhosttyRenderStateRowIterator_ptr, + raw: ffi::GhosttyRenderStateRowIterator, } impl RowIterator { @@ -2512,7 +2512,7 @@ impl<'a> RowIter<'a> { ffi::ghostty_render_state_row_get( self.iterator.raw, ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, - (&mut cells.raw as *mut ffi::GhosttyRenderStateRowCells_ptr).cast(), + (&mut cells.raw as *mut ffi::GhosttyRenderStateRowCells).cast(), ) .into_result()?; } @@ -2521,7 +2521,7 @@ impl<'a> RowIter<'a> { } pub struct RowCells { - raw: ffi::GhosttyRenderStateRowCells_ptr, + raw: ffi::GhosttyRenderStateRowCells, } impl RowCells { @@ -2799,51 +2799,63 @@ impl<'a> RowCellIter<'a> { } pub fn grapheme_text(&self) -> Result<String, Error> { - let mut codepoints = Vec::new(); + let mut bytes = Vec::new(); let mut text = String::new(); - self.grapheme_text_into(&mut codepoints, &mut text)?; + self.grapheme_text_into(&mut bytes, &mut text)?; Ok(text) } - pub fn grapheme_text_into( - &self, - codepoints: &mut Vec<u32>, - text: &mut String, - ) -> Result<(), Error> { + pub fn grapheme_text_into(&self, bytes: &mut Vec<u8>, text: &mut String) -> Result<(), Error> { text.clear(); - codepoints.clear(); + bytes.clear(); - let mut len = 0u32; - unsafe { + let mut buffer = ffi::GhosttyBuffer { + ptr: ptr::null_mut(), + cap: 0, + len: 0, + }; + let result = unsafe { ffi::ghostty_render_state_row_cells_get( self.cells.raw, - ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, - (&mut len as *mut u32).cast(), + ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8, + (&mut buffer as *mut ffi::GhosttyBuffer).cast(), ) - .into_result()?; }; + match result { + ffi::GhosttyResult_GHOSTTY_SUCCESS if buffer.len == 0 => { + return self.raw_cell_text_into(text); + } + ffi::GhosttyResult_GHOSTTY_SUCCESS => { + return Err(Error(ffi::GhosttyResult_GHOSTTY_INVALID_VALUE)); + } + ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE => {} + other => return Err(Error(other)), + } - if len == 0 { + if buffer.len == 0 { return self.raw_cell_text_into(text); } - - // Avoid GRAPHEMES_UTF8 here. In libghostty-vt 0.6.7's vendor, its - // required-byte query can underreport multi-codepoint graphemes. - codepoints.resize(len as usize, 0); + bytes.resize(buffer.len, 0); + let mut buffer = ffi::GhosttyBuffer { + ptr: bytes.as_mut_ptr(), + cap: bytes.len(), + len: 0, + }; unsafe { ffi::ghostty_render_state_row_cells_get( self.cells.raw, - ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, - codepoints.as_mut_ptr().cast(), + ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8, + (&mut buffer as *mut ffi::GhosttyBuffer).cast(), ) .into_result()?; } - - for codepoint in codepoints.iter().copied() { - match char::from_u32(codepoint) { - Some(ch) => text.push(ch), - None => text.push(char::REPLACEMENT_CHARACTER), - } + if buffer.len > bytes.len() { + return Err(Error(ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE)); + } + bytes.truncate(buffer.len); + match std::str::from_utf8(bytes) { + Ok(value) => text.push_str(value), + Err(_) => text.push_str(&String::from_utf8_lossy(bytes)), } Ok(()) } @@ -2864,6 +2876,29 @@ mod tests { terminal.write(line.repeat(count).as_bytes()); } + fn first_rendered_row_text(terminal: &Terminal) -> String { + let mut render_state = RenderState::new().unwrap(); + render_state.update(terminal).unwrap(); + let mut row_iterator = RowIterator::new().unwrap(); + let mut rows = render_state + .populate_row_iterator(&mut row_iterator) + .unwrap(); + let mut row_cells = RowCells::new().unwrap(); + let mut bytes = Vec::new(); + let mut cell_text = String::new(); + let mut row_text = String::new(); + + assert!(rows.next()); + let mut cells = rows.populate_cells(&mut row_cells).unwrap(); + while cells.next() { + cells + .grapheme_text_into(&mut bytes, &mut cell_text) + .unwrap(); + row_text.push_str(&cell_text); + } + row_text.trim_end().to_owned() + } + fn build_info_bool(data: ffi::GhosttyBuildInfo) -> bool { let mut out = false; unsafe { @@ -2907,6 +2942,14 @@ mod tests { .kitty_image_placements_with_data_filter(|_| true) .unwrap(); assert_eq!(first.len(), 1); + let first_generation = terminal + .kitty_fingerprints + .lock() + .unwrap() + .get(&7) + .unwrap() + .generation; + assert_ne!(first_generation, 0); // Same id and size, different pixels. terminal.write(b"\x1b_Ga=t,f=32,t=d,i=7,s=1,v=1,q=2;AAAAAA==\x1b\\"); @@ -2915,12 +2958,30 @@ mod tests { .unwrap(); assert_eq!(second.len(), 1); assert_ne!(first[0].data_fingerprint, second[0].data_fingerprint); - - // No retransmission, so the fingerprint stays stable across renders. + let second_generation = terminal + .kitty_fingerprints + .lock() + .unwrap() + .get(&7) + .unwrap() + .generation; + assert_ne!(first_generation, second_generation); + + // No retransmission, so the fingerprint and generation stay stable. let third = terminal .kitty_image_placements_with_data_filter(|_| true) .unwrap(); assert_eq!(second[0].data_fingerprint, third[0].data_fingerprint); + assert_eq!( + terminal + .kitty_fingerprints + .lock() + .unwrap() + .get(&7) + .unwrap() + .generation, + second_generation + ); } #[test] @@ -3166,6 +3227,44 @@ mod tests { } #[test] + fn deep_scrollback_resize_preserves_unicode_and_hyperlinks() { + use std::fmt::Write as _; + + let mut terminal = Terminal::new(20, 5, 100_000_000).unwrap(); + let mut input = String::from("\x1b]8;;https://example.com\x1b\\FIRST ๐ง๐ท\x1b]8;;\x1b\\\r\n"); + for line in 0..70_000 { + writeln!(input, "{line:05} ๐จโ๐ฉโ๐ง").unwrap(); + } + terminal.write(input.as_bytes()); + + assert!(terminal.scrollback_rows().unwrap() > u16::MAX as usize); + terminal.scroll_viewport_delta(-100_000); + assert_eq!(terminal.scrollbar().unwrap().offset, 0); + assert!(terminal + .read_text_viewport((0, 0), (19, 0), false) + .unwrap() + .starts_with("FIRST ๐ง๐ท")); + assert_eq!( + terminal.viewport_hyperlink_uri(0, 0).unwrap().as_deref(), + Some("https://example.com") + ); + + terminal.resize(10, 5, 8, 16).unwrap(); + terminal.scroll_viewport_delta(-100_000); + let metrics = terminal.scrollbar().unwrap(); + assert_eq!(metrics.offset, 0); + assert_eq!(metrics.len, 5); + assert!(terminal + .read_text_viewport((0, 0), (9, 0), false) + .unwrap() + .starts_with("FIRST ๐ง๐ท")); + assert_eq!( + terminal.viewport_hyperlink_uri(0, 0).unwrap().as_deref(), + Some("https://example.com") + ); + } + + #[test] fn active_screen_and_cursor_visibility_contract() { let mut terminal = Terminal::new(12, 3, 0).unwrap(); let mut render_state = RenderState::new().unwrap(); @@ -3251,56 +3350,13 @@ mod tests { } #[test] - fn render_cells_handle_issue_453_unicode_payload() { - let mut terminal = Terminal::new(80, 3, 100).unwrap(); - terminal.write("README ๐จโ๐ฉโ๐งโ๐ฆ ๐งโ๐ป โ
โก ๆผขๅญ cafรฉ eฬ ๐ณ๏ธโ๐ ๐\r\n".as_bytes()); - - let mut render_state = RenderState::new().unwrap(); - render_state.update(&terminal).unwrap(); - - let mut row_iterator = RowIterator::new().unwrap(); - let mut rows = render_state - .populate_row_iterator(&mut row_iterator) - .unwrap(); - let mut row_cells = RowCells::new().unwrap(); - let mut codepoints = Vec::new(); - let mut text = String::new(); - - while rows.next() { - let mut cells = rows.populate_cells(&mut row_cells).unwrap(); - while cells.next() { - cells - .grapheme_text_into(&mut codepoints, &mut text) - .unwrap(); - } - } - } - - #[test] - fn grapheme_cluster_mode_keeps_issue_453_payload_safe() { + fn render_cells_preserve_issue_453_unicode_payload_exactly() { + const PAYLOAD: &str = "README ๐จโ๐ฉโ๐งโ๐ฆ ๐งโ๐ป โ
โก ๆผขๅญ cafรฉ eฬ ๐ณ๏ธโ๐ ๐"; let mut terminal = Terminal::new(80, 3, 100).unwrap(); - terminal.enable_grapheme_cluster_mode().unwrap(); - terminal.write("README ๐จโ๐ฉโ๐งโ๐ฆ ๐งโ๐ป โ
โก ๆผขๅญ cafรฉ eฬ ๐ณ๏ธโ๐ ๐\r\n".as_bytes()); - - let mut render_state = RenderState::new().unwrap(); - render_state.update(&terminal).unwrap(); - - let mut row_iterator = RowIterator::new().unwrap(); - let mut rows = render_state - .populate_row_iterator(&mut row_iterator) - .unwrap(); - let mut row_cells = RowCells::new().unwrap(); - let mut codepoints = Vec::new(); - let mut text = String::new(); + assert!(terminal.mode_get(MODE_GRAPHEME_CLUSTER).unwrap()); + terminal.write(format!("{PAYLOAD}\r\n").as_bytes()); - while rows.next() { - let mut cells = rows.populate_cells(&mut row_cells).unwrap(); - while cells.next() { - cells - .grapheme_text_into(&mut codepoints, &mut text) - .unwrap(); - } - } + assert_eq!(first_rendered_row_text(&terminal), PAYLOAD); } #[test] |
