Skip to main content

telex/
lib.rs

1//! Telex - A DX-first TUI framework for Rust.
2//!
3//! Build terminal apps that feel good to write.
4
5// =============================================================================
6// API Versioning
7// =============================================================================
8
9/// Current API major version.
10/// For 0.x releases, minor version bumps may contain breaking changes.
11pub const API_VERSION_MAJOR: u32 = 0;
12
13/// Current API minor version.
14pub const API_VERSION_MINOR: u32 = 2;
15
16/// Current API patch version.
17pub const API_VERSION_PATCH: u32 = 0;
18
19/// Check that your code is compatible with the current Telex API version.
20///
21/// For pre-1.0 versions (0.x.y), this requires an exact major.minor match,
22/// since breaking changes can occur on minor version bumps.
23///
24/// For 1.0+ versions, this requires the same major version and that your
25/// required minor version is not newer than the library's minor version.
26///
27/// # Example
28/// ```rust,ignore
29/// use telex::prelude::*;
30///
31/// telex::require_api!(0, 2);  // Requires API version 0.2.x
32///
33/// fn main() {
34///     telex::run(App).unwrap();
35/// }
36/// ```
37///
38/// If the version doesn't match, you'll get a compile-time error with
39/// guidance on how to migrate.
40#[macro_export]
41macro_rules! require_api {
42    ($major:literal, $minor:literal) => {
43        const _: () = {
44            // For 0.x versions, require exact major.minor match (breaking changes on minor bumps)
45            // For 1.x+, require same major and compatible minor (required <= current)
46            if $crate::API_VERSION_MAJOR == 0 {
47                // Pre-1.0: exact match required
48                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                // Post-1.0: same major, compatible minor
59                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
82// =============================================================================
83// Modules
84// =============================================================================
85
86mod 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
140// Re-export canvas types for pixel-level drawing
141pub use canvas::{animated_canvas, AnimatedCanvasBuilder, DrawContext, PixelBuffer};
142
143// Re-export image types
144pub use image::ImageSource;
145
146// Re-export crossterm types needed for event handling and styling
147pub 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
161/// Trait for providing input events to the run loop.
162///
163/// The default implementation (`CrosstermEventSource`) uses crossterm's real
164/// terminal. Tests can provide a mock implementation via `run_headless()`.
165pub trait EventSource {
166    /// Poll for an event with the given timeout.
167    /// Returns `Ok(Some(event))` if an event is available, `Ok(None)` on timeout.
168    fn poll_event(&self, timeout: Duration) -> io::Result<Option<Event>>;
169
170    /// Called after each frame is rendered. Default is a no-op.
171    /// The test event source uses this to capture the rendered buffer.
172    fn on_frame_rendered(&self, _terminal: &Terminal) {}
173}
174
175/// Event source that reads from the real terminal via crossterm.
176struct 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
188/// Check if any modal is visible in the view tree.
189fn 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
206/// Check if any command palette is visible in the view tree.
207fn 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
226/// Call the dismiss callback on visible command palettes.
227fn 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
267/// Find visible modals in the view tree and call their on_dismiss callbacks.
268fn 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
308/// Check if debug mode is enabled via TELEX_DEBUG environment variable.
309pub fn is_debug_mode() -> bool {
310    std::env::var("TELEX_DEBUG")
311        .map(|v| v == "1" || v == "true")
312        .unwrap_or(false)
313}
314
315/// Run the application with the given root component and theme.
316///
317/// # Example
318/// ```rust,no_run
319/// use telex::prelude::*;
320/// use telex::theme::Theme;
321///
322/// telex::run_with_theme(
323///     |cx| view! { <Text>"Hello, Telex!"</Text> },
324///     Theme::nord(),
325/// ).unwrap();
326/// ```
327pub fn run_with_theme<C: Component>(root: C, theme: Theme) -> Result<()> {
328    theme::set_theme(theme);
329    run(root)
330}
331
332/// Run the application with the given root component.
333///
334/// This is the main entry point for Telex applications.
335///
336/// # Example
337/// ```rust,no_run
338/// use telex::prelude::*;
339///
340/// telex::run(|cx| view! { <Text>"Hello, Telex!"</Text> }).unwrap();
341/// ```
342///
343/// # Debug Mode
344/// Set `TELEX_DEBUG=1` to enable debug mode, which shows render timing
345/// and focus information.
346pub fn run<C: Component>(root: C) -> Result<()> {
347    // Set up custom panic handler to restore terminal on panic
348    let default_hook = panic::take_hook();
349    panic::set_hook(Box::new(move |panic_info| {
350        // Try to restore terminal state
351        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        // Print a helpful error message
359        eprintln!("\n┌─ Telex Panic ─────────────────────────────────────────────────┐");
360        eprintln!("│                                                              │");
361
362        // Extract panic message
363        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        // Word wrap the message
372        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        // Print location if available
386        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        // Call default hook for stack trace
402        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
410/// Run a component headlessly with scripted events. For testing only.
411///
412/// Runs the real event loop with a headless terminal and injected events.
413/// When all events are consumed, the loop exits and returns the final
414/// rendered frame as a string.
415///
416/// This exercises the same key dispatch logic as the real `run()` function.
417pub 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
429/// Inner event loop shared by `run()` and `run_headless()`.
430fn 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; // Always render on first frame
443    let wake_flag = storage.wake_flag().clone();
444
445    loop {
446        let render_start = std::time::Instant::now();
447
448        // Decay effect cycle counter (sliding window for infinite loop detection)
449        storage.decay_effect_counter();
450
451        // Drain all registered channels (external events -> frame buffers)
452        // Clear first, then drain so components see only this frame's messages.
453        storage.clear_channels();
454        storage.drain_channels();
455
456        // Channel data means we need to render
457        if storage.has_channel_data() {
458            needs_render = true;
459        }
460
461        // Poll terminal output (before rendering, so we pick up any new data)
462        focus.poll_terminals();
463
464        // Compute poll timeout: 0ms if wake flag is set (external event arrived),
465        // otherwise 16ms (~60fps). Reset the flag before polling.
466        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        // Skip render if nothing changed since last frame.
474        // If input arrives during the skip-render poll, save it so we can
475        // dispatch it after re-rendering (instead of dropping it on the floor).
476        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                // Input arrived — save it and fall through to render + dispatch
484                pending_event = Some(event);
485            } else {
486                continue; // No input, no channel data, skip frame
487            }
488        }
489        needs_render = false; // Reset for next frame; input/effects/channels will set it again
490
491        // Clear command registry before each render
492        commands.clear();
493
494        // Create scope with existing storage, command registry, and context
495        let cx = Scope::with_all(
496            Rc::clone(&storage),
497            Rc::clone(&commands),
498            Rc::clone(&context),
499        );
500
501        // Render the view
502        let view = root.render(cx);
503
504        // Collect focusables for navigation
505        focus.collect_focusables(&view);
506
507        // Set default wrap width for text areas based on terminal width
508        // (subtract 2 for TextArea borders)
509        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        // Get scroll and cursor offsets for all focusables
515        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        // Check if modal is visible for render context
523        let modal_visible = has_visible_modal(&view);
524
525        // Draw with focus and scroll info, get back clamped offsets
526        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        // Draw debug info if enabled
537        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        // Run pending effects (after render, before input handling)
547        // If effects ran and potentially modified state, re-render once
548        if storage.flush_effects() {
549            // Effects ran - re-render to show any state changes they made
550            // Only do this once per frame to prevent infinite loops
551            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            // Don't flush effects again - just one re-render per frame
576        }
577
578        // Store current buffer for test retrieval (headless mode)
579        event_source.on_frame_rendered(&terminal);
580
581        // For now, use a generous max_scroll to allow scrolling
582        // TODO: Calculate actual content height for focused scrollable
583        let max_scroll = 100u16;
584        let viewport_height = terminal.height().saturating_sub(6); // Approximate visible rows
585
586        // Handle input — use saved event from skip-render poll, or poll fresh
587        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            // Any input means we should re-render next frame
594            needs_render = true;
595
596            // Handle resize - just continue to trigger redraw
597            if let Event::Resize(_, _) = event {
598                continue;
599            }
600
601            if let Event::Key(key) = event {
602                // Check if a modal is visible - if so, only allow Escape
603                let modal_visible = has_visible_modal(&view);
604                let palette_visible = has_visible_command_palette(&view);
605
606                // When modal is visible, Escape dismisses it
607                // Other keys work normally (focus is scoped to modal content)
608                if modal_visible && key.code == KeyCode::Esc && key.modifiers == KeyModifiers::NONE
609                {
610                    call_modal_dismiss(&view);
611                    continue;
612                }
613
614                // Handle command palette input when visible
615                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                            // Navigate up in palette - handled by state in component
627                        }
628                        (KeyModifiers::NONE, KeyCode::Down) => {
629                            // Navigate down in palette - handled by state in component
630                        }
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                // Escape closes open menu bar dropdowns
652                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                // First, try user-registered commands
659                if commands.execute(key.code, key.modifiers) {
660                    continue;
661                }
662
663                match (key.modifiers, key.code) {
664                    // Ctrl+Q to quit (but not Ctrl+C, as that should pass through to terminal)
665                    (m, KeyCode::Char('q')) if m.contains(KeyModifiers::CONTROL) => {
666                        break;
667                    }
668                    // Ctrl+Shift+[ to escape terminal focus
669                    (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                    // Terminal passthrough - send all keys to terminal if focused
678                    _ if focus.is_focused_terminal() => {
679                        if let Err(e) = focus.terminal_key(key) {
680                            eprintln!("Terminal input error: {}", e);
681                        }
682                    }
683                    // Tab to focus next
684                    (KeyModifiers::NONE, KeyCode::Tab) => {
685                        focus.focus_next();
686                    }
687                    // Shift+Tab to focus previous
688                    (KeyModifiers::SHIFT, KeyCode::BackTab) => {
689                        focus.focus_prev();
690                    }
691                    // Enter or Space to activate (for buttons, checkboxes, tree, table, menu bar)
692                    (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                                // Enter in text input submits
702                                focus.text_input_submit();
703                            } else {
704                                // Space in text input adds a space
705                                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                                // Execute selected item
714                                focus.menu_bar_execute();
715                            } else {
716                                // Open first menu
717                                focus.menu_bar_open();
718                            }
719                        } else {
720                            focus.activate();
721                        }
722                    }
723                    // Backspace for text input/area/form field
724                    (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                    // Arrow keys for scrolling (when focused on scrollable) or list/tree/table/radio/textarea/menu/text input navigation
734                    (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                            // For auto_scroll_bottom, Up means scroll away from bottom (increase offset)
743                            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                            // For auto_scroll_bottom, Down means scroll toward bottom (decrease offset)
767                            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                    // Page Up/Down
783                    (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                    // Home/End
802                    (KeyModifiers::NONE, KeyCode::Home) => {
803                        if focus.is_focused_scrollable() {
804                            // For auto_scroll_bottom, Home goes to top (max offset from bottom)
805                            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                            // For auto_scroll_bottom, End goes to bottom (zero offset)
815                            if focus.is_focused_auto_scroll_bottom() {
816                                focus.scroll_home();
817                            } else {
818                                focus.scroll_end(max_scroll);
819                            }
820                        }
821                    }
822                    // Left/Right arrows for text inputs, text areas, tabs, tree, and menu bar
823                    (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                            // Left triggers activate (app should collapse)
840                            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                            // Right triggers activate (app should expand)
860                            focus.tree_activate();
861                        }
862                    }
863                    // Character input for text fields, tabs, tree, and form fields
864                    (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                            // Handle [ ] for tab cycling and 1-9 for direct selection
873                            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                            // j/k for vim-style navigation, space for activate
884                            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                            // j/k for vim-style navigation
892                            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                            // j/k for vim-style navigation
899                            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    // Run all effect cleanup functions before exiting
922    storage.cleanup_all_effects();
923
924    terminal.cleanup()?;
925    Ok(())
926}