1pub const API_VERSION_MAJOR: u32 = 0;
12
13pub const API_VERSION_MINOR: u32 = 2;
15
16pub const API_VERSION_PATCH: u32 = 0;
18
19#[macro_export]
41macro_rules! require_api {
42 ($major:literal, $minor:literal) => {
43 const _: () = {
44 if $crate::API_VERSION_MAJOR == 0 {
47 assert!(
49 $major == $crate::API_VERSION_MAJOR && $minor == $crate::API_VERSION_MINOR,
50 concat!(
51 "Telex API version mismatch: this code requires ", $major, ".", $minor,
52 " but the library is version ",
53 env!("CARGO_PKG_VERSION"),
54 ". See https://docs.rs/telex for migration guides."
55 )
56 );
57 } else {
58 assert!(
60 $major == $crate::API_VERSION_MAJOR,
61 concat!(
62 "Telex API major version mismatch: this code requires major version ", $major,
63 " but the library is version ",
64 env!("CARGO_PKG_VERSION"),
65 ". This is a breaking change - see https://docs.rs/telex for migration guides."
66 )
67 );
68 assert!(
69 $minor <= $crate::API_VERSION_MINOR,
70 concat!(
71 "Telex API minor version too new: this code requires ", $major, ".", $minor,
72 " but the library is version ",
73 env!("CARGO_PKG_VERSION"),
74 ". Please upgrade the telex dependency in your Cargo.toml."
75 )
76 );
77 }
78 };
79 };
80}
81
82mod async_state;
87pub mod buffer;
88pub mod canvas;
89pub mod channel;
90mod command;
91pub mod command_system;
92mod component;
93mod context;
94mod focus;
95pub mod form;
96pub mod image;
97pub mod markdown;
98mod render;
99mod scope;
100mod state;
101mod stream_state;
102mod terminal;
103mod terminal_state;
104pub mod testing;
105pub mod text;
106pub mod theme;
107pub mod toast;
108mod view;
109pub mod widget;
110
111pub mod prelude;
112
113pub use async_state::Async;
114pub use channel::{ChannelDrain, ChannelHandle, PortHandle, WakingSender};
115pub use command::KeyBinding;
116pub use component::Component;
117pub use scope::Scope;
118pub use state::State;
119pub use stream_state::{StreamHandle, StreamState, TextStreamHandle};
120pub use telex_macro::{async_data, channel as channel_macro, effect, effect_once, interval, port, reducer, state, stream, terminal, text_stream, text_stream_with_restart, view, with};
121pub use terminal::Terminal;
122pub use terminal_state::{TerminalBuffer, TerminalHandle};
123pub use view::{
124 Align, BoxBuilder, BoxNode, ButtonBuilder, ButtonNode, Callback, CanvasBuilder, CanvasNode,
125 ChangeCallback, CheckboxBuilder, CheckboxNode, ColumnWidth, CommandCallback,
126 CommandPaletteBuilder, CommandPaletteNode, CustomNode, ErrorBoundaryBuilder,
127 ErrorBoundaryNode, FormBuilder, FormFieldBuilder, FormFieldNode, FormNode,
128 FormSubmitCallback, HStackBuilder, HStackNode, ImageBuilder, ImageNode, Justify, LayoutMode,
129 SliderBuilder, SliderCallback, SliderNode,
130 ListBuilder, ListNode, Menu, MenuBarBuilder, MenuBarNode, MenuItemNode, ModalBuilder,
131 ModalNode, Orientation, PaletteCommand, RadioGroupBuilder, RadioGroupNode, SelectCallback,
132 SpacerNode, SplitBuilder, SplitNode, TabPosition, TableBuilder, TableColumn, TableNode,
133 TabsBuilder, TabsNode, TextAlign, TextAreaBuilder, TextAreaNode, TextBuilder,
134 TextInputBuilder, TextInputNode, TextNode, TerminalBuilder, TerminalNode,
135 ToastContainerBuilder, ToastContainerNode, ToastItem, ToastLevelView, ToastPosition,
136 ToggleCallback, TreeActivateCallback, TreeBuilder, TreeItem, TreeNode, TreePath,
137 TreeSelectCallback, VStackBuilder, VStackNode, View,
138};
139
140pub use canvas::{animated_canvas, AnimatedCanvasBuilder, DrawContext, PixelBuffer};
142
143pub use image::ImageSource;
145
146pub use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
148pub use crossterm::style::Color;
149
150use command::CommandRegistry;
151use context::ContextStorage;
152use focus::FocusManager;
153use scope::StateStorage;
154use std::io::{self, Result};
155use std::panic;
156use std::rc::Rc;
157use std::sync::atomic::Ordering;
158use std::time::Duration;
159use theme::Theme;
160
161pub trait EventSource {
166 fn poll_event(&self, timeout: Duration) -> io::Result<Option<Event>>;
169
170 fn on_frame_rendered(&self, _terminal: &Terminal) {}
173}
174
175struct CrosstermEventSource;
177
178impl EventSource for CrosstermEventSource {
179 fn poll_event(&self, timeout: Duration) -> io::Result<Option<Event>> {
180 if crossterm::event::poll(timeout)? {
181 Ok(Some(crossterm::event::read()?))
182 } else {
183 Ok(None)
184 }
185 }
186}
187
188fn has_visible_modal(view: &View) -> bool {
190 match view {
191 View::Modal(node) => node.visible,
192 View::VStack(node) => node.children.iter().any(has_visible_modal),
193 View::HStack(node) => node.children.iter().any(has_visible_modal),
194 View::Box(node) => node
195 .child
196 .as_ref()
197 .map(|c| has_visible_modal(c))
198 .unwrap_or(false),
199 View::Split(node) => has_visible_modal(&node.first) || has_visible_modal(&node.second),
200 View::Tabs(node) => node.children.iter().any(has_visible_modal),
201 View::ErrorBoundary(node) => has_visible_modal(&node.child),
202 _ => false,
203 }
204}
205
206fn has_visible_command_palette(view: &View) -> bool {
208 match view {
209 View::CommandPalette(node) => node.visible,
210 View::VStack(node) => node.children.iter().any(has_visible_command_palette),
211 View::HStack(node) => node.children.iter().any(has_visible_command_palette),
212 View::Box(node) => node
213 .child
214 .as_ref()
215 .map(|c| has_visible_command_palette(c))
216 .unwrap_or(false),
217 View::Split(node) => {
218 has_visible_command_palette(&node.first) || has_visible_command_palette(&node.second)
219 }
220 View::Tabs(node) => node.children.iter().any(has_visible_command_palette),
221 View::ErrorBoundary(node) => has_visible_command_palette(&node.child),
222 _ => false,
223 }
224}
225
226fn call_command_palette_dismiss(view: &View) {
228 match view {
229 View::CommandPalette(node) => {
230 if node.visible {
231 if let Some(callback) = &node.on_dismiss {
232 callback();
233 }
234 }
235 }
236 View::VStack(node) => {
237 for child in &node.children {
238 call_command_palette_dismiss(child);
239 }
240 }
241 View::HStack(node) => {
242 for child in &node.children {
243 call_command_palette_dismiss(child);
244 }
245 }
246 View::Box(node) => {
247 if let Some(child) = &node.child {
248 call_command_palette_dismiss(child);
249 }
250 }
251 View::Split(node) => {
252 call_command_palette_dismiss(&node.first);
253 call_command_palette_dismiss(&node.second);
254 }
255 View::Tabs(node) => {
256 for child in &node.children {
257 call_command_palette_dismiss(child);
258 }
259 }
260 View::ErrorBoundary(node) => {
261 call_command_palette_dismiss(&node.child);
262 }
263 _ => {}
264 }
265}
266
267fn call_modal_dismiss(view: &View) {
269 match view {
270 View::Modal(node) => {
271 if node.visible {
272 if let Some(callback) = &node.on_dismiss {
273 callback();
274 }
275 }
276 }
277 View::VStack(node) => {
278 for child in &node.children {
279 call_modal_dismiss(child);
280 }
281 }
282 View::HStack(node) => {
283 for child in &node.children {
284 call_modal_dismiss(child);
285 }
286 }
287 View::Box(node) => {
288 if let Some(child) = &node.child {
289 call_modal_dismiss(child);
290 }
291 }
292 View::Split(node) => {
293 call_modal_dismiss(&node.first);
294 call_modal_dismiss(&node.second);
295 }
296 View::Tabs(node) => {
297 for child in &node.children {
298 call_modal_dismiss(child);
299 }
300 }
301 View::ErrorBoundary(node) => {
302 call_modal_dismiss(&node.child);
303 }
304 _ => {}
305 }
306}
307
308pub fn is_debug_mode() -> bool {
310 std::env::var("TELEX_DEBUG")
311 .map(|v| v == "1" || v == "true")
312 .unwrap_or(false)
313}
314
315pub fn run_with_theme<C: Component>(root: C, theme: Theme) -> Result<()> {
328 theme::set_theme(theme);
329 run(root)
330}
331
332pub fn run<C: Component>(root: C) -> Result<()> {
347 let default_hook = panic::take_hook();
349 panic::set_hook(Box::new(move |panic_info| {
350 let _ = crossterm::terminal::disable_raw_mode();
352 let _ = crossterm::execute!(
353 std::io::stdout(),
354 crossterm::terminal::LeaveAlternateScreen,
355 crossterm::cursor::Show
356 );
357
358 eprintln!("\n┌─ Telex Panic ─────────────────────────────────────────────────┐");
360 eprintln!("│ │");
361
362 let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
364 s.to_string()
365 } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
366 s.clone()
367 } else {
368 "Unknown panic".to_string()
369 };
370
371 for line in message.lines() {
373 let chunks: Vec<&str> = line
374 .as_bytes()
375 .chunks(58)
376 .map(|c| std::str::from_utf8(c).unwrap_or(""))
377 .collect();
378 for chunk in chunks {
379 eprintln!("│ {:<58}│", chunk);
380 }
381 }
382
383 eprintln!("│ │");
384
385 if let Some(location) = panic_info.location() {
387 eprintln!(
388 "│ Location: {}:{}:{:<25}│",
389 location.file().split('/').next_back().unwrap_or(location.file()),
390 location.line(),
391 location.column()
392 );
393 }
394
395 eprintln!("│ │");
396 eprintln!("│ Tip: Check your hook order - hooks must be called │");
397 eprintln!("│ unconditionally in the same order every render. │");
398 eprintln!("│ │");
399 eprintln!("└──────────────────────────────────────────────────────────────┘\n");
400
401 default_hook(panic_info);
403 }));
404
405 let terminal = Terminal::new()?;
406 let event_source = CrosstermEventSource;
407 run_inner(root, terminal, &event_source)
408}
409
410pub fn run_headless<C: Component>(
418 root: C,
419 width: u16,
420 height: u16,
421 events: Vec<Event>,
422) -> String {
423 let terminal = Terminal::new_headless(width, height);
424 let event_source = testing::TestEventSource::new(events);
425 let _ = run_inner(root, terminal, &event_source);
426 event_source.last_buffer()
427}
428
429fn run_inner<C: Component, E: EventSource>(
431 root: C,
432 mut terminal: Terminal,
433 event_source: &E,
434) -> Result<()> {
435 let mut focus = FocusManager::new();
436 let storage = Rc::new(StateStorage::new());
437 let commands = Rc::new(CommandRegistry::new());
438 let context = Rc::new(ContextStorage::new());
439 let debug_mode = is_debug_mode();
440
441 let mut frame_count = 0u64;
442 let mut needs_render = true; let wake_flag = storage.wake_flag().clone();
444
445 loop {
446 let render_start = std::time::Instant::now();
447
448 storage.decay_effect_counter();
450
451 storage.clear_channels();
454 storage.drain_channels();
455
456 if storage.has_channel_data() {
458 needs_render = true;
459 }
460
461 focus.poll_terminals();
463
464 let woken = wake_flag.swap(false, Ordering::Acquire);
467 let poll_timeout = if woken || needs_render {
468 Duration::ZERO
469 } else {
470 Duration::from_millis(16)
471 };
472
473 let mut pending_event: Option<Event> = None;
477 if !needs_render {
478 if let Some(event) = event_source.poll_event(poll_timeout)? {
479 if let Event::Resize(_, _) = event {
480 needs_render = true;
481 continue;
482 }
483 pending_event = Some(event);
485 } else {
486 continue; }
488 }
489 needs_render = false; commands.clear();
493
494 let cx = Scope::with_all(
496 Rc::clone(&storage),
497 Rc::clone(&commands),
498 Rc::clone(&context),
499 );
500
501 let view = root.render(cx);
503
504 focus.collect_focusables(&view);
506
507 focus.set_default_textarea_wrap_width(terminal.width().saturating_sub(2));
510
511 let render_time = render_start.elapsed();
512 frame_count += 1;
513
514 let scroll_offsets: Vec<(u16, u16)> = (0..focus.focus_index() + 10)
516 .map(|i| focus.scroll_offset(i))
517 .collect();
518 let cursor_offsets: Vec<usize> = (0..focus.focus_index() + 10)
519 .map(|i| focus.cursor_offset(i))
520 .collect();
521
522 let modal_visible = has_visible_modal(&view);
524
525 let clamped_offsets = terminal.draw(
527 &view,
528 focus.focus_index(),
529 focus.is_focus_visible(),
530 scroll_offsets,
531 cursor_offsets,
532 modal_visible,
533 )?;
534 focus.update_scroll_states(&clamped_offsets);
535
536 if debug_mode {
538 terminal.draw_debug(
539 frame_count,
540 render_time.as_micros() as u64,
541 focus.focus_index(),
542 focus.focusable_count(),
543 )?;
544 }
545
546 if storage.flush_effects() {
549 needs_render = true;
552 let cx = Scope::with_all(
553 Rc::clone(&storage),
554 Rc::clone(&commands),
555 Rc::clone(&context),
556 );
557 let view = root.render(cx);
558 focus.collect_focusables(&view);
559 let scroll_offsets: Vec<(u16, u16)> = (0..focus.focus_index() + 10)
560 .map(|i| focus.scroll_offset(i))
561 .collect();
562 let cursor_offsets: Vec<usize> = (0..focus.focus_index() + 10)
563 .map(|i| focus.cursor_offset(i))
564 .collect();
565 let modal_visible = has_visible_modal(&view);
566 let clamped_offsets = terminal.draw(
567 &view,
568 focus.focus_index(),
569 focus.is_focus_visible(),
570 scroll_offsets,
571 cursor_offsets,
572 modal_visible,
573 )?;
574 focus.update_scroll_states(&clamped_offsets);
575 }
577
578 event_source.on_frame_rendered(&terminal);
580
581 let max_scroll = 100u16;
584 let viewport_height = terminal.height().saturating_sub(6); let input_event = if pending_event.is_some() {
588 pending_event.take()
589 } else {
590 event_source.poll_event(Duration::from_millis(16))?
591 };
592 if let Some(event) = input_event {
593 needs_render = true;
595
596 if let Event::Resize(_, _) = event {
598 continue;
599 }
600
601 if let Event::Key(key) = event {
602 let modal_visible = has_visible_modal(&view);
604 let palette_visible = has_visible_command_palette(&view);
605
606 if modal_visible && key.code == KeyCode::Esc && key.modifiers == KeyModifiers::NONE
609 {
610 call_modal_dismiss(&view);
611 continue;
612 }
613
614 if palette_visible {
616 match (key.modifiers, key.code) {
617 (KeyModifiers::NONE, KeyCode::Esc) => {
618 call_command_palette_dismiss(&view);
619 }
620 (KeyModifiers::NONE, KeyCode::Enter) => {
621 if focus.is_focused_command_palette() {
622 focus.command_palette_execute();
623 }
624 }
625 (KeyModifiers::NONE, KeyCode::Up) => {
626 }
628 (KeyModifiers::NONE, KeyCode::Down) => {
629 }
631 (KeyModifiers::NONE, KeyCode::Backspace) => {
632 if focus.is_focused_command_palette() {
633 focus.command_palette_backspace();
634 }
635 }
636 (KeyModifiers::NONE, KeyCode::Char(c)) => {
637 if focus.is_focused_command_palette() {
638 focus.command_palette_key(c);
639 }
640 }
641 (KeyModifiers::SHIFT, KeyCode::Char(c)) => {
642 if focus.is_focused_command_palette() {
643 focus.command_palette_key(c.to_ascii_uppercase());
644 }
645 }
646 _ => {}
647 }
648 continue;
649 }
650
651 if key.code == KeyCode::Esc && key.modifiers == KeyModifiers::NONE
653 && focus.is_focused_menu_bar() && focus.menu_bar_has_open_menu() {
654 focus.menu_bar_close();
655 continue;
656 }
657
658 if commands.execute(key.code, key.modifiers) {
660 continue;
661 }
662
663 match (key.modifiers, key.code) {
664 (m, KeyCode::Char('q')) if m.contains(KeyModifiers::CONTROL) => {
666 break;
667 }
668 (m, KeyCode::Char('['))
670 if m.contains(KeyModifiers::CONTROL)
671 && m.contains(KeyModifiers::SHIFT) =>
672 {
673 if focus.is_focused_terminal() {
674 focus.focus_next();
675 }
676 }
677 _ if focus.is_focused_terminal() => {
679 if let Err(e) = focus.terminal_key(key) {
680 eprintln!("Terminal input error: {}", e);
681 }
682 }
683 (KeyModifiers::NONE, KeyCode::Tab) => {
685 focus.focus_next();
686 }
687 (KeyModifiers::SHIFT, KeyCode::BackTab) => {
689 focus.focus_prev();
690 }
691 (KeyModifiers::NONE, KeyCode::Enter | KeyCode::Char(' ')) => {
693 if focus.is_focused_text_area() {
694 if key.code == KeyCode::Enter {
695 focus.text_area_enter();
696 } else {
697 focus.text_area_key(' ');
698 }
699 } else if focus.is_focused_text_input() {
700 if key.code == KeyCode::Enter {
701 focus.text_input_submit();
703 } else {
704 focus.text_input_key(' ');
706 }
707 } else if focus.is_focused_tree() {
708 focus.tree_activate();
709 } else if focus.is_focused_table() {
710 focus.table_activate();
711 } else if focus.is_focused_menu_bar() {
712 if focus.menu_bar_has_open_menu() {
713 focus.menu_bar_execute();
715 } else {
716 focus.menu_bar_open();
718 }
719 } else {
720 focus.activate();
721 }
722 }
723 (KeyModifiers::NONE, KeyCode::Backspace) => {
725 if focus.is_focused_text_input() {
726 focus.text_input_backspace();
727 } else if focus.is_focused_text_area() {
728 focus.text_area_backspace();
729 } else if focus.is_focused_form_field() {
730 focus.form_field_backspace();
731 }
732 }
733 (KeyModifiers::NONE, KeyCode::Up) => {
735 if focus.is_focused_text_input() {
736 focus.text_input_key_up();
737 } else if focus.is_focused_text_area() {
738 focus.text_area_cursor_up();
739 } else if focus.is_focused_menu_bar() && focus.menu_bar_has_open_menu() {
740 focus.menu_bar_select_prev();
741 } else if focus.is_focused_scrollable() {
742 if focus.is_focused_auto_scroll_bottom() {
744 focus.scroll_down(1, max_scroll);
745 } else {
746 focus.scroll_up(1);
747 }
748 } else if focus.is_focused_list() {
749 focus.list_select_prev();
750 } else if focus.is_focused_tree() {
751 focus.tree_select_prev();
752 } else if focus.is_focused_table() {
753 focus.table_select_prev();
754 } else if focus.is_focused_radio_group() {
755 focus.radio_group_select_prev();
756 }
757 }
758 (KeyModifiers::NONE, KeyCode::Down) => {
759 if focus.is_focused_text_input() {
760 focus.text_input_key_down();
761 } else if focus.is_focused_text_area() {
762 focus.text_area_cursor_down();
763 } else if focus.is_focused_menu_bar() && focus.menu_bar_has_open_menu() {
764 focus.menu_bar_select_next();
765 } else if focus.is_focused_scrollable() {
766 if focus.is_focused_auto_scroll_bottom() {
768 focus.scroll_up(1);
769 } else {
770 focus.scroll_down(1, max_scroll);
771 }
772 } else if focus.is_focused_list() {
773 focus.list_select_next();
774 } else if focus.is_focused_tree() {
775 focus.tree_select_next();
776 } else if focus.is_focused_table() {
777 focus.table_select_next();
778 } else if focus.is_focused_radio_group() {
779 focus.radio_group_select_next();
780 }
781 }
782 (KeyModifiers::NONE, KeyCode::PageUp) => {
784 if focus.is_focused_scrollable() {
785 if focus.is_focused_auto_scroll_bottom() {
786 focus.scroll_down(viewport_height, max_scroll);
787 } else {
788 focus.scroll_up(viewport_height);
789 }
790 }
791 }
792 (KeyModifiers::NONE, KeyCode::PageDown) => {
793 if focus.is_focused_scrollable() {
794 if focus.is_focused_auto_scroll_bottom() {
795 focus.scroll_up(viewport_height);
796 } else {
797 focus.scroll_down(viewport_height, max_scroll);
798 }
799 }
800 }
801 (KeyModifiers::NONE, KeyCode::Home) => {
803 if focus.is_focused_scrollable() {
804 if focus.is_focused_auto_scroll_bottom() {
806 focus.scroll_end(max_scroll);
807 } else {
808 focus.scroll_home();
809 }
810 }
811 }
812 (KeyModifiers::NONE, KeyCode::End) => {
813 if focus.is_focused_scrollable() {
814 if focus.is_focused_auto_scroll_bottom() {
816 focus.scroll_home();
817 } else {
818 focus.scroll_end(max_scroll);
819 }
820 }
821 }
822 (KeyModifiers::NONE, KeyCode::Left) => {
824 if focus.is_focused_text_input() {
825 focus.text_input_cursor_left();
826 } else if focus.is_focused_text_area() {
827 focus.text_area_cursor_left();
828 } else if focus.is_focused_menu_bar() {
829 if focus.menu_bar_has_open_menu() {
830 focus.menu_bar_prev();
831 } else {
832 focus.menu_bar_highlight_prev();
833 }
834 } else if focus.is_focused_tabs() {
835 focus.tabs_select_prev();
836 } else if focus.is_focused_slider() {
837 focus.slider_decrement();
838 } else if focus.is_focused_tree() {
839 focus.tree_activate();
841 }
842 }
843 (KeyModifiers::NONE, KeyCode::Right) => {
844 if focus.is_focused_text_input() {
845 focus.text_input_cursor_right();
846 } else if focus.is_focused_text_area() {
847 focus.text_area_cursor_right();
848 } else if focus.is_focused_menu_bar() {
849 if focus.menu_bar_has_open_menu() {
850 focus.menu_bar_next();
851 } else {
852 focus.menu_bar_highlight_next();
853 }
854 } else if focus.is_focused_tabs() {
855 focus.tabs_select_next();
856 } else if focus.is_focused_slider() {
857 focus.slider_increment();
858 } else if focus.is_focused_tree() {
859 focus.tree_activate();
861 }
862 }
863 (KeyModifiers::NONE, KeyCode::Char(c)) => {
865 if focus.is_focused_text_input() {
866 focus.text_input_key(c);
867 } else if focus.is_focused_text_area() {
868 focus.text_area_key(c);
869 } else if focus.is_focused_form_field() {
870 focus.form_field_key(c);
871 } else if focus.is_focused_tabs() {
872 match c {
874 '[' => focus.tabs_select_prev(),
875 ']' => focus.tabs_select_next(),
876 '1'..='9' => {
877 let idx = (c as usize) - ('1' as usize);
878 focus.tabs_select(idx);
879 }
880 _ => {}
881 }
882 } else if focus.is_focused_tree() {
883 match c {
885 'j' => focus.tree_select_next(),
886 'k' => focus.tree_select_prev(),
887 ' ' => focus.tree_activate(),
888 _ => {}
889 }
890 } else if focus.is_focused_table() {
891 match c {
893 'j' => focus.table_select_next(),
894 'k' => focus.table_select_prev(),
895 _ => {}
896 }
897 } else if focus.is_focused_radio_group() {
898 match c {
900 'j' => focus.radio_group_select_next(),
901 'k' => focus.radio_group_select_prev(),
902 _ => {}
903 }
904 }
905 }
906 (KeyModifiers::SHIFT, KeyCode::Char(c)) => {
907 if focus.is_focused_text_input() {
908 focus.text_input_key(c.to_ascii_uppercase());
909 } else if focus.is_focused_text_area() {
910 focus.text_area_key(c.to_ascii_uppercase());
911 } else if focus.is_focused_form_field() {
912 focus.form_field_key(c.to_ascii_uppercase());
913 }
914 }
915 _ => {}
916 }
917 }
918 }
919 }
920
921 storage.cleanup_all_effects();
923
924 terminal.cleanup()?;
925 Ok(())
926}