Fix wide-char (emoji/CJK) column drift through the whole render chain
Wide characters shifted everything after them one column to the right: - render.rs: vt100's empty wide-continuation cell was padded to a space, making wide chars occupy 2+1 columns — skip it entirely - ipc.rs buffer_to_ansi: same bug for daemon frames — skip cells covered by a preceding wide symbol (unicode-width) - app.rs: clamp virtual terminals to >= 2 columns (vt100 0.16 has a subtraction underflow panic on wide chars in a 1-col grid) and give content_area a sane 80x24 default so windows spawned before the first layout pass aren't degenerate - main.rs: use try_send for display frames so one slow/stalled client can no longer freeze the whole WM - integration test: new end-to-end check that emoji do not shift the row, plus fix a false match against the window title Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1731,6 +1731,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"toml",
|
||||
"toml_edit",
|
||||
"unicode-width",
|
||||
"vt100",
|
||||
]
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
12
src/app.rs
12
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();
|
||||
|
||||
13
src/ipc.rs
13
src/ipc.rs
@@ -198,9 +198,22 @@ pub fn buffer_to_ansi(buf: &Buffer) -> Vec<u8> {
|
||||
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");
|
||||
|
||||
18
src/main.rs
18
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
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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?",
|
||||
|
||||
Reference in New Issue
Block a user