flashkraft_tui/lib.rs
1//! FlashKraft TUI — Library crate
2//!
3//! This crate exposes the full Ratatui terminal UI as a library so that:
4//! - The `flashkraft-tui` binary can stay thin (argument parsing + `lib::run()`)
5//! - Examples can import types directly from `flashkraft_tui::`
6//!
7//! ## Module layout
8//!
9//! ```text
10//! flashkraft_tui
11//! ├── domain ← re-exported from flashkraft_core::domain
12//! ├── core ← re-exported from flashkraft_core (commands, flash_writer, …)
13//! └── tui ← Ratatui front-end (app / events / flash_runner / ui)
14//! ```
15//!
16//! The file-browser widget is provided by the [`tui-file-explorer`](https://crates.io/crates/tui-file-explorer)
17//! crate and consumed directly via `tui_file_explorer::*`.
18
19// ── Core re-exports ───────────────────────────────────────────────────────────
20
21/// Re-export `flashkraft_core` under the short alias `core` so that
22/// `crate::core::commands::load_drives()`, `crate::core::flash_helper::*`,
23/// etc. resolve correctly from every submodule and from examples via
24/// `flashkraft_tui::core::*`.
25pub mod core {
26 pub use flashkraft_core::commands;
27 pub use flashkraft_core::domain;
28 pub use flashkraft_core::flash_helper;
29 pub use flashkraft_core::utils;
30}
31
32/// Re-export `flashkraft_core::domain` at the crate root so that
33/// `crate::domain::DriveInfo` / `crate::domain::ImageInfo` resolve in
34/// submodules, and so that examples can write `flashkraft_tui::domain::*`.
35pub use flashkraft_core::domain;
36
37// ── TUI submodules ────────────────────────────────────────────────────────────
38
39/// Ratatui front-end — app state, event handling, flash runner, UI rendering.
40///
41/// Submodules: `app` (state machine), `events` (key handling),
42/// `flash_runner` (background flash task), `ui` (frame rendering).
43pub mod tui;
44
45// ── Convenience re-exports for examples and tests ────────────────────────────
46
47pub use flashkraft_core::flash_helper;
48pub use tui::app::{App, AppScreen, FlashEvent, InputMode, UsbEntry};
49pub use tui::events::handle_key;
50pub use tui::ui::render;
51pub use tui_file_explorer::{ExplorerOutcome, FileExplorer, FsEntry};
52
53// ── Public event-loop API ─────────────────────────────────────────────────────
54
55use std::io;
56use std::panic;
57use std::time::Duration;
58
59use anyhow::Result;
60use crossterm::{
61 event::{self, Event},
62 execute,
63 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
64};
65use ratatui::{backend::CrosstermBackend, Terminal};
66
67/// Set up the terminal, run the application event loop, then restore the
68/// terminal on exit (or on panic).
69///
70/// This is the single entry point called by `main.rs`. Having it here lets
71/// integration tests and examples exercise the loop without spawning a
72/// subprocess.
73pub async fn run() -> Result<()> {
74 // Install a panic hook that restores the terminal before printing the
75 // panic message — otherwise the output is invisible in raw / alt-screen
76 // mode.
77 let default_hook = panic::take_hook();
78 panic::set_hook(Box::new(move |info| {
79 let _ = restore_terminal();
80 default_hook(info);
81 }));
82
83 // Initialise raw mode + alternate screen.
84 enable_raw_mode()?;
85 let mut stdout = io::stdout();
86 execute!(stdout, EnterAlternateScreen)?;
87 let backend = CrosstermBackend::new(stdout);
88 let mut terminal = Terminal::new(backend)?;
89
90 // Drive the application.
91 let run_result = run_app(&mut terminal).await;
92
93 // Restore unconditionally (even if the app returned Err).
94 restore_terminal()?;
95 terminal.show_cursor()?;
96
97 run_result
98}
99
100/// Drive the [`App`] state machine until `should_quit` is set.
101///
102/// Each iteration:
103/// 1. Tick the internal counter (used for animations).
104/// 2. Drain any pending async channel messages (drive detection / flash events).
105/// 3. Render a single frame.
106/// 4. Block for up to 100 ms waiting for a keyboard event.
107///
108/// The generic backend parameter makes the function testable with ratatui's
109/// `TestBackend` without touching real terminal infrastructure.
110pub async fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>) -> Result<()>
111where
112 B::Error: Send + Sync + 'static,
113{
114 let mut app = App::new();
115
116 loop {
117 // ── Tick ─────────────────────────────────────────────────────────────
118 app.tick_count = app.tick_count.wrapping_add(1);
119
120 // ── Poll async channels ───────────────────────────────────────────────
121 app.poll_drives();
122 app.poll_flash();
123
124 // ── Render ────────────────────────────────────────────────────────────
125 terminal.draw(|frame| render(&mut app, frame))?;
126
127 // ── Keyboard events (100 ms timeout) ──────────────────────────────────
128 if event::poll(Duration::from_millis(100))? {
129 if let Event::Key(key) = event::read()? {
130 handle_key(&mut app, key);
131 }
132 }
133
134 // ── Quit guard ────────────────────────────────────────────────────────
135 if app.should_quit {
136 break;
137 }
138 }
139
140 Ok(())
141}
142
143/// Disable raw mode and leave the alternate screen.
144///
145/// Called both on normal exit and from the panic hook so the terminal is
146/// always left in a usable state.
147pub fn restore_terminal() -> Result<()> {
148 disable_raw_mode()?;
149 execute!(io::stdout(), LeaveAlternateScreen)?;
150 Ok(())
151}