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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ Curently, only a sub-set of the standard `ls` options are supported. These are:
- `-l` / `--long` - Show long format listing
- `-h` / `--human-readable` - Human readable file sizes
- `-D` / `--sort-dirs` - Sort directories first
- `-I` / `--gitignore` - Dim entries matched by Git ignore rules
- `--no-icons` - don't show file or folder icons
- `-V` / `--version` - Print version information and exit
- `-Z` / `--fuzzy-time` - Show fuzzy time for file modification times

You can combine the short options together, e.g. `-laph` will show a long format
Expand All @@ -111,6 +113,10 @@ file sizes.

Use the `--help` option to see the full list of options.

When `-I` is enabled, `lsp` checks the same ignore sources Git normally uses:
merged `.gitignore` files in the worktree, `.git/info/exclude`, and the
configured global Git excludes file.

The long-format listing is currently colorized by default and cannot be
disabled. This will be made configurable in the future along with adding more
of the original `ls` options.
Expand All @@ -129,7 +135,7 @@ mappings implemented at the moment, but more will be added in the future. Add
an issue if you have a specific icon you would like to see - even better, add
a Pull Request implementing it! 😁

You can disable the icons by using the `-no-icons` option.
You can disable the icons by using the `--no-icons` option.

### Configuration File

Expand Down
2 changes: 0 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
customizable and theme-able. Make it default but allow an option to
disable it (or vice-versa). Files that have a known extension should all
be colored the same way, and different to unknown file tipes.
- [ ] for a symlink, color the name as it is, but color the target depending on
whether it is a directory, file, or symlink.
- [ ] Add icons for partials like `TODO.*`, `LICENSE.*` and more - brands like
claude, codex, vscode and more where the nerdfont exists
- [ ] using the config file, allow extending the existing file and folder
Expand Down
3 changes: 2 additions & 1 deletion docs/src/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Curently, only a sub-set of the standard `ls` options are supported. These are:
- `-D` / `--sort-dirs` - Sort directories first
- `-I` / `--gitignore` - Dim entries matched by Git ignore rules
- `--no-icons` - don't show file or folder icons
- `-V` / `--version` - Print version information and exit
- `-Z` / `--fuzzy-time` - Show fuzzy time for file modification times

You can combine the short options together, e.g. `-laph` will show a long format
Expand Down Expand Up @@ -50,7 +51,7 @@ mappings implemented at the moment, but more will be added in the future. Add
an issue if you have a specific icon you would like to see - even better, add
a Pull Request implementing it! :grin:

You can disable the icons by using the `-no-icons` option.
You can disable the icons by using the `--no-icons` option.

## Aliases

Expand Down
47 changes: 30 additions & 17 deletions src/utils/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ struct FileDetails {
mtime: SystemTime,
user: String,
group: String,
executable: bool,
}

const STYLE_DIM: &str = "\x1B[2m";
Expand Down Expand Up @@ -60,13 +59,6 @@ fn get_file_details(metadata: &fs::Metadata) -> FileDetails {

let mtime = metadata.modified().unwrap();

#[cfg(unix)]
let executable = metadata.permissions().mode() & 0o111 != 0;

// for now just return false under windows
#[cfg(windows)]
let executable = false;

FileDetails {
file_type,
mode: rwx_mode,
Expand All @@ -75,7 +67,6 @@ fn get_file_details(metadata: &fs::Metadata) -> FileDetails {
mtime,
user,
group,
executable,
}
}

Expand Down Expand Up @@ -306,15 +297,8 @@ fn create_file_info_from_metadata_with_gitignore(
fs::read_link(path),
params,
)
} else if metadata.is_dir() {
format!("{color_blue}{}", safe_file_name)
} else if details.executable {
format!("{style_bold}{color_green}{}", safe_file_name)
} else {
// Regular files must have explicit color formatting (even if just reset)
// to ensure consistent ANSI escape sequence handling across all file types.
// This maintains proper alignment in table display format.
format!("{color_reset}{}", safe_file_name)
colorize_name_by_metadata(&safe_file_name, metadata)
};
let display_name = if params.gitignore
&& gitignore_cache.is_ignored(path, metadata.is_dir())
Expand Down Expand Up @@ -411,6 +395,29 @@ fn symlink_short_suffix(params: &Params) -> &'static str {
if params.append_slash { "*" } else { "" }
}

fn colorize_name_by_metadata(
safe_name: &str,
metadata: &fs::Metadata,
) -> String {
if metadata.is_symlink() {
format!("{color_cyan}{safe_name}")
} else if metadata.is_dir() {
format!("{color_blue}{safe_name}")
} else {
#[cfg(unix)]
let executable = metadata.permissions().mode() & 0o111 != 0;

#[cfg(windows)]
let executable = false;

if executable {
format!("{style_bold}{color_green}{safe_name}")
} else {
format!("{color_reset}{safe_name}")
}
}
}

pub(crate) fn format_symlink_display_name(
safe_file_name: &str,
path: &Path,
Expand All @@ -426,6 +433,12 @@ pub(crate) fn format_symlink_display_name(
};
let display_target = sanitize_path_for_terminal(&target_path);
if params.long_format {
let display_target = fs::symlink_metadata(&target_path)
.map(|metadata| {
colorize_name_by_metadata(&display_target, &metadata)
})
.unwrap_or(display_target);
Comment thread
seapagan marked this conversation as resolved.

if target_path.exists() {
format!(
"{color_cyan}{} -> {}",
Expand Down
58 changes: 57 additions & 1 deletion tests/crate/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use crate::utils::file::{
get_groupname, get_username, sanitize_for_terminal,
};
use crate::{FileInfo, Params};
use inline_colorization::{color_blue, color_green};
use inline_colorization::{
color_blue, color_cyan, color_green, color_reset, style_bold,
};
use std::ffi::OsString;
use std::fs::{self, File};
use std::io;
Expand Down Expand Up @@ -478,6 +480,60 @@ fn test_format_symlink_display_name_short_format_marks_append_slash() {
assert!(short.contains('*'));
}

#[test]
#[cfg(unix)]
fn test_format_symlink_display_name_colors_long_format_targets_by_type() {
let temp_dir = tempdir().unwrap();
let dir_target = temp_dir.path().join("dir-target");
let file_target = temp_dir.path().join("file-target.txt");
let symlink_target = temp_dir.path().join("symlink-target");
let exec_target = temp_dir.path().join("exec-target.sh");

fs::create_dir(&dir_target).unwrap();
fs::write(&file_target, "file").unwrap();
fs::write(&exec_target, "#!/bin/sh\nexit 0\n").unwrap();
fs::set_permissions(&exec_target, fs::Permissions::from_mode(0o755))
.unwrap();
std::os::unix::fs::symlink(&file_target, &symlink_target).unwrap();

let params = Params {
long_format: true,
..Params::default()
};

let dir_display = format_symlink_display_name(
"dir-link",
&temp_dir.path().join("dir-link"),
Ok(PathBuf::from("dir-target")),
&params,
);
assert!(dir_display.contains(&format!("-> {color_blue}")));

let file_display = format_symlink_display_name(
"file-link",
&temp_dir.path().join("file-link"),
Ok(PathBuf::from("file-target.txt")),
&params,
);
assert!(file_display.contains(&format!("-> {color_reset}")));

let symlink_display = format_symlink_display_name(
"symlink-link",
&temp_dir.path().join("symlink-link"),
Ok(PathBuf::from("symlink-target")),
&params,
);
assert!(symlink_display.contains(&format!("-> {color_cyan}")));

let exec_display = format_symlink_display_name(
"exec-link",
&temp_dir.path().join("exec-link"),
Ok(PathBuf::from("exec-target.sh")),
&params,
);
assert!(exec_display.contains(&format!("-> {style_bold}{color_green}")));
}

#[test]
fn test_format_symlink_display_name_unreadable_short_format_omits_marker_without_append_slash()
{
Expand Down
8 changes: 7 additions & 1 deletion tests/integration.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use assert_cmd::Command;
use filetime::FileTime;
use inline_colorization::color_reset;
use lsplus::utils::icons::Icon;
use std::fs;
use std::time::{Duration, SystemTime};
Expand Down Expand Up @@ -476,11 +477,16 @@ fn test_long_format_renders_symlink_icon() {
temp_env::with_var("HOME", Some(temp_dir.path()), || {
let mut cmd = Command::cargo_bin("lsp").unwrap();
cmd.arg("-l").arg(&link);
let (stdout, _stderr) = run_and_capture(&mut cmd);
let (stdout_raw, _stderr) = run_and_capture_raw(&mut cmd);
let stdout = strip_str(&stdout_raw).to_string();

assert!(stdout.contains(&Icon::Symlink.to_string()));
assert!(stdout.contains("link.txt"));
assert!(stdout.contains("->"));
assert!(stdout_raw.contains(&format!(
"-> {color_reset}{}",
target.to_string_lossy()
)));
});
}

Expand Down