aboutsummaryrefslogtreecommitdiff
path: root/src/ghostty/mod.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /src/ghostty/mod.rs
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'src/ghostty/mod.rs')
-rw-r--r--src/ghostty/mod.rs389
1 files changed, 252 insertions, 137 deletions
diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs
index cd774c6b..9ec9d324 100644
--- a/src/ghostty/mod.rs
+++ b/src/ghostty/mod.rs
@@ -146,12 +146,12 @@ pub const MOD_CTRL: u16 = ffi::GHOSTTY_MODS_CTRL as u16;
pub const MOD_ALT: u16 = ffi::GHOSTTY_MODS_ALT as u16;
pub const MOD_SUPER: u16 = ffi::GHOSTTY_MODS_SUPER as u16;
-pub const KEY_ENTER: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ENTER;
-pub const KEY_UP: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_UP;
-pub const KEY_DOWN: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_DOWN;
-pub const KEY_LEFT: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_LEFT;
-pub const KEY_RIGHT: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_RIGHT;
-pub const KEY_A: u32 = ffi::GhosttyKey_GHOSTTY_KEY_A;
+pub const KEY_ENTER: ffi::GhosttyKey = ffi::GhosttyKey_GHOSTTY_KEY_ENTER;
+pub const KEY_UP: ffi::GhosttyKey = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_UP;
+pub const KEY_DOWN: ffi::GhosttyKey = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_DOWN;
+pub const KEY_LEFT: ffi::GhosttyKey = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_LEFT;
+pub const KEY_RIGHT: ffi::GhosttyKey = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_RIGHT;
+pub const KEY_A: ffi::GhosttyKey = ffi::GhosttyKey_GHOSTTY_KEY_A;
pub const MOUSE_ACTION_PRESS: ffi::GhosttyMouseAction =
ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_PRESS;
@@ -345,6 +345,14 @@ pub struct CursorViewport {
pub wide_tail: bool,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct RenderCursor {
+ pub viewport: Option<CursorViewport>,
+ pub visible: bool,
+ pub blinking: bool,
+ pub visual_style: CursorVisualStyle,
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RgbColor {
pub r: u8,
@@ -557,12 +565,25 @@ unsafe extern "C" fn clipboard_write_trampoline(
_terminal: ffi::GhosttyTerminal,
userdata: *mut c_void,
write: *const ffi::GhosttyClipboardWrite,
-) -> ffi::GhosttyClipboardWriteResult {
- std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+) {
+ if write.is_null() || unsafe { (*write).size } < mem::size_of::<ffi::GhosttyClipboardWrite>() {
+ return;
+ }
+ // The reply and request are borrowed only until this callback returns.
+ let Some(reply) = (unsafe { (*write).reply }) else {
+ return;
+ };
+ let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
// SAFETY: libghostty-vt owns these values for the synchronous callback.
unsafe { capture_clipboard_write(userdata, write) }
}))
- .unwrap_or(ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA)
+ .unwrap_or(ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA);
+ let response = ffi::GhosttyClipboardWriteReply {
+ size: mem::size_of::<ffi::GhosttyClipboardWriteReply>(),
+ result,
+ remember: false,
+ };
+ unsafe { reply(write, &response) };
}
unsafe fn capture_clipboard_write(
@@ -798,14 +819,9 @@ pub struct Terminal {
impl Terminal {
pub fn new(cols: u16, rows: u16, max_scrollback: usize) -> Result<Self, Error> {
let mut raw = ptr::null_mut();
- let options = ffi::GhosttyTerminalOptions {
- cols,
- rows,
- max_scrollback,
- };
- // SAFETY: valid out pointer and options, null allocator means default allocator.
+ // SAFETY: valid out pointer, null allocator means default allocator.
unsafe {
- ffi::ghostty_terminal_new(ptr::null(), &mut raw, options).into_result()?;
+ ffi::ghostty_terminal_new(ptr::null(), &mut raw, cols, rows).into_result()?;
}
let mut terminal = Self {
@@ -826,9 +842,37 @@ impl Terminal {
};
let userdata = (&mut *terminal.callback_state as *mut TerminalCallbackState).cast();
let glyph_protocol = false;
+ let terminfo_name = ffi::GhosttyString {
+ ptr: crate::pane::PANE_TERM.as_ptr().cast(),
+ len: crate::pane::PANE_TERM.len(),
+ };
+ let grapheme_default = ffi::GhosttyTerminalModeConfig {
+ mode: MODE_GRAPHEME_CLUSTER,
+ value: true,
+ };
+ let clipboard_callback: ffi::GhosttyTerminalClipboardWriteFn =
+ Some(clipboard_write_trampoline);
unsafe {
ffi::ghostty_terminal_set(
terminal.raw,
+ ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_TERMINFO_NAME,
+ (&terminfo_name as *const ffi::GhosttyString).cast(),
+ )
+ .into_result()?;
+ ffi::ghostty_terminal_set(
+ terminal.raw,
+ ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES,
+ (&max_scrollback as *const usize).cast(),
+ )
+ .into_result()?;
+ ffi::ghostty_terminal_set(
+ terminal.raw,
+ ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_MODE_DEFAULT,
+ (&grapheme_default as *const ffi::GhosttyTerminalModeConfig).cast(),
+ )
+ .into_result()?;
+ ffi::ghostty_terminal_set(
+ terminal.raw,
ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_USERDATA,
userdata,
)
@@ -854,7 +898,7 @@ impl Terminal {
ffi::ghostty_terminal_set(
terminal.raw,
ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE,
- (clipboard_write_trampoline as *const ()).cast(),
+ clipboard_callback.map_or(ptr::null(), |callback| (callback as *const ()).cast()),
)
.into_result()?;
ffi::ghostty_terminal_set(
@@ -978,6 +1022,12 @@ impl Terminal {
install_png_decoder_once();
let storage_limit = KITTY_IMAGE_STORAGE_LIMIT_BYTES;
let enable_medium = true;
+ let temp_dir = std::env::temp_dir();
+ let temp_dir = temp_dir.to_string_lossy();
+ let temp_dir = ffi::GhosttyString {
+ ptr: temp_dir.as_ptr().cast(),
+ len: temp_dir.len(),
+ };
unsafe {
ffi::ghostty_terminal_set(
self.raw,
@@ -994,7 +1044,7 @@ impl Terminal {
ffi::ghostty_terminal_set(
self.raw,
ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE,
- (&enable_medium as *const bool).cast(),
+ (&temp_dir as *const ffi::GhosttyString).cast(),
)
.into_result()?;
ffi::ghostty_terminal_set(
@@ -1052,13 +1102,28 @@ impl Terminal {
}
pub fn mode_get(&self, mode: u16) -> Result<bool, Error> {
- let mut out = false;
- unsafe { ffi::ghostty_terminal_mode_get(self.raw, mode, &mut out).into_result()? };
- Ok(out)
+ let mut config = ffi::GhosttyTerminalModeConfig { mode, value: false };
+ unsafe {
+ ffi::ghostty_terminal_get(
+ self.raw,
+ ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_MODE,
+ (&mut config as *mut ffi::GhosttyTerminalModeConfig).cast(),
+ )
+ .into_result()?;
+ }
+ Ok(config.value)
}
pub fn mode_set(&mut self, mode: u16, value: bool) -> Result<(), Error> {
- unsafe { ffi::ghostty_terminal_mode_set(self.raw, mode, value).into_result() }
+ let config = ffi::GhosttyTerminalModeConfig { mode, value };
+ unsafe {
+ ffi::ghostty_terminal_set(
+ self.raw,
+ ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_MODE,
+ (&config as *const ffi::GhosttyTerminalModeConfig).cast(),
+ )
+ .into_result()
+ }
}
pub fn kitty_keyboard_flags(&self) -> Result<u8, Error> {
@@ -1282,64 +1347,6 @@ impl Terminal {
)
}
- pub fn keyboard_state_ansi(&self) -> Result<String, Error> {
- self.format_keyboard_state_ansi(false)
- }
-
- pub fn kitty_keyboard_state_ansi(&self) -> Result<String, Error> {
- self.format_keyboard_state_ansi(true)
- }
-
- fn format_keyboard_state_ansi(&self, kitty_keyboard: bool) -> Result<String, Error> {
- let mut formatter: ffi::GhosttyFormatter = ptr::null_mut();
- let options = ffi::GhosttyFormatterTerminalOptions {
- size: mem::size_of::<ffi::GhosttyFormatterTerminalOptions>(),
- emit: FormatterFormat::Vt.as_raw(),
- unwrap: false,
- trim: false,
- extra: ffi::GhosttyFormatterTerminalExtra {
- size: mem::size_of::<ffi::GhosttyFormatterTerminalExtra>(),
- keyboard: true,
- screen: ffi::GhosttyFormatterScreenExtra {
- size: mem::size_of::<ffi::GhosttyFormatterScreenExtra>(),
- kitty_keyboard,
- ..Default::default()
- },
- ..Default::default()
- },
- selection: ptr::null(),
- };
- unsafe {
- ffi::ghostty_formatter_terminal_new(ptr::null(), &mut formatter, self.raw, options)
- .into_result()?;
- }
-
- let mut out_ptr = ptr::null_mut();
- let mut out_len = 0usize;
- let result = unsafe {
- ffi::ghostty_formatter_format_alloc(formatter, ptr::null(), &mut out_ptr, &mut out_len)
- };
- unsafe {
- ffi::ghostty_formatter_free(formatter);
- }
- result.into_result()?;
-
- let text = if out_len == 0 {
- String::new()
- } else {
- let bytes = unsafe { slice::from_raw_parts(out_ptr.cast_const(), out_len) };
- String::from_utf8_lossy(bytes).into_owned()
- };
-
- if !out_ptr.is_null() {
- unsafe {
- ffi::ghostty_free(ptr::null(), out_ptr, out_len);
- }
- }
-
- Ok(text)
- }
-
fn read_formatted_selection(
&self,
start: ffi::GhosttyPoint,
@@ -2494,43 +2501,34 @@ impl RenderState {
Ok(Dirty::from_raw(out))
}
- pub fn cursor_visible(&self) -> Result<bool, Error> {
- self.get_bool(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE)
- }
-
- pub fn cursor_blinking(&self) -> Result<bool, Error> {
- self.get_bool(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING)
- }
-
- pub fn cursor_visual_style(&self) -> Result<CursorVisualStyle, Error> {
- let mut out: ffi::GhosttyRenderStateCursorVisualStyle = 0;
- // SAFETY: out points to the matching enum storage for the requested data kind.
+ pub fn cursor(&self) -> Result<RenderCursor, Error> {
+ let mut cursor = ffi::GhosttyRenderStateCursor {
+ size: mem::size_of::<ffi::GhosttyRenderStateCursor>(),
+ ..Default::default()
+ };
unsafe {
ffi::ghostty_render_state_get(
self.raw,
- ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE,
- (&mut out as *mut ffi::GhosttyRenderStateCursorVisualStyle).cast(),
+ ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR,
+ (&mut cursor as *mut ffi::GhosttyRenderStateCursor).cast(),
)
.into_result()?;
}
- Ok(CursorVisualStyle::from_raw(out))
- }
-
- pub fn cursor_viewport(&self) -> Result<Option<CursorViewport>, Error> {
- if !self.get_bool(
- ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE,
- )? {
- return Ok(None);
- }
- Ok(Some(CursorViewport {
- x: self
- .get_u16(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X)?,
- y: self
- .get_u16(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y)?,
- wide_tail: self.get_bool(
- ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL,
- )?,
- }))
+ Ok(RenderCursor {
+ // Upstream leaves these fields undefined when the cursor is offscreen.
+ viewport: if cursor.viewport_has_value {
+ Some(CursorViewport {
+ x: cursor.viewport_x,
+ y: cursor.viewport_y,
+ wide_tail: cursor.wide_tail,
+ })
+ } else {
+ None
+ },
+ visible: cursor.visible,
+ blinking: cursor.blinking,
+ visual_style: CursorVisualStyle::from_raw(cursor.visual_style),
+ })
}
pub fn colors(&self) -> Result<RenderColors, Error> {
@@ -2539,7 +2537,12 @@ impl RenderState {
..Default::default()
};
unsafe {
- ffi::ghostty_render_state_colors_get(self.raw, &mut colors).into_result()?;
+ ffi::ghostty_render_state_get(
+ self.raw,
+ ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_COLORS,
+ (&mut colors as *mut ffi::GhosttyRenderStateColors).cast(),
+ )
+ .into_result()?;
}
Ok(RenderColors {
background: colors.background.into(),
@@ -2548,6 +2551,10 @@ impl RenderState {
})
}
+ pub fn clean(&mut self) -> Result<(), Error> {
+ unsafe { ffi::ghostty_render_state_clean(self.raw).into_result() }
+ }
+
pub fn set_dirty(&mut self, dirty: Dirty) -> Result<(), Error> {
let value = dirty.as_raw();
// SAFETY: value pointer matches the expected option type.
@@ -2589,15 +2596,6 @@ impl RenderState {
}
Ok(out)
}
-
- fn get_bool(&self, data: ffi::GhosttyRenderStateData) -> Result<bool, Error> {
- let mut out = false;
- unsafe {
- ffi::ghostty_render_state_get(self.raw, data, (&mut out as *mut bool).cast())
- .into_result()?;
- }
- Ok(out)
- }
}
// SAFETY: these opaque handles are only used behind external synchronization in pane runtime.
@@ -2627,7 +2625,7 @@ impl KeyEvent {
unsafe { ffi::ghostty_key_event_set_action(self.raw, action) }
}
- pub fn set_key(&mut self, key: u32) {
+ pub fn set_key(&mut self, key: ffi::GhosttyKey) {
unsafe { ffi::ghostty_key_event_set_key(self.raw, key) }
}
@@ -2846,6 +2844,14 @@ impl<'a> RowIter<'a> {
unsafe { ffi::ghostty_render_state_row_iterator_next(self.iterator.raw) }
}
+ pub fn next_dirty(&mut self) -> Option<u16> {
+ let mut y = 0u16;
+ // SAFETY: iterator is live and y matches the viewport-coordinate output type.
+ let advanced =
+ unsafe { ffi::ghostty_render_state_row_iterator_next_dirty(self.iterator.raw, &mut y) };
+ advanced.then_some(y)
+ }
+
pub fn dirty(&self) -> Result<bool, Error> {
let mut dirty = false;
// SAFETY: dirty output matches requested row data type.
@@ -3294,6 +3300,7 @@ impl<'a> RowCellIter<'a> {
#[cfg(test)]
mod tests {
use super::*;
+ use std::sync::Arc;
fn write_numbered_lines(terminal: &mut Terminal, count: usize) {
for i in 0..count {
@@ -3413,8 +3420,8 @@ mod tests {
.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\\");
+ // Retransmission removes old placements; transmit-and-display replaces it.
+ terminal.write(b"\x1b[H\x1b_Ga=T,f=32,t=d,i=7,p=3,s=1,v=1,c=10,r=5,q=2;AAAAAA==\x1b\\");
let second = terminal
.kitty_image_placements_with_data_filter(|_| true)
.unwrap();
@@ -3486,7 +3493,11 @@ mod tests {
terminal.scroll_viewport_row(0);
assert_eq!(terminal.kitty_image_placements().unwrap().len(), 1);
+ // Delete-visible must preserve images in scrollback, even while viewed.
terminal.write(b"\x1b_Ga=d,d=A\x1b\\");
+ assert_eq!(terminal.kitty_graphics_generation().unwrap(), placed);
+ assert_eq!(terminal.kitty_image_placements().unwrap().len(), 1);
+ terminal.write(b"\x1b_Ga=d,d=I,i=1\x1b\\");
let deleted = terminal.kitty_graphics_generation().unwrap();
assert_ne!(deleted, placed);
assert!(terminal.kitty_graphics_may_have_placements().unwrap());
@@ -3540,9 +3551,20 @@ mod tests {
assert!(terminal
.get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_FILE)
.unwrap());
- assert!(terminal
- .get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_TEMP_FILE)
- .unwrap());
+ let mut temp_dir = ffi::GhosttyString::default();
+ unsafe {
+ ffi::ghostty_terminal_get(
+ terminal.raw,
+ ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_TEMP_FILE,
+ (&mut temp_dir as *mut ffi::GhosttyString).cast(),
+ )
+ .into_result()
+ .unwrap();
+ assert_eq!(
+ borrowed_bytes(temp_dir).unwrap(),
+ std::env::temp_dir().to_string_lossy().as_bytes()
+ );
+ }
assert!(terminal
.get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_SHARED_MEM)
.unwrap());
@@ -3874,6 +3896,59 @@ mod tests {
}
#[test]
+ fn raw_resize_preserves_content_without_replaying_terminal_effects() {
+ let mut terminal = Terminal::new(20, 6, 100_000).unwrap();
+ terminal.write(b"header\r\n\x1b[6;1Htail\x1b[6;18H");
+ for (cols, rows) in [(10, 3), (30, 8), (8, 4), (20, 6)] {
+ terminal.resize(cols, rows, 8, 16).unwrap();
+ terminal.write(b"X");
+ let text = terminal
+ .read_text_screen(
+ (0, 0),
+ (cols - 1, terminal.total_rows().unwrap() as u32 - 1),
+ false,
+ )
+ .unwrap();
+ assert!(
+ text.contains("tail"),
+ "lost tail after {cols}x{rows}: {text:?}"
+ );
+ assert_eq!(text.matches("tail").count(), 1);
+ assert!(terminal.cursor_y().unwrap() < rows);
+ }
+ terminal.write(b"\x1b[2J\x1b[H");
+ terminal.resize(12, 3, 8, 16).unwrap();
+ assert!(terminal
+ .read_text_viewport((0, 0), (11, 2), false)
+ .unwrap()
+ .trim()
+ .is_empty());
+ }
+
+ #[test]
+ fn clipboard_queries_never_disclose_contents_and_split_writes_complete_once() {
+ for suffix in [b"\x07".as_slice(), b"\x1b\\".as_slice()] {
+ let bytes = [b"\x1b]52;c;YQBi".as_slice(), suffix].concat();
+ for split in 0..=bytes.len() {
+ let mut terminal = Terminal::new(10, 3, 0).unwrap();
+ let replies = Arc::new(Mutex::new(Vec::new()));
+ let captured = Arc::clone(&replies);
+ terminal
+ .set_write_pty_callback(move |bytes| {
+ captured.lock().unwrap().extend_from_slice(bytes)
+ })
+ .unwrap();
+ terminal.write(&bytes[..split]);
+ terminal.write(&bytes[split..]);
+ assert_eq!(terminal.take_clipboard_writes(), vec![b"a\0b".to_vec()]);
+ terminal.write(b"\x1b]52;c;?\x07\x1b]52;p;YQBi\x07");
+ assert!(terminal.take_clipboard_writes().is_empty());
+ assert!(replies.lock().unwrap().is_empty());
+ }
+ }
+ }
+
+ #[test]
fn active_screen_and_cursor_visibility_contract() {
let mut terminal = Terminal::new(12, 3, 0).unwrap();
let mut render_state = RenderState::new().unwrap();
@@ -3886,10 +3961,10 @@ mod tests {
);
render_state.update(&terminal).unwrap();
- assert!(render_state.cursor_visible().unwrap());
+ assert!(render_state.cursor().unwrap().visible);
terminal.write(b"\x1b[?25l");
render_state.update(&terminal).unwrap();
- assert!(!render_state.cursor_visible().unwrap());
+ assert!(!render_state.cursor().unwrap().visible);
terminal.write(b"\x1b[?1049h\x1b[HALT");
assert_eq!(terminal.active_screen().unwrap(), ActiveScreen::Alternate);
@@ -3972,6 +4047,8 @@ mod tests {
fn grapheme_cluster_mode_is_default_and_survives_full_reset() {
let mut terminal = Terminal::new(80, 3, 100).unwrap();
assert!(terminal.mode_get(MODE_GRAPHEME_CLUSTER).unwrap());
+ terminal.mode_set(MODE_GRAPHEME_CLUSTER, false).unwrap();
+ assert!(!terminal.mode_get(MODE_GRAPHEME_CLUSTER).unwrap());
terminal.write(b"\x1bc");
@@ -4013,8 +4090,21 @@ mod tests {
assert!(!rows.dirty().unwrap());
}
}
+ {
+ let mut iterator = RowIterator::new().unwrap();
+ let mut rows = render_state.populate_row_iterator(&mut iterator).unwrap();
+ assert_eq!(rows.next_dirty(), Some(0));
+ assert_eq!(rows.next_dirty(), Some(1));
+ assert_eq!(rows.next_dirty(), Some(2));
+ assert_eq!(rows.next_dirty(), None);
+ }
render_state.set_dirty(Dirty::Clean).unwrap();
assert_eq!(render_state.dirty().unwrap(), Dirty::Clean);
+ {
+ let mut iterator = RowIterator::new().unwrap();
+ let mut rows = render_state.populate_row_iterator(&mut iterator).unwrap();
+ assert_eq!(rows.next_dirty(), None);
+ }
terminal.write(b"A");
render_state.update(&terminal).unwrap();
@@ -4026,12 +4116,12 @@ mod tests {
let mut rows = render_state
.populate_row_iterator(&mut row_iterator)
.unwrap();
- while rows.next() {
- if rows.dirty().unwrap() {
- dirty_rows += 1;
- rows.clear_dirty().unwrap();
- assert!(!rows.dirty().unwrap());
- }
+ while let Some(y) = rows.next_dirty() {
+ assert_eq!(y, 0);
+ assert!(rows.dirty().unwrap());
+ dirty_rows += 1;
+ rows.clear_dirty().unwrap();
+ assert!(!rows.dirty().unwrap());
}
}
assert_eq!(dirty_rows, 1);
@@ -4073,20 +4163,45 @@ mod tests {
contents: &[ffi::GhosttyClipboardContent],
size: usize,
) -> ffi::GhosttyClipboardWriteResult {
+ unsafe extern "C" fn reply(
+ request: *const ffi::GhosttyClipboardWrite,
+ response: *const ffi::GhosttyClipboardWriteReply,
+ ) {
+ let state = unsafe {
+ &mut *((*request).ctx as *mut (ffi::GhosttyClipboardWriteResult, bool, usize))
+ };
+ state.0 = unsafe { (*response).result };
+ state.1 = unsafe { (*response).remember };
+ state.2 += 1;
+ }
+ let mut response = (
+ ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA,
+ false,
+ 0usize,
+ );
let request = ffi::GhosttyClipboardWrite {
size,
location: ffi::GhosttyClipboardLocation_GHOSTTY_CLIPBOARD_LOCATION_STANDARD,
contents: contents.as_ptr(),
contents_len: contents.len(),
+ ctx: (&mut response as *mut (ffi::GhosttyClipboardWriteResult, bool, usize)).cast(),
+ reply: Some(reply),
+ ..Default::default()
};
- // SAFETY: the request and its borrowed content live through this call.
+ // SAFETY: the request, reply state and borrowed content live through this call.
unsafe {
clipboard_write_trampoline(
terminal.raw,
(&mut *terminal.callback_state as *mut TerminalCallbackState).cast(),
&request,
- )
+ );
}
+ assert_eq!(
+ response.2,
+ usize::from(size >= mem::size_of::<ffi::GhosttyClipboardWrite>())
+ );
+ assert!(!response.1, "migration must not create clipboard grants");
+ response.0
}
#[test]