diff --git a/src/backend.rs b/src/backend.rs new file mode 100644 index 0000000..9a48268 --- /dev/null +++ b/src/backend.rs @@ -0,0 +1,150 @@ +//! SafeBackend — en crossterm-baserad ratatui-backend som positionerar +//! markören EXPLICIT för varje cell som skrivs. +//! +//! Ratatuis vanliga backend skriver sammanhängande celler som en "run" +//! och litar på att terminalen avancerar markören exakt lika många +//! kolumner som `unicode-width` räknar. Terminaler och fonter är dock +//! oense om bredden på vissa glyfer (t.ex. emoji som ⭐/➕): en enda +//! oenighet förskjuter resten av raden, och felplacerade tecken blir +//! kvar som "spöken" tills cellen råkar ritas om. +//! +//! Genom att sätta markören med `ESC[y;xH` inför varje cell kan +//! oenigheter aldrig ackumuleras — varje glyf landar exakt där den ska. +//! Diffen från ratatui är oftast liten, så overheaden (~8 byte/cell) +//! är försumbar. + +use ratatui::backend::{Backend, ClearType, WindowSize}; +use ratatui::buffer::Cell; +use ratatui::layout::{Position, Size}; +use ratatui::style::Modifier; +use std::io::{self, Write}; + +pub struct SafeBackend { + writer: W, +} + +impl SafeBackend { + pub fn new(writer: W) -> Self { + SafeBackend { writer } + } +} + +impl Write for SafeBackend { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.writer.write(buf) + } + fn flush(&mut self) -> io::Result<()> { + self.writer.flush() + } +} + +fn modifier_ansi(m: Modifier) -> String { + let mut out = String::from("\x1b[0m"); + if m.contains(Modifier::BOLD) { + out.push_str("\x1b[1m"); + } + if m.contains(Modifier::DIM) { + out.push_str("\x1b[2m"); + } + if m.contains(Modifier::ITALIC) { + out.push_str("\x1b[3m"); + } + if m.contains(Modifier::UNDERLINED) { + out.push_str("\x1b[4m"); + } + if m.contains(Modifier::REVERSED) { + out.push_str("\x1b[7m"); + } + if m.contains(Modifier::CROSSED_OUT) { + out.push_str("\x1b[9m"); + } + out +} + +impl Backend for SafeBackend { + type Error = io::Error; + + fn draw<'a, I>(&mut self, content: I) -> io::Result<()> + where + I: Iterator, + { + use std::fmt::Write as _; + let mut buf = String::new(); + let mut last_fg = None; + let mut last_bg = None; + let mut last_mod = None; + for (x, y, cell) in content { + // Explicit position för varje cell — se modulkommentaren. + let _ = write!(buf, "\x1b[{};{}H", y + 1, x + 1); + if last_mod != Some(cell.modifier) { + buf.push_str(&modifier_ansi(cell.modifier)); + last_fg = None; + last_bg = None; + last_mod = Some(cell.modifier); + } + if last_fg != Some(cell.fg) { + buf.push_str(&crate::ipc::color_fg(cell.fg)); + last_fg = Some(cell.fg); + } + if last_bg != Some(cell.bg) { + buf.push_str(&crate::ipc::color_bg(cell.bg)); + last_bg = Some(cell.bg); + } + buf.push_str(cell.symbol()); + } + buf.push_str("\x1b[0m"); + self.writer.write_all(buf.as_bytes()) + } + + fn hide_cursor(&mut self) -> io::Result<()> { + self.writer.write_all(b"\x1b[?25l") + } + + fn show_cursor(&mut self) -> io::Result<()> { + self.writer.write_all(b"\x1b[?25h") + } + + fn get_cursor_position(&mut self) -> io::Result { + // Används inte av TUI-WM (markören är dold); undvik en blockerande + // terminal-query och svara med origo. + Ok(Position::new(0, 0)) + } + + fn set_cursor_position>(&mut self, position: P) -> io::Result<()> { + let p: Position = position.into(); + self.writer + .write_all(format!("\x1b[{};{}H", p.y + 1, p.x + 1).as_bytes()) + } + + fn clear(&mut self) -> io::Result<()> { + self.writer.write_all(b"\x1b[2J\x1b[H") + } + + fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> { + let seq: &[u8] = match clear_type { + ClearType::All => b"\x1b[2J", + ClearType::AfterCursor => b"\x1b[0J", + ClearType::BeforeCursor => b"\x1b[1J", + ClearType::CurrentLine => b"\x1b[2K", + ClearType::UntilNewLine => b"\x1b[0K", + }; + self.writer.write_all(seq) + } + + fn size(&self) -> io::Result { + let (w, h) = crossterm::terminal::size()?; + Ok(Size::new(w, h)) + } + + fn window_size(&mut self) -> io::Result { + let ws = crossterm::terminal::window_size()?; + Ok(WindowSize { + columns_rows: Size::new(ws.columns, ws.rows), + pixels: Size::new(ws.width, ws.height), + }) + } + + fn flush(&mut self) -> io::Result<()> { + self.writer.flush() + } +} diff --git a/src/ipc.rs b/src/ipc.rs index f416866..9bef3e1 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -253,7 +253,7 @@ pub fn buffer_to_ansi(buf: &Buffer) -> Vec { out.into_bytes() } -fn color_fg(c: ratatui::style::Color) -> String { +pub(crate) fn color_fg(c: ratatui::style::Color) -> String { use ratatui::style::Color::*; match c { Reset => "\x1b[39m".into(), @@ -350,7 +350,7 @@ mod tests { } } -fn color_bg(c: ratatui::style::Color) -> String { +pub(crate) fn color_bg(c: ratatui::style::Color) -> String { use ratatui::style::Color::*; match c { Reset => "\x1b[49m".into(), diff --git a/src/main.rs b/src/main.rs index 27cd2e4..a2d96be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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>, + terminal: &mut Terminal>, app: &mut App, ipc_rx: mpsc::Receiver, _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>, + terminal: &mut Terminal>, app: &App, bg_state: &mut Option, cols: u16,