use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use portable_pty::{PtySize, native_pty_system};
use ratatui::prelude::*;
use crate::types::{AppState, Mode, Pane, Node, LayoutKind, DragState, Window, FocusDir};
use crate::tree::{active_pane, active_pane_mut, compute_rects, compute_split_borders,
split_sizes_at, adjust_split_sizes, get_split_mut, resize_all_panes};
use crate::pane::{detect_shell, build_default_shell, set_tmux_env};
use crate::copy_mode::{enter_copy_mode, exit_copy_mode, scroll_copy_up, scroll_copy_down, yank_selection};
use crate::platform::mouse_inject;
fn mouse_log(msg: &str) {
use std::sync::LazyLock;
static ENABLED: LazyLock<bool> = LazyLock::new(|| {
std::env::var("PSMUX_MOUSE_DEBUG").unwrap_or_default() == "1"
});
if !*ENABLED { return; }
use std::sync::atomic::{AtomicU32, Ordering};
static COUNT: AtomicU32 = AtomicU32::new(0);
let n = COUNT.fetch_add(1, Ordering::Relaxed);
if n > 2000 { return; }
let home = std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME")).unwrap_or_default();
let path = format!("{}/.psmux/mouse_debug.log", home);
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&path) {
let _ = writeln!(f, "[{}] {}", chrono::Local::now().format("%H:%M:%S%.3f"), msg);
}
}
fn pane_inner_cell_0based(area: Rect, abs_x: u16, abs_y: u16) -> (i16, i16) {
let col = abs_x as i16 - area.x as i16;
let row = abs_y as i16 - area.y as i16;
(col, row)
}
fn pane_inner_cell(area: Rect, abs_x: u16, abs_y: u16) -> (u16, u16) {
let col = abs_x.saturating_sub(area.x) + 1;
let row = abs_y.saturating_sub(area.y) + 1;
(col, row)
}
fn map_client_coords(app: &AppState, x: u16, y: u16) -> (u16, u16) {
let cid = match app.latest_client_id {
Some(id) => id,
None => return (x, y),
};
let (cw, ch) = match app.client_sizes.get(&cid) {
Some(&size) => size,
None => return (x, y),
};
let ew = app.last_window_area.width;
let eh = app.last_window_area.height;
if cw == ew && ch == eh {
return (x, y);
}
let mx = if cw > 0 { ((x as u32) * (ew as u32) / (cw as u32)) as u16 } else { x };
let my = if ch > 0 { ((y as u32) * (eh as u32) / (ch as u32)) as u16 } else { y };
(mx.min(ew.saturating_sub(1)), my.min(eh.saturating_sub(1)))
}
pub fn write_mouse_event_remote(master: &mut dyn std::io::Write, button: u8, col: u16, row: u16, press: bool, enc: vt100::MouseProtocolEncoding) {
match enc {
vt100::MouseProtocolEncoding::Sgr => {
let ch = if press { 'M' } else { 'm' };
let _ = write!(master, "\x1b[<{};{};{}{}", button, col, row, ch);
let _ = master.flush();
}
_ => {
if press {
let cb = (button + 32) as u8;
let cx = ((col as u8).min(223)) + 32;
let cy = ((row as u8).min(223)) + 32;
let _ = master.write_all(&[0x1b, b'[', b'M', cb, cx, cy]);
let _ = master.flush();
}
}
}
}
fn inject_mouse(pane: &mut Pane, col: i16, row: i16, button_state: u32, event_flags: u32) -> bool {
if pane.child_pid.is_none() {
pane.child_pid = mouse_inject::get_child_pid(&*pane.child);
}
if let Some(pid) = pane.child_pid {
mouse_inject::send_mouse_event(pid, col, row, button_state, event_flags, false)
} else {
false
}
}
fn is_vt_bridge(name: &str) -> bool {
let lower = name.to_lowercase();
lower.contains("wsl") || lower.contains("ssh")
}
pub(crate) fn screen_has_tui_content(pane: &Pane) -> bool {
if let Ok(parser) = pane.term.lock() {
let screen = parser.screen();
if screen.alternate_screen() {
return true;
}
let last_row = pane.last_rows.saturating_sub(1);
for col in 0..pane.last_cols.min(80) {
if let Some(cell) = screen.cell(last_row, col) {
let t = cell.contents();
if !t.is_empty() && t != " " {
return true;
}
}
}
}
false
}
pub(crate) fn is_fullscreen_tui(pane: &Pane) -> bool {
if let Ok(parser) = pane.term.lock() {
let screen = parser.screen();
if screen.alternate_screen() {
return true;
}
let rows = pane.last_rows;
if rows < 3 { return false; }
let (cursor_row, _) = screen.cursor_position();
let last_row = rows.saturating_sub(1);
if cursor_row < last_row.saturating_sub(2) {
return false;
}
let check_rows = 4u16.min(rows);
let mut filled = 0u16;
for r in (last_row + 1 - check_rows)..=last_row {
let mut has_content = false;
for col in 0..pane.last_cols.min(40) { if let Some(cell) = screen.cell(r, col) {
let t = cell.contents();
if !t.is_empty() && t != " " {
has_content = true;
break;
}
}
}
if has_content { filled += 1; }
}
return filled >= 3;
}
false
}
pub(crate) fn pane_wants_mouse(pane: &Pane) -> bool {
if let Ok(parser) = pane.term.lock() {
let screen = parser.screen();
if screen.mouse_protocol_mode() != vt100::MouseProtocolMode::None {
return true;
}
if screen.alternate_screen() {
return true;
}
}
false
}
fn detect_vt_bridge(pane: &mut Pane) -> bool {
if let Some((ts, cached)) = pane.vt_bridge_cache {
if ts.elapsed().as_secs() < 2 {
return cached;
}
}
if pane.child_pid.is_none() {
pane.child_pid = mouse_inject::get_child_pid(&*pane.child);
}
let result = if let Some(pid) = pane.child_pid {
crate::platform::process_info::has_vt_bridge_descendant(pid)
} else {
false
};
pane.vt_bridge_cache = Some((std::time::Instant::now(), result));
result
}
fn detect_mouse_input(pane: &mut Pane) -> bool {
if let Some((ts, cached)) = pane.mouse_input_cache {
if ts.elapsed().as_secs() < 2 {
return cached;
}
}
if pane.child_pid.is_none() {
pane.child_pid = mouse_inject::get_child_pid(&*pane.child);
}
let result = if let Some(pid) = pane.child_pid {
mouse_inject::query_mouse_input_enabled(pid).unwrap_or(false)
} else {
false
};
pane.mouse_input_cache = Some((std::time::Instant::now(), result));
result
}
fn inject_sgr_mouse(pane: &mut Pane, col: i16, row: i16, vt_button: u8, press: bool) -> bool {
let vt_col = (col + 1).max(1) as u16;
let vt_row = (row + 1).max(1) as u16;
let ch = if press { 'M' } else { 'm' };
let sgr_seq = format!("\x1b[<{};{};{}{}", vt_button, vt_col, vt_row, ch);
mouse_log(&format!(" -> Console VT injection (KEY_EVENTs): seq={:?}", sgr_seq));
if pane.child_pid.is_none() {
pane.child_pid = mouse_inject::get_child_pid(&*pane.child);
}
if let Some(pid) = pane.child_pid {
let ok = mouse_inject::send_vt_sequence(pid, sgr_seq.as_bytes());
mouse_log(&format!(" -> Console VT inject result: {}", ok));
ok
} else {
false
}
}
fn write_mouse_to_pty(pane: &mut Pane, col: i16, row: i16, vt_button: u8, press: bool) {
use std::io::Write as _;
let vt_col = (col + 1).max(1) as u16;
let vt_row = (row + 1).max(1) as u16;
let ch = if press { b'M' } else { b'm' };
let mut buf = [0u8; 32];
let len = {
let mut cursor = std::io::Cursor::new(&mut buf[..]);
let _ = write!(cursor, "\x1b[<{};{};{}{}", vt_button, vt_col, vt_row, ch as char);
cursor.position() as usize
};
mouse_log(&format!(" -> PTY pipe SGR mouse: seq={:?}", std::str::from_utf8(&buf[..len]).unwrap_or("?")));
let _ = pane.writer.write_all(&buf[..len]);
let _ = pane.writer.flush();
}
pub(crate) fn inject_mouse_combined(pane: &mut Pane, col: i16, row: i16, vt_button: u8, press: bool,
_button_state: u32, _event_flags: u32, win_name: &str) {
let vt_bridge = detect_vt_bridge(pane);
if vt_bridge {
let wants = pane.term.lock().ok()
.map_or(false, |t| t.screen().mouse_protocol_mode() != vt100::MouseProtocolMode::None);
if !wants {
mouse_log(&format!("inject_mouse_combined: col={} row={} vt_btn={} press={} win={} vt_bridge=true -> SUPPRESSED (remote has no mouse tracking)",
col, row, vt_button, press, win_name));
return;
}
mouse_log(&format!("inject_mouse_combined: col={} row={} vt_btn={} press={} win={} vt_bridge=true -> WriteConsoleInputW KEY_EVENT injection",
col, row, vt_button, press, win_name));
inject_sgr_mouse(pane, col, row, vt_button, press);
} else {
mouse_log(&format!("inject_mouse_combined: col={} row={} vt_btn={} press={} win={} -> PTY pipe SGR mouse (Windows Terminal method)",
col, row, vt_button, press, win_name));
write_mouse_to_pty(pane, col, row, vt_button, press);
}
}
pub fn push_zoom(app: &mut AppState) -> bool {
if app.windows[app.active_idx].zoom_saved.is_some() {
unzoom_if_zoomed(app);
true
} else {
false
}
}
pub fn pop_zoom(app: &mut AppState, was_zoomed: bool) {
if was_zoomed && app.windows[app.active_idx].zoom_saved.is_none() {
toggle_zoom(app);
}
}
pub fn unzoom_if_zoomed(app: &mut AppState) -> bool {
if let Some(saved) = app.windows[app.active_idx].zoom_saved.take() {
let win = &mut app.windows[app.active_idx];
for (p, sz) in saved.into_iter() {
if let Some(Node::Split { sizes, .. }) = get_split_mut(&mut win.root, &p) { *sizes = sz; }
}
resize_all_panes(app);
true
} else {
false
}
}
pub fn toggle_zoom(app: &mut AppState) {
let win = &mut app.windows[app.active_idx];
if win.zoom_saved.is_none() {
let mut saved: Vec<(Vec<usize>, Vec<u16>)> = Vec::new();
for depth in 0..win.active_path.len() {
let p = win.active_path[..depth].to_vec();
if let Some(Node::Split { sizes, .. }) = get_split_mut(&mut win.root, &p) {
let idx = win.active_path.get(depth).copied().unwrap_or(0);
saved.push((p.clone(), sizes.clone()));
for i in 0..sizes.len() { sizes[i] = if i == idx { 100 } else { 0 }; }
}
}
win.zoom_saved = Some(saved);
} else {
if let Some(saved) = app.windows[app.active_idx].zoom_saved.take() {
let win = &mut app.windows[app.active_idx];
for (p, sz) in saved.into_iter() {
if let Some(Node::Split { sizes, .. }) = get_split_mut(&mut win.root, &p) { *sizes = sz; }
}
}
}
resize_all_panes(app);
}
#[allow(dead_code)]
pub fn update_tab_positions(app: &mut AppState) {
let mut tab_pos: Vec<(usize, u16, u16)> = Vec::new();
let mut cursor_x: u16 = 0;
let session_label_len = app.session_name.len() as u16 + 3; cursor_x += session_label_len;
for (i, w) in app.windows.iter().enumerate() {
let display_idx = i + app.window_base_index;
let label = format!("{}: {} ", display_idx, w.name);
let start_x = cursor_x;
cursor_x += label.len() as u16;
tab_pos.push((i, start_x, cursor_x));
}
app.tab_positions = tab_pos;
}
pub fn remote_mouse_down(app: &mut AppState, x: u16, y: u16) {
let (x, y) = map_client_coords(app, x, y);
let status_row = app.last_window_area.y + app.last_window_area.height;
if y == status_row {
return;
}
let win = &mut app.windows[app.active_idx];
let mut rects: Vec<(Vec<usize>, Rect)> = Vec::new();
compute_rects(&win.root, app.last_window_area, &mut rects);
let mut active_area: Option<Rect> = None;
for (path, area) in rects.iter() {
if area.contains(ratatui::layout::Position { x, y }) {
win.active_path = path.clone();
if let Some(pid) = crate::tree::get_active_pane_id(&win.root, path) {
crate::tree::touch_mru(&mut win.pane_mru, pid);
}
active_area = Some(*area);
}
}
if matches!(app.mode, Mode::CopyMode | Mode::CopySearch { .. }) {
if let Some(area) = active_area {
let (row, col) = copy_cell_for_area(area, x, y);
app.copy_anchor = None;
app.copy_pos = Some((row, col));
}
return;
}
let mut on_border = false;
let mut borders: Vec<(Vec<usize>, LayoutKind, usize, u16, u16)> = Vec::new();
if win.zoom_saved.is_none() {
compute_split_borders(&win.root, app.last_window_area, &mut borders);
}
let tol = 1u16;
for (path, kind, idx, pos, total_px) in borders.iter() {
match kind {
LayoutKind::Horizontal => {
if x >= pos.saturating_sub(tol) && x <= pos + tol { if let Some((left,right)) = split_sizes_at(&win.root, path.clone(), *idx) { app.drag = Some(DragState { split_path: path.clone(), kind: *kind, index: *idx, start_x: *pos, start_y: y, left_initial: left, _right_initial: right, total_pixels: *total_px }); } on_border = true; break; }
}
LayoutKind::Vertical => {
if y >= pos.saturating_sub(tol) && y <= pos + tol { if let Some((left,right)) = split_sizes_at(&win.root, path.clone(), *idx) { app.drag = Some(DragState { split_path: path.clone(), kind: *kind, index: *idx, start_x: x, start_y: *pos, left_initial: left, _right_initial: right, total_pixels: *total_px }); } on_border = true; break; }
}
}
}
if !on_border {
if let Some(area) = active_area {
let (col, row) = pane_inner_cell_0based(area, x, y);
let win_name = win.name.clone();
if let Some(active) = active_pane_mut(&mut win.root, &win.active_path) {
if pane_wants_mouse(active) {
inject_mouse_combined(active, col, row, 0, true,
mouse_inject::FROM_LEFT_1ST_BUTTON_PRESSED, 0, &win_name);
}
}
}
}
}
pub fn remote_mouse_drag(app: &mut AppState, x: u16, y: u16) {
let (x, y) = map_client_coords(app, x, y);
let win = &mut app.windows[app.active_idx];
let mut rects: Vec<(Vec<usize>, Rect)> = Vec::new();
compute_rects(&win.root, app.last_window_area, &mut rects);
if matches!(app.mode, Mode::CopyMode | Mode::CopySearch { .. }) {
if let Some((path, area)) = rects.iter().find(|(_, area)| area.contains(ratatui::layout::Position { x, y })) {
win.active_path = path.clone();
let (row, col) = copy_cell_for_area(*area, x, y);
if app.copy_anchor.is_none() {
app.copy_anchor = Some((row, col));
app.copy_anchor_scroll_offset = app.copy_scroll_offset;
app.copy_selection_mode = crate::types::SelectionMode::Char;
}
app.copy_pos = Some((row, col));
}
return;
}
if let Some(d) = &app.drag {
adjust_split_sizes(&mut win.root, d, x, y);
} else {
if let Some(area) = rects.iter().find(|(path, _)| *path == win.active_path).map(|(_, a)| *a) {
let (col, row) = pane_inner_cell_0based(area, x, y);
let win_name = win.name.clone();
if let Some(active) = active_pane_mut(&mut win.root, &win.active_path) {
if pane_wants_mouse(active) {
inject_mouse_combined(active, col, row, 32, true,
mouse_inject::FROM_LEFT_1ST_BUTTON_PRESSED, mouse_inject::MOUSE_MOVED, &win_name);
}
}
}
}
}
pub fn remote_mouse_up(app: &mut AppState, x: u16, y: u16) {
let (x, y) = map_client_coords(app, x, y);
let win = &mut app.windows[app.active_idx];
let mut rects: Vec<(Vec<usize>, Rect)> = Vec::new();
compute_rects(&win.root, app.last_window_area, &mut rects);
if matches!(app.mode, Mode::CopyMode | Mode::CopySearch { .. }) {
if let Some((path, area)) = rects.iter().find(|(_, area)| area.contains(ratatui::layout::Position { x, y })) {
win.active_path = path.clone();
let (row, col) = copy_cell_for_area(*area, x, y);
if app.copy_anchor.is_none() {
app.copy_anchor = Some((row, col));
app.copy_anchor_scroll_offset = app.copy_scroll_offset;
}
app.copy_pos = Some((row, col));
}
if let (Some(a), Some(p)) = (app.copy_anchor, app.copy_pos) {
if a != p {
let _ = yank_selection(app);
}
}
return;
}
let was_dragging = app.drag.is_some();
app.drag = None;
if was_dragging {
resize_all_panes(app);
return;
}
if let Some(area) = rects.iter().find(|(path, _)| *path == win.active_path).map(|(_, a)| *a) {
let (col, row) = pane_inner_cell_0based(area, x, y);
let win_name = win.name.clone();
if let Some(active) = active_pane_mut(&mut win.root, &win.active_path) {
if pane_wants_mouse(active) {
inject_mouse_combined(active, col, row, 0, false,
0, 0, &win_name);
}
}
}
}
pub fn remote_mouse_button(app: &mut AppState, x: u16, y: u16, button: u8, press: bool) {
let (x, y) = map_client_coords(app, x, y);
let win = &mut app.windows[app.active_idx];
let mut rects: Vec<(Vec<usize>, Rect)> = Vec::new();
compute_rects(&win.root, app.last_window_area, &mut rects);
if let Some(area) = rects.iter().find(|(path, _)| *path == win.active_path).map(|(_, a)| *a) {
let (col, row) = pane_inner_cell_0based(area, x, y);
let win_name = win.name.clone();
if let Some(active) = active_pane_mut(&mut win.root, &win.active_path) {
if pane_wants_mouse(active) {
let sgr_btn = match button {
1 => 1u8, 2 => 2u8, _ => 0u8,
};
let button_state = if press {
match button {
1 => mouse_inject::FROM_LEFT_2ND_BUTTON_PRESSED,
2 => mouse_inject::RIGHTMOST_BUTTON_PRESSED,
_ => 0,
}
} else {
0
};
inject_mouse_combined(active, col, row, sgr_btn, press,
button_state, 0, &win_name);
}
}
}
}
pub fn remote_mouse_motion(app: &mut AppState, x: u16, y: u16) {
let (x, y) = map_client_coords(app, x, y);
if app.last_hover_pos == Some((x, y)) {
return;
}
app.last_hover_pos = Some((x, y));
let win = &mut app.windows[app.active_idx];
let mut rects: Vec<(Vec<usize>, Rect)> = Vec::new();
compute_rects(&win.root, app.last_window_area, &mut rects);
mouse_log(&format!("remote_mouse_motion: x={} y={}", x, y));
if let Some(area) = rects.iter().find(|(path, _)| *path == win.active_path).map(|(_, a)| *a) {
let (col, row) = pane_inner_cell_0based(area, x, y);
let win_name = win.name.clone();
if let Some(active) = active_pane_mut(&mut win.root, &win.active_path) {
if pane_wants_mouse(active) {
inject_mouse_combined(active, col, row, 35, true,
0, mouse_inject::MOUSE_MOVED, &win_name);
}
}
}
}
fn wheel_cell_for_area(area: Rect, x: u16, y: u16) -> (u16, u16) {
let col = x.saturating_sub(area.x).min(area.width.saturating_sub(1)).saturating_add(1);
let row = y.saturating_sub(area.y).min(area.height.saturating_sub(1)).saturating_add(1);
(col, row)
}
fn copy_cell_for_area(area: Rect, x: u16, y: u16) -> (u16, u16) {
let col = x.saturating_sub(area.x).min(area.width.saturating_sub(1));
let row = y.saturating_sub(area.y).min(area.height.saturating_sub(1));
(row, col)
}
fn remote_scroll_wheel(app: &mut AppState, x: u16, y: u16, up: bool) {
let (x, y) = map_client_coords(app, x, y);
let mode_str = match &app.mode {
Mode::Passthrough => "Passthrough",
Mode::CopyMode => "CopyMode",
Mode::CopySearch { .. } => "CopySearch",
_ => "Other",
};
mouse_log(&format!("remote_scroll_wheel: x={} y={} up={} mode={}", x, y, up, mode_str));
if matches!(app.mode, Mode::PopupMode { .. }) {
mouse_log(" -> popup mode, ignoring scroll");
return;
}
if matches!(app.mode, Mode::CopyMode | Mode::CopySearch { .. }) {
mouse_log(" -> already in copy mode, scrolling within");
if up {
scroll_copy_up(app, 3);
} else {
scroll_copy_down(app, 3);
if app.copy_scroll_offset == 0 && app.copy_anchor.is_none() {
exit_copy_mode(app);
}
}
return;
}
let (child_in_alt_screen, target_area_opt, sgr_btn, button_state) = {
let win = &mut app.windows[app.active_idx];
let mut rects: Vec<(Vec<usize>, Rect)> = Vec::new();
compute_rects(&win.root, app.last_window_area, &mut rects);
let mut target_area: Option<Rect> = None;
for (path, area) in &rects {
if area.contains(ratatui::layout::Position { x, y }) {
win.active_path = path.clone();
target_area = Some(*area);
break;
}
}
if target_area.is_none() {
target_area = rects
.iter()
.find(|(path, _)| *path == win.active_path)
.map(|(_, area)| *area);
}
let alt = active_pane(&win.root, &win.active_path)
.map_or(false, |p| {
if let Ok(parser) = p.term.lock() {
return parser.screen().alternate_screen();
}
false
});
let sgr_btn: u8 = if up { 64 } else { 65 };
let wheel_delta: i16 = if up { 120 } else { -120 };
let bs = ((wheel_delta as i32) << 16) as u32;
(alt, target_area, sgr_btn, bs)
};
mouse_log(&format!(" -> alt_screen={}", child_in_alt_screen));
if child_in_alt_screen {
mouse_log(" -> forwarding scroll to child TUI (alt screen)");
let win = &mut app.windows[app.active_idx];
let (col, row) = target_area_opt.map_or((0, 0), |area| pane_inner_cell_0based(area, x, y));
let win_name = win.name.clone();
if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) {
inject_mouse_combined(p, col, row, sgr_btn, true,
button_state, mouse_inject::MOUSE_WHEELED, &win_name);
}
} else if up {
mouse_log(" -> entering copy mode (shell scroll-up)");
enter_copy_mode(app);
scroll_copy_up(app, 3);
} else {
mouse_log(" -> scroll-down at shell (no-op)");
}
}
pub fn remote_scroll_up(app: &mut AppState, x: u16, y: u16) { remote_scroll_wheel(app, x, y, true); }
pub fn remote_scroll_down(app: &mut AppState, x: u16, y: u16) { remote_scroll_wheel(app, x, y, false); }
pub fn handle_pane_mouse(app: &mut AppState, pane_id: usize, button: u8, col: i16, row: i16, press: bool) {
let win = &mut app.windows[app.active_idx];
let mut found_path: Option<Vec<usize>> = None;
let mut rects: Vec<(Vec<usize>, Rect)> = Vec::new();
compute_rects(&win.root, app.last_window_area, &mut rects);
for (path, _area) in &rects {
if let Some(pid) = crate::tree::get_active_pane_id(&win.root, path) {
if pid == pane_id {
found_path = Some(path.clone());
break;
}
}
}
let Some(path) = found_path else { return; };
let is_click = matches!(button, 0 | 1 | 2) && press;
if is_click && win.active_path != path {
win.active_path = path.clone();
if let Some(pid) = crate::tree::get_active_pane_id(&win.root, &path) {
crate::tree::touch_mru(&mut win.pane_mru, pid);
}
}
if matches!(app.mode, Mode::CopyMode | Mode::CopySearch { .. }) {
let r = row.max(0) as u16;
let c = col.max(0) as u16;
if button == 0 && press {
app.copy_anchor = None;
app.copy_pos = Some((r, c));
} else if button == 32 {
if app.copy_anchor.is_none() {
app.copy_anchor = Some((r, c));
app.copy_anchor_scroll_offset = app.copy_scroll_offset;
app.copy_selection_mode = crate::types::SelectionMode::Char;
}
app.copy_pos = Some((r, c));
} else if button == 0 && !press {
if app.copy_anchor.is_none() {
app.copy_anchor = Some((r, c));
app.copy_anchor_scroll_offset = app.copy_scroll_offset;
}
app.copy_pos = Some((r, c));
if let (Some(a), Some(p)) = (app.copy_anchor, app.copy_pos) {
if a != p { let _ = yank_selection(app); }
}
}
return;
}
let win = &mut app.windows[app.active_idx];
let win_name = win.name.clone();
if let Some(pane) = active_pane_mut(&mut win.root, &win.active_path) {
if pane_wants_mouse(pane) {
let button_state = match (button, press) {
(0, true) => mouse_inject::FROM_LEFT_1ST_BUTTON_PRESSED,
(1, true) => mouse_inject::FROM_LEFT_2ND_BUTTON_PRESSED,
(2, true) => mouse_inject::RIGHTMOST_BUTTON_PRESSED,
_ => 0,
};
let event_flags = if button == 32 || button == 35 { mouse_inject::MOUSE_MOVED } else { 0 };
inject_mouse_combined(pane, col, row, button, press, button_state, event_flags, &win_name);
}
}
}
pub fn handle_pane_scroll(app: &mut AppState, pane_id: usize, up: bool) {
if matches!(app.mode, Mode::PopupMode { .. }) { return; }
if matches!(app.mode, Mode::CopyMode | Mode::CopySearch { .. }) {
if up {
scroll_copy_up(app, 3);
} else {
scroll_copy_down(app, 3);
if app.copy_scroll_offset == 0 && app.copy_anchor.is_none() {
exit_copy_mode(app);
}
}
return;
}
let win = &mut app.windows[app.active_idx];
let mut rects: Vec<(Vec<usize>, Rect)> = Vec::new();
compute_rects(&win.root, app.last_window_area, &mut rects);
for (path, _area) in &rects {
if let Some(pid) = crate::tree::get_active_pane_id(&win.root, path) {
if pid == pane_id {
win.active_path = path.clone();
break;
}
}
}
let alt = active_pane(&win.root, &win.active_path)
.map_or(false, |p| {
p.term.lock().ok().map_or(false, |t| t.screen().alternate_screen())
});
if alt {
let win = &mut app.windows[app.active_idx];
let win_name = win.name.clone();
let sgr_btn: u8 = if up { 64 } else { 65 };
let wheel_delta: i16 = if up { 120 } else { -120 };
let button_state = ((wheel_delta as i32) << 16) as u32;
if let Some(pane) = active_pane_mut(&mut win.root, &win.active_path) {
inject_mouse_combined(pane, 0, 0, sgr_btn, true,
button_state, mouse_inject::MOUSE_WHEELED, &win_name);
}
} else if up {
enter_copy_mode(app);
scroll_copy_up(app, 3);
}
}
pub fn handle_split_set_sizes(app: &mut AppState, path: &[usize], sizes: &[u16]) {
let win = &mut app.windows[app.active_idx];
let mut cur: &mut Node = &mut win.root;
for &idx in path.iter() {
match cur {
Node::Split { children, .. } => {
if idx < children.len() {
cur = &mut children[idx];
} else {
return;
}
}
Node::Leaf(_) => return,
}
}
if let Node::Split { sizes: node_sizes, children, .. } = cur {
if sizes.len() == children.len() && sizes.len() == node_sizes.len() {
*node_sizes = sizes.to_vec();
}
}
}
pub fn handle_split_resize_done(app: &mut AppState) {
resize_all_panes(app);
}
pub fn swap_pane(app: &mut AppState, dir: FocusDir) {
let win = &mut app.windows[app.active_idx];
let mut rects: Vec<(Vec<usize>, Rect)> = Vec::new();
compute_rects(&win.root, app.last_window_area, &mut rects);
let mut active_idx = None;
for (i, (path, _)) in rects.iter().enumerate() {
if *path == win.active_path { active_idx = Some(i); break; }
}
let Some(ai) = active_idx else { return; };
let (_, arect) = &rects[ai];
let pane_ids: Vec<usize> = rects.iter().map(|(path, _)| {
crate::tree::get_active_pane_id(&win.root, path).unwrap_or(usize::MAX)
}).collect();
let target = crate::input::find_best_pane_in_direction(&rects, ai, arect, dir, &pane_ids, &win.pane_mru)
.or_else(|| crate::input::find_wrap_target(&rects, ai, arect, dir, &pane_ids, &win.pane_mru));
if let Some(ni) = target {
if let Some(new_pane_id) = pane_ids.get(ni) {
crate::tree::touch_mru(&mut win.pane_mru, *new_pane_id);
}
win.active_path = rects[ni].0.clone();
}
}
pub fn resize_pane_vertical(app: &mut AppState, amount: i16) {
let win = &mut app.windows[app.active_idx];
if win.active_path.is_empty() { return; }
for depth in (0..win.active_path.len()).rev() {
let parent_path = win.active_path[..depth].to_vec();
if let Some(Node::Split { kind, sizes, .. }) = get_split_mut(&mut win.root, &parent_path) {
if *kind == LayoutKind::Vertical {
let idx = win.active_path[depth];
if idx < sizes.len() {
if idx + 1 < sizes.len() {
let new_size = (sizes[idx] as i16 + amount).max(1) as u16;
let diff = new_size as i16 - sizes[idx] as i16;
sizes[idx] = new_size;
sizes[idx + 1] = (sizes[idx + 1] as i16 - diff).max(1) as u16;
} else if idx > 0 {
let new_size = (sizes[idx - 1] as i16 + amount).max(1) as u16;
let diff = new_size as i16 - sizes[idx - 1] as i16;
sizes[idx - 1] = new_size;
sizes[idx] = (sizes[idx] as i16 - diff).max(1) as u16;
}
}
return;
}
}
}
}
pub fn resize_pane_horizontal(app: &mut AppState, amount: i16) {
let win = &mut app.windows[app.active_idx];
if win.active_path.is_empty() { return; }
for depth in (0..win.active_path.len()).rev() {
let parent_path = win.active_path[..depth].to_vec();
if let Some(Node::Split { kind, sizes, .. }) = get_split_mut(&mut win.root, &parent_path) {
if *kind == LayoutKind::Horizontal {
let idx = win.active_path[depth];
if idx < sizes.len() {
if idx + 1 < sizes.len() {
let new_size = (sizes[idx] as i16 + amount).max(1) as u16;
let diff = new_size as i16 - sizes[idx] as i16;
sizes[idx] = new_size;
sizes[idx + 1] = (sizes[idx + 1] as i16 - diff).max(1) as u16;
} else if idx > 0 {
let new_size = (sizes[idx - 1] as i16 + amount).max(1) as u16;
let diff = new_size as i16 - sizes[idx - 1] as i16;
sizes[idx - 1] = new_size;
sizes[idx] = (sizes[idx] as i16 - diff).max(1) as u16;
}
}
return;
}
}
}
}
pub fn resize_pane_absolute(app: &mut AppState, axis: &str, target: u16) {
let win = &mut app.windows[app.active_idx];
if win.active_path.is_empty() { return; }
let target_kind = if axis == "x" { LayoutKind::Horizontal } else { LayoutKind::Vertical };
for depth in (0..win.active_path.len()).rev() {
let parent_path = win.active_path[..depth].to_vec();
if let Some(Node::Split { kind, sizes, .. }) = get_split_mut(&mut win.root, &parent_path) {
if *kind == target_kind {
let idx = win.active_path[depth];
if idx < sizes.len() {
let old = sizes[idx];
let new = target.max(1);
let diff = new as i16 - old as i16;
sizes[idx] = new;
if idx + 1 < sizes.len() {
sizes[idx + 1] = (sizes[idx + 1] as i16 - diff).max(1) as u16;
} else if idx > 0 {
sizes[idx - 1] = (sizes[idx - 1] as i16 - diff).max(1) as u16;
}
}
return;
}
}
}
}
pub fn rotate_panes(app: &mut AppState, reverse: bool) {
let win = &mut app.windows[app.active_idx];
match &mut win.root {
Node::Split { children, .. } if children.len() >= 2 => {
if reverse {
let first = children.remove(0);
children.push(first);
} else {
let last = children.pop().unwrap();
children.insert(0, last);
}
}
_ => {}
}
}
pub fn break_pane_to_window(app: &mut AppState) {
let src_idx = app.active_idx;
let src_path = app.windows[src_idx].active_path.clone();
let src_root = std::mem::replace(&mut app.windows[src_idx].root,
Node::Split { kind: LayoutKind::Horizontal, sizes: vec![], children: vec![] });
let (remaining, extracted) = crate::tree::extract_node(src_root, &src_path);
if let Some(pane_node) = extracted {
let src_empty = remaining.is_none();
if let Some(rem) = remaining {
app.windows[src_idx].root = rem;
app.windows[src_idx].active_path = crate::tree::first_leaf_path(&app.windows[src_idx].root);
}
let win_name = match &pane_node {
Node::Leaf(p) => p.title.clone(),
_ => format!("win {}", app.windows.len() + 1),
};
let initial_mru = crate::tree::collect_pane_ids(&pane_node);
app.windows.push(Window {
root: pane_node,
active_path: vec![],
name: win_name,
id: app.next_win_id,
activity_flag: false,
bell_flag: false,
silence_flag: false,
last_output_time: std::time::Instant::now(),
last_seen_version: 0,
manual_rename: false,
layout_index: 0,
pane_mru: initial_mru,
zoom_saved: None,
linked_from: None,
});
app.next_win_id += 1;
if src_empty {
app.windows.remove(src_idx);
}
app.active_idx = app.windows.len() - 1;
} else {
if let Some(rem) = remaining {
app.windows[src_idx].root = rem;
}
}
}
pub fn respawn_active_pane(app: &mut AppState, pty_system_ref: Option<&dyn portable_pty::PtySystem>) -> io::Result<()> {
let owned_pty;
let pty_system: &dyn portable_pty::PtySystem = if let Some(ps) = pty_system_ref {
ps
} else {
owned_pty = native_pty_system();
&*owned_pty
};
let expanded_shell = crate::format::expand_format(&app.default_shell, &app);
let win = &mut app.windows[app.active_idx];
let Some(pane) = active_pane_mut(&mut win.root, &win.active_path) else { return Ok(()); };
let pane_id = pane.id;
let size = PtySize { rows: pane.last_rows, cols: pane.last_cols, pixel_width: 0, pixel_height: 0 };
let pair = pty_system.openpty(size).map_err(|e| io::Error::new(io::ErrorKind::Other, format!("openpty error: {e}")))?;
let mut shell_cmd = if !expanded_shell.is_empty() {
build_default_shell(&expanded_shell, app.env_shim, app.allow_predictions)
} else {
detect_shell()
};
set_tmux_env(&mut shell_cmd, pane_id, app.control_port, app.socket_name.as_deref(), &app.session_name, app.claude_code_fix_tty, app.claude_code_force_interactive);
crate::pane::apply_user_environment(&mut shell_cmd, &app.environment);
let child = pair.slave.spawn_command(shell_cmd).map_err(|e| io::Error::new(io::ErrorKind::Other, format!("spawn shell error: {e}")))?;
drop(pair.slave);
let term: Arc<Mutex<vt100::Parser>> = Arc::new(Mutex::new(vt100::Parser::new(size.rows, size.cols, app.history_limit)));
let term_reader = term.clone();
let reader = pair.master.try_clone_reader().map_err(|e| io::Error::new(io::ErrorKind::Other, format!("clone reader error: {e}")))?;
let data_version = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let dv_writer = data_version.clone();
let cursor_shape = std::sync::Arc::new(std::sync::atomic::AtomicU8::new(crate::pane::CURSOR_SHAPE_UNSET));
let cs_writer = cursor_shape.clone();
let bell_pending = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let bell_writer = bell_pending.clone();
let output_ring = std::sync::Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new()));
crate::pane::spawn_reader_thread(reader, term_reader, dv_writer, cs_writer, bell_writer, output_ring.clone());
pane.output_ring = output_ring;
let mut pty_writer = pair.master.take_writer().map_err(|e| io::Error::new(io::ErrorKind::Other, format!("take writer error: {e}")))?;
crate::pane::conpty_preemptive_dsr_response(&mut *pty_writer);
pane.master = pair.master;
pane.writer = pty_writer;
pane.child = child;
pane.term = term;
pane.data_version = data_version;
pane.cursor_shape = cursor_shape;
pane.bell_pending = bell_pending;
pane.child_pid = None;
pane.vt_bridge_cache = None;
pane.vti_mode_cache = None;
pane.mouse_input_cache = None;
pane.dead = false;
Ok(())
}
#[cfg(test)]
#[path = "../tests-rs/test_issue81_resize_direction.rs"]
mod test_issue81_resize_direction;