From 4e6dae4c4d35d911841c99d5e3ca350d4a9100e7 Mon Sep 17 00:00:00 2001 From: Giant Date: Fri, 2 Jan 2026 15:50:38 +0100 Subject: [PATCH 1/3] copy marked lines (and header) to clipboard ctrl+m --- src/app.rs | 36 ++++++++++++++++++++++++++++++++++++ src/input.rs | 2 ++ src/view.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/src/app.rs b/src/app.rs index 1845e5d..5276bd1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -525,6 +525,42 @@ impl App { .replace("Marking of rows only works in row mode".to_string()); } } + #[cfg(feature = "clipboard")] + Control::CopyMarks => { + let marked = self.rows_view.marked_rows(); + if marked.is_empty() { + self.transient_message + .replace("No marked rows to copy".to_string()); + } else { + let mut record_numbers: Vec = marked.iter().copied().collect(); + record_numbers.sort_unstable(); + + let headers_line = self.rows_view.get_headers_line(); + match self.rows_view.get_rows_values(&record_numbers) { + Ok(lines) => { + let mut content_lines = + Vec::with_capacity(lines.len().saturating_add(1)); + content_lines.push(headers_line); + content_lines.extend(lines); + let content = content_lines.join("\n"); + match self.clipboard.as_mut().map(|c| c.set_text(&content)) { + Ok(_) => self.transient_message.replace(format!( + "Copied {} marked row{} to clipboard", + record_numbers.len(), + if record_numbers.len() == 1 { "" } else { "s" } + )), + Err(e) => self + .transient_message + .replace(format!("Failed to copy to clipboard: {e}")), + }; + } + Err(e) => { + self.transient_message + .replace(format!("Failed to copy marked rows: {e}")); + } + }; + } + } Control::ResetMarks => { self.rows_view.clear_marks(); self.transient_message diff --git a/src/input.rs b/src/input.rs index e14fa4c..5a0aae4 100644 --- a/src/input.rs +++ b/src/input.rs @@ -38,6 +38,7 @@ pub enum Control { BufferReset, Select, CopySelection, + CopyMarks, ToggleSelectionType, ToggleLineWrap(WrapMode), ToggleMark, @@ -196,6 +197,7 @@ impl InputHandler { KeyCode::Left => Control::ScrollLeftMost, KeyCode::Right => Control::ScrollRightMost, KeyCode::Char('j') => Control::ToggleNaturalSort, + KeyCode::Char('m') => Control::CopyMarks, _ => Control::Nothing, }, _ => Control::Nothing, diff --git a/src/view.rs b/src/view.rs index d95cb86..c658ce8 100644 --- a/src/view.rs +++ b/src/view.rs @@ -367,6 +367,35 @@ impl RowsView { None } + pub fn get_headers_line(&self) -> String { + self.headers() + .iter() + .map(|h| h.name.clone()) + .collect::>() + .join("\t") + } + + pub fn get_rows_values(&mut self, record_numbers: &[usize]) -> CsvlensResult> { + if record_numbers.is_empty() { + return Ok(vec![]); + } + + // marked rows store 1-based record numbers; convert to 0-based indices for fetching + let mut indices: Vec = record_numbers + .iter() + .map(|&n| n.saturating_sub(1) as u64) + .collect(); + indices.sort_unstable(); + + let (mut rows, _) = self.reader.get_rows_for_indices(&indices)?; + + if let Some(columns_filter) = &self.columns_filter { + rows = Self::subset_columns(&rows, columns_filter.indices()); + } + + Ok(rows.into_iter().map(|row| row.fields.join("\t")).collect()) + } + pub fn num_rows(&self) -> u64 { self.num_rows } From 22b257e4f90d1ae660a2d5915f3df6313a500a68 Mon Sep 17 00:00:00 2001 From: Giant Date: Fri, 2 Jan 2026 16:46:56 +0100 Subject: [PATCH 2/3] print marked rows to stdout ctrl+m --- src/app.rs | 62 ++++++++++++++++++++++------------------------------ src/help.rs | 1 + src/input.rs | 4 ++-- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5276bd1..fe4f243 100644 --- a/src/app.rs +++ b/src/app.rs @@ -332,6 +332,11 @@ impl App { { return Ok(Some(result)); } + if matches!(control, Control::SelectMarks) + && let Some(result) = self.get_marked_rows() + { + return Ok(Some(result)); + } if matches!(control, Control::Help) { self.help_page_state.activate(); self.input_handler.enter_help_mode(); @@ -525,42 +530,6 @@ impl App { .replace("Marking of rows only works in row mode".to_string()); } } - #[cfg(feature = "clipboard")] - Control::CopyMarks => { - let marked = self.rows_view.marked_rows(); - if marked.is_empty() { - self.transient_message - .replace("No marked rows to copy".to_string()); - } else { - let mut record_numbers: Vec = marked.iter().copied().collect(); - record_numbers.sort_unstable(); - - let headers_line = self.rows_view.get_headers_line(); - match self.rows_view.get_rows_values(&record_numbers) { - Ok(lines) => { - let mut content_lines = - Vec::with_capacity(lines.len().saturating_add(1)); - content_lines.push(headers_line); - content_lines.extend(lines); - let content = content_lines.join("\n"); - match self.clipboard.as_mut().map(|c| c.set_text(&content)) { - Ok(_) => self.transient_message.replace(format!( - "Copied {} marked row{} to clipboard", - record_numbers.len(), - if record_numbers.len() == 1 { "" } else { "s" } - )), - Err(e) => self - .transient_message - .replace(format!("Failed to copy to clipboard: {e}")), - }; - } - Err(e) => { - self.transient_message - .replace(format!("Failed to copy marked rows: {e}")); - } - }; - } - } Control::ResetMarks => { self.rows_view.clear_marks(); self.transient_message @@ -783,6 +752,27 @@ impl App { None } + fn get_marked_rows(&mut self) -> Option { + let marked = self.rows_view.marked_rows(); + if marked.is_empty() { + return Some(String::new()); + } + + let mut record_numbers: Vec = marked.iter().copied().collect(); + record_numbers.sort_unstable(); + + let headers_line = self.rows_view.get_headers_line(); + match self.rows_view.get_rows_values(&record_numbers) { + Ok(lines) => { + let mut content_lines = Vec::with_capacity(lines.len().saturating_add(1)); + content_lines.push(headers_line); + content_lines.extend(lines); + Some(content_lines.join("\n")) + } + Err(_) => None, + } + } + fn get_finder_starting_row_index(&self) -> usize { self.rows_view.selected_offset().unwrap_or(0) as usize } diff --git a/src/help.rs b/src/help.rs index 7fd9c82..c20ea81 100644 --- a/src/help.rs +++ b/src/help.rs @@ -55,6 +55,7 @@ r : Reset to default view (clear all filters and custom co H (or ?) : Display this help m : Mark / unmark the selected row visually M : Clear all row marks +Ctrl + m : Print the marked rows (with header) to stdout and exit q : Exit"; pub struct HelpPage {} diff --git a/src/input.rs b/src/input.rs index 5a0aae4..5f31347 100644 --- a/src/input.rs +++ b/src/input.rs @@ -38,7 +38,7 @@ pub enum Control { BufferReset, Select, CopySelection, - CopyMarks, + SelectMarks, ToggleSelectionType, ToggleLineWrap(WrapMode), ToggleMark, @@ -197,7 +197,7 @@ impl InputHandler { KeyCode::Left => Control::ScrollLeftMost, KeyCode::Right => Control::ScrollRightMost, KeyCode::Char('j') => Control::ToggleNaturalSort, - KeyCode::Char('m') => Control::CopyMarks, + KeyCode::Char('m') => Control::SelectMarks, _ => Control::Nothing, }, _ => Control::Nothing, From 7a250cb4a9514bc6839a86bdf8cbf7149c78dd80 Mon Sep 17 00:00:00 2001 From: Giant Date: Thu, 8 Jan 2026 18:32:59 +0100 Subject: [PATCH 3/3] print marks new shortcut and some minor fixes --- README.md | 1 + src/app.rs | 2 +- src/input.rs | 2 +- src/view.rs | 3 +-- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 940da05..88bdc9f 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Key | Action `Ctrl + l` | Scroll one window right `Ctrl + ←` | Scroll left to first column `Ctrl + →` | Scroll right to last column +`Ctrl + e` | Print the marked lines to stdout and exit `G` (or `End`) | Go to bottom `g` (or `Home`) | Go to top `G` | Go to line `n` diff --git a/src/app.rs b/src/app.rs index fe4f243..e9afe85 100644 --- a/src/app.rs +++ b/src/app.rs @@ -755,7 +755,7 @@ impl App { fn get_marked_rows(&mut self) -> Option { let marked = self.rows_view.marked_rows(); if marked.is_empty() { - return Some(String::new()); + return None; } let mut record_numbers: Vec = marked.iter().copied().collect(); diff --git a/src/input.rs b/src/input.rs index 5f31347..ba95866 100644 --- a/src/input.rs +++ b/src/input.rs @@ -197,7 +197,7 @@ impl InputHandler { KeyCode::Left => Control::ScrollLeftMost, KeyCode::Right => Control::ScrollRightMost, KeyCode::Char('j') => Control::ToggleNaturalSort, - KeyCode::Char('m') => Control::SelectMarks, + KeyCode::Char('e') => Control::SelectMarks, _ => Control::Nothing, }, _ => Control::Nothing, diff --git a/src/view.rs b/src/view.rs index c658ce8..4b817f8 100644 --- a/src/view.rs +++ b/src/view.rs @@ -381,11 +381,10 @@ impl RowsView { } // marked rows store 1-based record numbers; convert to 0-based indices for fetching - let mut indices: Vec = record_numbers + let indices: Vec = record_numbers .iter() .map(|&n| n.saturating_sub(1) as u64) .collect(); - indices.sort_unstable(); let (mut rows, _) = self.reader.get_rows_for_indices(&indices)?;