Skip to content

Commit 15a6e4b

Browse files
ioma8claude
andcommitted
Add cd-on-exit: --cwd-file flag, fishez --init, fz shell wrapper in installers
Quit now returns from the event loop instead of process::exit so main can write the active panel dir. install.sh and make install append eval "$(fishez --init)" to the shell rc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fdcf670 commit 15a6e4b

6 files changed

Lines changed: 143 additions & 46 deletions

File tree

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,14 @@ doc-html:
8383
doc-check:
8484
cargo doc --no-deps --document-private-items --quiet
8585

86-
# Install fishez binary
86+
# Install fishez binary + fz shell function
8787
install:
8888
cargo install --path .
89+
@RC="$$HOME/.zshrc"; case "$$SHELL" in */bash) RC="$$HOME/.bashrc";; esac; \
90+
if ! grep -q "fishez --init" "$$RC" 2>/dev/null; then \
91+
printf '\neval "$$(fishez --init)" # fz: fishez wrapper that cds to the last viewed dir\n' >> "$$RC"; \
92+
echo "Added fz shell function to $$RC (open a new shell to use it)."; \
93+
fi
8994

9095
# Uninstall fishez binary
9196
uninstall:

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ fishez ~/projects
2525

2626
Press `Ctrl+T` to split into two panes. Press `Esc` to back out of overlays, and from a clean normal state it quits.
2727

28+
### cd on exit
29+
30+
Add this line to your `~/.zshrc` / `~/.bashrc` (the [install script](install.sh) and `make install` do it for you):
31+
32+
```bash
33+
eval "$(fishez --init)"
34+
```
35+
36+
It defines `fz`, a wrapper that launches fishez and — when you quit — drops your shell in the directory you were browsing. Use `fz` instead of `fishez` and stop typing `cd`.
37+
2838
## ✨ What is fishez?
2939

3040
fishez is a keyboard-driven terminal file manager for developers who live in the terminal. No config file, no plugin hunt, no startup ceremony.

install.sh

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,22 @@ else
5454
sudo mv /tmp/fishez /usr/local/bin/$BIN
5555
fi
5656

57-
echo "✓ fishez installed. Run 'fishez' to start."
57+
# Install the fz() shell wrapper (cd to last viewed dir on exit)
58+
case "${SHELL:-}" in
59+
*/zsh) RC="$HOME/.zshrc" ;;
60+
*/bash) RC="$HOME/.bashrc" ;;
61+
*) RC="" ;;
62+
esac
63+
64+
FZ_LINE='eval "$(fishez --init)" # fz: fishez wrapper that cds to the last viewed dir'
65+
if [ -n "$RC" ]; then
66+
if ! grep -q "fishez --init" "$RC" 2>/dev/null; then
67+
printf '\n%s\n' "$FZ_LINE" >> "$RC"
68+
echo "✓ Added fz shell function to $RC (open a new shell to use it)."
69+
fi
70+
else
71+
echo "To get the fz function (cd on exit), add this to your shell rc:"
72+
echo " $FZ_LINE"
73+
fi
74+
75+
echo "✓ fishez installed. Run 'fishez' to start, or 'fz' to cd on exit."

src/main.rs

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,26 @@ use std::path::PathBuf;
2222
use std::sync::mpsc;
2323
use tui_input::Input;
2424

25+
/// Shell function installed via `eval "$(fishez --init)"` (bash/zsh).
26+
const FZ_INIT: &str = r#"fz() {
27+
local tmp
28+
tmp="$(mktemp)"
29+
command fishez --cwd-file "$tmp" "$@"
30+
if [ -s "$tmp" ]; then
31+
cd "$(cat "$tmp")" || true
32+
fi
33+
rm -f "$tmp"
34+
}"#;
35+
2536
fn main() {
2637
setup_panic_handler();
2738

28-
let (two_pane, start_path) = parse_args(env::args_os().skip(1));
39+
let args = parse_args(env::args_os().skip(1));
40+
if args.print_init {
41+
println!("{FZ_INIT}");
42+
return;
43+
}
44+
let (two_pane, start_path) = (args.two_pane, args.start_path);
2945

3046
// Initialize infrastructure (adapters)
3147
let fs = StdFileSystem;
@@ -65,6 +81,8 @@ fn main() {
6581
let mut favorites_items = load_favorites();
6682
let mut favorites_selected: usize = 0;
6783

84+
let cwd_file = args.cwd_file;
85+
6886
// Initial draw and run event loop
6987
overlays::draw(&mut renderer, &state);
7088
run(
@@ -92,21 +110,45 @@ fn main() {
92110
&mut favorites_items,
93111
&mut favorites_selected,
94112
);
113+
114+
if let Some(path) = cwd_file {
115+
write_cwd_file(&path, &state.active_panel().current_path);
116+
}
117+
}
118+
119+
struct CliArgs {
120+
two_pane: bool,
121+
start_path: Option<PathBuf>,
122+
cwd_file: Option<PathBuf>,
123+
print_init: bool,
95124
}
96125

97-
fn parse_args(args: impl IntoIterator<Item = OsString>) -> (bool, Option<PathBuf>) {
98-
let mut two_pane = false;
99-
let mut start_path = None;
126+
fn parse_args(args: impl IntoIterator<Item = OsString>) -> CliArgs {
127+
let mut parsed = CliArgs {
128+
two_pane: false,
129+
start_path: None,
130+
cwd_file: None,
131+
print_init: false,
132+
};
100133

101-
for arg in args {
134+
let mut args = args.into_iter();
135+
while let Some(arg) = args.next() {
102136
if arg == "--two-pane" || arg == "-2" {
103-
two_pane = true;
104-
} else if start_path.is_none() {
105-
start_path = Some(PathBuf::from(arg));
137+
parsed.two_pane = true;
138+
} else if arg == "--init" {
139+
parsed.print_init = true;
140+
} else if arg == "--cwd-file" {
141+
parsed.cwd_file = args.next().map(PathBuf::from);
142+
} else if parsed.start_path.is_none() {
143+
parsed.start_path = Some(PathBuf::from(arg));
106144
}
107145
}
108146

109-
(two_pane, start_path)
147+
parsed
148+
}
149+
150+
fn write_cwd_file(file: &std::path::Path, dir: &std::path::Path) {
151+
let _ = std::fs::write(file, dir.display().to_string());
110152
}
111153

112154
fn setup_panic_handler() {
@@ -131,10 +173,33 @@ mod tests {
131173

132174
#[test]
133175
fn parses_two_pane_flag_and_start_path() {
134-
let (two_pane, path) =
135-
parse_args([OsString::from("--two-pane"), OsString::from("/tmp/project")]);
176+
let args = parse_args([OsString::from("--two-pane"), OsString::from("/tmp/project")]);
177+
178+
assert!(args.two_pane);
179+
assert_eq!(args.start_path, Some(PathBuf::from("/tmp/project")));
180+
assert_eq!(args.cwd_file, None);
181+
assert!(!args.print_init);
182+
}
183+
184+
#[test]
185+
fn parses_cwd_file_and_init() {
186+
let args = parse_args([
187+
OsString::from("--cwd-file"),
188+
OsString::from("/tmp/out"),
189+
OsString::from("--init"),
190+
]);
191+
192+
assert_eq!(args.cwd_file, Some(PathBuf::from("/tmp/out")));
193+
assert!(args.print_init);
194+
assert_eq!(args.start_path, None);
195+
}
196+
197+
#[test]
198+
fn write_cwd_file_writes_panel_dir() {
199+
let file = std::env::temp_dir().join("fishez-cwd-test");
200+
super::write_cwd_file(&file, &PathBuf::from("/some/dir"));
136201

137-
assert!(two_pane);
138-
assert_eq!(path, Some(PathBuf::from("/tmp/project")));
202+
assert_eq!(std::fs::read_to_string(&file).unwrap(), "/some/dir");
203+
let _ = std::fs::remove_file(&file);
139204
}
140205
}

src/presentation/event_loop.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ use crate::presentation::shortcuts;
1818
use crate::presentation::terminal::overlays;
1919
use crossterm::event::{self, Event, KeyCode, KeyEvent};
2020
use std::path::PathBuf;
21-
use std::process::exit;
2221
use std::sync::mpsc::{Receiver, Sender};
2322
use std::time::{Duration, Instant};
2423
use tui_input::Input;
@@ -113,7 +112,7 @@ pub fn run(
113112
)
114113
{
115114
renderer.reset_terminal();
116-
exit(0);
115+
return;
117116
}
118117
route_input(
119118
ev,

src/presentation/terminal/renderer.rs

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -646,42 +646,40 @@ impl TerminalRenderer {
646646
);
647647

648648
let max_row = self.rows.saturating_sub(1);
649-
let mut draw_column = |s: &mut Self,
650-
sections: &[(&str, &[(&str, &str)])],
651-
x: u16,
652-
key_w: usize| {
653-
let mut y = content_start + 2;
654-
for (title, entries) in sections {
655-
if y >= max_row {
656-
return;
657-
}
658-
let _ = queue!(
659-
s.stdout,
660-
cursor::MoveTo(x, y),
661-
Print(
662-
(*title)
663-
.with(Color::Cyan)
664-
.attribute(crossterm::style::Attribute::Bold)
665-
)
666-
);
667-
y += 1;
668-
for (key, desc) in entries.iter() {
649+
let draw_column =
650+
|s: &mut Self, sections: &[(&str, &[(&str, &str)])], x: u16, key_w: usize| {
651+
let mut y = content_start + 2;
652+
for (title, entries) in sections {
669653
if y >= max_row {
670654
return;
671655
}
672-
let pad = key_w.saturating_sub(key.chars().count());
673656
let _ = queue!(
674657
s.stdout,
675658
cursor::MoveTo(x, y),
676-
Print((*key).with(Color::Yellow)),
677-
Print(" ".repeat(pad + 3)),
678-
Print((*desc).with(Color::Green))
659+
Print(
660+
(*title)
661+
.with(Color::Cyan)
662+
.attribute(crossterm::style::Attribute::Bold)
663+
)
679664
);
680665
y += 1;
666+
for (key, desc) in entries.iter() {
667+
if y >= max_row {
668+
return;
669+
}
670+
let pad = key_w.saturating_sub(key.chars().count());
671+
let _ = queue!(
672+
s.stdout,
673+
cursor::MoveTo(x, y),
674+
Print((*key).with(Color::Yellow)),
675+
Print(" ".repeat(pad + 3)),
676+
Print((*desc).with(Color::Green))
677+
);
678+
y += 1;
679+
}
680+
y += 1; // blank line between sections
681681
}
682-
y += 1; // blank line between sections
683-
}
684-
};
682+
};
685683
if two_col {
686684
draw_column(self, left, sc as u16, lk);
687685
draw_column(self, right, (sc + lw + gap) as u16, rk);
@@ -848,7 +846,7 @@ impl TerminalRenderer {
848846
columns,
849847
rows,
850848
stdout: StdoutKind::Test(writer),
851-
logo_png: None,
849+
logo_png: None,
852850
image_protocol: ImageProtocol::ITerm2,
853851
kitty_image_hash: None,
854852
loading_frame: 0,
@@ -924,7 +922,9 @@ mod tests {
924922
let mut state = AppState::new(true);
925923
state.right_panel.current_path =
926924
std::path::PathBuf::from("/some/really/long/right/pane/path/beyond/half");
927-
state.left_panel.set_notification("Copied 2 files".to_string());
925+
state
926+
.left_panel
927+
.set_notification("Copied 2 files".to_string());
928928
renderer.draw_two_panes(&state);
929929
let s = String::from_utf8_lossy(&buffer.borrow()).to_string();
930930
assert!(

0 commit comments

Comments
 (0)