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",
|
"serde_json",
|
||||||
"toml",
|
"toml",
|
||||||
"toml_edit",
|
"toml_edit",
|
||||||
|
"unicode-width",
|
||||||
"vt100",
|
"vt100",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ vt100 = "0.16"
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "bmp", "webp"] }
|
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "bmp", "webp"] }
|
||||||
interprocess = "2.4"
|
interprocess = "2.4"
|
||||||
|
unicode-width = "0.2"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|||||||
12
src/app.rs
12
src/app.rs
@@ -441,7 +441,10 @@ impl App {
|
|||||||
config,
|
config,
|
||||||
panel_rects: Vec::new(),
|
panel_rects: Vec::new(),
|
||||||
panel_item_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_panel_item: None,
|
||||||
hovered_window_close: None,
|
hovered_window_close: None,
|
||||||
hovered_resize: None,
|
hovered_resize: None,
|
||||||
@@ -574,7 +577,9 @@ impl App {
|
|||||||
// ── PTY resize om fönsterstorlek ändrats ─────────────────────────────
|
// ── PTY resize om fönsterstorlek ändrats ─────────────────────────────
|
||||||
for window in &mut self.windows {
|
for window in &mut self.windows {
|
||||||
let new_rows = window.height.saturating_sub(2).max(1);
|
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 };
|
let WindowContent::Terminal { pty, parser, alive, .. } = &mut window.content else { continue };
|
||||||
if !*alive {
|
if !*alive {
|
||||||
continue;
|
continue;
|
||||||
@@ -1445,7 +1450,8 @@ impl App {
|
|||||||
let x = ca.x as i32 + 2 + offset;
|
let x = ca.x as i32 + 2 + offset;
|
||||||
let y = ca.y as i32 + 1 + offset;
|
let y = ca.y as i32 + 1 + offset;
|
||||||
let rows = h.saturating_sub(2).max(1);
|
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 id = self.next_id;
|
||||||
let mut env_vars: Vec<(String, String)> = Vec::new();
|
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;
|
use std::fmt::Write as FmtWrite;
|
||||||
let _ = write!(out, "\x1b[{};1H", row + 1);
|
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 {
|
for col in 0..area.width {
|
||||||
|
if skip_cols > 0 {
|
||||||
|
skip_cols -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let idx = (row * area.width + col) as usize;
|
let idx = (row * area.width + col) as usize;
|
||||||
let cell = &buf.content[idx];
|
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;
|
let new_mod = cell.modifier;
|
||||||
if new_mod != last_mod {
|
if new_mod != last_mod {
|
||||||
out.push_str("\x1b[0m");
|
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)| {
|
all_clients.retain(|_, (role, _, _, tx)| {
|
||||||
if *role == ClientRole::Display {
|
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 {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -344,7 +351,14 @@ fn run_daemon_mode() -> io::Result<()> {
|
|||||||
};
|
};
|
||||||
all_clients.retain(|_, (role, _, _, tx)| {
|
all_clients.retain(|_, (role, _, _, tx)| {
|
||||||
if *role == ClientRole::Display {
|
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 {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -439,6 +439,13 @@ fn render_terminal(
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let (sym, style) = match screen.cell(r as u16, c as u16) {
|
let (sym, style) = match screen.cell(r as u16, c as u16) {
|
||||||
Some(cell) => {
|
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 = cell.contents();
|
||||||
let s = if s.is_empty() { " ".to_string() } else { s.to_string() };
|
let s = if s.is_empty() { " ".to_string() } else { s.to_string() };
|
||||||
let fg = vt_color(cell.fgcolor());
|
let fg = vt_color(cell.fgcolor());
|
||||||
|
|||||||
@@ -172,6 +172,22 @@ def main():
|
|||||||
text = disp.frame_text(timeout=5.0, contains="x")
|
text = disp.frame_text(timeout=5.0, contains="x")
|
||||||
check("key_event når den virtuella terminalen", "x" in text)
|
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 ───────────────────────────
|
# ── 5. Popup + popup_result via Enter ───────────────────────────
|
||||||
app.send({
|
app.send({
|
||||||
"type": "spawn_popup", "message": "Integrationstest?",
|
"type": "spawn_popup", "message": "Integrationstest?",
|
||||||
|
|||||||
Reference in New Issue
Block a user