Skip to main content

basalt_tui/note_editor/
mod.rs

1pub mod ast;
2mod cursor;
3pub mod editor;
4pub mod parser;
5mod render;
6mod rich_text;
7pub mod state;
8mod text_buffer;
9mod text_wrap;
10mod viewport;
11mod virtual_document;
12
13use std::time::Duration;
14
15use ratatui::{
16    crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
17    layout::Size,
18};
19
20use crate::{
21    app::{calc_scroll_amount, ActivePane, Message as AppMessage, ScrollAmount},
22    explorer,
23    note_editor::state::{EditMode, NoteEditorState, View},
24    outline, toast,
25};
26
27#[derive(Clone, Debug, PartialEq)]
28pub enum Message {
29    Save,
30    SwitchPaneNext,
31    SwitchPanePrevious,
32    ToggleExplorer,
33    ToggleOutline,
34    ToggleView,
35    EditView,
36    ReadView,
37    Exit,
38    KeyEvent(KeyEvent),
39    CursorUp,
40    CursorLeft,
41    CursorRight,
42    CursorWordForward,
43    CursorWordBackward,
44    CursorDown,
45    ScrollUp(ScrollAmount),
46    ScrollDown(ScrollAmount),
47    ScrollToTop,
48    ScrollToBottom,
49    JumpToBlock(usize),
50    Delete,
51    InsertMode,
52}
53
54// FIXME: Add resize message to handle resize related updates like cursor positioning
55pub fn update<'a>(
56    message: Message,
57    screen_size: Size,
58    state: &mut NoteEditorState,
59) -> Option<AppMessage<'a>> {
60    let vim_mode = state.vim_mode();
61    match message {
62        Message::CursorLeft => state.cursor_left(1),
63        Message::CursorRight => state.cursor_right(1),
64        Message::JumpToBlock(idx) => state.cursor_jump(idx),
65        Message::CursorUp => {
66            state.cursor_up(1);
67            return Some(AppMessage::Outline(outline::Message::SelectAt(
68                state.current_block(),
69            )));
70        }
71        Message::CursorDown => {
72            state.cursor_down(1);
73            return Some(AppMessage::Outline(outline::Message::SelectAt(
74                state.current_block(),
75            )));
76        }
77        Message::ScrollUp(scroll_amount) => {
78            state.cursor_up(calc_scroll_amount(
79                &scroll_amount,
80                screen_size.height.into(),
81            ));
82            return Some(AppMessage::Outline(outline::Message::SelectAt(
83                state.current_block(),
84            )));
85        }
86        Message::ScrollDown(scroll_amount) => {
87            state.cursor_down(calc_scroll_amount(
88                &scroll_amount,
89                screen_size.height.into(),
90            ));
91            return Some(AppMessage::Outline(outline::Message::SelectAt(
92                state.current_block(),
93            )));
94        }
95        Message::ScrollToTop => {
96            state.cursor_up(usize::MAX);
97            return Some(AppMessage::Outline(outline::Message::SelectAt(
98                state.current_block(),
99            )));
100        }
101        Message::ScrollToBottom => {
102            state.cursor_to_end();
103            return Some(AppMessage::Outline(outline::Message::SelectAt(
104                state.current_block(),
105            )));
106        }
107        _ => {}
108    };
109
110    match state.view {
111        View::Edit(..) if state.insert_mode() => match message {
112            Message::CursorWordForward => state.cursor_word_forward(),
113            Message::CursorWordBackward => state.cursor_word_backward(),
114            Message::ToggleView | Message::ReadView => {
115                state.set_insert_mode(false);
116                state.exit_insert();
117                state.set_view(View::Read);
118                return Some(AppMessage::UpdateSelectedNoteContent((
119                    state.content.to_string(),
120                    Some(state.ast_nodes.clone()),
121                )));
122            }
123            Message::KeyEvent(key) => {
124                match key.code {
125                    KeyCode::Char(c) => {
126                        state.insert_char(c);
127                    }
128                    KeyCode::Enter => {
129                        state.insert_char('\n');
130                    }
131                    _ => {}
132                }
133
134                return Some(AppMessage::UpdateSelectedNoteContent((
135                    state.content.to_string(),
136                    None,
137                )));
138            }
139            Message::Delete => {
140                state.delete_char();
141            }
142            Message::Exit => {
143                state.set_insert_mode(false);
144                state.exit_insert();
145                if !vim_mode {
146                    state.set_view(View::Read);
147                }
148                return Some(AppMessage::UpdateSelectedNoteContent((
149                    state.content.to_string(),
150                    Some(state.ast_nodes.clone()),
151                )));
152            }
153            _ => {}
154        },
155        View::Edit(..) => match message {
156            // Normal mode (vim): navigation and read-mode equivalents
157            Message::CursorWordForward => state.cursor_word_forward(),
158            Message::CursorWordBackward => state.cursor_word_backward(),
159            Message::InsertMode | Message::EditView => state.set_insert_mode(true),
160            Message::ToggleView | Message::ReadView => {
161                state.exit_insert();
162                state.set_view(View::Read);
163                return Some(AppMessage::UpdateSelectedNoteContent((
164                    state.content.to_string(),
165                    Some(state.ast_nodes.clone()),
166                )));
167            }
168            Message::ToggleExplorer => {
169                return Some(AppMessage::Explorer(explorer::Message::Toggle));
170            }
171            Message::ToggleOutline => {
172                return Some(AppMessage::Outline(outline::Message::Toggle));
173            }
174            Message::SwitchPaneNext => {
175                state.set_active(false);
176                return Some(AppMessage::SetActivePane(ActivePane::Outline));
177            }
178            Message::SwitchPanePrevious => {
179                state.set_active(false);
180                return Some(AppMessage::SetActivePane(ActivePane::Explorer));
181            }
182            Message::Save => {
183                let modified = state.modified();
184                match state.save_to_file() {
185                    Ok(_) if modified => {
186                        return Some(AppMessage::Batch(vec![
187                            AppMessage::UpdateSelectedNoteContent((
188                                state.content.to_string(),
189                                None,
190                            )),
191                            AppMessage::Toast(toast::Message::Create(toast::Toast::success(
192                                "File saved",
193                                Duration::from_secs(2),
194                            ))),
195                        ]))
196                    }
197                    Err(_) => {
198                        return Some(AppMessage::Toast(toast::Message::Create(
199                            toast::Toast::error("Failed to save file", Duration::from_secs(2)),
200                        )))
201                    }
202                    _ => {}
203                }
204            }
205            _ => {}
206        },
207        View::Read => match message {
208            Message::ToggleView if state.editor_enabled() => {
209                state.set_view(View::Edit(EditMode::Source))
210            }
211            Message::EditView | Message::InsertMode if state.editor_enabled() => {
212                state.set_view(View::Edit(EditMode::Source));
213                state.set_insert_mode(true);
214            }
215            Message::ReadView => state.set_view(View::Read),
216            Message::ToggleExplorer => {
217                return Some(AppMessage::Explorer(explorer::Message::Toggle));
218            }
219            Message::ToggleOutline => {
220                return Some(AppMessage::Outline(outline::Message::Toggle));
221            }
222            Message::SwitchPaneNext => {
223                state.set_active(false);
224                return Some(AppMessage::SetActivePane(ActivePane::Outline));
225            }
226            Message::SwitchPanePrevious => {
227                state.set_active(false);
228                return Some(AppMessage::SetActivePane(ActivePane::Explorer));
229            }
230            Message::Save => {
231                let modified = state.modified();
232                match state.save_to_file() {
233                    Ok(_) if modified => {
234                        return Some(AppMessage::Batch(vec![
235                            AppMessage::UpdateSelectedNoteContent((
236                                state.content.to_string(),
237                                None,
238                            )),
239                            AppMessage::Toast(toast::Message::Create(toast::Toast::success(
240                                "File saved",
241                                Duration::from_secs(2),
242                            ))),
243                        ]))
244                    }
245                    Err(_) => {
246                        return Some(AppMessage::Toast(toast::Message::Create(
247                            toast::Toast::error("Failed to save file", Duration::from_secs(2)),
248                        )))
249                    }
250                    _ => {}
251                }
252            }
253            _ => {}
254        },
255    }
256
257    None
258}
259
260pub fn handle_editing_event(key: KeyEvent) -> Option<Message> {
261    match key.code {
262        KeyCode::Up => Some(Message::CursorUp),
263        KeyCode::Down => Some(Message::CursorDown),
264        KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::ALT) => {
265            Some(Message::CursorWordForward)
266        }
267        KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::ALT) => {
268            Some(Message::CursorWordBackward)
269        }
270        KeyCode::Left => Some(Message::CursorLeft),
271        KeyCode::Right => Some(Message::CursorRight),
272        KeyCode::Esc => Some(Message::Exit),
273        KeyCode::Backspace => Some(Message::Delete),
274        KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
275            Some(Message::ToggleView)
276        }
277        _ => Some(Message::KeyEvent(key)),
278    }
279}