aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-04 00:56:07 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-04 00:56:07 +0300
commit3cbe4ec7936745543e16d1ceb37f6db6c38d65ac (patch)
tree0252b71e0b92653c4e81168a98b7a341a7717424 /src
parente05bd5cb2a26e3690d73ca241e1e457d3eb664cb (diff)
fix: avoid ghostty utf8 grapheme buffer crash
refs #453
Diffstat (limited to 'src')
-rw-r--r--src/ghostty/mod.rs90
-rw-r--r--src/pane/terminal.rs12
2 files changed, 58 insertions, 44 deletions
diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs
index 20161d80..a64fece1 100644
--- a/src/ghostty/mod.rs
+++ b/src/ghostty/mod.rs
@@ -2546,63 +2546,51 @@ impl<'a> RowCellIter<'a> {
}
pub fn grapheme_text(&self) -> Result<String, Error> {
- let mut bytes = Vec::new();
+ let mut codepoints = Vec::new();
let mut text = String::new();
- self.grapheme_text_into(&mut bytes, &mut text)?;
+ self.grapheme_text_into(&mut codepoints, &mut text)?;
Ok(text)
}
- pub fn grapheme_text_into(&self, bytes: &mut Vec<u8>, text: &mut String) -> Result<(), Error> {
+ pub fn grapheme_text_into(
+ &self,
+ codepoints: &mut Vec<u32>,
+ text: &mut String,
+ ) -> Result<(), Error> {
text.clear();
- bytes.clear();
+ codepoints.clear();
- let mut buffer = ffi::GhosttyBuffer {
- ptr: ptr::null_mut(),
- cap: 0,
- len: 0,
- };
- let result = unsafe {
+ let mut len = 0u32;
+ unsafe {
ffi::ghostty_render_state_row_cells_get(
self.cells.raw,
- ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8,
- (&mut buffer as *mut ffi::GhosttyBuffer).cast(),
+ ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN,
+ (&mut len as *mut u32).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 buffer.len == 0 {
+ if len == 0 {
return self.raw_cell_text_into(text);
}
- bytes.resize(buffer.len, 0);
- let mut buffer = ffi::GhosttyBuffer {
- ptr: bytes.as_mut_ptr(),
- cap: bytes.len(),
- len: 0,
- };
+
+ // 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);
unsafe {
ffi::ghostty_render_state_row_cells_get(
self.cells.raw,
- ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8,
- (&mut buffer as *mut ffi::GhosttyBuffer).cast(),
+ ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF,
+ codepoints.as_mut_ptr().cast(),
)
.into_result()?;
}
- 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)),
+
+ for codepoint in codepoints.iter().copied() {
+ match char::from_u32(codepoint) {
+ Some(ch) => text.push(ch),
+ None => text.push(char::REPLACEMENT_CHARACTER),
+ }
}
Ok(())
}
@@ -2948,6 +2936,32 @@ 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 render_state_row_dirty_can_be_cleared_independently() {
let mut terminal = Terminal::new(8, 3, 100).unwrap();
let mut render_state = RenderState::new().unwrap();
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index 0a67f11a..88924d36 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -1058,7 +1058,7 @@ impl GhosttyPaneTerminal {
Ok(rows) => rows,
Err(_) => return,
};
- let mut grapheme_bytes = Vec::new();
+ let mut grapheme_codepoints = Vec::new();
let mut symbol_scratch = String::new();
let mut y = 0u16;
while y < area.height && rows.next() {
@@ -1081,7 +1081,7 @@ impl GhosttyPaneTerminal {
&cells,
basic.wide,
hide_kitty_placeholders,
- &mut grapheme_bytes,
+ &mut grapheme_codepoints,
&mut symbol_scratch,
) {
Ok(symbol) => symbol,
@@ -1226,7 +1226,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_bytes = Vec::new();
+ let mut grapheme_codepoints = Vec::new();
let mut symbol_scratch = String::new();
let mut patch_rows = Vec::new();
let mut y = 0u16;
@@ -1264,7 +1264,7 @@ fn ghostty_collect_dirty_patch(
&cells,
basic.wide,
hide_kitty_placeholders,
- &mut grapheme_bytes,
+ &mut grapheme_codepoints,
&mut symbol_scratch,
) {
Ok(symbol) => symbol.to_owned(),
@@ -1539,7 +1539,7 @@ fn ghostty_buffer_symbol_into<'a>(
cells: &crate::ghostty::RowCellIter<'_>,
wide: crate::ghostty::CellWide,
hide_kitty_placeholders: bool,
- grapheme_bytes: &mut Vec<u8>,
+ grapheme_codepoints: &mut Vec<u32>,
symbol_scratch: &'a mut String,
) -> Result<&'a str, crate::ghostty::Error> {
symbol_scratch.clear();
@@ -1547,7 +1547,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_bytes, symbol_scratch)?;
+ cells.grapheme_text_into(grapheme_codepoints, symbol_scratch)?;
let hidden_kitty_placeholder = hide_kitty_placeholders
&& symbol_scratch.chars().next().map(u32::from)
== Some(crate::ghostty::KITTY_UNICODE_PLACEHOLDER);