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::cell::Cell;
155use std::io::{self, Result};
156use std::panic;
157use std::rc::Rc;
158use std::sync::atomic::Ordering;
159use std::time::Duration;
160
161thread_local! {
162 pub(crate) static IN_ERROR_BOUNDARY: Cell<bool> = const { Cell::new(false) };
165}
166use theme::Theme;
167
168pub trait EventSource {
173 fn poll_event(&self, timeout: Duration) -> io::Result<Option<Event>>;
176
177 fn on_frame_rendered(&self, _terminal: &Terminal) {}
180}
181
182struct CrosstermEventSource;
184
185impl EventSource for CrosstermEventSource {
186 fn poll_event(&self, timeout: Duration) -> io::Result<Option<Event>> {
187 if crossterm::event::poll(timeout)? {
188 Ok(Some(crossterm::event::read()?))
189 } else {
190 Ok(None)
191 }
192 }
193}
194
195fn has_visible_modal(view: &View) -> bool {
197 match view {
198 View::Modal(node) => node.visible,
199 View::VStack(node) => node.children.iter().any(has_visible_modal),
200 View::HStack(node) => node.children.iter().any(has_visible_modal),
201 View::Box(node) => node
202 .child
203 .as_ref()
204 .map(|c| has_visible_modal(c))
205 .unwrap_or(false),
206 View::Split(node) => has_visible_modal(&node.first) || has_visible_modal(&node.second),
207 View::Tabs(node) => node.children.iter().any(has_visible_modal),
208 View::ErrorBoundary(node) => has_visible_modal(&node.child),
209 _ => false,
210 }
211}
212
213fn has_visible_command_palette(view: &View) -> bool {
215 match view {
216 View::CommandPalette(node) => node.visible,
217 View::VStack(node) => node.children.iter().any(has_visible_command_palette),
218 View::HStack(node) => node.children.iter().any(has_visible_command_palette),
219 View::Box(node) => node
220 .child
221 .as_ref()
222 .map(|c| has_visible_command_palette(c))
223 .unwrap_or(false),
224 View::Split(node) => {
225 has_visible_command_palette(&node.first) || has_visible_command_palette(&node.second)
226 }
227 View::Tabs(node) => node.children.iter().any(has_visible_command_palette),
228 View::ErrorBoundary(node) => has_visible_command_palette(&node.child),
229 _ => false,
230 }
231}
232
233fn call_command_palette_dismiss(view: &View) {
235 match view {
236 View::CommandPalette(node) => {
237 if node.visible {
238 if let Some(callback) = &node.on_dismiss {
239 callback();
240 }
241 }
242 }
243 View::VStack(node) => {
244 for child in &node.children {
245 call_command_palette_dismiss(child);
246 }
247 }
248 View::HStack(node) => {
249 for child in &node.children {
250 call_command_palette_dismiss(child);
251 }
252 }
253 View::Box(node) => {
254 if let Some(child) = &node.child {
255 call_command_palette_dismiss(child);
256 }
257 }
258 View::Split(node) => {
259 call_command_palette_dismiss(&node.first);
260 call_command_palette_dismiss(&node.second);
261 }
262 View::Tabs(node) => {
263 for child in &node.children {
264 call_command_palette_dismiss(child);
265 }
266 }
267 View::ErrorBoundary(node) => {
268 call_command_palette_dismiss(&node.child);
269 }
270 _ => {}
271 }
272}
273
274fn call_modal_dismiss(view: &View) {
276 match view {
277 View::Modal(node) => {
278 if node.visible {
279 if let Some(callback) = &node.on_dismiss {
280 callback();
281 }
282 }
283 }
284 View::VStack(node) => {
285 for child in &node.children {
286 call_modal_dismiss(child);
287 }
288 }
289 View::HStack(node) => {
290 for child in &node.children {
291 call_modal_dismiss(child);
292 }
293 }
294 View::Box(node) => {
295 if let Some(child) = &node.child {
296 call_modal_dismiss(child);
297 }
298 }
299 View::Split(node) => {
300 call_modal_dismiss(&node.first);
301 call_modal_dismiss(&node.second);
302 }
303 View::Tabs(node) => {
304 for child in &node.children {
305 call_modal_dismiss(child);
306 }
307 }
308 View::ErrorBoundary(node) => {
309 call_modal_dismiss(&node.child);
310 }
311 _ => {}
312 }
313}
314
315pub fn is_debug_mode() -> bool {
317 std::env::var("TELEX_DEBUG")
318 .map(|v| v == "1" || v == "true")
319 .unwrap_or(false)
320}
321
322pub fn run_with_theme<C: Component>(root: C, theme: Theme) -> Result<()> {
335 theme::set_theme(theme);
336 run(root)
337}
338
339pub fn run<C: Component>(root: C) -> Result<()> {
354 let default_hook = panic::take_hook();
356 panic::set_hook(Box::new(move |panic_info| {
357 if IN_ERROR_BOUNDARY.with(|f| f.get()) {
360 return;
361 }
362
363 let _ = crossterm::terminal::disable_raw_mode();
365 let _ = crossterm::execute!(
366 std::io::stdout(),
367 crossterm::terminal::LeaveAlternateScreen,
368 crossterm::cursor::Show
369 );
370
371 eprintln!("\n┌─ Telex Panic ─────────────────────────────────────────────────┐");
373 eprintln!("│ │");
374
375 let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
377 s.to_string()
378 } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
379 s.clone()
380 } else {
381 "Unknown panic".to_string()
382 };
383
384 for line in message.lines() {
386 let chunks: Vec<&str> = line
387 .as_bytes()
388 .chunks(58)
389 .map(|c| std::str::from_utf8(c).unwrap_or(""))
390 .collect();
391 for chunk in chunks {
392 eprintln!("│ {:<58}│", chunk);
393 }
394 }
395
396 eprintln!("│ │");
397
398 if let Some(location) = panic_info.location() {
400 eprintln!(
401 "│ Location: {}:{}:{:<25}│",
402 location.file().split('/').next_back().unwrap_or(location.file()),
403 location.line(),
404 location.column()
405 );
406 }
407
408 eprintln!("│ │");
409 eprintln!("│ Tip: Check your hook order - hooks must be called │");
410 eprintln!("│ unconditionally in the same order every render. │");
411 eprintln!("│ │");
412 eprintln!("└──────────────────────────────────────────────────────────────┘\n");
413
414 default_hook(panic_info);
416 }));
417
418 let terminal = Terminal::new()?;
419 let event_source = CrosstermEventSource;
420 run_inner(root, terminal, &event_source)
421}
422
423pub fn run_headless<C: Component>(
431 root: C,
432 width: u16,
433 height: u16,
434 events: Vec<Event>,
435) -> String {
436 let terminal = Terminal::new_headless(width, height);
437 let event_source = testing::TestEventSource::new(events);
438 let _ = run_inner(root, terminal, &event_source);
439 event_source.last_buffer()
440}
441
442pub fn run_headless_timed<C: Component>(
449 root: C,
450 width: u16,
451 height: u16,
452 duration: Duration,
453) -> Vec<String> {
454 let terminal = Terminal::new_headless(width, height);
455 let event_source = testing::StreamTestEventSource::new(duration);
456 let _ = run_inner(root, terminal, &event_source);
457 event_source.frames()
458}
459
460fn run_inner<C: Component, E: EventSource>(
462 root: C,
463 mut terminal: Terminal,
464 event_source: &E,
465) -> Result<()> {
466 let mut focus = FocusManager::new();
467 let storage = Rc::new(StateStorage::new());
468 let commands = Rc::new(CommandRegistry::new());
469 let context = Rc::new(ContextStorage::new());
470 let debug_mode = is_debug_mode();
471
472 let mut frame_count = 0u64;
473 let mut needs_render = true; let wake_flag = storage.wake_flag().clone();
475
476 loop {
477 let render_start = std::time::Instant::now();
478
479 storage.decay_effect_counter();
481
482 storage.clear_channels();
485 storage.drain_channels();
486
487 if storage.has_channel_data() {
489 needs_render = true;
490 }
491
492 focus.poll_terminals();
494
495 let woken = wake_flag.swap(false, Ordering::Acquire);
498 if woken {
499 needs_render = true;
500 }
501 let poll_timeout = if needs_render {
502 Duration::ZERO
503 } else {
504 Duration::from_millis(16)
505 };
506
507 let mut pending_event: Option<Event> = None;
511 if !needs_render {
512 if let Some(event) = event_source.poll_event(poll_timeout)? {
513 if let Event::Resize(_, _) = event {
514 needs_render = true;
515 continue;
516 }
517 pending_event = Some(event);
519 } else {
520 continue; }
522 }
523 needs_render = false; commands.clear();
527
528 let cx = Scope::with_all(
530 Rc::clone(&storage),
531 Rc::clone(&commands),
532 Rc::clone(&context),
533 );
534
535 let view = root.render(cx);
537
538 focus.collect_focusables(&view);
540
541 focus.set_default_textarea_wrap_width(terminal.width().saturating_sub(2));
544
545 let render_time = render_start.elapsed();
546 frame_count += 1;
547
548 let scroll_offsets: Vec<(u16, u16)> = (0..focus.focus_index() + 10)
550 .map(|i| focus.scroll_offset(i))
551 .collect();
552 let cursor_offsets: Vec<usize> = (0..focus.focus_index() + 10)
553 .map(|i| focus.cursor_offset(i))
554 .collect();
555
556 let modal_visible = has_visible_modal(&view);
558
559 let clamped_offsets = terminal.draw(
561 &view,
562 focus.focus_index(),
563 focus.is_focus_visible(),
564 scroll_offsets,
565 cursor_offsets,
566 modal_visible,
567 )?;
568 focus.update_scroll_states(&clamped_offsets);
569
570 if debug_mode {
572 terminal.draw_debug(
573 frame_count,
574 render_time.as_micros() as u64,
575 focus.focus_index(),
576 focus.focusable_count(),
577 )?;
578 }
579
580 if storage.flush_effects() {
583 needs_render = true;
586 let cx = Scope::with_all(
587 Rc::clone(&storage),
588 Rc::clone(&commands),
589 Rc::clone(&context),
590 );
591 let view = root.render(cx);
592 focus.collect_focusables(&view);
593 let scroll_offsets: Vec<(u16, u16)> = (0..focus.focus_index() + 10)
594 .map(|i| focus.scroll_offset(i))
595 .collect();
596 let cursor_offsets: Vec<usize> = (0..focus.focus_index() + 10)
597 .map(|i| focus.cursor_offset(i))
598 .collect();
599 let modal_visible = has_visible_modal(&view);
600 let clamped_offsets = terminal.draw(
601 &view,
602 focus.focus_index(),
603 focus.is_focus_visible(),
604 scroll_offsets,
605 cursor_offsets,
606 modal_visible,
607 )?;
608 focus.update_scroll_states(&clamped_offsets);
609 }
611
612 event_source.on_frame_rendered(&terminal);
614
615 let max_scroll = 100u16;
618 let viewport_height = terminal.height().saturating_sub(6); let input_event = if pending_event.is_some() {
622 pending_event.take()
623 } else {
624 event_source.poll_event(Duration::from_millis(16))?
625 };
626 if let Some(event) = input_event {
627 needs_render = true;
629
630 if let Event::Resize(_, _) = event {
632 continue;
633 }
634
635 if let Event::Key(key) = event {
636 let modal_visible = has_visible_modal(&view);
638 let palette_visible = has_visible_command_palette(&view);
639
640 if modal_visible && key.code == KeyCode::Esc && key.modifiers == KeyModifiers::NONE
643 {
644 call_modal_dismiss(&view);
645 continue;
646 }
647
648 if palette_visible {
650 match (key.modifiers, key.code) {
651 (KeyModifiers::NONE, KeyCode::Esc) => {
652 call_command_palette_dismiss(&view);
653 }
654 (KeyModifiers::NONE, KeyCode::Enter) => {
655 if focus.is_focused_command_palette() {
656 focus.command_palette_execute();
657 }
658 }
659 (KeyModifiers::NONE, KeyCode::Up) => {
660 }
662 (KeyModifiers::NONE, KeyCode::Down) => {
663 }
665 (KeyModifiers::NONE, KeyCode::Backspace) => {
666 if focus.is_focused_command_palette() {
667 focus.command_palette_backspace();
668 }
669 }
670 (KeyModifiers::NONE, KeyCode::Char(c)) => {
671 if focus.is_focused_command_palette() {
672 focus.command_palette_key(c);
673 }
674 }
675 (KeyModifiers::SHIFT, KeyCode::Char(c)) => {
676 if focus.is_focused_command_palette() {
677 focus.command_palette_key(c.to_ascii_uppercase());
678 }
679 }
680 _ => {}
681 }
682 continue;
683 }
684
685 if key.code == KeyCode::Esc && key.modifiers == KeyModifiers::NONE
687 && focus.is_focused_menu_bar() && focus.menu_bar_has_open_menu() {
688 focus.menu_bar_close();
689 continue;
690 }
691
692 if commands.execute(key.code, key.modifiers) {
694 continue;
695 }
696
697 match (key.modifiers, key.code) {
698 (m, KeyCode::Char('q')) if m.contains(KeyModifiers::CONTROL) => {
700 break;
701 }
702 (m, KeyCode::Char('['))
704 if m.contains(KeyModifiers::CONTROL)
705 && m.contains(KeyModifiers::SHIFT) =>
706 {
707 if focus.is_focused_terminal() {
708 focus.focus_next();
709 }
710 }
711 _ if focus.is_focused_terminal() => {
713 if let Err(e) = focus.terminal_key(key) {
714 eprintln!("Terminal input error: {}", e);
715 }
716 }
717 (KeyModifiers::NONE, KeyCode::Tab) => {
719 focus.focus_next();
720 }
721 (KeyModifiers::SHIFT, KeyCode::BackTab) => {
723 focus.focus_prev();
724 }
725 (KeyModifiers::NONE, KeyCode::Enter | KeyCode::Char(' ')) => {
727 if focus.is_focused_text_area() {
728 if key.code == KeyCode::Enter {
729 focus.text_area_enter();
730 } else {
731 focus.text_area_key(' ');
732 }
733 } else if focus.is_focused_text_input() {
734 if key.code == KeyCode::Enter {
735 focus.text_input_submit();
737 } else {
738 focus.text_input_key(' ');
740 }
741 } else if focus.is_focused_tree() {
742 focus.tree_activate();
743 } else if focus.is_focused_table() {
744 focus.table_activate();
745 } else if focus.is_focused_menu_bar() {
746 if focus.menu_bar_has_open_menu() {
747 focus.menu_bar_execute();
749 } else {
750 focus.menu_bar_open();
752 }
753 } else {
754 focus.activate();
755 }
756 }
757 (KeyModifiers::NONE, KeyCode::Backspace) => {
759 if focus.is_focused_text_input() {
760 focus.text_input_backspace();
761 } else if focus.is_focused_text_area() {
762 focus.text_area_backspace();
763 } else if focus.is_focused_form_field() {
764 focus.form_field_backspace();
765 }
766 }
767 (KeyModifiers::NONE, KeyCode::Up) => {
769 if focus.is_focused_text_input() {
770 focus.text_input_key_up();
771 } else if focus.is_focused_text_area() {
772 focus.text_area_cursor_up();
773 } else if focus.is_focused_menu_bar() && focus.menu_bar_has_open_menu() {
774 focus.menu_bar_select_prev();
775 } else if focus.is_focused_scrollable() {
776 if focus.is_focused_auto_scroll_bottom() {
778 focus.scroll_down(1, max_scroll);
779 } else {
780 focus.scroll_up(1);
781 }
782 } else if focus.is_focused_list() {
783 focus.list_select_prev();
784 } else if focus.is_focused_tree() {
785 focus.tree_select_prev();
786 } else if focus.is_focused_table() {
787 focus.table_select_prev();
788 } else if focus.is_focused_radio_group() {
789 focus.radio_group_select_prev();
790 }
791 }
792 (KeyModifiers::NONE, KeyCode::Down) => {
793 if focus.is_focused_text_input() {
794 focus.text_input_key_down();
795 } else if focus.is_focused_text_area() {
796 focus.text_area_cursor_down();
797 } else if focus.is_focused_menu_bar() && focus.menu_bar_has_open_menu() {
798 focus.menu_bar_select_next();
799 } else if focus.is_focused_scrollable() {
800 if focus.is_focused_auto_scroll_bottom() {
802 focus.scroll_up(1);
803 } else {
804 focus.scroll_down(1, max_scroll);
805 }
806 } else if focus.is_focused_list() {
807 focus.list_select_next();
808 } else if focus.is_focused_tree() {
809 focus.tree_select_next();
810 } else if focus.is_focused_table() {
811 focus.table_select_next();
812 } else if focus.is_focused_radio_group() {
813 focus.radio_group_select_next();
814 }
815 }
816 (KeyModifiers::NONE, KeyCode::PageUp) => {
818 if focus.is_focused_scrollable() {
819 if focus.is_focused_auto_scroll_bottom() {
820 focus.scroll_down(viewport_height, max_scroll);
821 } else {
822 focus.scroll_up(viewport_height);
823 }
824 }
825 }
826 (KeyModifiers::NONE, KeyCode::PageDown) => {
827 if focus.is_focused_scrollable() {
828 if focus.is_focused_auto_scroll_bottom() {
829 focus.scroll_up(viewport_height);
830 } else {
831 focus.scroll_down(viewport_height, max_scroll);
832 }
833 }
834 }
835 (KeyModifiers::NONE, KeyCode::Home) => {
837 if focus.is_focused_scrollable() {
838 if focus.is_focused_auto_scroll_bottom() {
840 focus.scroll_end(max_scroll);
841 } else {
842 focus.scroll_home();
843 }
844 }
845 }
846 (KeyModifiers::NONE, KeyCode::End) => {
847 if focus.is_focused_scrollable() {
848 if focus.is_focused_auto_scroll_bottom() {
850 focus.scroll_home();
851 } else {
852 focus.scroll_end(max_scroll);
853 }
854 }
855 }
856 (KeyModifiers::NONE, KeyCode::Left) => {
858 if focus.is_focused_text_input() {
859 focus.text_input_cursor_left();
860 } else if focus.is_focused_text_area() {
861 focus.text_area_cursor_left();
862 } else if focus.is_focused_menu_bar() {
863 if focus.menu_bar_has_open_menu() {
864 focus.menu_bar_prev();
865 } else {
866 focus.menu_bar_highlight_prev();
867 }
868 } else if focus.is_focused_tabs() {
869 focus.tabs_select_prev();
870 } else if focus.is_focused_slider() {
871 focus.slider_decrement();
872 } else if focus.is_focused_tree() {
873 focus.tree_activate();
875 }
876 }
877 (KeyModifiers::NONE, KeyCode::Right) => {
878 if focus.is_focused_text_input() {
879 focus.text_input_cursor_right();
880 } else if focus.is_focused_text_area() {
881 focus.text_area_cursor_right();
882 } else if focus.is_focused_menu_bar() {
883 if focus.menu_bar_has_open_menu() {
884 focus.menu_bar_next();
885 } else {
886 focus.menu_bar_highlight_next();
887 }
888 } else if focus.is_focused_tabs() {
889 focus.tabs_select_next();
890 } else if focus.is_focused_slider() {
891 focus.slider_increment();
892 } else if focus.is_focused_tree() {
893 focus.tree_activate();
895 }
896 }
897 (KeyModifiers::NONE, KeyCode::Char(c)) => {
899 if focus.is_focused_text_input() {
900 focus.text_input_key(c);
901 } else if focus.is_focused_text_area() {
902 focus.text_area_key(c);
903 } else if focus.is_focused_form_field() {
904 focus.form_field_key(c);
905 } else if focus.is_focused_tabs() {
906 match c {
908 '[' => focus.tabs_select_prev(),
909 ']' => focus.tabs_select_next(),
910 '1'..='9' => {
911 let idx = (c as usize) - ('1' as usize);
912 focus.tabs_select(idx);
913 }
914 _ => {}
915 }
916 } else if focus.is_focused_tree() {
917 match c {
919 'j' => focus.tree_select_next(),
920 'k' => focus.tree_select_prev(),
921 ' ' => focus.tree_activate(),
922 _ => {}
923 }
924 } else if focus.is_focused_table() {
925 match c {
927 'j' => focus.table_select_next(),
928 'k' => focus.table_select_prev(),
929 _ => {}
930 }
931 } else if focus.is_focused_radio_group() {
932 match c {
934 'j' => focus.radio_group_select_next(),
935 'k' => focus.radio_group_select_prev(),
936 _ => {}
937 }
938 }
939 }
940 (KeyModifiers::SHIFT, KeyCode::Char(c)) => {
941 if focus.is_focused_text_input() {
942 focus.text_input_key(c.to_ascii_uppercase());
943 } else if focus.is_focused_text_area() {
944 focus.text_area_key(c.to_ascii_uppercase());
945 } else if focus.is_focused_form_field() {
946 focus.form_field_key(c.to_ascii_uppercase());
947 }
948 }
949 _ => {}
950 }
951 }
952 }
953 }
954
955 storage.cleanup_all_effects();
957
958 terminal.cleanup()?;
959 Ok(())
960}