use crate::app::form::ListMove;
use crate::app::jobs::{Answer, Job};
use crate::{
App, AppEvent, Overlay, analysis::value_counts, analysis::value_counts_modal,
app::modals::copy_modal, clipboard,
};
use crossterm::event::{KeyCode, KeyEvent};
use std::sync::Arc;
impl App {
pub(crate) fn open_value_counts(&mut self) {
let Some(state) = self.data_table_state.as_ref() else {
return;
};
let names = state.get_column_order().to_vec();
let Some(at) = state
.current_column()
.and_then(|current| names.iter().position(|n| n == current))
else {
return;
};
self.value_counts.open(names, at, state.len_generation());
self.open_overlay(Overlay::ValueCounts);
self.count_values(false);
}
pub(crate) fn value_counts_shown(&self) -> bool {
self.overlay.shows(&Overlay::ValueCounts)
}
pub(crate) fn value_counts_computing(&self) -> bool {
self.overlay == Overlay::ValueCounts && self.value_counts.computing.is_some()
}
fn count_values(&mut self, exact: bool) {
let Some(column) = self.value_counts.column().map(str::to_string) else {
return;
};
let held_sample = self.value_counts.current().map(|c| c.is_sample());
if held_sample == Some(false) || (held_sample == Some(true) && !exact) {
return;
}
if self
.value_counts
.computing
.as_ref()
.is_some_and(|c| c.column == column && (c.exact || !exact))
{
return;
}
self.stop_value_count();
let Some(state) = self.data_table_state.as_ref() else {
return;
};
let read = if exact {
value_counts::Read::Exact
} else {
value_counts::Read::Quick {
sample_rows: self.app_config.analysis.sample_rows,
seed: self.analysis_modal.sample.seed,
remote: state.is_remote_source(),
}
};
let plan = value_counts::Plan {
lf: state.analysis_lf(),
column: column.clone(),
read,
known_total: state.num_rows_if_valid(),
streaming: state.polars_streaming(),
};
let watch = crate::analysis::sampling::ReadWatch::default();
self.value_counts.failed = None;
self.value_counts.computing = Some(value_counts_modal::Computing {
column,
exact,
watch: watch.clone(),
file_starts: state.file_row_starts().map(Arc::new),
});
self.spawn_job(Job::ValueCounts, None, move |_| {
plan.run(&watch)
.map(|counts| Answer::ValueCounts(Box::new(counts)))
.map_err(|e| crate::error_display::user_message_from_report(&e, None))
});
}
pub(crate) fn stop_value_count(&mut self) {
if let Some(computing) = self.value_counts.computing.take() {
computing.watch.stop();
self.jobs.cancel(|job| matches!(job, Job::ValueCounts));
}
}
pub(crate) fn value_counts_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
if !event.is_press() {
return None;
}
let listing = !self.value_counts.shows_histogram();
if listing && let Some(step) = ListMove::from_key(event) {
match step {
ListMove::Home => self.value_counts.move_to_start(),
ListMove::End => self.value_counts.move_to_end(),
_ => self
.value_counts
.move_by(step.delta(self.value_counts_page())),
}
return None;
}
match event.code {
KeyCode::Esc => {
let counting = self.value_counts.counting();
self.stop_value_count();
if counting && self.value_counts.current().is_some() {
self.flash_note("Count stopped".to_string());
} else {
self.close_overlay();
}
}
KeyCode::Char('G') if listing => self.value_counts.move_to_end(),
KeyCode::Left | KeyCode::Right | KeyCode::Char('h') | KeyCode::Char('l') => {
let by = if matches!(event.code, KeyCode::Left | KeyCode::Char('h')) {
-1
} else {
1
};
if self.value_counts.step(by) {
if let (Some(state), Some(column)) =
(self.data_table_state.as_mut(), self.value_counts.column())
{
state.set_current_column(column);
}
self.count_values(false);
}
}
KeyCode::Char('s') if listing => self.value_counts.toggle_order(),
KeyCode::Char('c') => self.value_counts.toggle_view(),
KeyCode::Char('a') => {
if self.value_counts.current().is_some_and(|c| c.is_sample()) {
self.count_values(true);
}
}
KeyCode::Enter if listing => self.drill_into_counted_value(),
KeyCode::Char('y') => self.copy_value_counts(),
KeyCode::Char('e') => self.export_value_counts(),
KeyCode::Char('t') if self.follow_rows_waiting() => {
self.stop_value_count();
self.take_follow_rows(false);
if let Some(state) = self.data_table_state.as_ref() {
let names = self.value_counts.columns.clone();
let at = self.value_counts.at;
self.value_counts.open(names, at, state.len_generation());
}
self.count_values(false);
}
_ => {}
}
None
}
fn value_counts_page(&self) -> usize {
self.value_counts.page.max(1)
}
fn drill_into_counted_value(&mut self) {
let Some(kind) = self.value_counts.selected_kind() else {
return;
};
let (Some(counts), Some(column)) = (
self.value_counts.current().cloned(),
self.value_counts.column().map(str::to_string),
) else {
return;
};
let value = match kind {
value_counts::LineKind::Value(at) => match counts.value(at) {
Ok(value) => value,
Err(_) => return,
},
value_counts::LineKind::Null => polars::prelude::AnyValue::Null,
value_counts::LineKind::Other(_) => {
self.flash_note("Other is many values: pick one to see its rows".to_string());
return;
}
};
let Some(state) = self.data_table_state.as_mut() else {
return;
};
let nested = state.is_drilled_down();
match state.deferred(|s| s.drill_into_value(&column, value)) {
Ok(()) => {
self.stop_value_count();
self.value_counts.drill_return = !nested;
self.close_overlay();
self.sync_sort_filter_modal();
self.spawn_async_collect(Self::LOADING_BUFFER);
}
Err(e) => self.flash_note(format!(
"Could not drill in: {}",
crate::error_display::user_message_from_report(&e, None)
)),
}
}
fn copy_value_counts(&mut self) {
let Some(counts) = self.value_counts.current().cloned() else {
return;
};
let order = self.value_counts.order;
let html = match self.copy_destination() {
Ok(destination) => destination.accepts().html,
Err(e) => {
self.error_modal.show(e);
return;
}
};
self.spawn_job(Job::Copy, Some("Copying..."), move |_| {
let table = counts
.table(order)
.map_err(|e| format!("Copy failed: {e}"))?;
let payload =
crate::clipboard::tabular_payload(&table, clipboard::CopyFormat::Tsv, true, html)
.map_err(|e| format!("Copy failed: {e}"))?;
Ok(Answer::Copied {
payload,
message: format!(
"Copied {} values as TSV",
copy_modal::thousands(table.height())
),
})
});
}
fn export_value_counts(&mut self) {
let Some(counts) = self.value_counts.current() else {
return;
};
let table = match counts.table(self.value_counts.order) {
Ok(table) => table,
Err(e) => {
self.error_modal
.show(format!("Cannot export the counts: {e}"));
return;
}
};
self.export_modal.open(
self.source.original_file_format,
self.display.history_limit,
&self.theme,
self.source.original_file_delimiter,
);
let stem = self.dataset_stem();
self.export_modal.suggest_path(&format!("{stem}-counts"));
self.export_modal.offer_source_file = false;
self.export_modal.nested_columns = false;
self.export_modal.avro_renames = table
.columns()
.iter()
.any(|c| crate::export::avro_types::renames(c.name(), c.dtype()));
self.export_modal.counts = Some(table);
self.open_over(|returns_to| Overlay::Export { returns_to });
}
}