1use basalt_core::obsidian::{self, create_untitled_dir, create_untitled_note, Note, Vault};
2use ratatui::{
3 buffer::Buffer,
4 crossterm::event::{self, Event, KeyEvent, KeyEventKind},
5 layout::{Constraint, Flex, Layout, Rect, Size},
6 widgets::{StatefulWidget, Widget},
7 DefaultTerminal,
8};
9use tracing::{debug, error, info, warn};
10
11use std::{
12 cell::RefCell,
13 fmt::Debug,
14 fs,
15 io::Result,
16 path::{Path, PathBuf},
17 time::{Duration, Instant},
18};
19
20use crate::{
21 command,
22 config::{self, Config, Keystroke},
23 debug_log::{self, DebugLogModal, DebugLogModalState, LogLevel},
24 explorer::{self, Explorer, ExplorerState, Item, Visibility},
25 help_modal::{self, HelpModal, HelpModalState},
26 input::{self, Input, InputModalState},
27 note_editor::{
28 self, ast,
29 editor::NoteEditor,
30 state::{EditMode, NoteEditorState, View},
31 },
32 outline::{self, Outline, OutlineState},
33 splash_modal::{self, SplashModal, SplashModalState},
34 statusbar::{StatusBar, StatusBarState},
35 stylized_text::{self, FontStyle},
36 text_counts::{CharCount, WordCount},
37 toast::{self, Toast, TOAST_WIDTH},
38 vault_selector_modal::{self, VaultSelectorModal, VaultSelectorModalState},
39 vault_watcher::VaultWatcher,
40};
41
42const VERSION: &str = env!("CARGO_PKG_VERSION");
43
44const HELP_TEXT: &str = include_str!("./help.txt");
45
46#[derive(Debug, Default, Clone, PartialEq)]
47pub enum ScrollAmount {
48 #[default]
49 One,
50 HalfPage,
51}
52
53pub fn calc_scroll_amount(scroll_amount: &ScrollAmount, height: usize) -> usize {
54 match scroll_amount {
55 ScrollAmount::One => 1,
56 ScrollAmount::HalfPage => height / 2,
57 }
58}
59
60#[derive(Default, Clone)]
61pub struct AppState<'a> {
62 vault: Vault,
63 screen_size: Size,
64 is_running: bool,
65 pending_keys: Vec<Keystroke>,
66
67 active_pane: ActivePane,
68 explorer: ExplorerState,
69 note_editor: NoteEditorState<'a>,
70 outline: OutlineState,
71 selected_note: Option<SelectedNote>,
72 toasts: Vec<Toast>,
73
74 input_modal: InputModalState,
75 splash_modal: SplashModalState<'a>,
76 help_modal: HelpModalState,
77 vault_selector_modal: VaultSelectorModalState<'a>,
78 debug_log_modal: DebugLogModalState,
79}
80
81impl<'a> AppState<'a> {
82 pub fn vault(&self) -> &Vault {
83 &self.vault
84 }
85
86 pub fn active_component(&self) -> ActivePane {
87 if self.debug_log_modal.visible {
88 return ActivePane::DebugLogModal;
89 }
90
91 if self.help_modal.visible {
92 return ActivePane::HelpModal;
93 }
94
95 if self.vault_selector_modal.visible {
96 return ActivePane::VaultSelectorModal;
97 }
98
99 if self.splash_modal.visible {
100 return ActivePane::Splash;
101 }
102
103 self.active_pane
104 }
105
106 pub fn set_running(&self, is_running: bool) -> Self {
107 Self {
108 is_running,
109 ..self.clone()
110 }
111 }
112}
113
114#[derive(Clone, Debug, PartialEq)]
115pub enum Message<'a> {
116 Quit,
117 Exec(String),
118 Spawn(String),
119 Resize(Size),
120 SetActivePane(ActivePane),
121 RefreshVault {
122 rename: Option<(PathBuf, PathBuf)>,
123 select: Option<PathBuf>,
124 },
125 RescanVault,
126 CreateUntitledNote,
127 CreateUntitledFolder,
128 OpenVault(&'a Vault),
129 SelectNote(SelectedNote),
130 UpdateSelectedNoteContent((String, Option<Vec<ast::Node>>)),
131
132 Batch(Vec<Message<'a>>),
133 Toast(toast::Message),
134 Input(input::Message),
135 Splash(splash_modal::Message),
136 Explorer(explorer::Message),
137 NoteEditor(note_editor::Message),
138 Outline(outline::Message),
139 HelpModal(help_modal::Message),
140 VaultSelectorModal(vault_selector_modal::Message),
141 DebugLog(debug_log::Message),
142}
143
144#[derive(Debug, Default, Clone, Copy, PartialEq)]
145pub enum ActivePane {
146 #[default]
147 Splash,
148 Explorer,
149 NoteEditor,
150 Outline,
151 Input,
152 HelpModal,
153 VaultSelectorModal,
154 DebugLogModal,
155}
156
157impl From<ActivePane> for &str {
158 fn from(value: ActivePane) -> Self {
159 match value {
160 ActivePane::Splash => "Splash",
161 ActivePane::Explorer => "Explorer",
162 ActivePane::NoteEditor => "Note Editor",
163 ActivePane::Outline => "Outline",
164 ActivePane::Input => "Input",
165 ActivePane::HelpModal => "Help",
166 ActivePane::VaultSelectorModal => "Vault Selector",
167 ActivePane::DebugLogModal => "Debug Log",
168 }
169 }
170}
171
172#[derive(Debug, Default, Clone, PartialEq)]
173pub struct SelectedNote {
174 name: String,
175 path: PathBuf,
176 content: String,
177}
178
179impl From<Note> for SelectedNote {
180 fn from(value: Note) -> Self {
181 Self {
182 name: value.name().to_string(),
183 path: value.path().to_path_buf(),
184 content: fs::read_to_string(value.path()).unwrap_or_default(),
185 }
186 }
187}
188
189impl From<&Note> for SelectedNote {
190 fn from(value: &Note) -> Self {
191 Self {
192 name: value.name().to_string(),
193 path: value.path().to_path_buf(),
194 content: fs::read_to_string(value.path()).unwrap_or_default(),
195 }
196 }
197}
198
199fn help_text(version: &str) -> String {
200 HELP_TEXT.replace("%version-notice", version)
201}
202
203fn active_config_section<'a>(
204 config: &'a Config,
205 active: ActivePane,
206) -> &'a config::ConfigSection<'a> {
207 match active {
208 ActivePane::Splash => &config.splash,
209 ActivePane::Explorer => &config.explorer,
210 ActivePane::Outline => &config.outline,
211 ActivePane::HelpModal => &config.help_modal,
212 ActivePane::VaultSelectorModal => &config.vault_selector_modal,
213 ActivePane::Input => &config.input_modal,
214 ActivePane::NoteEditor => &config.note_editor,
215 ActivePane::DebugLogModal => &config.debug_log_modal,
216 }
217}
218
219pub struct App<'a> {
220 state: AppState<'a>,
221 config: Config<'a>,
222 terminal: RefCell<DefaultTerminal>,
223 vault_watcher: RefCell<Option<VaultWatcher>>,
224}
225
226impl<'a> App<'a> {
227 pub fn new(state: AppState<'a>, config: Config<'a>, terminal: DefaultTerminal) -> Self {
228 Self {
229 state,
230 config,
232 terminal: RefCell::new(terminal),
233 vault_watcher: RefCell::new(None),
234 }
235 }
236
237 fn ensure_watcher_for(&self, path: &Path) {
238 let mut current = self.vault_watcher.borrow_mut();
239 let needs_swap = match current.as_ref() {
240 Some(watcher) => watcher.path() != path,
241 None => !path.as_os_str().is_empty(),
242 };
243 if !needs_swap {
244 return;
245 }
246 *current = if path.is_dir() {
247 VaultWatcher::new(path).ok()
248 } else {
249 None
250 };
251 }
252
253 fn watcher_has_changes(&self) -> bool {
254 self.vault_watcher
255 .borrow()
256 .as_ref()
257 .is_some_and(|w| w.drain())
258 }
259
260 pub fn start(
261 terminal: DefaultTerminal,
262 vaults: Vec<&Vault>,
263 initial_vault: Option<Vault>,
264 debug: bool,
265 log_level: LogLevel,
266 ) -> Result<()> {
267 let version = stylized_text::stylize(VERSION, FontStyle::Script);
268 let size = terminal.size()?;
269 let (config, warnings) = config::load().unwrap();
270
271 let vault = initial_vault.clone().unwrap_or_default();
272 let explorer = match &initial_vault {
273 Some(v) => ExplorerState::new(&v.name, v.entries(), &config.symbols),
274 None => ExplorerState::default(),
275 };
276 let active_pane = if initial_vault.is_some() {
277 ActivePane::Explorer
278 } else {
279 ActivePane::default()
280 };
281
282 let state = AppState {
283 vault,
284 explorer,
285 active_pane,
286 screen_size: size,
287 help_modal: HelpModalState::new(&help_text(&version)),
288 vault_selector_modal: VaultSelectorModalState::new(vaults.clone()),
289 splash_modal: SplashModalState::new(&version, vaults, initial_vault.is_none()),
290 outline: OutlineState {
291 symbols: config.symbols.clone(),
292 ..Default::default()
293 },
294 debug_log_modal: DebugLogModalState {
295 visible: debug,
296 min_level: log_level,
297 ..Default::default()
298 },
299 toasts: warnings
300 .into_iter()
301 .map(|message| {
302 warn!(message, "config warning");
303 toast::Toast::warn(&message, Duration::from_secs(5))
304 })
305 .collect(),
306 ..Default::default()
307 };
308
309 App::new(state, config, terminal).run()
310 }
311
312 fn run(&'a mut self) -> Result<()> {
313 self.state.is_running = true;
314
315 let mut state = self.state.clone();
316 let config = self.config.clone();
317
318 self.ensure_watcher_for(&state.vault.path);
319
320 let tick_rate = Duration::from_millis(250);
321 let mut last_tick = Instant::now();
322
323 while state.is_running {
324 self.draw(&mut state)?;
325
326 let timeout = tick_rate.saturating_sub(last_tick.elapsed());
327
328 if event::poll(timeout)? {
329 let event = event::read()?;
330
331 let mut message = App::handle_event(&config, &mut state, event);
332 while message.is_some() {
333 message = App::update(self.terminal.get_mut(), &config, &mut state, message);
334 }
335 self.ensure_watcher_for(&state.vault.path);
336 }
337
338 if self.watcher_has_changes() {
339 let mut message = Some(Message::RescanVault);
340 while message.is_some() {
341 message = App::update(self.terminal.get_mut(), &config, &mut state, message);
342 }
343 }
344
345 if last_tick.elapsed() >= tick_rate {
346 App::update(
347 self.terminal.get_mut(),
348 &config,
349 &mut state,
350 Some(Message::Toast(toast::Message::Tick)),
351 );
352 last_tick = Instant::now();
353 }
354 }
355
356 Ok(())
357 }
358
359 fn draw(&self, state: &mut AppState<'a>) -> Result<()> {
360 let mut terminal = self.terminal.borrow_mut();
361
362 terminal.draw(move |frame| {
363 let area = frame.area();
364 let buf = frame.buffer_mut();
365 self.render(area, buf, state);
366 })?;
367
368 Ok(())
369 }
370
371 fn handle_event(
372 config: &'a Config,
373 state: &mut AppState<'_>,
374 event: Event,
375 ) -> Option<Message<'a>> {
376 match event {
377 Event::Resize(cols, rows) => Some(Message::Resize(Size::new(cols, rows))),
378 Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
379 App::handle_key_event(config, state, key_event)
380 }
381 _ => None,
382 }
383 }
384
385 fn handle_key_event(
386 config: &'a Config,
387 state: &mut AppState<'_>,
388 key_event: KeyEvent,
389 ) -> Option<Message<'a>> {
390 match state.active_component() {
391 ActivePane::NoteEditor
392 if state.note_editor.is_editing() && state.note_editor.insert_mode() =>
393 {
394 state.pending_keys.clear();
395 note_editor::handle_editing_event(key_event).map(Message::NoteEditor)
396 }
397 ActivePane::Input if state.input_modal.is_editing() => {
398 state.pending_keys.clear();
399 input::handle_editing_event(key_event).map(Message::Input)
400 }
401 active => App::handle_pending_keys(
402 Keystroke::from(key_event),
403 config,
404 active,
405 &mut state.pending_keys,
406 ),
407 }
408 }
409
410 fn handle_pending_keys(
411 key: Keystroke,
412 config: &'a Config,
413 active: ActivePane,
414 pending_keys: &mut Vec<Keystroke>,
415 ) -> Option<Message<'a>> {
416 pending_keys.push(key.clone());
417 let section = active_config_section(config, active);
418
419 let global_message = config.global.sequence_to_message(pending_keys);
420 if global_message.is_some() {
421 pending_keys.clear();
422 return global_message;
423 }
424
425 let section_message = section.sequence_to_message(pending_keys);
426 if section_message.is_some() {
427 pending_keys.clear();
428 return section_message;
429 }
430
431 let is_sequence_prefix = config.global.is_sequence_prefix(pending_keys)
432 || section.is_sequence_prefix(pending_keys);
433
434 if is_sequence_prefix {
435 return None;
436 }
437
438 let is_sequence = pending_keys.len() > 1;
439
440 pending_keys.clear();
441 is_sequence
442 .then(|| App::handle_pending_keys(key, config, active, pending_keys))
443 .flatten()
444 }
445
446 fn update(
447 terminal: &mut DefaultTerminal,
448 config: &Config,
449 state: &mut AppState<'a>,
450 message: Option<Message<'a>>,
451 ) -> Option<Message<'a>> {
452 match message? {
453 Message::Batch(messages) => {
454 for msg in messages {
455 let mut next = Some(msg);
456 while next.is_some() {
457 next = App::update(terminal, config, state, next);
458 }
459 }
460 }
461 Message::Quit => state.is_running = false,
462 Message::Resize(size) => state.screen_size = size,
463 Message::RefreshVault { rename, select } => {
464 if let Some((old, new)) = &rename {
465 if let Err(error) = obsidian::vault::update_wiki_links(state.vault(), old, new)
467 {
468 warn!(?error, "failed to update wiki links");
469 }
470 }
471 state.explorer.with_entries(state.vault.entries(), select);
472 debug!(?rename, "refreshed vault");
473
474 let selected_note = if state
476 .explorer
477 .list_state
478 .selected()
479 .zip(state.explorer.selected_item_index)
480 .is_some_and(|(a, b)| a == b)
481 {
482 if let Some(Item::File { note, .. }) = state.explorer.current_item() {
483 Some(SelectedNote::from(note))
484 } else {
485 None
486 }
487 } else {
488 state.selected_note.clone()
489 };
490
491 if let Some(note) = selected_note {
492 return Some(Message::Batch(vec![
493 Message::SelectNote(note),
494 Message::SetActivePane(ActivePane::Explorer),
495 ]));
496 }
497 return Some(Message::SetActivePane(ActivePane::Explorer));
498 }
499 Message::RescanVault => {
500 let select = state.selected_note.as_ref().map(|note| note.path.clone());
501 state.explorer.with_entries(state.vault.entries(), select);
502 debug!("rescanned vault after watcher change");
503 }
504 Message::CreateUntitledNote => {
505 let path = match state.explorer.current_item() {
506 Some(Item::Directory { path, .. }) => path,
507 Some(Item::File { note, .. }) => {
508 note.path().parent().unwrap_or(&state.vault.path)
509 }
510 _ => &state.vault.path,
511 };
512 match create_untitled_note(path) {
513 Ok(note) => {
514 info!(path = %note.path().display(), "created note");
515 return Some(Message::Batch(vec![
516 Message::Explorer(explorer::Message::Open),
517 Message::RefreshVault {
518 rename: None,
519 select: Some(note.path().to_path_buf()),
520 },
521 Message::Toast(toast::Message::Create(toast::Toast::success(
522 "Note created",
523 Duration::from_secs(2),
524 ))),
525 Message::SelectNote(note.into()),
526 ]));
527 }
528 Err(error) => {
529 error!(?error, "failed to create note");
530 return Some(Message::Toast(toast::Message::Create(toast::Toast::error(
531 "Failed to create a new note",
532 Duration::from_secs(2),
533 ))));
534 }
535 }
536 }
537 Message::CreateUntitledFolder => {
538 let path = match state.explorer.current_item() {
539 Some(Item::Directory { path, .. }) => path,
540 Some(Item::File { note, .. }) => {
541 note.path().parent().unwrap_or(&state.vault.path)
542 }
543 _ => &state.vault.path,
544 };
545 match create_untitled_dir(path) {
546 Ok(note) => {
547 info!(path = %note.path().display(), "created folder");
548 return Some(Message::Batch(vec![
549 Message::Explorer(explorer::Message::Open),
550 Message::RefreshVault {
551 rename: None,
552 select: Some(note.path().to_path_buf()),
553 },
554 Message::Toast(toast::Message::Create(toast::Toast::success(
555 "Folder created",
556 Duration::from_secs(2),
557 ))),
558 ]));
559 }
560 Err(error) => {
561 error!(?error, "failed to create folder");
562 return Some(Message::Toast(toast::Message::Create(toast::Toast::error(
563 "Failed to create a new folder",
564 Duration::from_secs(2),
565 ))));
566 }
567 }
568 }
569 Message::SetActivePane(active_pane) => match active_pane {
570 ActivePane::Explorer => {
571 state.active_pane = active_pane;
572 state.explorer.set_active(true);
574 }
575 ActivePane::NoteEditor => {
576 state.active_pane = active_pane;
577 state.note_editor.set_active(true);
579 if state.explorer.visibility == Visibility::FullWidth {
580 return Some(Message::Explorer(explorer::Message::HidePane));
581 }
582 }
583 ActivePane::Outline => {
584 state.active_pane = active_pane;
585 state.outline.set_active(true);
587 }
588 ActivePane::Input => {
589 state.active_pane = active_pane;
590 }
591 _ => {}
592 },
593 Message::OpenVault(vault) => {
594 info!(vault = %vault.name, "opened vault");
595 state.vault = vault.clone();
596 state.explorer = ExplorerState::new(&vault.name, vault.entries(), &config.symbols);
597 state.note_editor = NoteEditorState::default();
598 return Some(Message::SetActivePane(ActivePane::Explorer));
599 }
600 Message::SelectNote(selected_note) => {
601 info!(note = %selected_note.name, "selected note");
602 let is_different = state
603 .selected_note
604 .as_ref()
605 .is_some_and(|note| note.content != selected_note.content);
606 state.selected_note = Some(selected_note.clone());
607
608 state.note_editor = NoteEditorState::new(
609 &selected_note.content,
610 &selected_note.name,
611 &selected_note.path,
612 &config.symbols,
613 );
614
615 let vim_mode = config.vim_mode;
616 state.note_editor.set_vim_mode(vim_mode);
617
618 let editor_enabled = config.experimental_editor;
619 state.note_editor.set_editor_enabled(editor_enabled);
620
621 if editor_enabled && vim_mode {
622 state.note_editor.set_view(View::Edit(EditMode::Source));
623 } else {
624 state.note_editor.set_view(View::Read);
625 }
626
627 state.outline = OutlineState::new(
629 &state.note_editor.ast_nodes,
630 state.note_editor.current_block_idx(),
631 state.outline.is_open(),
632 &config.symbols,
633 );
634
635 if state.explorer.visibility == Visibility::FullWidth && is_different {
636 return Some(Message::Explorer(explorer::Message::HidePane));
637 }
638 }
639 Message::UpdateSelectedNoteContent((updated_content, nodes)) => {
640 if let Some(selected_note) = state.selected_note.as_mut() {
641 selected_note.content = updated_content;
642 return nodes.map(|nodes| Message::Outline(outline::Message::SetNodes(nodes)));
643 }
644 }
645 Message::Exec(command) => {
646 let (note_name, note_path) = state
647 .selected_note
648 .as_ref()
649 .map(|note| (note.name.as_str(), note.path.to_string_lossy()))
650 .unwrap_or_default();
651
652 return command::sync_command(
653 terminal,
654 command,
655 &state.vault.name,
656 note_name,
657 ¬e_path,
658 );
659 }
660
661 Message::Spawn(command) => {
662 let (note_name, note_path) = state
663 .selected_note
664 .as_ref()
665 .map(|note| (note.name.as_str(), note.path.to_string_lossy()))
666 .unwrap_or_default();
667
668 return command::spawn_command(command, &state.vault.name, note_name, ¬e_path);
669 }
670
671 Message::HelpModal(message) => {
672 return help_modal::update(&message, state.screen_size, &mut state.help_modal);
673 }
674 Message::VaultSelectorModal(message) => {
675 return vault_selector_modal::update(&message, &mut state.vault_selector_modal);
676 }
677 Message::Splash(message) => {
678 return splash_modal::update(&message, &mut state.splash_modal);
679 }
680 Message::Explorer(message) => {
681 return explorer::update(&message, state.screen_size, &mut state.explorer);
682 }
683 Message::Outline(message) => {
684 return outline::update(&message, &mut state.outline);
685 }
686 Message::NoteEditor(message) => {
687 return note_editor::update(message, state.screen_size, &mut state.note_editor);
688 }
689 Message::Input(message) => return input::update(message, &mut state.input_modal),
690 Message::DebugLog(message) => {
691 return debug_log::update(&message, state.screen_size, &mut state.debug_log_modal);
692 }
693 Message::Toast(message) => return toast::update(message, &mut state.toasts),
694 };
695
696 None
697 }
698
699 fn render_splash(&self, area: Rect, buf: &mut Buffer, state: &mut SplashModalState<'a>) {
700 let border_modal = self.config.symbols.border_modal.into();
701 let vault_active = self.config.symbols.vault_active.clone();
702 SplashModal::new(border_modal, vault_active).render(area, buf, state)
703 }
704
705 fn render_main(&self, area: Rect, buf: &mut Buffer, state: &mut AppState<'a>) {
706 let [content, statusbar] = Layout::vertical([Constraint::Fill(1), Constraint::Length(1)])
707 .horizontal_margin(1)
708 .areas(area);
709
710 let (left, right) = match state.explorer.visibility {
711 Visibility::Hidden => (Constraint::Length(4), Constraint::Fill(1)),
712 Visibility::Visible => (Constraint::Length(35), Constraint::Fill(1)),
713 Visibility::FullWidth => (Constraint::Fill(1), Constraint::Length(0)),
714 };
715
716 let [explorer_pane, note, outline] = Layout::horizontal([
717 left,
718 right,
719 if state.outline.is_open() {
720 Constraint::Length(35)
721 } else {
722 Constraint::Length(4)
723 },
724 ])
725 .areas(content);
726
727 Explorer::new().render(explorer_pane, buf, &mut state.explorer);
728 NoteEditor::default().render(note, buf, &mut state.note_editor);
729 Outline.render(outline, buf, &mut state.outline);
730 let border_modal = self.config.symbols.border_modal.into();
731 Input::new(border_modal).render(explorer_pane, buf, &mut state.input_modal);
732
733 let (_, counts) = state
734 .selected_note
735 .clone()
736 .map(|note| {
737 let content = note.content.as_str();
738 (
739 note.name,
740 (WordCount::from(content), CharCount::from(content)),
741 )
742 })
743 .unzip();
744
745 let (word_count, char_count) = counts.unwrap_or_default();
746
747 let mut status_bar_state = StatusBarState::new(
748 state.active_pane.into(),
749 word_count.into(),
750 char_count.into(),
751 );
752
753 let status_bar = StatusBar::new(&self.config.symbols);
754 status_bar.render(statusbar, buf, &mut status_bar_state);
755
756 self.render_modals(area, buf, state);
757 self.render_toasts(area, buf, state);
758
759 if state.debug_log_modal.visible {
760 let border_modal = self.config.symbols.border_modal.into();
761 let memory_mb =
762 memory_stats::memory_stats().map(|stats| stats.physical_mem as f64 / 1_048_576.0);
763 DebugLogModal::new(border_modal, memory_mb).render(
764 area,
765 buf,
766 &mut state.debug_log_modal,
767 );
768 }
769 }
770
771 fn render_modals(&self, area: Rect, buf: &mut Buffer, state: &mut AppState<'a>) {
772 if state.splash_modal.visible {
773 self.render_splash(area, buf, &mut state.splash_modal);
774 }
775
776 if state.vault_selector_modal.visible {
777 let border_modal = self.config.symbols.border_modal.into();
778 let vault_active = self.config.symbols.vault_active.clone();
779 VaultSelectorModal::new(border_modal, vault_active).render(
780 area,
781 buf,
782 &mut state.vault_selector_modal,
783 );
784 }
785
786 if state.help_modal.visible {
787 let border_modal = self.config.symbols.border_modal.into();
788 HelpModal::new(border_modal).render(area, buf, &mut state.help_modal);
789 }
790 }
791
792 fn render_toasts(&self, area: Rect, buf: &mut Buffer, state: &mut AppState<'a>) {
793 let [_, toast_area] =
794 Layout::horizontal([Constraint::Fill(1), Constraint::Length(TOAST_WIDTH)])
795 .horizontal_margin(1)
796 .flex(Flex::End)
797 .areas(area);
798
799 let mut y_offset: u16 = 0;
800 state.toasts.iter().rev().for_each(|toast| {
801 let mut toast_area = toast_area;
802 toast_area.y += y_offset;
803 y_offset += toast.height();
804 if toast_area.y >= area.bottom() {
805 return;
806 }
807 let mut toast = toast.clone();
808 toast.border_type = self.config.symbols.border_modal.into();
809 toast.icon = toast.level_icon(&self.config.symbols);
810 toast.render(toast_area, buf)
811 });
812 }
813}
814
815impl<'a> StatefulWidget for &App<'a> {
816 type State = AppState<'a>;
817
818 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
819 self.render_main(area, buf, state);
820 }
821}