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};
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};
12use crate::components::panel::{ModalSpec, modal_chrome};
13use crate::components::single_line_input::{InputOutcome, SingleLineInput};
14use crate::settings::themes::Theme;
15
16pub struct QuickNoteModal {
17 input: SingleLineInput,
18 vault: Arc<NoteVault>,
19 pub error: Option<String>,
20}
21
22impl QuickNoteModal {
23 pub fn new(vault: Arc<NoteVault>) -> Self {
24 Self {
25 input: SingleLineInput::new(),
26 vault,
27 error: None,
28 }
29 }
30
31 pub fn handle_key(&mut self, key: KeyEvent, tx: &AppTx) -> EventState {
32 if let KeyCode::Enter = key.code {
34 if self.input.value().trim().is_empty() {
35 tx.send(AppEvent::CloseOverlay).ok();
36 } else {
37 self.submit(tx, key.modifiers.contains(KeyModifiers::SHIFT));
38 }
39 return EventState::Consumed;
40 }
41 match self.input.handle_key(&key) {
42 InputOutcome::Cancel => {
43 tx.send(AppEvent::CloseOverlay).ok();
44 EventState::Consumed
45 }
46 InputOutcome::Changed => {
47 self.error = None;
48 EventState::Consumed
49 }
50 InputOutcome::Consumed | InputOutcome::Submit => EventState::Consumed,
51 InputOutcome::NotConsumed => EventState::NotConsumed,
52 }
53 }
54
55 fn submit(&self, tx: &AppTx, open_after: bool) {
56 let text = self.input.value().to_string();
57 let vault = Arc::clone(&self.vault);
58 let tx_clone = tx.clone();
59 tokio::spawn(async move {
60 match vault.quick_note(&text).await {
61 Ok(details) => {
62 tx_clone
65 .send(AppEvent::EntryCreated(details.path.clone()))
66 .ok();
67 if open_after {
68 tx_clone.send(AppEvent::open(details.path)).ok();
69 } else {
70 tx_clone.send(AppEvent::CloseOverlay).ok();
71 }
72 }
73 Err(e) => {
74 tx_clone.send(AppEvent::DialogError(e.to_string())).ok();
75 }
76 }
77 });
78 }
79
80 pub fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, _focused: bool) {
81 let height = if self.error.is_some() { 9 } else { 8 };
82 let popup_area = super::fixed_centered_rect(62, height, rect);
83
84 let fg = theme.fg.to_ratatui();
85 let gray = theme.gray.to_ratatui();
86 let bg = theme.bg_panel.to_ratatui();
87
88 let inner = modal_chrome(
89 f,
90 popup_area,
91 theme,
92 ModalSpec {
93 title: Some(" Quick Note "),
94 border: Some(Style::default().fg(theme.focus_border.to_ratatui())),
95 ..Default::default()
96 },
97 );
98
99 let rows = Layout::default()
100 .direction(Direction::Vertical)
101 .constraints([
102 Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(0), ])
110 .split(inner);
111
112 if self.input.is_empty() {
113 f.render_widget(
115 Paragraph::new(" Type your thought...").style(Style::default().fg(gray).bg(bg)),
116 rows[1],
117 );
118 f.set_cursor_position((rows[1].x + 2, rows[1].y));
119 } else {
120 self.input
122 .render(f, rows[1], Style::default().fg(fg).bg(bg), 2, true);
123 }
124
125 super::render_separator(f, rows[2], gray, bg);
126
127 f.render_widget(
128 Paragraph::new(" [Enter] Save [Shift+Enter] Save & Open")
129 .style(Style::default().fg(gray).bg(bg)),
130 rows[3],
131 );
132 f.render_widget(
133 Paragraph::new(" [Esc] Cancel").style(Style::default().fg(gray).bg(bg)),
134 rows[4],
135 );
136
137 if let Some(msg) = &self.error {
138 super::render_error_row(f, rows[5], msg, theme);
139 }
140 }
141}