Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<n>` | 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
Expand Down
50 changes: 50 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -182,6 +184,7 @@ pub struct App {
sorter: Option<Arc<sort::Sorter>>,
sort_order: SortOrder,
wrap_mode: WrapMode,
clipboard_limit: Option<usize>,
#[cfg(feature = "clipboard")]
clipboard: Result<Clipboard>,
}
Expand All @@ -204,6 +207,7 @@ impl App {
prompt: Option<String>,
wrap_mode: Option<WrapMode>,
auto_reload: bool,
clipboard_limit: Option<usize>,
) -> CsvlensResult<Self> {
let watcher = if auto_reload {
Some(Arc::new(Watcher::new(filename)?))
Expand Down Expand Up @@ -280,6 +284,7 @@ impl App {
sorter: None,
sort_order: SortOrder::Ascending,
wrap_mode: WrapMode::default(),
clipboard_limit,
#[cfg(feature = "clipboard")]
clipboard,
};
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -1105,6 +1154,7 @@ mod tests {
self.prompt,
self.wrap_mode,
false,
None,
)
}

Expand Down
68 changes: 68 additions & 0 deletions src/csv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,74 @@ impl CsvLensReader {
thread::sleep(time::Duration::from_millis(100));
}
}

pub fn get_column_values(
&self,
column_index: usize,
limit: Option<usize>,
) -> CsvlensResult<Vec<String>> {
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(
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading a large number of rows this way without using the indexing in CsvLensReader can be slow. It also seems to duplicate quite a bit of the code there.

&self,
column_index: usize,
indices: &[u64],
) -> CsvlensResult<Vec<String>> {
let mut get_row_indices = indices
.iter()
.enumerate()
.map(|x| GetRowIndex {
record_index: *x.1,
order_index: x.0,
})
.collect::<Vec<_>>();
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)]
Expand Down
9 changes: 9 additions & 0 deletions src/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,15 @@ impl Finder {
indices
}

pub fn get_all_found_indices(&self) -> Vec<u64> {
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 {
Expand Down
2 changes: 1 addition & 1 deletion src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize>,

/// Show stats for debugging
#[clap(long)]
debug: bool,
Expand Down Expand Up @@ -159,6 +163,7 @@ impl From<Args> 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,
}
}
}
Expand All @@ -182,6 +187,7 @@ pub struct CsvlensOptions {
pub prompt: Option<String>,
pub wrap_mode: Option<WrapMode>,
pub auto_reload: bool,
pub clipboard_limit: Option<usize>,
}

struct AppRunner {
Expand Down Expand Up @@ -274,6 +280,7 @@ pub fn run_csvlens_with_options(options: CsvlensOptions) -> CsvlensResult<Option
options.prompt,
options.wrap_mode,
options.auto_reload,
options.clipboard_limit,
)?;

let mut app_runner = AppRunner::new(app);
Expand Down
19 changes: 19 additions & 0 deletions src/sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,25 @@ impl Sorter {
None
}

pub fn get_all_sorted_indices(&self, order: SortOrder) -> Option<Vec<u64>> {
let m_guard = self.internal.lock().unwrap();
if let Some(sort_result) = &m_guard.sort_result {
let mut out = Vec::with_capacity(sort_result.num_rows());
let index_range: Box<dyn Iterator<Item = usize>> = if order == SortOrder::Ascending {
Box::new(0..sort_result.num_rows())
} else {
Box::new((0..sort_result.num_rows()).rev())
};
for i in index_range {
if let Some(record_index) = sort_result.record_indices.get(i) {
out.push(*record_index as u64)
}
}
return Some(out);
}
None
}

pub fn get_record_order(&self, row_index: u64, order: SortOrder) -> Option<u64> {
let m_guard = self.internal.lock().unwrap();
if let Some(sort_result) = &m_guard.sort_result
Expand Down
37 changes: 37 additions & 0 deletions src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,39 @@ impl RowsView {
None
}

pub fn get_column_values_from_selection(
&self,
indices: Option<&Vec<u64>>,
limit: Option<usize>,
) -> Option<(usize, String, usize)> {
if let Some(column_index) = self.selection.column.index() {
let filtered_index = self.cols_offset.get_filtered_column_index(column_index) as usize;
if let Some(header) = self.headers.get(filtered_index) {
let origin_index = header.origin_index;
let values = if let Some(indices) = indices {
let indices_to_use = if let Some(l) = limit {
if indices.len() > l {
&indices[0..l]
} else {
indices.as_slice()
}
} else {
indices.as_slice()
};
self.reader
.get_column_values_for_indices(origin_index, indices_to_use)
} else {
self.reader.get_column_values(origin_index, limit)
};
if let Ok(values) = values {
let count = values.len();
return Some((column_index as usize, values.join("\n"), count));
}
}
}
None
}

pub fn get_row_value(&self) -> Option<(usize, String)> {
if let Some(row_index) = self.selection.row.index()
&& let Some(row) = self.rows().get(row_index as usize)
Expand Down Expand Up @@ -734,4 +767,8 @@ impl RowsView {
pub fn wait_internal(&self) {
self.reader.wait_internal()
}

pub fn sort_order(&self) -> SortOrder {
self.sort_order
}
}