Skip to content
Open
6 changes: 5 additions & 1 deletion src/archive/rar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ pub fn list_archive(
let is_dir = item.is_directory();
let path = item.filename;

Ok(FileInArchive { path, is_dir })
Ok(FileInArchive {
path,
is_dir,
symlink_target: None,
})
}))
}

Expand Down
1 change: 1 addition & 0 deletions src/archive/sevenz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ where
files.push(Ok(FileInArchive {
path: entry.name().into(),
is_dir: entry.is_directory(),
symlink_target: None,
}));
Ok(true)
};
Expand Down
7 changes: 6 additions & 1 deletion src/archive/tar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ pub fn list_archive(mut archive: tar::Archive<impl Read>) -> Result<impl Iterato
let file = file?;
let path = file.path()?.into_owned();
let is_dir = file.header().entry_type().is_dir();
Ok(FileInArchive { path, is_dir })
let symlink_target = file.link_name()?.map(|p| p.into_owned());
Ok(FileInArchive {
path,
is_dir,
symlink_target,
})
});

Ok(entries.collect::<Vec<_>>().into_iter())
Expand Down
16 changes: 14 additions & 2 deletions src/archive/zip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,27 @@ where
None => archive.by_index(idx),
};

let file = match zip_result {
let mut file = match zip_result {
Ok(f) => f,
Err(e) => return Err(e.into()),
};

let path = file.enclosed_name().unwrap_or_else(|| file.mangled_name()).to_owned();
let is_dir = file.is_dir();

Ok(FileInArchive { path, is_dir })
let symlink_target = file
.unix_mode()
.filter(|mode| mode & 0o170000 == 0o120000)
.and_then(|_| {
let mut s = String::new();
file.read_to_string(&mut s).ok().map(|_| PathBuf::from(s))
});

Ok(FileInArchive {
path,
is_dir,
symlink_target,
})
})
}

Expand Down
49 changes: 41 additions & 8 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub struct FileInArchive {

/// Whether this file is a directory
pub is_dir: bool,

/// The target of the symlink, if this file is a symlink
pub symlink_target: Option<PathBuf>,
}

/// Actually print the files
Expand All @@ -41,21 +44,44 @@ pub fn list_files(
tree.print(&mut out);
} else {
for file in files {
let FileInArchive { path, is_dir } = file?;
print_entry(&mut out, path.display(), is_dir);
let FileInArchive {
path,
is_dir,
symlink_target,
} = file?;
print_entry(&mut out, path.display(), is_dir, symlink_target);
}
}
Ok(())
}

/// Print an entry and highlight directories, either by coloring them
/// if that's supported or by adding a trailing /
fn print_entry(out: &mut impl Write, name: impl fmt::Display, is_dir: bool) {
fn print_entry(out: &mut impl Write, name: impl fmt::Display, is_dir: bool, symlink_target: Option<PathBuf>) {
use crate::utils::colors::*;

if !is_dir {
// Not a directory -> just print the file name
let _ = writeln!(out, "{name}");
if let Some(target) = symlink_target {
if is_running_in_accessible_mode() {
// Accessible mode: use "->" for screen readers
let _ = writeln!(out, "{} -> {}", name, target.display());
Copy link
Copy Markdown
Member

@marcospb19 marcospb19 Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we added ->, now ouch list can't be used in scripts to provide a file list 🤔 .

Can you add a check for, when --quiet is set but --tree isn't, we don't print out the arrow nor the target? This way you can still ouch list -q | xargs rm -i even if there is a symlink in it.

Let me know if you think there is a better and simpler approach, to allow for -> but keeping this compatible with scripts.

} else {
// Normal mode: use "->" with colors
let _ = writeln!(
out,
"{}{}{} -> {}{}{}",
Copy link
Copy Markdown
Member

@marcospb19 marcospb19 Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if for hardlink we do:

A -> B (hardlink)

instead of

A -> B

To be explicit, I can't think of symbols that could replace the " (hardlink)" suffix

Copy link
Copy Markdown
Contributor Author

@tommady tommady Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if we display hard links with =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not the best idea since only me and you would be able to tell that -> means symlink and => means hardlink.

specially assuming the user will see one or the other, usually, and not both, so they'll just assume it's a symlink I believe

Copy link
Copy Markdown
Contributor Author

@tommady tommady Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since tar itself uses link to to represent hard links:

tar -tvf example.tar
drwxr-xr-x 1000/1000         0 2026-03-06 05:17 example
-rw-r--r-- 1000/1000         0 2026-03-05 08:42 example/hard_link
h--------- 0/0               0 1970-01-01 00:00 example/file link to example/hard_link

maybe we could just stick with the same wording.
WDYT?

Copy link
Copy Markdown
Member

@marcospb19 marcospb19 Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still prefer the -> symbol for symlinks and -> + (hardlink) :v sorry

Similar to what tree -a does in the CLI (the one I always download from pacman).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

roger that~

*CYAN,
name,
*ALL_RESET,
*CYAN,
target.display(),
*ALL_RESET
);
}
} else {
let _ = writeln!(out, "{name}");
}
return;
}

Expand Down Expand Up @@ -151,11 +177,18 @@ mod tree {
};

let _ = write!(out, "{prefix}{final_part}");
let is_dir = match self.file {
Some(FileInArchive { is_dir, .. }) => is_dir,
None => true,
let (is_dir, symlink_target) = match &self.file {
Some(FileInArchive {
is_dir, symlink_target, ..
}) => (*is_dir, symlink_target.clone()),
None => (true, None),
};
super::print_entry(out, <Vec<u8> as ByteVec>::from_os_str_lossy(name).as_bstr(), is_dir);
super::print_entry(
out,
<Vec<u8> as ByteVec>::from_os_str_lossy(name).as_bstr(),
is_dir,
symlink_target,
);

// Construct prefix for children, adding either a line if this isn't
// the last entry in the parent dir or empty space if it is.
Expand Down