1use crate::clipboard::CopyFormat;
7use crate::widgets::ui::PickerState;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum CopyScope {
12 Cell,
13 #[default]
14 Row,
15 View,
16 Table,
17 Python,
19}
20
21impl CopyScope {
22 pub const ALL: [Self; 5] = [Self::Cell, Self::Row, Self::View, Self::Table, Self::Python];
23
24 pub fn as_str(self) -> &'static str {
25 match self {
26 Self::Cell => "Cell",
27 Self::Row => "Row",
28 Self::View => "View",
29 Self::Table => "Table",
30 Self::Python => "Python (Polars)",
31 }
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum CopyFocus {
37 #[default]
38 Scope,
39 Column,
40 Format,
41 Header,
42}
43
44#[derive(Debug, Clone, Copy, Default)]
47pub struct CopyContext {
48 pub row_number: usize,
50 pub view_rows: usize,
52 pub view_cols: usize,
53 pub total_rows: Option<usize>,
55}
56
57pub struct CopyModal {
58 pub focus: CopyFocus,
59 pub scope: CopyScope,
60 pub format: CopyFormat,
61 pub header_row: bool,
65 pub header_view: bool,
66 pub header_table: bool,
67 pub column: Option<String>,
69 pub available_columns: Vec<String>,
70 pub picker: Option<PickerState>,
72 pub attention: bool,
75 pub context: CopyContext,
76}
77
78impl crate::app::form::Form for CopyModal {
79 type Field = CopyFocus;
80
81 fn shown_picker(&mut self) -> Option<(&mut crate::widgets::ui::PickerState, bool)> {
82 self.picker.as_mut().map(|p| (p, false))
83 }
84
85 fn dismiss_picker(&mut self) {
86 self.picker = None;
87 }
88
89 fn pick(&mut self, _toggle: bool) {
90 self.picker_choose();
91 }
92
93 fn fields(&self) -> Vec<(CopyFocus, crate::app::form::FieldKind)> {
94 use crate::app::form::FieldKind;
95 self.row_order()
96 .into_iter()
97 .map(|row| {
98 let kind = match row {
99 CopyFocus::Scope | CopyFocus::Format => FieldKind::Choice,
100 CopyFocus::Column => FieldKind::Picker { multi: false },
101 CopyFocus::Header => FieldKind::Checkbox,
102 };
103 (row, kind)
104 })
105 .collect()
106 }
107
108 fn focused(&self) -> CopyFocus {
109 self.focus
110 }
111
112 fn set_focused(&mut self, field: CopyFocus) {
113 self.focus = field;
114 }
115}
116
117impl Default for CopyModal {
118 fn default() -> Self {
119 Self {
120 focus: CopyFocus::Scope,
121 scope: CopyScope::default(),
122 format: CopyFormat::default(),
123 header_row: false,
124 header_view: true,
125 header_table: true,
126 column: None,
127 available_columns: Vec::new(),
128 picker: None,
129 attention: false,
130 context: CopyContext::default(),
131 }
132 }
133}
134
135impl CopyModal {
136 pub fn new() -> Self {
137 Self::default()
138 }
139
140 pub fn open(&mut self, columns: Vec<String>, current: Option<&str>, context: CopyContext) {
143 self.focus = CopyFocus::Scope;
144 self.picker = None;
145 self.attention = false;
146 self.column = current
147 .filter(|current| columns.iter().any(|c| c == current))
148 .map(str::to_string);
149 self.available_columns = columns;
150 self.context = context;
151 }
152
153 pub fn close(&mut self) {
154 self.picker = None;
155 }
156
157 pub const MOST_ROWS: u16 = 3;
159
160 pub fn row_order(&self) -> Vec<CopyFocus> {
164 match self.scope {
165 CopyScope::Cell => vec![CopyFocus::Scope, CopyFocus::Column],
166 CopyScope::Python => vec![CopyFocus::Scope],
168 _ => {
169 let mut order = vec![CopyFocus::Scope, CopyFocus::Format];
170 if self.format != CopyFormat::Markdown {
171 order.push(CopyFocus::Header);
172 }
173 order
174 }
175 }
176 }
177
178 pub fn step_scope(&mut self, delta: i8) {
181 self.scope = crate::app::form::step_value(&CopyScope::ALL, self.scope, delta);
182 if !self.row_order().contains(&self.focus) {
183 self.focus = CopyFocus::Scope;
184 }
185 }
186
187 pub fn step_format(&mut self, delta: i8) {
189 self.format = crate::app::form::step_value(&CopyFormat::ALL, self.format, delta);
190 if !self.row_order().contains(&self.focus) {
191 self.focus = CopyFocus::Format;
192 }
193 }
194
195 pub fn step_column(&mut self, delta: i8) {
197 let columns = &self.available_columns;
198 if columns.is_empty() {
199 return;
200 }
201 let at = self
202 .column
203 .as_ref()
204 .and_then(|c| columns.iter().position(|name| name == c));
205 let next = match at {
206 Some(at) => crate::app::form::step_index(at, columns.len(), delta),
207 None if delta < 0 => columns.len() - 1,
208 None => 0,
209 };
210 self.column = Some(columns[next].clone());
211 }
212
213 pub fn header(&self) -> bool {
215 match self.scope {
216 CopyScope::Cell | CopyScope::Python => false,
217 CopyScope::Row => self.header_row,
218 CopyScope::View => self.header_view,
219 CopyScope::Table => self.header_table,
220 }
221 }
222
223 pub fn toggle_header(&mut self) {
224 match self.scope {
225 CopyScope::Cell | CopyScope::Python => {}
226 CopyScope::Row => self.header_row = !self.header_row,
227 CopyScope::View => self.header_view = !self.header_view,
228 CopyScope::Table => self.header_table = !self.header_table,
229 }
230 }
231
232 pub fn is_picker_row(&self, focus: CopyFocus) -> bool {
235 focus == CopyFocus::Column
236 }
237
238 pub fn picker_items(&self) -> Vec<String> {
240 match self.focus {
241 CopyFocus::Column => self.available_columns.clone(),
242 CopyFocus::Scope | CopyFocus::Format | CopyFocus::Header => Vec::new(),
243 }
244 }
245
246 pub fn open_picker(&mut self) {
248 if !self.is_picker_row(self.focus) {
249 return;
250 }
251 let items = self.picker_items();
252 let current = self.column.as_deref();
253 let mut state = PickerState::new(items.clone());
254 if let Some(current) = current
255 && let Some(i) = items.iter().position(|item| item == current)
256 {
257 state.select_original(i);
258 }
259 self.picker = Some(state);
260 }
261
262 pub fn picker_choose(&mut self) {
264 let Some(state) = self.picker.take() else {
265 return;
266 };
267 let Some(i) = state.selected_original() else {
268 return;
269 };
270 if self.focus == CopyFocus::Column {
271 self.column = self.available_columns.get(i).cloned();
272 }
273 }
274
275 pub fn validation_error(&self) -> Option<&'static str> {
277 match self.scope {
278 CopyScope::Cell if self.column.is_none() => Some("pick a column to copy"),
279 _ => None,
280 }
281 }
282
283 pub fn spec_line(&self) -> Result<String, String> {
285 let ctx = &self.context;
286 match self.scope {
287 CopyScope::Cell => match &self.column {
288 Some(column) => Ok(format!(
289 "Copy cell {column} of row {}",
290 thousands(ctx.row_number)
291 )),
292 None => Err("pick a column to copy".to_string()),
293 },
294 CopyScope::Row => Ok(format!(
295 "Copy row {} as {}",
296 thousands(ctx.row_number),
297 self.format.as_str()
298 )),
299 CopyScope::View => Ok(format!(
300 "Copy the {} {} {} view as {}",
301 thousands(ctx.view_rows),
302 crate::glyphs::get().times,
303 thousands(ctx.view_cols),
304 self.format.as_str()
305 )),
306 CopyScope::Table => match ctx.total_rows {
307 Some(total) => Ok(format!(
308 "Copy all {} rows as {}",
309 thousands(total),
310 self.format.as_str()
311 )),
312 None => Ok(format!("Copy every row as {}", self.format.as_str())),
313 },
314 CopyScope::Python => Ok("Copy the view as a Python (Polars) script".to_string()),
315 }
316 }
317}
318
319pub fn thousands(n: usize) -> String {
321 let digits = n.to_string();
322 let mut out = String::with_capacity(digits.len() + digits.len() / 3);
323 for (i, ch) in digits.chars().enumerate() {
324 if i > 0 && (digits.len() - i).is_multiple_of(3) {
325 out.push(',');
326 }
327 out.push(ch);
328 }
329 out
330}
331
332#[cfg(test)]
333mod tests;