diff --git a/src/app.rs b/src/app.rs index 2d3b104..0f6add4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -188,8 +188,15 @@ pub struct FloatingWindow { pub scrollback_total: usize, /// Geometri före snap/maximering — används för återställning. 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, } +/// 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. pub struct CopyMode { /// Fönstret som copy-mode gäller @@ -671,6 +678,12 @@ impl App { &mut window.kitty_gfx_carry, ); 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::Disconnected) => { @@ -702,9 +715,12 @@ impl App { 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)); let _ = pty.resize(new_rows, new_cols); - // set_size behåller skärminnehåll och scrollback-historik — - // att återskapa parsern här raderade allt vid varje resize. - parser.screen_mut().set_size(new_rows, new_cols); + // Reflow: spela upp den sparade PTY-historiken i en ny + // parser med den nya bredden, så att radbrytningar räknas + // 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(), scrollback_total: 0, prev_geom: None, + pty_log: Vec::new(), }); self.focus_window(id); } @@ -1550,6 +1567,7 @@ impl App { pending_graphics: Vec::new(), scrollback_total: 0, prev_geom: None, + pty_log: Vec::new(), }); self.focus_window(id); } @@ -1694,6 +1712,7 @@ impl App { pending_graphics: Vec::new(), scrollback_total: 0, prev_geom: None, + pty_log: Vec::new(), }); self.focus_window(id); } @@ -1734,6 +1753,7 @@ impl App { pending_graphics: Vec::new(), scrollback_total: 0, prev_geom: None, + pty_log: Vec::new(), }); self.focus_window(id); } @@ -2076,11 +2096,17 @@ fn encode_mouse_event( let term_col = col.saturating_sub(content_rect.x) + 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 { MouseEventKind::Down(MouseButton::Left) => (0, false), MouseEventKind::Down(MouseButton::Middle) => (1, 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::Middle) => (33, false), MouseEventKind::Drag(MouseButton::Right) => (34, false), @@ -2123,14 +2149,17 @@ fn encode_mouse_event( Some(match encoding { vt100::MouseProtocolEncoding::Default => { 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 => { let suffix = if is_release { 'm' } else { 'M' }; format!("\x1b[<{};{};{}{}", button, term_col, term_row, suffix).into_bytes() } 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] { let code_point = coord as u32 + 32; if let Some(c) = char::from_u32(code_point) { diff --git a/tests/integration.py b/tests/integration.py index e556fce..36dac3d 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -198,6 +198,11 @@ def main(): text = disp.frame_text(timeout=5.0, contains="[<0;") check("musklick forwardas som SGR till PTY", "[<0;" in text, 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) disp.send(mouse_event("ScrollDown", cx, cy)) @@ -247,6 +252,30 @@ def main(): f"frame-tail: {text[-200:]!r}") 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 ─ app.send({"type": "spawn_window", "command": "seq 1 200; exec cat",