Skip to main content

gravityfile_tui/
lib.rs

1#![doc = include_str!("../README.md")]
2
3mod app;
4mod config;
5mod event;
6mod event_source;
7mod graphics_query;
8mod keymap;
9mod opener;
10mod preview;
11mod search;
12mod shell_integration;
13mod terminal_session;
14#[cfg(test)]
15mod test_support;
16mod theme;
17mod ui;
18
19use app::App;
20pub use app::AppResult;
21
22/// Configuration for the TUI application.
23#[derive(Debug, Clone, Default)]
24#[non_exhaustive]
25pub struct TuiConfig {
26    /// Host executable supporting the private plugin worker protocol. If absent,
27    /// embedded TUI hosts do not load plugins into their own process implicitly.
28    pub plugin_worker_executable: Option<std::path::PathBuf>,
29    /// Whether to automatically start scanning on startup.
30    /// `None` means defer to the user's saved settings.
31    /// `Some(true)` forces scanning on, `Some(false)` forces it off.
32    pub scan_on_startup: Option<bool>,
33    /// Path to a file where the last working directory will be written on exit.
34    /// This enables shell integration for `cd` on exit functionality.
35    pub cwd_file: Option<std::path::PathBuf>,
36    /// Whether to print the last working directory to stdout on exit.
37    pub print_cwd: bool,
38    /// What happens to an item the user chooses to overwrite on paste.
39    pub replace_policy: gravityfile_ops::ReplacePolicy,
40    /// What happens to items the user deletes.
41    pub delete_mode: gravityfile_ops::DeleteMode,
42    /// How sizes are displayed.
43    pub units: gravityfile_core::units::Units,
44    /// Show file lengths instead of disk usage.
45    pub apparent_size: bool,
46    /// A saved scan to browse instead of the filesystem.
47    pub snapshot: Option<Box<Snapshot>>,
48}
49
50/// A saved scan opened read-only in the explorer.
51#[derive(Debug, Clone)]
52pub struct Snapshot {
53    /// The file the scan was read from.
54    pub source: std::path::PathBuf,
55    /// The scanned tree.
56    pub tree: gravityfile_core::FileTree,
57}
58
59impl TuiConfig {
60    /// Enable supervised plugins using a worker-capable host executable.
61    pub fn with_plugin_worker_executable(mut self, path: std::path::PathBuf) -> Self {
62        self.plugin_worker_executable = Some(path);
63        self
64    }
65    /// Create a new config with default values.
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// Browse `tree`, read from `source`, instead of the filesystem.
71    pub fn with_snapshot(
72        mut self,
73        source: std::path::PathBuf,
74        tree: gravityfile_core::FileTree,
75    ) -> Self {
76        self.snapshot = Some(Box::new(Snapshot { source, tree }));
77        self
78    }
79
80    /// Show file lengths instead of disk usage.
81    pub fn with_apparent_size(mut self, apparent: bool) -> Self {
82        self.apparent_size = apparent;
83        self
84    }
85
86    /// Enable auto-scan on startup.
87    pub fn with_scan_on_startup(mut self, scan: bool) -> Self {
88        self.scan_on_startup = Some(scan);
89        self
90    }
91
92    /// Set the cwd file path for shell integration.
93    pub fn with_cwd_file(mut self, path: Option<std::path::PathBuf>) -> Self {
94        self.cwd_file = path;
95        self
96    }
97
98    /// Enable printing cwd on exit.
99    pub fn with_print_cwd(mut self, print: bool) -> Self {
100        self.print_cwd = print;
101        self
102    }
103
104    /// Display sizes in `units`.
105    pub fn with_units(mut self, units: gravityfile_core::units::Units) -> Self {
106        self.units = units;
107        self
108    }
109}
110
111/// Data collected at exit for shell integration.
112#[derive(Debug, Clone)]
113pub struct ExitData {
114    /// The directory being viewed at exit, for the shell to change to. None
115    /// for a snapshot, whose paths belong to the machine that scanned them.
116    pub last_cwd: Option<std::path::PathBuf>,
117}
118
119/// Run the TUI application with default configuration.
120pub fn run(path: std::path::PathBuf) -> AppResult<()> {
121    run_with_config(path, TuiConfig::default())
122}
123
124/// Run the TUI application with custom configuration.
125pub fn run_with_config(path: std::path::PathBuf, config: TuiConfig) -> AppResult<()> {
126    use crossterm::event::EnableMouseCapture;
127    use crossterm::execute;
128
129    ui::set_units(config.units);
130
131    // Create tokio runtime for async operations - setup services BEFORE terminal
132    let rt = tokio::runtime::Runtime::new()?;
133
134    // Store config options we need after the app runs
135    let cwd_file = config.cwd_file.clone();
136    let print_cwd = config.print_cwd;
137
138    // Initialize terminal with Ratatui's high-level API
139    // This automatically installs a panic hook that restores the terminal.
140    let mut session = terminal_session::TerminalSession::new();
141    let terminal = ratatui::try_init()?;
142
143    // The guard also restores partially initialized state on this error path.
144    execute!(std::io::stdout(), EnableMouseCapture)?;
145
146    // Detect the terminal's graphics protocol and font size.
147    //
148    // This writes control sequences and reads the reply, so it has to happen
149    // after raw mode is enabled and before the event loop takes ownership of
150    // stdin. Unix probing reads synchronously with a 250 ms deadline and an
151    // 8 KiB reply budget; no reader survives a timeout. Other platforms use
152    // half-block previews without a stdio query. `image_previews` opts out.
153    //
154    // Failed detection is not an error: image files simply fall back to the
155    // ordinary binary preview.
156    // Inside the runtime, so the app can start background work as it is built.
157    let app = {
158        let _runtime = rt.enter();
159        App::with_config(path, config)
160    };
161    let image_picker = if app.image_previews_enabled() {
162        match graphics_query::query_picker() {
163            Ok(picker) => Some(picker),
164            Err(error) => {
165                tracing::debug!("Terminal graphics detection failed: {error}");
166                None
167            }
168        }
169    } else {
170        None
171    };
172
173    let app = app.with_image_picker(image_picker);
174    let result = rt.block_on(async { app.run_and_return_exit_data(terminal).await });
175
176    // Discard all pending terminal output sitting in the kernel's PTY buffer.
177    // During heavy TUI rendering (e.g. scanning 45K files at 20fps), the Rust
178    // process writes escape sequences faster than the terminal emulator can
179    // consume them. This creates a multi-second backlog in the PTY buffer.
180    // Without flushing, LeaveAlternateScreen queues behind all that output
181    // and the user sees the TUI "frozen" for seconds after pressing quit.
182    #[cfg(unix)]
183    unsafe {
184        libc::tcflush(libc::STDOUT_FILENO, libc::TCOFLUSH);
185    }
186
187    // Now these commands go to the front of an empty buffer — instant.
188    let terminal_restore = session.restore();
189    rt.shutdown_background();
190
191    // Prefer the application error, but never skip the cleanup attempts above.
192    let exit_data = result?;
193    terminal_restore?;
194
195    // Handle exit data for shell integration
196    // Write cwd to file if requested.
197    if let (Some(cwd_path), Some(last_cwd)) = (cwd_file, &exit_data.last_cwd) {
198        shell_integration::write_cwd_file(&cwd_path, last_cwd)?;
199    }
200
201    // Print cwd if requested.
202    if print_cwd && let Some(last_cwd) = &exit_data.last_cwd {
203        shell_integration::print_cwd(last_cwd, &mut std::io::stdout().lock())?;
204    }
205
206    Ok(())
207}