Skip to main content

datui_lib/widgets/
copy.rs

1//! The copy dialog: a FormView of its axes, the spec line saying what Enter will do.
2
3use crate::app::modals::copy_modal::{CopyFocus, CopyModal};
4use crate::render::context::RenderContext;
5use crate::widgets::ui::{FormLine, FormValue, FormView, HintBar};
6use datui_cli::keys::Context;
7use ratatui::buffer::Buffer;
8use ratatui::layout::Rect;
9use ratatui::style::Style;
10
11/// Past the longest label, "Format:", plus air.
12const LABEL_WIDTH: u16 = 9;
13
14fn row_label(focus: CopyFocus) -> &'static str {
15    match focus {
16        CopyFocus::Scope => "Scope:",
17        CopyFocus::Column => "Column:",
18        CopyFocus::Format => "Format:",
19        CopyFocus::Header => "Header:",
20    }
21}
22
23pub fn render_copy_modal(area: Rect, buf: &mut Buffer, modal: &mut CopyModal, ctx: &RenderContext) {
24    let footer = HintBar::from_ctx(ctx)
25        .screen(Context::Copy)
26        .group("Form")
27        .key("Enter")
28        .weight(3);
29    let footer = match modal.focus {
30        CopyFocus::Header => footer.key_as("Space", "Toggle"),
31        CopyFocus::Column => footer.key_as("Space", "Pick"),
32        CopyFocus::Scope | CopyFocus::Format => footer.key("← / →"),
33    };
34    let footer = footer.weight(2).key("Tab").weight(1).key("Esc").weight(4);
35    let rows = modal
36        .row_order()
37        .into_iter()
38        .map(|row| {
39            let value = match row {
40                CopyFocus::Scope => FormValue::Choice(modal.scope.as_str()),
41                CopyFocus::Format => FormValue::Choice(modal.format.as_str()),
42                CopyFocus::Header => FormValue::Toggle(modal.header()),
43                CopyFocus::Column => match modal.column.as_deref() {
44                    Some(column) => FormValue::Choice(column),
45                    None => FormValue::Placeholder("none"),
46                },
47            };
48            FormLine::Field(row, row_label(row), value)
49        })
50        .collect();
51    // What Enter will do, echoed live; the gap re-accents when Enter hit it.
52    let status = match modal.spec_line() {
53        Ok(line) => (line, Style::default().fg(ctx.text_primary)),
54        Err(gap) if modal.attention => (gap, Style::default().fg(ctx.warning)),
55        Err(gap) => (gap, Style::default().fg(ctx.dimmed)),
56    };
57    FormView {
58        title: "Copy",
59        screen: Context::Copy,
60        footer: Some(footer),
61        label_width: LABEL_WIDTH,
62        rows,
63        focused: Some(modal.focus),
64        picker: modal.picker.as_ref(),
65        status: Some(status),
66        shields: true,
67    }
68    .render::<CopyModal>(area, buf, ctx);
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::app::modals::copy_modal::{CopyContext, CopyScope};
75
76    fn render_to_text(modal: &mut CopyModal) -> String {
77        let area = Rect::new(0, 0, 46, 14);
78        let mut buf = Buffer::empty(area);
79        let ctx = RenderContext::for_test();
80        render_copy_modal(area, &mut buf, modal, &ctx);
81        crate::tests::buffer_text(&buf)
82    }
83
84    #[test]
85    fn the_dialog_echoes_every_choice_and_the_spec() {
86        let mut modal = CopyModal::new();
87        modal.open(
88            vec!["city".into(), "pop".into()],
89            None,
90            CopyContext {
91                row_number: 1235,
92                view_rows: 42,
93                view_cols: 8,
94                total_rows: Some(1_000_000),
95            },
96        );
97        let text = render_to_text(&mut modal);
98        assert!(text.contains("Scope:"), "{text}");
99        assert!(text.contains("Row"), "{text}");
100        assert!(text.contains("Format:"), "{text}");
101        assert!(text.contains("Copy row 1,235 as TSV"), "{text}");
102    }
103
104    #[test]
105    fn the_python_scope_shows_only_the_scope_row() {
106        let mut modal = CopyModal::new();
107        modal.open(vec!["city".into()], None, CopyContext::default());
108        modal.scope = CopyScope::Python;
109        let text = render_to_text(&mut modal);
110        assert!(text.contains("Python (Polars)"), "{text}");
111        assert!(!text.contains("Format:"), "{text}");
112        assert!(!text.contains("Header:"), "{text}");
113        assert!(
114            text.contains("Copy the view as a Python (Polars) script"),
115            "{text}"
116        );
117    }
118
119    #[test]
120    fn the_open_picker_lists_and_narrows() {
121        let mut modal = CopyModal::new();
122        modal.open(
123            vec!["city".into(), "population".into()],
124            None,
125            CopyContext::default(),
126        );
127        modal.scope = CopyScope::Cell;
128        modal.focus = CopyFocus::Column;
129        modal.open_picker();
130        let text = render_to_text(&mut modal);
131        assert!(text.contains("city"), "{text}");
132        assert!(text.contains("population"), "{text}");
133        modal
134            .picker
135            .as_mut()
136            .unwrap()
137            .filter_key('p', crossterm::event::KeyModifiers::NONE);
138        let text = render_to_text(&mut modal);
139        assert!(text.contains("population"), "{text}");
140        assert!(!text.contains("\n city"), "{text}");
141    }
142}