SafeBackend: explicit per-cell cursor positioning eliminates ghost glyphs

Terminals and fonts disagree about the rendered width of some glyphs
(emoji like /). Ratatui's stock backend writes contiguous runs and
trusts the terminal to advance exactly unicode-width columns, so a
single disagreement shifts the rest of the run — misplaced characters
then stick around ('ghosts') until something else repaints the cell.

The new SafeBackend positions the cursor explicitly (ESC[y;xH) before
every cell it writes, so width disagreements can never accumulate.
Diffs are small, so the ~8 bytes/cell overhead is negligible. Verified
in tmux: rendering, dragging and IPC unchanged; 41/41 integration
checks pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 00:24:19 +02:00
parent a621fa10d7
commit 3db0c82e03
3 changed files with 159 additions and 6 deletions

View File

@@ -1,4 +1,5 @@
mod app;
mod backend;
mod client;
mod config;
mod ipc;
@@ -17,7 +18,7 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ipc::{ClientMessage, ClientRole, ServerMessage, WindowInfo};
use ratatui::{backend::CrosstermBackend, Terminal};
use ratatui::Terminal;
use server::IpcEvent;
use std::collections::HashMap;
use std::io::{self, Write as _};
@@ -220,7 +221,9 @@ fn run_standalone() -> io::Result<()> {
)
);
}
let backend = CrosstermBackend::new(stdout);
// SafeBackend positionerar varje cell explicit — immun mot terminal/
// font-oenighet om glyfbredder (spök-tecken vid fönsterflytt).
let backend = backend::SafeBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let config = Config::load_or_default("config.toml");
@@ -239,7 +242,7 @@ fn run_standalone() -> io::Result<()> {
}
fn run_standalone_loop(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
terminal: &mut Terminal<backend::SafeBackend<io::Stdout>>,
app: &mut App,
ipc_rx: mpsc::Receiver<IpcEvent>,
_socket_path: &str,
@@ -401,7 +404,7 @@ fn get_terminal_pixel_size() -> (u16, u16) {
/// Uppdatera Kitty-bakgrundsbild om det behövs (ny bild, storleksändring, borttagen).
fn update_background(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
terminal: &mut Terminal<backend::SafeBackend<io::Stdout>>,
app: &App,
bg_state: &mut Option<kitty_gfx::BgImage>,
cols: u16,