use std::io::{self, Write};
#[cfg(windows)]
use std::thread;
#[cfg(windows)]
use std::time::Duration;
#[cfg(windows)]
use windows_sys::Win32::Foundation::{GlobalFree, HGLOBAL};
#[cfg(windows)]
use windows_sys::Win32::System::DataExchange::{CloseClipboard, EmptyClipboard, GetClipboardData, OpenClipboard, SetClipboardData};
#[cfg(windows)]
use windows_sys::Win32::System::Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE};
use crate::types::{AppState, Mode, CopyModeState};
use crate::tree::{active_pane, active_pane_mut};
pub fn emit_osc52<W: Write>(writer: &mut W, text: &str) {
let encoded = crate::util::base64_encode(text);
let _ = write!(writer, "\x1b]52;c;{}\x07", encoded);
let _ = writer.flush();
}
pub fn enter_copy_mode(app: &mut AppState) {
app.mode = Mode::CopyMode;
app.copy_scroll_offset = 0;
app.copy_selection_mode = crate::types::SelectionMode::Char;
app.copy_anchor = None;
app.copy_pos = current_prompt_pos(app);
app.copy_find_char_pending = None;
app.copy_text_object_pending = None;
app.copy_register_pending = false;
app.copy_register = None;
app.copy_count = None;
save_copy_state_to_pane(app);
}
pub fn exit_copy_mode(app: &mut AppState) {
app.mode = Mode::Passthrough;
app.copy_anchor = None;
app.copy_pos = None;
app.copy_scroll_offset = 0;
let win = &mut app.windows[app.active_idx];
if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) {
p.copy_state = None;
if let Ok(mut parser) = p.term.lock() {
parser.screen_mut().set_scrollback(0);
}
}
}
pub fn save_copy_state_to_pane(app: &mut AppState) {
let (in_search, search_input, search_input_forward) = match &app.mode {
Mode::CopySearch { input, forward } => (true, input.clone(), *forward),
_ => (false, String::new(), true),
};
let state = CopyModeState {
anchor: app.copy_anchor,
anchor_scroll_offset: app.copy_anchor_scroll_offset,
pos: app.copy_pos,
scroll_offset: app.copy_scroll_offset,
selection_mode: app.copy_selection_mode,
search_query: app.copy_search_query.clone(),
count: app.copy_count,
search_matches: app.copy_search_matches.clone(),
search_idx: app.copy_search_idx,
search_forward: app.copy_search_forward,
find_char_pending: app.copy_find_char_pending,
text_object_pending: app.copy_text_object_pending,
register_pending: app.copy_register_pending,
register: app.copy_register,
in_search,
search_input,
search_input_forward,
};
let win = &mut app.windows[app.active_idx];
if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) {
p.copy_state = Some(state);
}
}
pub fn restore_copy_state_from_pane(app: &mut AppState) {
let win = &app.windows[app.active_idx];
let state = active_pane(&win.root, &win.active_path)
.and_then(|p| p.copy_state.clone());
if let Some(s) = state {
app.copy_anchor = s.anchor;
app.copy_anchor_scroll_offset = s.anchor_scroll_offset;
app.copy_pos = s.pos;
app.copy_scroll_offset = s.scroll_offset;
app.copy_selection_mode = s.selection_mode;
app.copy_search_query = s.search_query;
app.copy_count = s.count;
app.copy_search_matches = s.search_matches;
app.copy_search_idx = s.search_idx;
app.copy_search_forward = s.search_forward;
app.copy_find_char_pending = s.find_char_pending;
app.copy_text_object_pending = s.text_object_pending;
app.copy_register_pending = s.register_pending;
app.copy_register = s.register;
if s.in_search {
app.mode = Mode::CopySearch { input: s.search_input, forward: s.search_input_forward };
} else {
app.mode = Mode::CopyMode;
}
} else {
app.mode = Mode::Passthrough;
}
}
pub fn switch_with_copy_save<F: FnOnce(&mut AppState)>(app: &mut AppState, switch_fn: F) {
let was_copy = matches!(app.mode, Mode::CopyMode | Mode::CopySearch { .. });
if was_copy {
save_copy_state_to_pane(app);
}
switch_fn(app);
let win = &app.windows[app.active_idx];
let new_pane_has_copy = active_pane(&win.root, &win.active_path)
.map_or(false, |p| p.copy_state.is_some());
if new_pane_has_copy {
restore_copy_state_from_pane(app);
} else if was_copy {
app.mode = Mode::Passthrough;
}
}
#[cfg(windows)]
pub fn copy_to_system_clipboard(text: &str) {
const CF_UNICODETEXT: u32 = 13;
for _ in 0..5 {
let opened = unsafe { OpenClipboard(std::ptr::null_mut()) };
if opened == 0 {
thread::sleep(Duration::from_millis(2));
continue;
}
let mut utf16: Vec<u16> = text.encode_utf16().collect();
utf16.push(0); let size_bytes = utf16.len() * std::mem::size_of::<u16>();
let mut hmem: HGLOBAL = std::ptr::null_mut();
unsafe {
if EmptyClipboard() != 0 {
hmem = GlobalAlloc(GMEM_MOVEABLE, size_bytes);
if !hmem.is_null() {
let dst = GlobalLock(hmem) as *mut u16;
if !dst.is_null() {
std::ptr::copy_nonoverlapping(utf16.as_ptr(), dst, utf16.len());
GlobalUnlock(hmem);
if !SetClipboardData(CF_UNICODETEXT, hmem).is_null() {
hmem = std::ptr::null_mut();
}
}
}
}
if !hmem.is_null() {
let _ = GlobalFree(hmem);
}
let _ = CloseClipboard();
}
break;
}
}
#[cfg(not(windows))]
pub fn copy_to_system_clipboard(_text: &str) {}
#[cfg(windows)]
pub fn read_from_system_clipboard() -> Option<String> {
const CF_UNICODETEXT: u32 = 13;
for _ in 0..5 {
let opened = unsafe { OpenClipboard(std::ptr::null_mut()) };
if opened == 0 {
thread::sleep(Duration::from_millis(2));
continue;
}
let result = unsafe {
let hmem = GetClipboardData(CF_UNICODETEXT);
if hmem.is_null() {
let _ = CloseClipboard();
return None;
}
let ptr = GlobalLock(hmem) as *const u16;
if ptr.is_null() {
let _ = CloseClipboard();
return None;
}
let mut len = 0usize;
while *ptr.add(len) != 0 {
len += 1;
if len > 1_000_000 { break; } }
let slice = std::slice::from_raw_parts(ptr, len);
let text = String::from_utf16_lossy(slice);
GlobalUnlock(hmem);
let _ = CloseClipboard();
let text = text.replace("\r\n", "\n");
Some(text)
};
return result;
}
None
}
#[cfg(not(windows))]
pub fn read_from_system_clipboard() -> Option<String> { None }
pub fn current_prompt_pos(app: &mut AppState) -> Option<(u16,u16)> {
let win = &mut app.windows[app.active_idx];
let p = active_pane_mut(&mut win.root, &win.active_path)?;
let parser = p.term.lock().ok()?;
let (r,c) = parser.screen().cursor_position();
Some((r,c))
}
pub fn move_copy_cursor(app: &mut AppState, dx: i16, dy: i16) {
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return };
let mut parser = match p.term.lock() { Ok(g) => g, Err(_) => return };
let (r, c) = app.copy_pos.unwrap_or_else(|| parser.screen().cursor_position());
let rows = p.last_rows;
let cols = p.last_cols;
let desired_r = r as i16 + dy;
let nc = (c as i16 + dx).max(0).min(cols as i16 - 1) as u16;
if desired_r < 0 {
let scroll_lines = (-desired_r) as usize;
let current = parser.screen().scrollback();
parser.screen_mut().set_scrollback(current.saturating_add(scroll_lines));
app.copy_scroll_offset = parser.screen().scrollback();
app.copy_pos = Some((0, nc));
}
else if desired_r >= rows as i16 {
let scroll_lines = (desired_r - rows as i16 + 1) as usize;
let current = parser.screen().scrollback();
if current > 0 {
parser.screen_mut().set_scrollback(current.saturating_sub(scroll_lines));
app.copy_scroll_offset = parser.screen().scrollback();
app.copy_pos = Some((rows.saturating_sub(1), nc));
} else {
app.copy_pos = Some((rows.saturating_sub(1), nc));
}
} else {
app.copy_pos = Some((desired_r as u16, nc));
}
}
fn read_row_text(app: &mut AppState, row: u16) -> Option<(String, u16)> {
let win = &mut app.windows[app.active_idx];
let p = active_pane_mut(&mut win.root, &win.active_path)?;
let parser = p.term.lock().ok()?;
let screen = parser.screen();
let cols = p.last_cols;
let mut text = String::with_capacity(cols as usize);
for c in 0..cols {
if let Some(cell) = screen.cell(row, c) {
let t = cell.contents();
if t.is_empty() { text.push(' '); } else { text.push_str(t); }
} else {
text.push(' ');
}
}
Some((text, cols))
}
pub fn get_copy_pos(app: &mut AppState) -> Option<(u16, u16)> {
if let Some(pos) = app.copy_pos { return Some(pos); }
current_prompt_pos(app)
}
pub fn move_to_line_start(app: &mut AppState) {
if let Some((r, _)) = get_copy_pos(app) {
app.copy_pos = Some((r, 0));
}
}
pub fn move_to_line_end(app: &mut AppState) {
if let Some((r, _)) = get_copy_pos(app) {
let win = &app.windows[app.active_idx];
if let Some(p) = active_pane(&win.root, &win.active_path) {
let cols = p.last_cols;
app.copy_pos = Some((r, cols.saturating_sub(1)));
}
}
}
pub fn move_to_first_nonblank(app: &mut AppState) {
if let Some((r, _)) = get_copy_pos(app) {
if let Some((text, _)) = read_row_text(app, r) {
let col = text.find(|c: char| !c.is_whitespace()).unwrap_or(0) as u16;
app.copy_pos = Some((r, col));
}
}
}
#[inline]
fn char_class(ch: char, seps: &str) -> u8 {
if ch.is_whitespace() { 0 }
else if seps.contains(ch) { 2 }
else if ch.is_alphanumeric() || ch == '_' { 1 }
else { 2 }
}
pub fn move_word_forward(app: &mut AppState) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
let seps = app.word_separators.clone();
let (text, cols) = match read_row_text(app, r) { Some(t) => t, None => return };
let bytes: Vec<char> = text.chars().collect();
let mut col = c as usize;
let rows = app.windows.get(app.active_idx)
.and_then(|w| active_pane(&w.root, &w.active_path))
.map(|p| p.last_rows).unwrap_or(24);
if col < bytes.len() {
let cls = char_class(bytes[col], &seps);
while col < bytes.len() && char_class(bytes[col], &seps) == cls { col += 1; }
}
while col < bytes.len() && bytes[col].is_whitespace() { col += 1; }
if col < cols as usize {
app.copy_pos = Some((r, col as u16));
} else {
let nr = (r + 1).min(rows.saturating_sub(1));
if nr != r {
if let Some((next_text, _)) = read_row_text(app, nr) {
let next_bytes: Vec<char> = next_text.chars().collect();
let mut nc = 0usize;
while nc < next_bytes.len() && next_bytes[nc].is_whitespace() { nc += 1; }
app.copy_pos = Some((nr, nc as u16));
} else {
app.copy_pos = Some((nr, 0));
}
}
}
}
pub fn move_word_backward(app: &mut AppState) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
let seps = app.word_separators.clone();
let (text, _) = match read_row_text(app, r) { Some(t) => t, None => return };
let bytes: Vec<char> = text.chars().collect();
let mut col = c as usize;
if col == 0 {
if r > 0 {
let nr = r - 1;
if let Some((prev_text, prev_cols)) = read_row_text(app, nr) {
let prev_bytes: Vec<char> = prev_text.chars().collect();
let mut nc = (prev_cols as usize).min(prev_bytes.len()).saturating_sub(1);
while nc > 0 && prev_bytes[nc].is_whitespace() { nc -= 1; }
let cls = char_class(prev_bytes[nc], &seps);
while nc > 0 && char_class(prev_bytes[nc - 1], &seps) == cls { nc -= 1; }
app.copy_pos = Some((nr, nc as u16));
} else {
app.copy_pos = Some((r - 1, 0));
}
}
return;
}
while col > 0 && bytes[col - 1].is_whitespace() { col -= 1; }
if col > 0 {
let cls = char_class(bytes[col - 1], &seps);
while col > 0 && char_class(bytes[col - 1], &seps) == cls { col -= 1; }
}
app.copy_pos = Some((r, col as u16));
}
pub fn move_word_end(app: &mut AppState) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
let seps = app.word_separators.clone();
let (text, cols) = match read_row_text(app, r) { Some(t) => t, None => return };
let bytes: Vec<char> = text.chars().collect();
let mut col = (c as usize) + 1; let rows = app.windows.get(app.active_idx)
.and_then(|w| active_pane(&w.root, &w.active_path))
.map(|p| p.last_rows).unwrap_or(24);
while col < bytes.len() && bytes[col].is_whitespace() { col += 1; }
if col < bytes.len() {
let cls = char_class(bytes[col], &seps);
while col + 1 < bytes.len() && char_class(bytes[col + 1], &seps) == cls { col += 1; }
}
if col < cols as usize {
app.copy_pos = Some((r, col as u16));
} else {
let nr = (r + 1).min(rows.saturating_sub(1));
if nr != r {
if let Some((next_text, _)) = read_row_text(app, nr) {
let next_bytes: Vec<char> = next_text.chars().collect();
let mut nc = 0usize;
while nc < next_bytes.len() && next_bytes[nc].is_whitespace() { nc += 1; }
let cls = if nc < next_bytes.len() { char_class(next_bytes[nc], &seps) } else { 0 };
while nc + 1 < next_bytes.len() && char_class(next_bytes[nc + 1], &seps) == cls { nc += 1; }
app.copy_pos = Some((nr, nc as u16));
} else {
app.copy_pos = Some((nr, 0));
}
}
}
}
pub fn scroll_copy_up(app: &mut AppState, lines: usize) {
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return };
let mut parser = match p.term.lock() { Ok(g) => g, Err(_) => return };
let current = parser.screen().scrollback();
let new_offset = current.saturating_add(lines);
parser.screen_mut().set_scrollback(new_offset);
app.copy_scroll_offset = parser.screen().scrollback();
}
pub fn scroll_copy_down(app: &mut AppState, lines: usize) {
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return };
let mut parser = match p.term.lock() { Ok(g) => g, Err(_) => return };
let current = parser.screen().scrollback();
let new_offset = current.saturating_sub(lines);
parser.screen_mut().set_scrollback(new_offset);
app.copy_scroll_offset = parser.screen().scrollback();
}
pub fn scroll_to_top(app: &mut AppState) {
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return };
let mut parser = match p.term.lock() { Ok(g) => g, Err(_) => return };
parser.screen_mut().set_scrollback(usize::MAX);
app.copy_scroll_offset = parser.screen().scrollback();
}
pub fn scroll_to_bottom(app: &mut AppState) {
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return };
let mut parser = match p.term.lock() { Ok(g) => g, Err(_) => return };
parser.screen_mut().set_scrollback(0);
app.copy_scroll_offset = 0;
}
pub fn yank_selection(app: &mut AppState) -> io::Result<()> {
let (anchor, pos) = match (app.copy_anchor, app.copy_pos) { (Some(a), Some(p)) => (a,p), _ => return Ok(()) };
let sel_mode = app.copy_selection_mode;
let anchor_scroll = app.copy_anchor_scroll_offset;
let current_scroll = app.copy_scroll_offset;
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return Ok(()) };
let mut parser = match p.term.lock() { Ok(g) => g, Err(_) => return Ok(()) };
let rows = p.last_rows;
let cols = p.last_cols;
let anchor_abs = anchor.0 as i64 - anchor_scroll as i64;
let cursor_abs = pos.0 as i64 - current_scroll as i64;
let sel_top_abs = anchor_abs.min(cursor_abs);
let sel_bot_abs = anchor_abs.max(cursor_abs);
let total_lines = (sel_bot_abs - sel_top_abs + 1) as usize;
let (top_col, bot_col) = if anchor_abs <= cursor_abs {
(anchor.1, pos.1)
} else {
(pos.1, anchor.1)
};
let mut text = String::new();
let mut abs_idx: usize = 0; let mut next_abs = sel_top_abs;
while next_abs <= sel_bot_abs {
let target_sb = (-next_abs).max(0) as usize;
parser.screen_mut().set_scrollback(target_sb);
let actual_sb = parser.screen().scrollback() as i64;
let vis_start_abs = -actual_sb;
let vis_end_abs = -actual_sb + rows as i64 - 1;
let read_start = next_abs.max(vis_start_abs);
let read_end = sel_bot_abs.min(vis_end_abs);
if read_start > read_end { break; }
for aline in read_start..=read_end {
let r = (aline + actual_sb) as u16;
let is_first = abs_idx == 0;
let is_last = abs_idx + 1 == total_lines;
match sel_mode {
crate::types::SelectionMode::Rect => {
let c0 = anchor.1.min(pos.1); let c1 = anchor.1.max(pos.1);
let mut line = String::new();
for c in c0..=c1 {
if let Some(cell) = parser.screen().cell(r, c) { line.push_str(&cell.contents().to_string()); } else { line.push(' '); }
}
text.push_str(line.trim_end());
if !is_last { text.push('\n'); }
}
crate::types::SelectionMode::Line => {
let mut line = String::new();
for c in 0..cols {
if let Some(cell) = parser.screen().cell(r, c) { line.push_str(&cell.contents().to_string()); } else { line.push(' '); }
}
text.push_str(line.trim_end());
text.push('\n');
}
crate::types::SelectionMode::Char => {
if total_lines == 1 {
let c0 = anchor.1.min(pos.1); let c1 = anchor.1.max(pos.1);
for c in c0..=c1 {
if let Some(cell) = parser.screen().cell(r, c) { text.push_str(&cell.contents().to_string()); } else { text.push(' '); }
}
} else {
let line_start = if is_first { top_col } else { 0 };
let line_end = if is_last { bot_col } else { cols.saturating_sub(1) };
let mut line = String::new();
for c in line_start..=line_end {
if let Some(cell) = parser.screen().cell(r, c) { line.push_str(&cell.contents().to_string()); } else { line.push(' '); }
}
text.push_str(line.trim_end());
if !is_last { text.push('\n'); }
}
}
}
abs_idx += 1;
}
next_abs = read_end + 1;
}
parser.screen_mut().set_scrollback(current_scroll);
if let Some(reg) = app.copy_register.take() {
app.named_registers.insert(reg, text.clone());
}
app.paste_buffers.insert(0, text.clone());
if app.paste_buffers.len() > 10 { app.paste_buffers.pop(); }
copy_to_system_clipboard(&text);
if app.set_clipboard != "off" {
app.clipboard_osc52 = Some(text.clone());
}
if !app.copy_command.is_empty() {
let cmd = app.copy_command.clone();
pipe_text_to_command(&text, &cmd);
}
Ok(())
}
fn pipe_text_to_command(text: &str, cmd: &str) {
let shell = if cfg!(windows) { "pwsh" } else { "sh" };
let args: Vec<&str> = if cfg!(windows) {
vec!["-NoProfile", "-Command", cmd]
} else {
vec!["-c", cmd]
};
if let Ok(mut child) = std::process::Command::new(shell)
.args(&args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(text.as_bytes());
}
let _ = child.wait();
}
}
pub fn paste_latest(app: &mut AppState) -> io::Result<()> {
if let Some(reg) = app.copy_register.take() {
if let Some(text) = app.named_registers.get(®).cloned() {
let win = &mut app.windows[app.active_idx];
if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) { let _ = write!(p.writer, "{}", text); }
}
return Ok(());
}
if let Some(buf) = app.paste_buffers.first() {
let win = &mut app.windows[app.active_idx];
if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) { let _ = write!(p.writer, "{}", buf); }
}
Ok(())
}
pub fn capture_active_pane(app: &mut AppState) -> io::Result<()> {
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return Ok(()) };
let parser = match p.term.lock() { Ok(g) => g, Err(_) => return Ok(()) };
let screen = parser.screen();
let mut text = String::new();
for r in 0..p.last_rows {
let mut row = String::new();
for c in 0..p.last_cols { if let Some(cell) = screen.cell(r, c) { row.push_str(&cell.contents().to_string()); } else { row.push(' '); } }
text.push_str(row.trim_end());
text.push('\n');
}
app.paste_buffers.insert(0, text);
if app.paste_buffers.len() > 10 { app.paste_buffers.pop(); }
Ok(())
}
pub fn capture_active_pane_text(app: &mut AppState) -> io::Result<Option<String>> {
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return Ok(None) };
let parser = match p.term.lock() { Ok(g) => g, Err(_) => return Ok(None) };
let screen = parser.screen();
let mut text = String::new();
for r in 0..p.last_rows {
let mut row = String::new();
for c in 0..p.last_cols { if let Some(cell) = screen.cell(r, c) { row.push_str(&cell.contents().to_string()); } else { row.push(' '); } }
text.push_str(row.trim_end());
text.push('\n');
}
Ok(Some(text))
}
pub fn save_latest_buffer(app: &mut AppState, file: &str) -> io::Result<()> {
if let Some(buf) = app.paste_buffers.first() { std::fs::write(file, buf)?; }
Ok(())
}
pub fn search_copy_mode(app: &mut AppState, query: &str, forward: bool) {
app.copy_search_matches.clear();
app.copy_search_idx = 0;
if query.is_empty() { return; }
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return };
let parser = match p.term.lock() { Ok(g) => g, Err(_) => return };
let screen = parser.screen();
let query_lower = query.to_lowercase();
let qlen = query_lower.len() as u16;
for r in 0..p.last_rows {
let mut row_text = String::with_capacity(p.last_cols as usize);
for c in 0..p.last_cols {
if let Some(cell) = screen.cell(r, c) {
let t = cell.contents();
if t.is_empty() { row_text.push(' '); } else { row_text.push_str(t); }
} else {
row_text.push(' ');
}
}
let row_lower = row_text.to_lowercase();
let mut start = 0;
while let Some(pos) = row_lower[start..].find(&query_lower) {
let col_start = (start + pos) as u16;
let col_end = col_start + qlen;
app.copy_search_matches.push((r, col_start, col_end));
start += pos + 1;
}
}
if !forward {
app.copy_search_matches.reverse();
}
}
pub fn search_next(app: &mut AppState) {
if app.copy_search_matches.is_empty() { return; }
let wrap = app.user_options.get("wrap-search").map(|v| v.as_str()) != Some("off");
let next = app.copy_search_idx + 1;
if next >= app.copy_search_matches.len() {
if !wrap { return; }
app.copy_search_idx = 0;
} else {
app.copy_search_idx = next;
}
let (r, c, _) = app.copy_search_matches[app.copy_search_idx];
app.copy_pos = Some((r, c));
}
pub fn move_word_forward_big(app: &mut AppState) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
let (text, cols) = match read_row_text(app, r) { Some(t) => t, None => return };
let bytes: Vec<char> = text.chars().collect();
let mut col = c as usize;
let rows = app.windows.get(app.active_idx)
.and_then(|w| active_pane(&w.root, &w.active_path))
.map(|p| p.last_rows).unwrap_or(24);
while col < bytes.len() && !bytes[col].is_whitespace() { col += 1; }
while col < bytes.len() && bytes[col].is_whitespace() { col += 1; }
if col < cols as usize {
app.copy_pos = Some((r, col as u16));
} else {
let nr = (r + 1).min(rows.saturating_sub(1));
if nr != r {
if let Some((next_text, _)) = read_row_text(app, nr) {
let next_bytes: Vec<char> = next_text.chars().collect();
let mut nc = 0usize;
while nc < next_bytes.len() && next_bytes[nc].is_whitespace() { nc += 1; }
app.copy_pos = Some((nr, nc as u16));
} else { app.copy_pos = Some((nr, 0)); }
}
}
}
pub fn move_word_backward_big(app: &mut AppState) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
let (text, _prev_cols) = match read_row_text(app, r) { Some(t) => t, None => return };
let bytes: Vec<char> = text.chars().collect();
let mut col = c as usize;
if col == 0 {
if r > 0 {
let nr = r - 1;
if let Some((prev_text, prev_cols)) = read_row_text(app, nr) {
let prev_bytes: Vec<char> = prev_text.chars().collect();
let mut nc = (prev_cols as usize).min(prev_bytes.len()).saturating_sub(1);
while nc > 0 && prev_bytes[nc].is_whitespace() { nc -= 1; }
while nc > 0 && !prev_bytes[nc - 1].is_whitespace() { nc -= 1; }
app.copy_pos = Some((nr, nc as u16));
} else { app.copy_pos = Some((r - 1, 0)); }
}
return;
}
while col > 0 && bytes[col - 1].is_whitespace() { col -= 1; }
while col > 0 && !bytes[col - 1].is_whitespace() { col -= 1; }
app.copy_pos = Some((r, col as u16));
}
pub fn move_word_end_big(app: &mut AppState) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
let (text, cols) = match read_row_text(app, r) { Some(t) => t, None => return };
let bytes: Vec<char> = text.chars().collect();
let mut col = (c as usize) + 1;
let rows = app.windows.get(app.active_idx)
.and_then(|w| active_pane(&w.root, &w.active_path))
.map(|p| p.last_rows).unwrap_or(24);
while col < bytes.len() && bytes[col].is_whitespace() { col += 1; }
while col + 1 < bytes.len() && !bytes[col + 1].is_whitespace() { col += 1; }
if col < cols as usize {
app.copy_pos = Some((r, col as u16));
} else {
let nr = (r + 1).min(rows.saturating_sub(1));
if nr != r {
if let Some((next_text, _)) = read_row_text(app, nr) {
let next_bytes: Vec<char> = next_text.chars().collect();
let mut nc = 0usize;
while nc < next_bytes.len() && next_bytes[nc].is_whitespace() { nc += 1; }
while nc + 1 < next_bytes.len() && !next_bytes[nc + 1].is_whitespace() { nc += 1; }
app.copy_pos = Some((nr, nc as u16));
} else { app.copy_pos = Some((nr, 0)); }
}
}
}
pub fn move_to_screen_top(app: &mut AppState) {
app.copy_pos = Some((0, 0));
}
pub fn move_to_screen_middle(app: &mut AppState) {
let rows = app.windows.get(app.active_idx)
.and_then(|w| active_pane(&w.root, &w.active_path))
.map(|p| p.last_rows).unwrap_or(24);
app.copy_pos = Some((rows / 2, 0));
}
pub fn move_to_screen_bottom(app: &mut AppState) {
let rows = app.windows.get(app.active_idx)
.and_then(|w| active_pane(&w.root, &w.active_path))
.map(|p| p.last_rows).unwrap_or(24);
app.copy_pos = Some((rows.saturating_sub(1), 0));
}
pub fn find_char_forward(app: &mut AppState, ch: char) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
if let Some((text, _)) = read_row_text(app, r) {
let bytes: Vec<char> = text.chars().collect();
for i in (c as usize + 1)..bytes.len() {
if bytes[i] == ch { app.copy_pos = Some((r, i as u16)); return; }
}
}
}
pub fn find_char_backward(app: &mut AppState, ch: char) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
if let Some((text, _)) = read_row_text(app, r) {
let bytes: Vec<char> = text.chars().collect();
for i in (0..(c as usize)).rev() {
if bytes[i] == ch { app.copy_pos = Some((r, i as u16)); return; }
}
}
}
pub fn find_char_to_forward(app: &mut AppState, ch: char) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
if let Some((text, _)) = read_row_text(app, r) {
let bytes: Vec<char> = text.chars().collect();
for i in (c as usize + 1)..bytes.len() {
if bytes[i] == ch { app.copy_pos = Some((r, (i as u16).saturating_sub(1))); return; }
}
}
}
pub fn find_char_to_backward(app: &mut AppState, ch: char) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
if let Some((text, _)) = read_row_text(app, r) {
let bytes: Vec<char> = text.chars().collect();
for i in (0..(c as usize)).rev() {
if bytes[i] == ch { app.copy_pos = Some((r, (i as u16) + 1)); return; }
}
}
}
pub fn copy_end_of_line(app: &mut AppState) -> io::Result<()> {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return Ok(()) };
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return Ok(()) };
let parser = match p.term.lock() { Ok(g) => g, Err(_) => return Ok(()) };
let screen = parser.screen();
let cols = p.last_cols;
let mut text = String::new();
for col in c..cols {
if let Some(cell) = screen.cell(r, col) { text.push_str(&cell.contents().to_string()); } else { text.push(' '); }
}
let text = text.trim_end().to_string();
app.paste_buffers.insert(0, text.clone());
if app.paste_buffers.len() > 10 { app.paste_buffers.pop(); }
copy_to_system_clipboard(&text);
Ok(())
}
pub fn search_prev(app: &mut AppState) {
if app.copy_search_matches.is_empty() { return; }
let wrap = app.user_options.get("wrap-search").map(|v| v.as_str()) != Some("off");
if app.copy_search_idx == 0 {
if !wrap { return; }
app.copy_search_idx = app.copy_search_matches.len() - 1;
} else {
app.copy_search_idx -= 1;
}
let (r, c, _) = app.copy_search_matches[app.copy_search_idx];
app.copy_pos = Some((r, c));
}
pub fn capture_active_pane_range(app: &mut AppState, s: Option<i32>, e: Option<i32>) -> io::Result<Option<String>> {
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return Ok(None) };
let parser = match p.term.lock() { Ok(g) => g, Err(_) => return Ok(None) };
let screen = parser.screen();
let bottom = p.last_rows.saturating_sub(1) as i32;
let start = match s {
Some(v) if v < 0 => (bottom + v + 1).max(0) as u16,
Some(v) => (v as u16).min(p.last_rows.saturating_sub(1)),
None => 0,
};
let end = match e {
Some(v) if v < 0 => (bottom + v + 1).max(0) as u16,
Some(v) => (v as u16).min(p.last_rows.saturating_sub(1)),
None => p.last_rows.saturating_sub(1),
};
let mut text = String::new();
for r in start..=end {
let mut row = String::new();
for c in 0..p.last_cols { if let Some(cell) = screen.cell(r, c) { row.push_str(&cell.contents().to_string()); } else { row.push(' '); } }
text.push_str(row.trim_end());
text.push('\n');
}
Ok(Some(text))
}
pub fn capture_active_pane_styled(app: &mut AppState, s: Option<i32>, e: Option<i32>) -> io::Result<Option<String>> {
let win = &mut app.windows[app.active_idx];
let p = match active_pane_mut(&mut win.root, &win.active_path) { Some(p) => p, None => return Ok(None) };
let parser = match p.term.lock() { Ok(g) => g, Err(_) => return Ok(None) };
let screen = parser.screen();
let bottom = p.last_rows.saturating_sub(1) as i32;
let start_row = match s {
Some(v) if v < 0 => (bottom + v + 1).max(0) as u16,
Some(v) => (v as u16).min(p.last_rows.saturating_sub(1)),
None => 0,
};
let end_row = match e {
Some(v) if v < 0 => (bottom + v + 1).max(0) as u16,
Some(v) => (v as u16).min(p.last_rows.saturating_sub(1)),
None => p.last_rows.saturating_sub(1),
};
let mut text = String::new();
let mut prev_fg: Option<vt100::Color> = None;
let mut prev_bg: Option<vt100::Color> = None;
let mut prev_bold = false;
let mut prev_dim = false;
let mut prev_italic = false;
let mut prev_underline = false;
let mut prev_blink = false;
let mut prev_inverse = false;
let mut prev_hidden = false;
let mut prev_strikethrough = false;
for r in start_row..=end_row {
let mut row_chars: Vec<String> = Vec::new();
let mut row_sgr: Vec<Option<String>> = Vec::new();
let mut any_style_active = false;
for c in 0..p.last_cols {
if let Some(cell) = screen.cell(r, c) {
let fg = cell.fgcolor();
let bg = cell.bgcolor();
let bold = cell.bold();
let dim = cell.dim();
let italic = cell.italic();
let underline = cell.underline();
let blink = cell.blink();
let inverse = cell.inverse();
let hidden = cell.hidden();
let strikethrough = cell.strikethrough();
let style_changed = Some(fg) != prev_fg || Some(bg) != prev_bg
|| bold != prev_bold || dim != prev_dim
|| italic != prev_italic
|| underline != prev_underline || blink != prev_blink
|| inverse != prev_inverse || hidden != prev_hidden
|| strikethrough != prev_strikethrough;
let sgr = if style_changed {
let mut params = Vec::new();
params.push("0".to_string()); if bold { params.push("1".to_string()); }
if dim { params.push("2".to_string()); }
if italic { params.push("3".to_string()); }
if underline { params.push("4".to_string()); }
if blink { params.push("5".to_string()); }
if inverse { params.push("7".to_string()); }
if hidden { params.push("8".to_string()); }
if strikethrough { params.push("9".to_string()); }
match fg {
vt100::Color::Default => {}
vt100::Color::Idx(n) => {
if n < 8 { params.push(format!("{}", 30 + n)); }
else if n < 16 { params.push(format!("{}", 90 + n - 8)); }
else { params.push(format!("38;5;{}", n)); }
}
vt100::Color::Rgb(r, g, b) => { params.push(format!("38;2;{};{};{}", r, g, b)); }
}
match bg {
vt100::Color::Default => {}
vt100::Color::Idx(n) => {
if n < 8 { params.push(format!("{}", 40 + n)); }
else if n < 16 { params.push(format!("{}", 100 + n - 8)); }
else { params.push(format!("48;5;{}", n)); }
}
vt100::Color::Rgb(r, g, b) => { params.push(format!("48;2;{};{};{}", r, g, b)); }
}
prev_fg = Some(fg);
prev_bg = Some(bg);
prev_bold = bold;
prev_dim = dim;
prev_italic = italic;
prev_underline = underline;
prev_blink = blink;
prev_inverse = inverse;
prev_hidden = hidden;
prev_strikethrough = strikethrough;
any_style_active = true;
Some(format!("\x1b[{}m", params.join(";")))
} else {
None
};
row_sgr.push(sgr);
row_chars.push(cell.contents().to_string());
} else {
row_sgr.push(None);
row_chars.push(" ".to_string());
}
}
let last_non_ws = row_chars.iter().rposition(|s| !s.is_empty() && s.trim() != "");
let trim_end = match last_non_ws {
Some(pos) => pos + 1,
None => 0, };
for c in 0..trim_end {
if let Some(ref sgr) = row_sgr[c] { text.push_str(sgr); }
text.push_str(&row_chars[c]);
}
if any_style_active {
text.push_str("\x1b[0m");
prev_fg = None;
prev_bg = None;
prev_bold = false;
prev_dim = false;
prev_italic = false;
prev_underline = false;
prev_blink = false;
prev_inverse = false;
prev_hidden = false;
}
text.push('\n');
}
Ok(Some(text))
}
pub fn move_next_paragraph(app: &mut AppState) {
let (r, _) = match get_copy_pos(app) { Some(p) => p, None => return };
let rows = app.windows.get(app.active_idx)
.and_then(|w| active_pane(&w.root, &w.active_path))
.map(|p| p.last_rows).unwrap_or(24);
let mut row = r + 1;
while row < rows {
if let Some((text, _)) = read_row_text(app, row) {
if text.trim().is_empty() { break; }
} else { break; }
row += 1;
}
while row < rows {
if let Some((text, _)) = read_row_text(app, row) {
if !text.trim().is_empty() { break; }
} else { break; }
row += 1;
}
app.copy_pos = Some((row.min(rows.saturating_sub(1)), 0));
}
pub fn move_prev_paragraph(app: &mut AppState) {
let (r, _) = match get_copy_pos(app) { Some(p) => p, None => return };
if r == 0 { return; }
let mut row = r.saturating_sub(1);
loop {
if let Some((text, _)) = read_row_text(app, row) {
if text.trim().is_empty() { break; }
} else { break; }
if row == 0 { app.copy_pos = Some((0, 0)); return; }
row -= 1;
}
loop {
if let Some((text, _)) = read_row_text(app, row) {
if !text.trim().is_empty() { break; }
} else { break; }
if row == 0 { app.copy_pos = Some((0, 0)); return; }
row -= 1;
}
app.copy_pos = Some((row, 0));
}
pub fn move_matching_bracket(app: &mut AppState) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
let win = match app.windows.get(app.active_idx) { Some(w) => w, None => return };
let p = match active_pane(&win.root, &win.active_path) { Some(p) => p, None => return };
let parser = match p.term.lock() { Ok(g) => g, Err(_) => return };
let screen = parser.screen();
let ch = screen.cell(r, c).map(|cell| {
let t = cell.contents();
t.chars().next().unwrap_or(' ')
}).unwrap_or(' ');
let (open, close, forward) = match ch {
'(' => ('(', ')', true),
')' => ('(', ')', false),
'[' => ('[', ']', true),
']' => ('[', ']', false),
'{' => ('{', '}', true),
'}' => ('{', '}', false),
'<' => ('<', '>', true),
'>' => ('<', '>', false),
_ => return,
};
let rows = p.last_rows;
let cols = p.last_cols;
let mut depth = 1i32;
let mut cr = r;
let mut cc = c;
loop {
if forward {
cc += 1;
if cc >= cols { cc = 0; cr += 1; }
if cr >= rows { return; }
} else {
if cc == 0 {
if cr == 0 { return; }
cr -= 1;
cc = cols.saturating_sub(1);
} else { cc -= 1; }
}
let cell_ch = screen.cell(cr, cc).map(|cell| {
cell.contents().chars().next().unwrap_or(' ')
}).unwrap_or(' ');
if cell_ch == open { depth += if forward { 1 } else { -1 }; }
if cell_ch == close { depth += if forward { -1 } else { 1 }; }
if depth == 0 {
app.copy_pos = Some((cr, cc));
return;
}
}
}
pub fn select_inner_word(app: &mut AppState) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
let seps = app.word_separators.clone();
let (text, _cols) = match read_row_text(app, r) { Some(t) => t, None => return };
let bytes: Vec<char> = text.chars().collect();
let col = c as usize;
if col >= bytes.len() { return; }
let cls = char_class(bytes[col], &seps);
let mut start = col;
while start > 0 && char_class(bytes[start - 1], &seps) == cls { start -= 1; }
let mut end = col;
while end + 1 < bytes.len() && char_class(bytes[end + 1], &seps) == cls { end += 1; }
app.copy_anchor = Some((r, start as u16));
app.copy_anchor_scroll_offset = app.copy_scroll_offset;
app.copy_pos = Some((r, end as u16));
app.copy_selection_mode = crate::types::SelectionMode::Char;
}
pub fn select_a_word(app: &mut AppState) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
let seps = app.word_separators.clone();
let (text, _cols) = match read_row_text(app, r) { Some(t) => t, None => return };
let bytes: Vec<char> = text.chars().collect();
let col = c as usize;
if col >= bytes.len() { return; }
let cls = char_class(bytes[col], &seps);
let mut start = col;
while start > 0 && char_class(bytes[start - 1], &seps) == cls { start -= 1; }
let mut end = col;
while end + 1 < bytes.len() && char_class(bytes[end + 1], &seps) == cls { end += 1; }
while end + 1 < bytes.len() && bytes[end + 1].is_whitespace() { end += 1; }
app.copy_anchor = Some((r, start as u16));
app.copy_anchor_scroll_offset = app.copy_scroll_offset;
app.copy_pos = Some((r, end as u16));
app.copy_selection_mode = crate::types::SelectionMode::Char;
}
pub fn select_inner_word_big(app: &mut AppState) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
let (text, _cols) = match read_row_text(app, r) { Some(t) => t, None => return };
let bytes: Vec<char> = text.chars().collect();
let col = c as usize;
if col >= bytes.len() { return; }
if bytes[col].is_whitespace() {
let mut start = col;
while start > 0 && bytes[start - 1].is_whitespace() { start -= 1; }
let mut end = col;
while end + 1 < bytes.len() && bytes[end + 1].is_whitespace() { end += 1; }
app.copy_anchor = Some((r, start as u16));
app.copy_anchor_scroll_offset = app.copy_scroll_offset;
app.copy_pos = Some((r, end as u16));
} else {
let mut start = col;
while start > 0 && !bytes[start - 1].is_whitespace() { start -= 1; }
let mut end = col;
while end + 1 < bytes.len() && !bytes[end + 1].is_whitespace() { end += 1; }
app.copy_anchor = Some((r, start as u16));
app.copy_anchor_scroll_offset = app.copy_scroll_offset;
app.copy_pos = Some((r, end as u16));
}
app.copy_selection_mode = crate::types::SelectionMode::Char;
}
pub fn select_a_word_big(app: &mut AppState) {
let (r, c) = match get_copy_pos(app) { Some(p) => p, None => return };
let (text, _cols) = match read_row_text(app, r) { Some(t) => t, None => return };
let bytes: Vec<char> = text.chars().collect();
let col = c as usize;
if col >= bytes.len() { return; }
if bytes[col].is_whitespace() {
let mut start = col;
while start > 0 && bytes[start - 1].is_whitespace() { start -= 1; }
let mut end = col;
while end + 1 < bytes.len() && bytes[end + 1].is_whitespace() { end += 1; }
app.copy_anchor = Some((r, start as u16));
app.copy_anchor_scroll_offset = app.copy_scroll_offset;
app.copy_pos = Some((r, end as u16));
} else {
let mut start = col;
while start > 0 && !bytes[start - 1].is_whitespace() { start -= 1; }
let mut end = col;
while end + 1 < bytes.len() && !bytes[end + 1].is_whitespace() { end += 1; }
while end + 1 < bytes.len() && bytes[end + 1].is_whitespace() { end += 1; }
app.copy_anchor = Some((r, start as u16));
app.copy_anchor_scroll_offset = app.copy_scroll_offset;
app.copy_pos = Some((r, end as u16));
}
app.copy_selection_mode = crate::types::SelectionMode::Char;
}