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::{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 if let KeyCode::Enter = key.code {
33 if self.input.value().trim().is_empty() {
34 tx.send(AppEvent::CloseDialog).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::CloseDialog).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::CloseDialog).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), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(0), ])
103 .split(inner);
104
105 if self.input.is_empty() {
106 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 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}