aboutsummaryrefslogtreecommitdiff
path: root/src/ghostty/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/ghostty/mod.rs')
-rw-r--r--src/ghostty/mod.rs274
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]