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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
//! Command-line interface for the `tfe` binary.
use std::path::PathBuf;
use clap::Parser;
#[derive(Parser)]
#[command(
name = "tfe",
version,
about = "Keyboard-driven two-pane terminal file explorer",
after_help = "\
SHELL INTEGRATION (cd on exit):\n\
Step 1 — enable the feature (persisted across sessions):\n\
tfe --cd\n\
\n\
Step 2 — install the shell wrapper (one time):\n\
tfe --init bash # ~/.bashrc\n\
tfe --init zsh # ~/.zshrc\n\
tfe --init fish # ~/.config/fish/functions/tfe.fish\n\
tfe --init powershell # $PROFILE (Windows / cross-platform PowerShell)\n\
tfe --init nushell # <config-dir>/nushell/config.nu\n\
\n\
After both steps, dismissing tfe with Esc/q cd's your terminal to the\n\
directory you were browsing. Works on macOS, Linux, and Windows.\n\
\n\
Open selected file in $EDITOR: command tfe | xargs -r $EDITOR\n\
NUL-delimited output: command tfe -0 | xargs -0 wc -l"
)]
pub struct Cli {
/// Starting directory or file to open [default: current directory]
///
/// If PATH is a directory, the TUI file explorer opens there.
/// If PATH is a file and an editor is configured (--editor or persisted),
/// the file is opened directly in that editor without launching the TUI.
#[arg(value_name = "PATH")]
pub path: Option<PathBuf>,
/// Only show/select files with these extensions (repeatable: -e rs -e toml)
#[arg(short, long = "ext", value_name = "EXT", action = clap::ArgAction::Append)]
pub extensions: Vec<String>,
/// Show file and folder size info (persisted; toggle at runtime with z)
#[arg(long = "sizes", overrides_with = "no_sizes")]
pub sizes: bool,
/// Hide all file and folder size info (persisted)
#[arg(long = "no-sizes", overrides_with = "sizes")]
pub no_sizes: bool,
/// Compute recursive folder sizes (persisted; toggle at runtime with Z).
/// Disable on slow or huge filesystems — folders then show item counts.
#[arg(long = "dir-sizes", overrides_with = "no_dir_sizes")]
pub dir_sizes: bool,
/// Do not compute recursive folder sizes (persisted); show item counts
#[arg(long = "no-dir-sizes", overrides_with = "dir_sizes")]
pub no_dir_sizes: bool,
/// Send deleted entries to the OS trash / recycle bin (persisted; default)
#[arg(long = "trash", overrides_with = "no_trash")]
pub trash: bool,
/// Delete permanently instead of using the trash (persisted)
#[arg(long = "no-trash", overrides_with = "trash")]
pub no_trash: bool,
/// Show git change markers (M, A, ?, …) next to entries (persisted; toggle with v)
#[arg(long = "git", overrides_with = "no_git")]
pub git: bool,
/// Hide git change markers (persisted)
#[arg(long = "no-git", overrides_with = "git")]
pub no_git: bool,
/// Print a complete keymap.toml with the built-in key bindings and exit
#[arg(long)]
pub dump_keymap: bool,
/// Show hidden (dot-file) entries on startup
#[arg(short = 'H', long)]
pub hidden: bool,
/// Colour theme [default: persisted selection, or "default"]. Use --list-themes to see options.
#[arg(short, long, value_name = "THEME")]
pub theme: Option<String>,
/// List all available themes and exit
#[arg(long)]
pub list_themes: bool,
/// Open the theme panel on startup (toggle at runtime with T)
#[arg(long)]
pub show_themes: bool,
/// Start in single-pane mode (toggle at runtime with w)
#[arg(long)]
pub single_pane: bool,
/// Print the selected file's parent directory instead of the full path
#[arg(long)]
pub print_dir: bool,
/// Terminate output with a NUL byte instead of a newline
#[arg(short = '0', long)]
pub null: bool,
/// Install the shell wrapper for cd-on-exit integration and exit.
/// Install the shell wrapper for `shell` (auto-detected when `None`).
///
/// Appends the wrapper function to your rc file (creating it if needed)
/// and prints instructions. Supported shells: bash, zsh, fish, powershell, nushell.
/// Examples: tfe --init zsh tfe --init nushell tfe --init powershell
#[arg(long, value_name = "SHELL")]
pub init: Option<String>,
/// Enable cd-on-exit: on dismiss, print the active pane's current directory
/// to stdout so the shell wrapper can cd to it. The setting is persisted —
/// run once to enable, run with --no-cd to disable.
#[arg(long = "cd", overrides_with = "no_cd")]
pub cd_on_exit: bool,
/// Disable cd-on-exit (persisted). Dismissing without a selection will
/// print nothing and exit with code 1.
#[arg(long = "no-cd", overrides_with = "cd_on_exit")]
pub no_cd: bool,
/// Print version, platform, and environment info, then exit.
#[arg(long)]
pub info: bool,
/// Run diagnostic checks (environment, shell integration, config,
/// terminal, editor) and exit. Each check is assessed with
/// pass / warn / fail indicators and actionable advice.
#[arg(long)]
pub doctor: bool,
/// Enable verbose mode: print startup diagnostics to stderr, write all
/// logs to a file (path printed at launch), and show a debug log panel
/// inside the TUI. Use `tail -f /tmp/tfe-debug.log` from another
/// terminal for real-time monitoring.
#[arg(long, short = 'v')]
pub verbose: bool,
/// Editor to open when pressing `e` on a file.
///
/// Accepted values: helix (hx), nvim, vim, nano, micro, or any binary
/// name / path. Overrides the persisted setting for this session only.
///
/// Examples:
/// tfe --editor nvim
/// tfe --editor vim
/// tfe --editor "code --wait"
#[arg(long, value_name = "EDITOR")]
pub editor: Option<String>,
}
/// Editor names `--editor` legitimately accepts. Anything else that also
/// exists as a file on disk was almost certainly meant as the PATH argument.
const KNOWN_EDITORS: &[&str] = &[
"none",
"helix",
"hx",
"nvim",
"vim",
"nano",
"micro",
"emacs",
"vscode",
"code",
"zed",
"xcode",
"android-studio",
"rustrover",
"intellij",
"webstorm",
"pycharm",
"goland",
"clion",
"fleet",
"sublime",
"rubymine",
"phpstorm",
"rider",
"eclipse",
];
impl Cli {
/// Resolve `(--editor, PATH)`, fixing the `tfe --editor file.txt` misparse.
///
/// clap hands `file.txt` to `--editor` and leaves PATH empty. When the
/// `--editor` value is not a known editor, exists as a file, and no PATH
/// was given, treat it as the PATH instead.
pub fn editor_and_path(&self) -> (Option<String>, Option<PathBuf>) {
match (&self.editor, &self.path) {
(Some(val), None) => {
let known = KNOWN_EDITORS.iter().any(|n| n.eq_ignore_ascii_case(val));
if !known && std::path::Path::new(val).is_file() {
(None, Some(PathBuf::from(val)))
} else {
(Some(val.clone()), None)
}
}
_ => (self.editor.clone(), self.path.clone()),
}
}
/// `Some(true/false)` when `--sizes` / `--no-sizes` was given.
pub fn sizes_override(&self) -> Option<bool> {
flag_pair(self.sizes, self.no_sizes)
}
/// `Some(true/false)` when `--git` / `--no-git` was given.
pub fn git_override(&self) -> Option<bool> {
flag_pair(self.git, self.no_git)
}
/// `Some(true/false)` when `--trash` / `--no-trash` was given.
pub fn trash_override(&self) -> Option<bool> {
flag_pair(self.trash, self.no_trash)
}
/// `Some(true/false)` when `--dir-sizes` / `--no-dir-sizes` was given.
pub fn dir_sizes_override(&self) -> Option<bool> {
flag_pair(self.dir_sizes, self.no_dir_sizes)
}
}
fn flag_pair(on: bool, off: bool) -> Option<bool> {
match (on, off) {
(true, _) => Some(true),
(_, true) => Some(false),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(args: &[&str]) -> Cli {
Cli::parse_from(std::iter::once("tfe").chain(args.iter().copied()))
}
#[test]
fn size_flags_default_to_no_override() {
let c = parse(&[]);
assert_eq!(c.sizes_override(), None);
assert_eq!(c.dir_sizes_override(), None);
}
#[test]
fn no_sizes_flag_disables() {
assert_eq!(parse(&["--no-sizes"]).sizes_override(), Some(false));
assert_eq!(parse(&["--sizes"]).sizes_override(), Some(true));
}
#[test]
fn last_size_flag_wins() {
assert_eq!(
parse(&["--no-sizes", "--sizes"]).sizes_override(),
Some(true)
);
assert_eq!(
parse(&["--dir-sizes", "--no-dir-sizes"]).dir_sizes_override(),
Some(false)
);
}
#[test]
fn trash_flags_override() {
assert_eq!(parse(&[]).trash_override(), None);
assert_eq!(parse(&["--trash"]).trash_override(), Some(true));
assert_eq!(parse(&["--no-trash"]).trash_override(), Some(false));
}
#[test]
fn git_flags_override() {
assert_eq!(parse(&[]).git_override(), None);
assert_eq!(parse(&["--git"]).git_override(), Some(true));
assert_eq!(parse(&["--git", "--no-git"]).git_override(), Some(false));
}
#[test]
fn known_editor_is_not_mistaken_for_path() {
let c = parse(&["--editor", "nvim"]);
assert_eq!(c.editor_and_path(), (Some("nvim".into()), None));
}
#[test]
fn editor_value_that_is_a_file_becomes_path() {
let dir = std::env::temp_dir().join("tfe-cli-test-file.txt");
std::fs::write(&dir, "x").unwrap();
let c = parse(&["--editor", dir.to_str().unwrap()]);
assert_eq!(c.editor_and_path(), (None, Some(dir.clone())));
let _ = std::fs::remove_file(dir);
}
}