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