Skip to main content

kimun_notes/components/dialogs/
quick_note_modal.rs

1use std::sync::Arc;
2
3use kimun_core::NoteVault;
4use ratatui::Frame;
5use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6use ratatui::layout::{Constraint, Direction, Layout, Rect};
7use ratatui::style::Style;
8use ratatui::widgets::{Block, Borders, Clear, Paragraph};
9
10use crate::components::event_state::EventState;
11use crate::components::events::{AppEvent, AppTx};
12use crate::components::single_line_input::{InputOutcome, SingleLineInput};
13use crate::settings::themes::Theme;
14
15pub struct QuickNoteModal {
16    input: SingleLineInput,
17    vault: Arc<NoteVault>,
18    pub error: Option<String>,
19}
20
21impl QuickNoteModal {
22    pub fn new(vault: Arc<NoteVault>) -> Self {
23        Self {
24            input: SingleLineInput::new(),
25            vault,
26            error: None,
27        }
28    }
29
30    pub fn handle_key(&mut self, key: KeyEvent, tx: &AppTx) -> EventState {
31        // Enter — possibly with Shift to open the new note after creating it.
32        if let KeyCode::Enter = key.code {
33            if self.input.value().trim().is_empty() {
34                tx.send(AppEvent::CloseOverlay).ok();
35            } else {
36                self.submit(tx, key.modifiers.contains(KeyModifiers::SHIFT));
37            }
38            return EventState::Consumed;
39        }
40        match self.input.handle_key(&key) {
41            InputOutcome::Cancel => {
42                tx.send(AppEvent::CloseOverlay).ok();
43                EventState::Consumed
44            }
45            InputOutcome::Changed => {
46                self.error = None;
47                EventState::Consumed
48            }
49            InputOutcome::Consumed | InputOutcome::Submit => EventState::Consumed,
50            InputOutcome::NotConsumed => EventState::NotConsumed,
51        }
52    }
53
54    fn submit(&self, tx: &AppTx, open_after: bool) {
55        let text = self.input.value().to_string();
56        let vault = Arc::clone(&self.vault);
57        let tx_clone = tx.clone();
58        tokio::spawn(async move {
59            match vault.quick_note(&text).await {
60                Ok(details) => {
61                    if open_after {
62                        tx_clone.send(AppEvent::EntryCreated(details.path)).ok();
63                    } else {
64                        tx_clone.send(AppEvent::CloseOverlay).ok();
65                    }
66                }
67                Err(e) => {
68                    tx_clone.send(AppEvent::DialogError(e.to_string())).ok();
69                }
70            }
71        });
72    }
73
74    pub fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, _focused: bool) {
75        let height = if self.error.is_some() { 9 } else { 8 };
76        let popup_area = super::fixed_centered_rect(62, height, rect);
77
78        f.render_widget(Clear, popup_area);
79
80        let fg = theme.fg.to_ratatui();
81        let fg_muted = theme.fg_muted.to_ratatui();
82        let bg = theme.bg_panel.to_ratatui();
83
84        let outer_block = Block::default()
85            .title(" Quick Note ")
86            .borders(Borders::ALL)
87            .border_style(Style::default().fg(theme.border_focused.to_ratatui()))
88            .style(theme.panel_style());
89        let inner = outer_block.inner(popup_area);
90        f.render_widget(outer_block, popup_area);
91
92        let rows = Layout::default()
93            .direction(Direction::Vertical)
94            .constraints([
95                Constraint::Length(1), // 0: spacer
96                Constraint::Length(1), // 1: input
97                Constraint::Length(1), // 2: separator
98                Constraint::Length(1), // 3: hint line 1
99                Constraint::Length(1), // 4: hint line 2
100                Constraint::Length(1), // 5: error (optional)
101                Constraint::Min(0),    // 6: remainder
102            ])
103            .split(inner);
104
105        if self.input.is_empty() {
106            // Placeholder text + caret in muted style.
107            f.render_widget(
108                Paragraph::new("  Type your thought...")
109                    .style(Style::default().fg(fg_muted).bg(bg)),
110                rows[1],
111            );
112            f.set_cursor_position((rows[1].x + 2, rows[1].y));
113        } else {
114            // 2-space indent matches the placeholder above.
115            self.input
116                .render(f, rows[1], Style::default().fg(fg).bg(bg), 2, true);
117        }
118
119        super::render_separator(f, rows[2], fg_muted, bg);
120
121        f.render_widget(
122            Paragraph::new("  [Enter] Save  [Shift+Enter] Save & Open")
123                .style(Style::default().fg(fg_muted).bg(bg)),
124            rows[3],
125        );
126        f.render_widget(
127            Paragraph::new("  [Esc] Cancel").style(Style::default().fg(fg_muted).bg(bg)),
128            rows[4],
129        );
130
131        if let Some(msg) = &self.error {
132            super::render_error_row(f, rows[5], msg, bg);
133        }
134    }
135}