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}