diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 04:27:49 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 15:37:01 +0300 |
| commit | 46c65725036f1778a059f9dbf3939bc3be8c27c4 (patch) | |
| tree | ffef5a05d26eb250d79227ec24698ffc0cdd65d8 /src | |
| parent | 26ec677193ef7e207feaec19c3ed1504d6f23029 (diff) | |
fix: prevent ghostty pane redraw corruption on wide cells
Diffstat (limited to 'src')
| -rw-r--r-- | src/ghostty/mod.rs | 47 | ||||
| -rw-r--r-- | src/pane.rs | 102 |
2 files changed, 134 insertions, 15 deletions
diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index fe76d00d..c5214472 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -199,6 +199,26 @@ pub struct RenderColors { pub foreground: RgbColor, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CellWide { + Narrow, + Wide, + SpacerTail, + SpacerHead, +} + +impl CellWide { + fn from_raw(value: ffi::GhosttyCellWide) -> Self { + match value { + ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_NARROW => Self::Narrow, + ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_WIDE => Self::Wide, + ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_SPACER_TAIL => Self::SpacerTail, + ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_SPACER_HEAD => Self::SpacerHead, + _ => Self::Narrow, + } + } +} + struct WritePtyCallbackState { callback: Box<dyn FnMut(&[u8]) + Send>, } @@ -917,6 +937,33 @@ impl<'a> RowCellIter<'a> { unsafe { ffi::ghostty_render_state_row_cells_select(self.cells.raw, x).into_result() } } + fn raw_cell(&self) -> Result<ffi::GhosttyCell, Error> { + let mut raw = ffi::GhosttyCell::default(); + unsafe { + ffi::ghostty_render_state_row_cells_get( + self.cells.raw, + ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW, + (&mut raw as *mut ffi::GhosttyCell).cast(), + ) + .into_result()?; + } + Ok(raw) + } + + pub fn wide(&self) -> Result<CellWide, Error> { + let raw = self.raw_cell()?; + let mut wide = ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_NARROW; + unsafe { + ffi::ghostty_cell_get( + raw, + ffi::GhosttyCellData_GHOSTTY_CELL_DATA_WIDE, + (&mut wide as *mut ffi::GhosttyCellWide).cast(), + ) + .into_result()?; + } + Ok(CellWide::from_raw(wide)) + } + pub fn style(&self) -> Result<CellStyle, Error> { let mut style = ffi::GhosttyStyle { size: mem::size_of::<ffi::GhosttyStyle>(), diff --git a/src/pane.rs b/src/pane.rs index 2e014027..3a4c4864 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -13,6 +13,8 @@ use ratatui::{layout::Rect, Frame}; use tokio::sync::{mpsc, Notify}; use tracing::{debug, error, info, warn}; use tui_term::widget::PseudoTerminal; +#[cfg(feature = "ghostty-vt")] +use unicode_width::UnicodeWidthStr; use crate::detect::{Agent, AgentState}; use crate::events::AppEvent; @@ -965,22 +967,19 @@ impl GhosttyPaneTerminal { }; let mut x = 0u16; while x < area.width && cells.next() { + let wide = cells.wide().unwrap_or(crate::ghostty::CellWide::Narrow); let style = ghostty_cell_style(&cells, default_fg, default_bg); - let symbol = ghostty_cell_symbol(&cells).unwrap_or_else(|_| " ".to_string()); + let symbol = ghostty_buffer_symbol(&cells, wide) + .unwrap_or_else(|_| ghostty_blank_symbol_for_width(wide).to_string()); let cell = &mut buf[(area.x + x, area.y + y)]; + cell.reset(); cell.set_symbol(&symbol); cell.set_style(style); x += 1; } while x < area.width { let cell = &mut buf[(area.x + x, area.y + y)]; - cell.set_symbol(" "); - if let Some(bg) = default_bg { - cell.set_bg(bg); - } - if let Some(fg) = default_fg { - cell.set_fg(fg); - } + ghostty_reset_cell(cell, default_fg, default_bg); x += 1; } y += 1; @@ -988,13 +987,7 @@ impl GhosttyPaneTerminal { while y < area.height { for x in 0..area.width { let cell = &mut buf[(area.x + x, area.y + y)]; - cell.set_symbol(" "); - if let Some(bg) = default_bg { - cell.set_bg(bg); - } - if let Some(fg) = default_fg { - cell.set_fg(fg); - } + ghostty_reset_cell(cell, default_fg, default_bg); } y += 1; } @@ -1454,6 +1447,60 @@ fn ghostty_cell_symbol( } #[cfg(feature = "ghostty-vt")] +fn ghostty_blank_symbol_for_width(wide: crate::ghostty::CellWide) -> &'static str { + match wide { + crate::ghostty::CellWide::Wide => " ", + crate::ghostty::CellWide::SpacerTail => "", + crate::ghostty::CellWide::Narrow | crate::ghostty::CellWide::SpacerHead => " ", + } +} + +#[cfg(feature = "ghostty-vt")] +fn ghostty_normalize_buffer_symbol(symbol: &str, wide: crate::ghostty::CellWide) -> String { + let expected_width = match wide { + crate::ghostty::CellWide::Wide => 2, + crate::ghostty::CellWide::Narrow | crate::ghostty::CellWide::SpacerHead => 1, + crate::ghostty::CellWide::SpacerTail => 0, + }; + let actual_width = symbol.width(); + if actual_width == expected_width { + return symbol.to_string(); + } + ghostty_blank_symbol_for_width(wide).to_string() +} + +#[cfg(feature = "ghostty-vt")] +fn ghostty_buffer_symbol( + cells: &crate::ghostty::RowCellIter<'_>, + wide: crate::ghostty::CellWide, +) -> Result<String, crate::ghostty::Error> { + let symbol = match wide { + crate::ghostty::CellWide::SpacerTail => String::new(), + crate::ghostty::CellWide::SpacerHead => " ".to_string(), + crate::ghostty::CellWide::Narrow | crate::ghostty::CellWide::Wide => { + ghostty_cell_symbol(cells)? + } + }; + Ok(ghostty_normalize_buffer_symbol(&symbol, wide)) +} + +#[cfg(feature = "ghostty-vt")] +fn ghostty_reset_cell( + cell: &mut ratatui::buffer::Cell, + default_fg: Option<Color>, + default_bg: Option<Color>, +) { + cell.reset(); + cell.set_symbol(" "); + if let Some(bg) = default_bg { + cell.set_bg(bg); + } + if let Some(fg) = default_fg { + cell.set_fg(fg); + } +} + +#[cfg(feature = "ghostty-vt")] fn ghostty_cell_style( cells: &crate::ghostty::RowCellIter<'_>, default_fg: Option<Color>, @@ -2275,6 +2322,31 @@ mod tests { } #[cfg(feature = "ghostty-vt")] + #[test] + fn ghostty_normalize_buffer_symbol_uses_ratatui_width_contract() { + assert_eq!( + ghostty_normalize_buffer_symbol("🙂", crate::ghostty::CellWide::Wide), + "🙂" + ); + assert_eq!( + ghostty_normalize_buffer_symbol("a", crate::ghostty::CellWide::Wide), + " " + ); + assert_eq!( + ghostty_normalize_buffer_symbol("⌨️", crate::ghostty::CellWide::Narrow), + " " + ); + assert_eq!( + ghostty_normalize_buffer_symbol(" ", crate::ghostty::CellWide::SpacerTail), + "" + ); + assert_eq!( + ghostty_normalize_buffer_symbol("xx", crate::ghostty::CellWide::SpacerHead), + " " + ); + } + + #[cfg(feature = "ghostty-vt")] #[tokio::test] async fn focus_events_are_forwarded_when_enabled() { let (tx, mut rx) = mpsc::channel(4); |
