Fix SGR mouse release button + reflow wrapped lines on resize
- SGR (1006) release events now carry the same button number as the
press ('m' suffix marks release); the legacy 'button 3 = release'
is only used for X10/UTF8 encodings. ncurses apps (htop etc.) could
not pair press+release before, so clicks were ignored while scroll
(press-only wheel events) worked
- Each window keeps its recent raw PTY output (capped at 512 KB); on
resize the history is replayed into a fresh parser at the new width
so long lines re-wrap correctly instead of keeping old break points
- Integration: release-button and reflow checks, 33/33 passing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
41
src/app.rs
41
src/app.rs
@@ -188,8 +188,15 @@ pub struct FloatingWindow {
|
|||||||
pub scrollback_total: usize,
|
pub scrollback_total: usize,
|
||||||
/// Geometri före snap/maximering — används för återställning.
|
/// Geometri före snap/maximering — används för återställning.
|
||||||
pub prev_geom: Option<(i32, i32, u16, u16)>,
|
pub prev_geom: Option<(i32, i32, u16, u16)>,
|
||||||
|
/// Rå PTY-utdata (kapad till de senaste ~512 KB). Spelas upp i en
|
||||||
|
/// ny parser vid resize så att långa rader radbryts om ("reflow")
|
||||||
|
/// för den nya bredden — vt100:s set_size behåller gamla brytningar.
|
||||||
|
pty_log: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Största mängden rå PTY-historik som sparas per fönster (för reflow).
|
||||||
|
const PTY_LOG_CAP: usize = 512 * 1024;
|
||||||
|
|
||||||
/// Copy-mode: tangentbordsstyrd bläddring och sökning i scrollback.
|
/// Copy-mode: tangentbordsstyrd bläddring och sökning i scrollback.
|
||||||
pub struct CopyMode {
|
pub struct CopyMode {
|
||||||
/// Fönstret som copy-mode gäller
|
/// Fönstret som copy-mode gäller
|
||||||
@@ -671,6 +678,12 @@ impl App {
|
|||||||
&mut window.kitty_gfx_carry,
|
&mut window.kitty_gfx_carry,
|
||||||
);
|
);
|
||||||
window.pending_graphics.extend(gfx);
|
window.pending_graphics.extend(gfx);
|
||||||
|
// Spara rå-datat för reflow vid resize
|
||||||
|
window.pty_log.extend_from_slice(&data);
|
||||||
|
if window.pty_log.len() > PTY_LOG_CAP {
|
||||||
|
let excess = window.pty_log.len() - PTY_LOG_CAP;
|
||||||
|
window.pty_log.drain(..excess);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(mpsc::TryRecvError::Empty) => break,
|
Err(mpsc::TryRecvError::Empty) => break,
|
||||||
Err(mpsc::TryRecvError::Disconnected) => {
|
Err(mpsc::TryRecvError::Disconnected) => {
|
||||||
@@ -702,9 +715,12 @@ impl App {
|
|||||||
if cur_rows != new_rows || cur_cols != new_cols {
|
if cur_rows != new_rows || cur_cols != new_cols {
|
||||||
crate::log::log(&format!("PTY resize: {}x{} -> {}x{} (mouse mode was {:?})", cur_cols, cur_rows, new_cols, new_rows, window.mouse_mode));
|
crate::log::log(&format!("PTY resize: {}x{} -> {}x{} (mouse mode was {:?})", cur_cols, cur_rows, new_cols, new_rows, window.mouse_mode));
|
||||||
let _ = pty.resize(new_rows, new_cols);
|
let _ = pty.resize(new_rows, new_cols);
|
||||||
// set_size behåller skärminnehåll och scrollback-historik —
|
// Reflow: spela upp den sparade PTY-historiken i en ny
|
||||||
// att återskapa parsern här raderade allt vid varje resize.
|
// parser med den nya bredden, så att radbrytningar räknas
|
||||||
parser.screen_mut().set_size(new_rows, new_cols);
|
// om (set_size behåller de gamla brytpunkterna).
|
||||||
|
let mut fresh = vt100::Parser::new(new_rows, new_cols, self.config.scrollback_lines);
|
||||||
|
fresh.process(&window.pty_log);
|
||||||
|
*parser = fresh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1508,6 +1524,7 @@ impl App {
|
|||||||
pending_graphics: Vec::new(),
|
pending_graphics: Vec::new(),
|
||||||
scrollback_total: 0,
|
scrollback_total: 0,
|
||||||
prev_geom: None,
|
prev_geom: None,
|
||||||
|
pty_log: Vec::new(),
|
||||||
});
|
});
|
||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
}
|
}
|
||||||
@@ -1550,6 +1567,7 @@ impl App {
|
|||||||
pending_graphics: Vec::new(),
|
pending_graphics: Vec::new(),
|
||||||
scrollback_total: 0,
|
scrollback_total: 0,
|
||||||
prev_geom: None,
|
prev_geom: None,
|
||||||
|
pty_log: Vec::new(),
|
||||||
});
|
});
|
||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
}
|
}
|
||||||
@@ -1694,6 +1712,7 @@ impl App {
|
|||||||
pending_graphics: Vec::new(),
|
pending_graphics: Vec::new(),
|
||||||
scrollback_total: 0,
|
scrollback_total: 0,
|
||||||
prev_geom: None,
|
prev_geom: None,
|
||||||
|
pty_log: Vec::new(),
|
||||||
});
|
});
|
||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
}
|
}
|
||||||
@@ -1734,6 +1753,7 @@ impl App {
|
|||||||
pending_graphics: Vec::new(),
|
pending_graphics: Vec::new(),
|
||||||
scrollback_total: 0,
|
scrollback_total: 0,
|
||||||
prev_geom: None,
|
prev_geom: None,
|
||||||
|
pty_log: Vec::new(),
|
||||||
});
|
});
|
||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
}
|
}
|
||||||
@@ -2076,11 +2096,17 @@ fn encode_mouse_event(
|
|||||||
let term_col = col.saturating_sub(content_rect.x) + 1;
|
let term_col = col.saturating_sub(content_rect.x) + 1;
|
||||||
let term_row = row.saturating_sub(content_rect.y) + 1;
|
let term_row = row.saturating_sub(content_rect.y) + 1;
|
||||||
|
|
||||||
|
// OBS: i SGR-kodningen (1006) ska släpp-eventet bära SAMMA knappnummer
|
||||||
|
// som trycket (suffix 'm' markerar släpp). Det legacy-baserade "knapp 3
|
||||||
|
// = släpp" gäller bara X10/UTF8-kodningarna — annars kan ncurses-appar
|
||||||
|
// (htop m.fl.) inte para ihop tryck+släpp och inget klick registreras.
|
||||||
let (button, is_release): (u8, bool) = match kind {
|
let (button, is_release): (u8, bool) = match kind {
|
||||||
MouseEventKind::Down(MouseButton::Left) => (0, false),
|
MouseEventKind::Down(MouseButton::Left) => (0, false),
|
||||||
MouseEventKind::Down(MouseButton::Middle) => (1, false),
|
MouseEventKind::Down(MouseButton::Middle) => (1, false),
|
||||||
MouseEventKind::Down(MouseButton::Right) => (2, false),
|
MouseEventKind::Down(MouseButton::Right) => (2, false),
|
||||||
MouseEventKind::Up(_) => (3, true),
|
MouseEventKind::Up(MouseButton::Left) => (0, true),
|
||||||
|
MouseEventKind::Up(MouseButton::Middle) => (1, true),
|
||||||
|
MouseEventKind::Up(MouseButton::Right) => (2, true),
|
||||||
MouseEventKind::Drag(MouseButton::Left) => (32, false),
|
MouseEventKind::Drag(MouseButton::Left) => (32, false),
|
||||||
MouseEventKind::Drag(MouseButton::Middle) => (33, false),
|
MouseEventKind::Drag(MouseButton::Middle) => (33, false),
|
||||||
MouseEventKind::Drag(MouseButton::Right) => (34, false),
|
MouseEventKind::Drag(MouseButton::Right) => (34, false),
|
||||||
@@ -2123,14 +2149,17 @@ fn encode_mouse_event(
|
|||||||
Some(match encoding {
|
Some(match encoding {
|
||||||
vt100::MouseProtocolEncoding::Default => {
|
vt100::MouseProtocolEncoding::Default => {
|
||||||
if term_col > 223 || term_row > 223 { return None; }
|
if term_col > 223 || term_row > 223 { return None; }
|
||||||
vec![0x1b, b'[', b'M', button + 32, term_col as u8 + 32, term_row as u8 + 32]
|
// Legacy-protokollet kan inte ange vilken knapp som släpptes
|
||||||
|
let b = if is_release { 3 } else { button };
|
||||||
|
vec![0x1b, b'[', b'M', b + 32, term_col as u8 + 32, term_row as u8 + 32]
|
||||||
}
|
}
|
||||||
vt100::MouseProtocolEncoding::Sgr => {
|
vt100::MouseProtocolEncoding::Sgr => {
|
||||||
let suffix = if is_release { 'm' } else { 'M' };
|
let suffix = if is_release { 'm' } else { 'M' };
|
||||||
format!("\x1b[<{};{};{}{}", button, term_col, term_row, suffix).into_bytes()
|
format!("\x1b[<{};{};{}{}", button, term_col, term_row, suffix).into_bytes()
|
||||||
}
|
}
|
||||||
vt100::MouseProtocolEncoding::Utf8 => {
|
vt100::MouseProtocolEncoding::Utf8 => {
|
||||||
let mut bytes = vec![0x1b, b'[', b'M', button + 32];
|
let b = if is_release { 3 } else { button };
|
||||||
|
let mut bytes = vec![0x1b, b'[', b'M', b + 32];
|
||||||
for coord in [term_col, term_row] {
|
for coord in [term_col, term_row] {
|
||||||
let code_point = coord as u32 + 32;
|
let code_point = coord as u32 + 32;
|
||||||
if let Some(c) = char::from_u32(code_point) {
|
if let Some(c) = char::from_u32(code_point) {
|
||||||
|
|||||||
@@ -198,6 +198,11 @@ def main():
|
|||||||
text = disp.frame_text(timeout=5.0, contains="[<0;")
|
text = disp.frame_text(timeout=5.0, contains="[<0;")
|
||||||
check("musklick forwardas som SGR till PTY", "[<0;" in text,
|
check("musklick forwardas som SGR till PTY", "[<0;" in text,
|
||||||
f"frame-text: {text[-200:]!r}")
|
f"frame-text: {text[-200:]!r}")
|
||||||
|
# Släppet ska bära SAMMA knappnummer med 'm'-suffix (SGR), inte
|
||||||
|
# legacy-koden 3 — annars registrerar ncurses-appar inga klick.
|
||||||
|
text = disp.frame_text(timeout=5.0, contains="[<0;4;3m")
|
||||||
|
check("mus-släpp forwardas med korrekt knappnummer", "[<0;4;3m" in text,
|
||||||
|
f"frame-text: {text[-200:]!r}")
|
||||||
|
|
||||||
# Scroll ska också forwardas (SGR-knapp 64/65)
|
# Scroll ska också forwardas (SGR-knapp 64/65)
|
||||||
disp.send(mouse_event("ScrollDown", cx, cy))
|
disp.send(mouse_event("ScrollDown", cx, cy))
|
||||||
@@ -247,6 +252,30 @@ def main():
|
|||||||
f"frame-tail: {text[-200:]!r}")
|
f"frame-tail: {text[-200:]!r}")
|
||||||
app.send({"type": "close_window", "window_id": rz_open["id"]})
|
app.send({"type": "close_window", "window_id": rz_open["id"]})
|
||||||
|
|
||||||
|
# ── 4b3. Reflow: långa rader radbryts om efter resize ────────────
|
||||||
|
long = "A" * 90
|
||||||
|
app.send({"type": "spawn_window",
|
||||||
|
"command": f"printf '{long}\\n'; exec cat",
|
||||||
|
"request_id": "w-reflow"})
|
||||||
|
rf_open = app.recv_until("window_opened")
|
||||||
|
disp.frame_text(timeout=5.0, contains="AAAA")
|
||||||
|
app.send({"type": "list_windows", "request_id": "lrf"})
|
||||||
|
wrf = next(w for w in app.recv_until("window_list")["windows"]
|
||||||
|
if w["id"] == rf_open["id"])
|
||||||
|
# 90 A på 80 kolumner bred terminal → raden är bruten
|
||||||
|
text = disp.frame_text(timeout=2.0)
|
||||||
|
check("lång rad är bruten före resize", long not in text)
|
||||||
|
# dra nedre högra hörnet så innehållet blir ≥ 90 kolumner brett
|
||||||
|
cx0, cy0 = wrf["x"] + wrf["width"] - 1, wrf["y"] + wrf["height"] - 1
|
||||||
|
grow = 94 - (wrf["width"] - 2)
|
||||||
|
disp.send(mouse_event({"Down": "Left"}, cx0, cy0))
|
||||||
|
disp.send(mouse_event({"Drag": "Left"}, min(cx0 + grow, 99), cy0))
|
||||||
|
disp.send(mouse_event({"Up": "Left"}, min(cx0 + grow, 99), cy0))
|
||||||
|
text = disp.frame_text(timeout=5.0, contains=long)
|
||||||
|
check("raden radbryts om (reflow) efter resize", long in text,
|
||||||
|
f"bredd efter: {wrf['width']}+{grow}")
|
||||||
|
app.send({"type": "close_window", "window_id": rf_open["id"]})
|
||||||
|
|
||||||
# ── 4c. Scrollback: mushjul utan mus-tracking scrollar historiken ─
|
# ── 4c. Scrollback: mushjul utan mus-tracking scrollar historiken ─
|
||||||
app.send({"type": "spawn_window",
|
app.send({"type": "spawn_window",
|
||||||
"command": "seq 1 200; exec cat",
|
"command": "seq 1 200; exec cat",
|
||||||
|
|||||||
Reference in New Issue
Block a user