Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ fn main() -> Result<()> {
r#"function try
set -l script_path "{}"
set -l cmd (/usr/bin/env "{}" cd{} $argv 2>/dev/tty | string collect)
test $status -eq 0 && eval $cmd || echo $cmd
set -l cmd_status $status
test $cmd_status -eq 0 && eval $cmd || echo $cmd
end"#,
script_path.display(),
script_path.display(),
Expand All @@ -125,8 +126,16 @@ end"#,
return;;
esac;;
esac
cmd=$(/usr/bin/env "{}" cd{} "$@" 2>/dev/tty);
[ $? -eq 0 ] && eval "$cmd" || echo "$cmd";
tmp=$(mktemp 2>/dev/null || echo "/tmp/try-cmd-$$")
/usr/bin/env "{}" cd{} "$@" > "$tmp" 2>/dev/tty
cmd_status=$?
cmd=$(cat "$tmp" 2>/dev/null)
rm -f "$tmp" 2>/dev/null
if [ $cmd_status -eq 0 ] && [ -n "$cmd" ]; then
eval "$cmd"
else
[ -n "$cmd" ] && echo "$cmd"
fi
}}"#,
script_path.display(),
script_path.display(),
Expand Down Expand Up @@ -474,4 +483,48 @@ mod tests {
// join_shell trivial
assert_eq!(crate::util::join_shell(&["a".into(), "b".into()]), "a && b");
}

#[test]
fn test_init_script_uses_cmd_status_not_status() {
// Regression test: ensure init script uses cmd_status instead of status
// (status is read-only in zsh)
use std::process::Command;
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "init"])
.output()
.expect("Failed to run try init");
let stdout = String::from_utf8(output.stdout).expect("Invalid UTF-8");

// Check for appropriate syntax based on shell type
if crate::util::is_fish_shell() {
// Fish shell syntax: set -l cmd_status $status
assert!(
stdout.contains("set -l cmd_status $status"),
"Init script should use 'set -l cmd_status $status' in fish shell, got: {stdout}"
);
assert!(
stdout.contains("$cmd_status"),
"Init script should reference $cmd_status, got: {stdout}"
);
} else {
// POSIX shell wrapper should use cmd_status, not status
assert!(
stdout.contains("cmd_status=$?"),
"Init script should use cmd_status=$?, got: {stdout}"
);
assert!(
stdout.contains("$cmd_status"),
"Init script should reference $cmd_status, got: {stdout}"
);
// Should NOT contain the problematic standalone status= assignment
// Check for the pattern with word boundaries to avoid false matches
let has_standalone_status = stdout
.lines()
.any(|line| line.trim().starts_with("status=$?") || line.contains(" status=$?"));
assert!(
!has_standalone_status,
"Init script should not use status=$? (read-only in zsh), got: {stdout}"
);
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}
1 change: 1 addition & 0 deletions src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ pub struct TryDir {
pub ctime: Option<SystemTime>,
pub mtime: Option<SystemTime>,
pub score: f64,
pub size: Option<u64>,
}
54 changes: 37 additions & 17 deletions src/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crossterm::{
};

use std::{
collections::HashMap,
env, fs,
io::{self, Write},
path::{Path, PathBuf},
Expand Down Expand Up @@ -47,6 +48,7 @@ pub(crate) struct TrySelector {
pub(crate) base_path: PathBuf,
pub(crate) selected: Option<Selection>,
status_msg: Option<String>,
size_cache: HashMap<PathBuf, u64>,
// no vim/undo mode in Ruby semantics
}

Expand Down Expand Up @@ -74,6 +76,7 @@ impl TrySelector {
base_path,
selected: None,
status_msg: None,
size_cache: HashMap::new(),
})
}

Expand Down Expand Up @@ -109,6 +112,24 @@ impl TrySelector {
tries = self.get_tries();
let total_items = tries.len() + EXTRA_LIST_ROWS;
self.cursor = self.cursor.min(total_items.saturating_sub(1));

// Calculate sizes lazily for visible items only
let max_visible = usize::max(
self.term_h.saturating_sub(8) as usize, // RESERVED_LINES from tui.rs
3, // MIN_VISIBLE_ITEMS
);
let (scroll, end) =
tui::compute_viewport(self.cursor, self.scroll, max_visible, total_items);
self.scroll = scroll;

// Calculate sizes for visible items only to avoid blocking
let tries_len = tries.len();
let start_idx = scroll.min(tries_len);
let end_idx = end.min(tries_len);
for t in &mut tries[start_idx..end_idx] {
self.ensure_size_calculated(t);
}

let ctx = tui::RenderCtx {
term_w: self.term_w,
term_h: self.term_h,
Expand Down Expand Up @@ -253,6 +274,7 @@ impl TrySelector {
ctime,
mtime,
score: 0.0,
size: None, // Calculated lazily during render
});
}
}
Expand All @@ -275,6 +297,20 @@ impl TrySelector {
}
}

/// Ensures the size is calculated for a TryDir, using cache if available.
fn ensure_size_calculated(&mut self, t: &mut TryDir) {
if t.size.is_some() {
return;
}
if let Some(&cached) = self.size_cache.get(&t.path) {
t.size = Some(cached);
return;
}
let size = crate::util::calculate_dir_size(&t.path);
self.size_cache.insert(t.path.clone(), size);
t.size = Some(size);
}

fn handle_select_existing(&mut self, t: &TryDir) {
self.selected = Some(Selection {
kind: ActionType::Cd,
Expand Down Expand Up @@ -363,7 +399,7 @@ impl TrySelector {
t.basename,
t.path.display(),
files,
format_human_size(bytes)
crate::util::format_human_size(bytes)
)?;
tui::styled(
err,
Expand Down Expand Up @@ -400,19 +436,3 @@ impl TrySelector {
}
}
}

fn format_human_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "K", "M", "G", "T"];
const BYTES_PER_KIB: f64 = 1_024.0;
let mut val = bytes as f64;
let mut idx = 0;
while val >= BYTES_PER_KIB && idx + 1 < UNITS.len() {
val /= BYTES_PER_KIB;
idx += 1;
}
if idx == 0 {
format!("{bytes}B")
} else {
format!("{:.1}{}", val, UNITS[idx])
}
}
9 changes: 6 additions & 3 deletions src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,13 @@ pub(crate) fn render(err: &mut io::Stderr, ctx: &RenderCtx<'_>) -> Result<()> {
}
write_highlighted(err, &t.basename, ctx.input_buf, is_sel)?;

// Right-side meta: mtime then score
// Right-side meta: size and mtime
let size_text = t
.size
.map(crate::util::format_human_size)
.unwrap_or_else(|| "...".to_string());
let time_text = format_relative_time(t.mtime);
let score_text = format!("{:.1}", t.score);
let meta = format!("{time_text}, {score_text}");
let meta = format!("{size_text}, {time_text}");

// Compute remaining columns; ensure we never overflow terminal width
let name_w = display_width(&t.basename);
Expand Down
37 changes: 37 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,40 @@ pub(crate) fn dir_assign_for_shell(dir: &Path) -> String {
format!("dir={escaped}")
}
}

/// Format a byte size as a human-readable string (e.g., "1.5K", "23.4M").
pub(crate) fn format_human_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "K", "M", "G", "T"];
const BYTES_PER_KIB: f64 = 1_024.0;
let mut val = bytes as f64;
let mut idx = 0;
while val >= BYTES_PER_KIB && idx + 1 < UNITS.len() {
val /= BYTES_PER_KIB;
idx += 1;
}
if idx == 0 {
format!("{bytes}B")
} else {
format!("{:.1}{}", val, UNITS[idx])
}
}

/// Calculate the total size of a directory recursively.
pub(crate) fn calculate_dir_size(path: &Path) -> u64 {
let mut total = 0u64;
fn walk(p: &Path, total: &mut u64) {
if let Ok(md) = std::fs::symlink_metadata(p) {
if md.is_file() {
*total += md.len();
} else if md.is_dir()
&& let Ok(rd) = std::fs::read_dir(p)
{
for e in rd.flatten() {
walk(&e.path(), total);
}
}
}
}
walk(path, &mut total);
total
}