use crate::clipboard::CopyFormat;
use crate::widgets::ui::PickerState;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CopyScope {
Cell,
#[default]
Row,
View,
Table,
Python,
}
impl CopyScope {
pub const ALL: [Self; 5] = [Self::Cell, Self::Row, Self::View, Self::Table, Self::Python];
pub fn as_str(self) -> &'static str {
match self {
Self::Cell => "Cell",
Self::Row => "Row",
Self::View => "View",
Self::Table => "Table",
Self::Python => "Python (Polars)",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CopyFocus {
#[default]
Scope,
Column,
Format,
Header,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CopyContext {
pub row_number: usize,
pub view_rows: usize,
pub view_cols: usize,
pub total_rows: Option<usize>,
}
pub struct CopyModal {
pub focus: CopyFocus,
pub scope: CopyScope,
pub format: CopyFormat,
pub header_row: bool,
pub header_view: bool,
pub header_table: bool,
pub column: Option<String>,
pub available_columns: Vec<String>,
pub picker: Option<PickerState>,
pub attention: bool,
pub context: CopyContext,
}
impl crate::app::form::Form for CopyModal {
type Field = CopyFocus;
fn shown_picker(&mut self) -> Option<(&mut crate::widgets::ui::PickerState, bool)> {
self.picker.as_mut().map(|p| (p, false))
}
fn dismiss_picker(&mut self) {
self.picker = None;
}
fn pick(&mut self, _toggle: bool) {
self.picker_choose();
}
fn fields(&self) -> Vec<(CopyFocus, crate::app::form::FieldKind)> {
use crate::app::form::FieldKind;
self.row_order()
.into_iter()
.map(|row| {
let kind = match row {
CopyFocus::Scope | CopyFocus::Format => FieldKind::Choice,
CopyFocus::Column => FieldKind::Picker { multi: false },
CopyFocus::Header => FieldKind::Checkbox,
};
(row, kind)
})
.collect()
}
fn focused(&self) -> CopyFocus {
self.focus
}
fn set_focused(&mut self, field: CopyFocus) {
self.focus = field;
}
}
impl Default for CopyModal {
fn default() -> Self {
Self {
focus: CopyFocus::Scope,
scope: CopyScope::default(),
format: CopyFormat::default(),
header_row: false,
header_view: true,
header_table: true,
column: None,
available_columns: Vec::new(),
picker: None,
attention: false,
context: CopyContext::default(),
}
}
}
impl CopyModal {
pub fn new() -> Self {
Self::default()
}
pub fn open(&mut self, columns: Vec<String>, current: Option<&str>, context: CopyContext) {
self.focus = CopyFocus::Scope;
self.picker = None;
self.attention = false;
self.column = current
.filter(|current| columns.iter().any(|c| c == current))
.map(str::to_string);
self.available_columns = columns;
self.context = context;
}
pub fn close(&mut self) {
self.picker = None;
}
pub const MOST_ROWS: u16 = 3;
pub fn row_order(&self) -> Vec<CopyFocus> {
match self.scope {
CopyScope::Cell => vec![CopyFocus::Scope, CopyFocus::Column],
CopyScope::Python => vec![CopyFocus::Scope],
_ => {
let mut order = vec![CopyFocus::Scope, CopyFocus::Format];
if self.format != CopyFormat::Markdown {
order.push(CopyFocus::Header);
}
order
}
}
}
pub fn step_scope(&mut self, delta: i8) {
self.scope = crate::app::form::step_value(&CopyScope::ALL, self.scope, delta);
if !self.row_order().contains(&self.focus) {
self.focus = CopyFocus::Scope;
}
}
pub fn step_format(&mut self, delta: i8) {
self.format = crate::app::form::step_value(&CopyFormat::ALL, self.format, delta);
if !self.row_order().contains(&self.focus) {
self.focus = CopyFocus::Format;
}
}
pub fn step_column(&mut self, delta: i8) {
let columns = &self.available_columns;
if columns.is_empty() {
return;
}
let at = self
.column
.as_ref()
.and_then(|c| columns.iter().position(|name| name == c));
let next = match at {
Some(at) => crate::app::form::step_index(at, columns.len(), delta),
None if delta < 0 => columns.len() - 1,
None => 0,
};
self.column = Some(columns[next].clone());
}
pub fn header(&self) -> bool {
match self.scope {
CopyScope::Cell | CopyScope::Python => false,
CopyScope::Row => self.header_row,
CopyScope::View => self.header_view,
CopyScope::Table => self.header_table,
}
}
pub fn toggle_header(&mut self) {
match self.scope {
CopyScope::Cell | CopyScope::Python => {}
CopyScope::Row => self.header_row = !self.header_row,
CopyScope::View => self.header_view = !self.header_view,
CopyScope::Table => self.header_table = !self.header_table,
}
}
pub fn is_picker_row(&self, focus: CopyFocus) -> bool {
focus == CopyFocus::Column
}
pub fn picker_items(&self) -> Vec<String> {
match self.focus {
CopyFocus::Column => self.available_columns.clone(),
CopyFocus::Scope | CopyFocus::Format | CopyFocus::Header => Vec::new(),
}
}
pub fn open_picker(&mut self) {
if !self.is_picker_row(self.focus) {
return;
}
let items = self.picker_items();
let current = self.column.as_deref();
let mut state = PickerState::new(items.clone());
if let Some(current) = current
&& let Some(i) = items.iter().position(|item| item == current)
{
state.select_original(i);
}
self.picker = Some(state);
}
pub fn picker_choose(&mut self) {
let Some(state) = self.picker.take() else {
return;
};
let Some(i) = state.selected_original() else {
return;
};
if self.focus == CopyFocus::Column {
self.column = self.available_columns.get(i).cloned();
}
}
pub fn validation_error(&self) -> Option<&'static str> {
match self.scope {
CopyScope::Cell if self.column.is_none() => Some("pick a column to copy"),
_ => None,
}
}
pub fn spec_line(&self) -> Result<String, String> {
let ctx = &self.context;
match self.scope {
CopyScope::Cell => match &self.column {
Some(column) => Ok(format!(
"Copy cell {column} of row {}",
thousands(ctx.row_number)
)),
None => Err("pick a column to copy".to_string()),
},
CopyScope::Row => Ok(format!(
"Copy row {} as {}",
thousands(ctx.row_number),
self.format.as_str()
)),
CopyScope::View => Ok(format!(
"Copy the {} {} {} view as {}",
thousands(ctx.view_rows),
crate::glyphs::get().times,
thousands(ctx.view_cols),
self.format.as_str()
)),
CopyScope::Table => match ctx.total_rows {
Some(total) => Ok(format!(
"Copy all {} rows as {}",
thousands(total),
self.format.as_str()
)),
None => Ok(format!("Copy every row as {}", self.format.as_str())),
},
CopyScope::Python => Ok("Copy the view as a Python (Polars) script".to_string()),
}
}
}
pub fn thousands(n: usize) -> String {
let digits = n.to_string();
let mut out = String::with_capacity(digits.len() + digits.len() / 3);
for (i, ch) in digits.chars().enumerate() {
if i > 0 && (digits.len() - i).is_multiple_of(3) {
out.push(',');
}
out.push(ch);
}
out
}
#[cfg(test)]
mod tests;