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::*;
use crate::tree::*;
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;
}
#[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();
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 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 r0 = anchor.0.min(pos.0); let r1 = anchor.0.max(pos.0);
let mut text = String::new();
match sel_mode {
crate::types::SelectionMode::Rect => {
let c0 = anchor.1.min(pos.1); let c1 = anchor.1.max(pos.1);
for r in r0..=r1 {
let mut line = String::new();
for c in c0..=c1 {
if let Some(cell) = screen.cell(r, c) { line.push_str(&cell.contents().to_string()); } else { line.push(' '); }
}
text.push_str(line.trim_end());
if r < r1 { text.push('\n'); }
}
}
crate::types::SelectionMode::Line => {
let cols = p.last_cols;
for r in r0..=r1 {
let mut line = String::new();
for c in 0..cols {
if let Some(cell) = 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 => {
let cols = p.last_cols;
let ac = anchor.1.min(pos.1); let pc = anchor.1.max(pos.1);
if r0 == r1 {
let c0 = ac; let c1 = pc;
for c in c0..=c1 {
if let Some(cell) = screen.cell(r0, c) { text.push_str(&cell.contents().to_string()); } else { text.push(' '); }
}
} else {
let (start_r, start_c, end_r, end_c) = if (anchor.0, anchor.1) <= (pos.0, pos.1) {
(anchor.0, anchor.1, pos.0, pos.1)
} else {
(pos.0, pos.1, anchor.0, anchor.1)
};
for r in r0..=r1 {
let line_start = if r == start_r { start_c } else { 0 };
let line_end = if r == end_r { end_c } else { cols.saturating_sub(1) };
let mut line = String::new();
for c in line_start..=line_end {
if let Some(cell) = screen.cell(r, c) { line.push_str(&cell.contents().to_string()); } else { line.push(' '); }
}
text.push_str(line.trim_end());
if r < r1 { text.push('\n'); }
}
}
}
}
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 paste_latest(app: &mut AppState) -> io::Result<()> {
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.master, "{}", 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 { for c in 0..p.last_cols { if let Some(cell) = screen.cell(r, c) { text.push_str(&cell.contents().to_string()); } else { text.push(' '); } } 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 { for c in 0..p.last_cols { if let Some(cell) = screen.cell(r, c) { text.push_str(&cell.contents().to_string()); } else { text.push(' '); } } 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; }
app.copy_search_idx = (app.copy_search_idx + 1) % app.copy_search_matches.len();
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; }
if app.copy_search_idx == 0 {
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<u16>, e: Option<u16>) -> 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 start = s.unwrap_or(0).min(p.last_rows.saturating_sub(1));
let end = e.unwrap_or(p.last_rows.saturating_sub(1)).min(p.last_rows.saturating_sub(1));
let mut text = String::new();
for r in start..=end { for c in 0..p.last_cols { if let Some(cell) = screen.cell(r, c) { text.push_str(&cell.contents().to_string()); } else { text.push(' '); } } text.push('\n'); }
Ok(Some(text))
}
pub fn capture_active_pane_styled(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();
let mut prev_fg: Option<vt100::Color> = None;
let mut prev_bg: Option<vt100::Color> = None;
let mut prev_bold = false;
let mut prev_italic = false;
let mut prev_underline = false;
let mut prev_inverse = false;
for r in 0..p.last_rows {
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 italic = cell.italic();
let underline = cell.underline();
let inverse = cell.inverse();
let style_changed = Some(fg) != prev_fg || Some(bg) != prev_bg
|| bold != prev_bold || italic != prev_italic
|| underline != prev_underline || inverse != prev_inverse;
if style_changed {
let mut params = Vec::new();
params.push("0".to_string()); if bold { params.push("1".to_string()); }
if italic { params.push("3".to_string()); }
if underline { params.push("4".to_string()); }
if inverse { params.push("7".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)); }
}
text.push_str(&format!("\x1b[{}m", params.join(";")));
prev_fg = Some(fg);
prev_bg = Some(bg);
prev_bold = bold;
prev_italic = italic;
prev_underline = underline;
prev_inverse = inverse;
any_style_active = true;
}
text.push_str(&cell.contents().to_string());
} else {
text.push(' ');
}
}
if any_style_active {
text.push_str("\x1b[0m");
prev_fg = None;
prev_bg = None;
prev_bold = false;
prev_italic = false;
prev_underline = false;
prev_inverse = 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;
}
}
}