kimun_notes/components/dialogs/
quick_note_modal.rs1use std::sync::Arc;
2
3use kimun_core::NoteVault;
4use ratatui::Frame;
5use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
6use ratatui::layout::{Constraint, Direction, Layout, Rect};
7use ratatui::style::Style;
8use ratatui::widgets::Paragraph;
9
10use crate::components::event_state::EventState;
11use crate::components::events::{AppEvent, AppTx, FileOp, OverlayData};
12use crate::components::hint_row::HintRow;
13use crate::components::panel::{ModalSpec, modal_chrome};
14use crate::components::single_line_input::{InputOutcome, SingleLineInput};
15use crate::settings::themes::Theme;
16
17pub struct QuickNoteModal {
18 input: SingleLineInput,
19 vault: Arc<NoteVault>,
20 pub error: Option<String>,
21 save_hints: HintRow,
22 cancel_hint: HintRow,
23}
24
25impl QuickNoteModal {
26 pub fn new(vault: Arc<NoteVault>) -> Self {
27 Self {
28 input: SingleLineInput::new(),
29 vault,
30 error: None,
31 save_hints: HintRow::new(&[
32 (KeyCode::Enter, "Enter", "Save"),
33 (KeyCode::Enter, "Shift+Enter", "Save & Open"),
34 ])
35 .with_modifiers(1, KeyModifiers::SHIFT),
36 cancel_hint: HintRow::new(&[(KeyCode::Esc, "Esc", "Cancel")]),
37 }
38 }
39
40 pub fn handle_mouse(&mut self, m: &MouseEvent, tx: &AppTx) -> EventState {
43 if let Some(key) = self.save_hints.hit(m).or_else(|| self.cancel_hint.hit(m)) {
44 self.handle_key(key, tx);
45 }
46 EventState::Consumed
47 }
48
49 pub fn handle_key(&mut self, key: KeyEvent, tx: &AppTx) -> EventState {
50 if let KeyCode::Enter = key.code {
52 if self.input.value().trim().is_empty() {
53 tx.send(AppEvent::CloseOverlay).ok();
54 } else {
55 self.submit(tx, key.modifiers.contains(KeyModifiers::SHIFT));
56 }
57 return EventState::Consumed;
58 }
59 match self.input.handle_key(&key) {
60 InputOutcome::Cancel => {
61 tx.send(AppEvent::CloseOverlay).ok();
62 EventState::Consumed
63 }
64 InputOutcome::Changed => {
65 self.error = None;
66 EventState::Consumed
67 }
68 InputOutcome::Consumed | InputOutcome::Submit => EventState::Consumed,
69 InputOutcome::NotConsumed => EventState::NotConsumed,
70 }
71 }
72
73 fn submit(&self, tx: &AppTx, open_after: bool) {
74 let text = self.input.value().to_string();
75 let vault = Arc::clone(&self.vault);
76 let tx_clone = tx.clone();
77 tokio::spawn(async move {
78 match vault.quick_note(&text).await {
79 Ok(details) => {
80 tx_clone
83 .send(AppEvent::FileOp(FileOp::Created(details.path.clone())))
84 .ok();
85 if open_after {
86 tx_clone.send(AppEvent::open(details.path)).ok();
87 } else {
88 tx_clone.send(AppEvent::CloseOverlay).ok();
89 }
90 }
91 Err(e) => {
92 tx_clone
93 .send(AppEvent::OverlayData(OverlayData::Error(e.to_string())))
94 .ok();
95 }
96 }
97 });
98 }
99
100 pub fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, _focused: bool) {
101 let height = if self.error.is_some() { 9 } else { 8 };
102 let popup_area = super::fixed_centered_rect(62, height, rect);
103
104 let fg = theme.fg.to_ratatui();
105 let gray = theme.gray.to_ratatui();
106 let bg = theme.bg_panel.to_ratatui();
107
108 let inner = modal_chrome(
109 f,
110 popup_area,
111 theme,
112 ModalSpec {
113 title: Some(" Quick Note "),
114 border: Some(Style::default().fg(theme.focus_border.to_ratatui())),
115 ..Default::default()
116 },
117 );
118
119 let rows = Layout::default()
120 .direction(Direction::Vertical)
121 .constraints([
122 Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(0), ])
130 .split(inner);
131
132 if self.input.is_empty() {
133 f.render_widget(
135 Paragraph::new(" Type your thought...").style(Style::default().fg(gray).bg(bg)),
136 rows[1],
137 );
138 f.set_cursor_position((rows[1].x + 2, rows[1].y));
139 } else {
140 self.input
142 .render(f, rows[1], Style::default().fg(fg).bg(bg), 2, true);
143 }
144
145 super::render_separator(f, rows[2], gray, bg);
146
147 let hint_style = Style::default().fg(gray).bg(bg);
148 self.save_hints.render(f, rows[3], hint_style, theme);
149 self.cancel_hint.render(f, rows[4], hint_style, theme);
150
151 if let Some(msg) = &self.error {
152 super::render_error_row(f, rows[5], msg, theme);
153 }
154 }
155}