aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-05 04:27:49 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 15:37:01 +0300
commit46c65725036f1778a059f9dbf3939bc3be8c27c4 (patch)
treeffef5a05d26eb250d79227ec24698ffc0cdd65d8 /src
parent26ec677193ef7e207feaec19c3ed1504d6f23029 (diff)
fix: prevent ghostty pane redraw corruption on wide cells
Diffstat (limited to 'src')
-rw-r--r--src/ghostty/mod.rs47
-rw-r--r--src/pane.rs102
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);