diff --git a/README.md b/README.md index 3f15494..271b4ac 100644 --- a/README.md +++ b/README.md @@ -46,13 +46,15 @@ Key | Action `Ctrl + j` | Same as above, but sort by natural ordering (e.g. "file2" < "file10") `#` (in Cell mode) | Find and highlight rows like the selected cell `@` (in Cell mode) | Filter rows like the selected cell -`y` | Copy the selected row or cell to clipboard +`y` | Copy the selected row, cell or column to clipboard `Enter` (in Cell mode) | Print the selected cell to stdout and exit `-S` | Toggle line wrapping `-W` | Toggle line wrapping by words `f` | Freeze this number of columns from the left `r` | Reset to default view (clear all filters and custom column widths) `H` (or `?`) | Display help +`m` | Toggle line mark +`M` | Reset line marks `q` | Exit ### Optional parameters diff --git a/src/app.rs b/src/app.rs index 05a5e1e..eedac3c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,6 +24,8 @@ use std::cmp::min; use std::sync::Arc; use std::time::{Duration, Instant}; +const DEFAULT_CLIPBOARD_LIMIT: usize = 10000; + fn get_offsets_to_make_visible( found_record: &find::FoundEntry, rows_view: &view::RowsView, @@ -182,6 +184,7 @@ pub struct App { sorter: Option>, sort_order: SortOrder, wrap_mode: WrapMode, + clipboard_limit: Option, #[cfg(feature = "clipboard")] clipboard: Result, } @@ -204,6 +207,7 @@ impl App { prompt: Option, wrap_mode: Option, auto_reload: bool, + clipboard_limit: Option, ) -> CsvlensResult { let watcher = if auto_reload { Some(Arc::new(Watcher::new(filename)?)) @@ -280,6 +284,7 @@ impl App { sorter: None, sort_order: SortOrder::Ascending, wrap_mode: WrapMode::default(), + clipboard_limit, #[cfg(feature = "clipboard")] clipboard, }; @@ -594,6 +599,50 @@ impl App { .transient_message .replace(format!("Failed to copy to clipboard: {e}")), }; + } else if let SelectionType::Column = self.rows_view.selection.selection_type() { + let indices = if self.rows_view.is_filter() { + self.finder.as_ref().map(|f| f.get_all_found_indices()) + } else if let Some(sorter) = self.rows_view.sorter() { + sorter.get_all_sorted_indices(self.rows_view.sort_order()) + } else { + None + }; + let limit = self.clipboard_limit.unwrap_or(DEFAULT_CLIPBOARD_LIMIT); + if let Some((column_index, column_values, count)) = self + .rows_view + .get_column_values_from_selection(indices.as_ref(), Some(limit)) + { + let filtered_index = self + .rows_view + .cols_offset() + .get_filtered_column_index(column_index as u64) + as usize; + let column_name = self + .rows_view + .get_column_name_from_local_index(filtered_index); + match self.clipboard.as_mut().map(|c| c.set_text(&column_values)) { + Ok(_) => { + let msg = if count == limit { + format!( + "Copied first {} rows of column \"{}\" to clipboard", + count, column_name + ) + } else { + format!( + "Copied {} rows of column \"{}\" to clipboard", + count, column_name + ) + }; + self.transient_message.replace(msg) + } + Err(e) => self + .transient_message + .replace(format!("Failed to copy column to clipboard: {}", e)), + } + } else { + self.transient_message + .replace("Could not copy column".to_string()) + }; } } Control::FileChanged => { @@ -1105,6 +1154,7 @@ mod tests { self.prompt, self.wrap_mode, false, + None, ) } diff --git a/src/csv.rs b/src/csv.rs index 6b59b0b..b5b3da5 100644 --- a/src/csv.rs +++ b/src/csv.rs @@ -330,6 +330,74 @@ impl CsvLensReader { thread::sleep(time::Duration::from_millis(100)); } } + + pub fn get_column_values( + &self, + column_index: usize, + limit: Option, + ) -> CsvlensResult> { + let mut reader = self.config.new_reader()?; + let mut values = vec![]; + for result in reader.records() { + if let Some(l) = limit + && values.len() >= l + { + break; + } + let record = result?; + if let Some(field) = record.get(column_index) { + values.push(field.to_string()); + } + } + Ok(values) + } + + pub fn get_column_values_for_indices( + &self, + column_index: usize, + indices: &[u64], + ) -> CsvlensResult> { + let mut get_row_indices = indices + .iter() + .enumerate() + .map(|x| GetRowIndex { + record_index: *x.1, + order_index: x.0, + }) + .collect::>(); + get_row_indices.sort_by(|a, b| a.record_index.cmp(&b.record_index)); + + let mut reader = self.config.new_reader()?; + let mut values = vec![String::new(); indices.len()]; + + let mut indices_iter = get_row_indices.iter(); + let mut next_wanted = indices_iter.next(); + // let mut current_record_index = 0; + + if next_wanted.is_none() { + return Ok(values); + } + + for (current_record_index, result) in reader.records().enumerate() { + let record = result?; + while let Some(wanted) = next_wanted { + if current_record_index == wanted.record_index as usize { + if let Some(field) = record.get(column_index) { + values[wanted.order_index] = field.to_string(); + } + next_wanted = indices_iter.next(); + } else if current_record_index > wanted.record_index as usize { + next_wanted = indices_iter.next(); + } else { + break; + } + } + if next_wanted.is_none() { + break; + } + } + Ok(values) + } } #[derive(Debug, Clone, PartialEq)] diff --git a/src/find.rs b/src/find.rs index a837fd4..ae99069 100644 --- a/src/find.rs +++ b/src/find.rs @@ -425,6 +425,15 @@ impl Finder { indices } + pub fn get_all_found_indices(&self) -> Vec { + let m_guard = self.internal.lock().unwrap(); + m_guard + .founds + .iter() + .map(|x| x.row_index() as u64) + .collect() + } + #[cfg(test)] pub fn wait_internal(&self) { loop { diff --git a/src/help.rs b/src/help.rs index ac53ac5..31451d4 100644 --- a/src/help.rs +++ b/src/help.rs @@ -43,7 +43,7 @@ Shift + ↓ (or J) : Sort rows by the selected column (auto by type: nume Ctrl + J : Sort rows by the selected column (natural; e.g. \"file2\" < \"file10\") # (in Cell mode) : Find and highlight rows like the selected cell @ (in Cell mode) : Filter rows like the selected cell -y : Copy the selected row or cell to clipboard +y : Copy the selected row, cell or column to clipboard Enter (in Cell mode) : Print the selected cell to stdout and exit # Other options diff --git a/src/runner.rs b/src/runner.rs index fdd52c2..d6472be 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -110,6 +110,10 @@ struct Args { #[clap(long)] pub auto_reload: bool, + /// Limit the number of rows to copy to clipboard + #[clap(long)] + pub clipboard_limit: Option, + /// Show stats for debugging #[clap(long)] debug: bool, @@ -159,6 +163,7 @@ impl From for CsvlensOptions { prompt: args.prompt, wrap_mode: Args::get_wrap_mode(args.wrap, args.wrap_chars, args.wrap_words), auto_reload: args.auto_reload, + clipboard_limit: args.clipboard_limit, } } } @@ -182,6 +187,7 @@ pub struct CsvlensOptions { pub prompt: Option, pub wrap_mode: Option, pub auto_reload: bool, + pub clipboard_limit: Option, } struct AppRunner { @@ -274,6 +280,7 @@ pub fn run_csvlens_with_options(options: CsvlensOptions) -> CsvlensResult