diff --git a/Cargo.lock b/Cargo.lock index e305d0d..f8d8c86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1731,6 +1731,7 @@ dependencies = [ "serde_json", "toml", "toml_edit", + "unicode-width", "vt100", ] diff --git a/Cargo.toml b/Cargo.toml index 8c5fe10..8ac964d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ vt100 = "0.16" anyhow = "1" image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "bmp", "webp"] } interprocess = "2.4" +unicode-width = "0.2" [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/src/app.rs b/src/app.rs index 72da06e..f15623e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -441,7 +441,10 @@ impl App { config, panel_rects: Vec::new(), panel_item_rects: Vec::new(), - content_area: Rect::default(), + // Vettig startyta tills första update_layout — fönster som spawnas + // innan dess får annars 0-storlek (och en 1-kolumns vt100 kan + // panika på breda tecken). + content_area: Rect::new(0, 0, 80, 24), hovered_panel_item: None, hovered_window_close: None, hovered_resize: None, @@ -574,7 +577,9 @@ impl App { // ── PTY resize om fönsterstorlek ändrats ───────────────────────────── for window in &mut self.windows { let new_rows = window.height.saturating_sub(2).max(1); - let new_cols = window.width.saturating_sub(2).max(1); + // Minst 2 kolumner: vt100 0.16 har en underflow-panik när ett + // brett tecken (emoji/CJK) skrivs i en 1-kolumns terminal. + let new_cols = window.width.saturating_sub(2).max(2); let WindowContent::Terminal { pty, parser, alive, .. } = &mut window.content else { continue }; if !*alive { continue; @@ -1445,7 +1450,8 @@ impl App { let x = ca.x as i32 + 2 + offset; let y = ca.y as i32 + 1 + offset; let rows = h.saturating_sub(2).max(1); - let cols = w.saturating_sub(2).max(1); + // Minst 2 kolumner — se kommentaren vid PTY-resize i tick(). + let cols = w.saturating_sub(2).max(2); let id = self.next_id; let mut env_vars: Vec<(String, String)> = Vec::new(); diff --git a/src/ipc.rs b/src/ipc.rs index e3b7c8f..0923e3d 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -198,9 +198,22 @@ pub fn buffer_to_ansi(buf: &Buffer) -> Vec { use std::fmt::Write as FmtWrite; let _ = write!(out, "\x1b[{};1H", row + 1); } + // Celler som täcks av ett brett tecken (emoji/CJK i cellen innan) + // ska INTE skrivas ut — terminalen har redan avancerat 2 kolumner + // för det breda tecknet, och ett extra mellanslag skulle förskjuta + // resten av raden åt höger. + let mut skip_cols = 0u16; for col in 0..area.width { + if skip_cols > 0 { + skip_cols -= 1; + continue; + } let idx = (row * area.width + col) as usize; let cell = &buf.content[idx]; + let sym_width = unicode_width::UnicodeWidthStr::width(cell.symbol()); + if sym_width > 1 { + skip_cols = (sym_width as u16).saturating_sub(1); + } let new_mod = cell.modifier; if new_mod != last_mod { out.push_str("\x1b[0m"); diff --git a/src/main.rs b/src/main.rs index 3daa5e4..0cccee5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -185,7 +185,14 @@ fn run_standalone_loop( }; all_clients.retain(|_, (role, _, _, tx)| { if *role == ClientRole::Display { - tx.send(frame_msg.clone()).is_ok() + // try_send: en långsam/hängd display-klient får INTE + // blockera hela WM:et — droppa framen till den klienten + // och koppla bara bort vid stängd kanal. + match tx.try_send(frame_msg.clone()) { + Ok(()) => true, + Err(std::sync::mpsc::TrySendError::Full(_)) => true, + Err(std::sync::mpsc::TrySendError::Disconnected(_)) => false, + } } else { true } @@ -344,7 +351,14 @@ fn run_daemon_mode() -> io::Result<()> { }; all_clients.retain(|_, (role, _, _, tx)| { if *role == ClientRole::Display { - tx.send(frame_msg.clone()).is_ok() + // try_send: en långsam/hängd display-klient får INTE + // blockera hela WM:et — droppa framen till den klienten + // och koppla bara bort vid stängd kanal. + match tx.try_send(frame_msg.clone()) { + Ok(()) => true, + Err(std::sync::mpsc::TrySendError::Full(_)) => true, + Err(std::sync::mpsc::TrySendError::Disconnected(_)) => false, + } } else { true } diff --git a/src/render.rs b/src/render.rs index 77267ca..e368d64 100644 --- a/src/render.rs +++ b/src/render.rs @@ -439,6 +439,13 @@ fn render_terminal( .unwrap_or(false); let (sym, style) = match screen.cell(r as u16, c as u16) { Some(cell) => { + // Fortsättningscellen efter ett brett tecken (emoji, + // CJK) är tom i vt100 — den får INTE bli ett mellanslag, + // för då tar tecknet 2+1 kolumner och resten av raden + // förskjuts åt höger. Hoppa över den helt. + if cell.is_wide_continuation() { + continue; + } let s = cell.contents(); let s = if s.is_empty() { " ".to_string() } else { s.to_string() }; let fg = vt_color(cell.fgcolor()); diff --git a/tests/integration.py b/tests/integration.py index 746b1c2..c3e3b8c 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -172,6 +172,22 @@ def main(): text = disp.frame_text(timeout=5.0, contains="x") check("key_event når den virtuella terminalen", "x" in text) + # ── 4b. Breda tecken (emoji) får inte förskjuta raden ─────────── + # vt100 lämnar en tom fortsättningscell efter breda tecken; om + # renderaren gör den till mellanslag blir "📁X" → "📁 X" och allt + # efter emojin skjuts åt höger. + # OBS: fönstertiteln visar kommandosträngen (innehåller "X-MARKER" + # som text) — vänta därför på emojin+markören som bara finns i + # fönstrets INNEHÅLL. + app.send({"type": "spawn_window", + "command": "printf '\\360\\237\\223\\201X-MARKER\\n'; exec cat", + "request_id": "w-emoji"}) + emoji_open = app.recv_until("window_opened") + text = disp.frame_text(timeout=5.0, contains="📁X-MARKER") + check("emoji förskjuter inte raden", "📁X-MARKER" in text, + f"frame-text: {text[-200:]!r}") + app.send({"type": "close_window", "window_id": emoji_open["id"]}) + # ── 5. Popup + popup_result via Enter ─────────────────────────── app.send({ "type": "spawn_popup", "message": "Integrationstest?",