Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
57f5055
Initial plan
Copilot Mar 6, 2026
0bf8a92
Add --color flag and Visualizer coloring support
Copilot Mar 6, 2026
eb866f3
Address review feedback: Color enum, minimize const visibility, simpl…
Copilot Mar 6, 2026
80e99c3
Address second review round: rename Colorless→Normal, refactor is_dir…
Copilot Mar 6, 2026
33316ce
Address third review round: ColoredSlice, rename coloring_map/is_dir,…
Copilot Mar 6, 2026
f622673
feat: lscolors integration, multi-color support, render_row macro, de…
Copilot Mar 7, 2026
c0d0062
refactor: single LazyLock<AnsiPrefixes> + format_args! with row destr…
Copilot Mar 7, 2026
3c22b24
refactor: rename aligned_colored/aligned_normal to aligned_colored_sl…
Copilot Mar 7, 2026
9e47e4d
refactor: consolidate AnsiPrefixes at crate root; Coloring struct; Co…
Copilot Mar 7, 2026
ab5cc0a
refactor(r7): rename AnsiPrefixes→LsColors, private fields, restructu…
Copilot Mar 7, 2026
bc6faff
refactor(r8): rename field, minimize visibility, move color calc, div…
Copilot Mar 7, 2026
92cede4
test: add predefined LS_COLORS constant to color tests
Copilot Mar 7, 2026
1b2eb7f
Replace unsafe set_var in color_always test with LsColors FromStr
Copilot Mar 7, 2026
c78baaa
Address review comments: refactor LsColors, add color test without LS…
Copilot Mar 7, 2026
2c0b44f
fix: rename LsColors::from_str to from_ls_colors_string to fix clippy…
Copilot Mar 7, 2026
d78e31e
fix: use full PathBuf key in build_coloring_map to prevent basename c…
Copilot Mar 7, 2026
1d3a5d3
chore: remove dead src/ansi_prefixes.rs file
Copilot Mar 7, 2026
860d755
refactor: address review threads 71-75: remove Hash+Eq bounds, rename…
Copilot Mar 7, 2026
2f6425a
revert: remove color feature flag, make lscolors an unconditional dep…
Copilot Mar 7, 2026
7fb5375
Initial plan
Copilot Mar 7, 2026
5eb9190
Merge branch 'pr339' into copilot/address-pr-339-discussion-point
Copilot Mar 7, 2026
6c4da4c
refactor: use Vec<OsString> as coloring map keys instead of PathBuf
Copilot Mar 7, 2026
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
48 changes: 48 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ rayon = "1.10.0"
rounded-div = "0.1.4"
serde = { version = "1.0.228", optional = true }
serde_json = { version = "1.0.149", optional = true }
lscolors = { version = "0.21", features = ["nu-ansi-term"] }
smart-default = "0.7.1"
sysinfo = "0.38.2"
terminal_size = "0.4.3"
Expand All @@ -83,3 +84,4 @@ maplit = "1.0.2"
normalize-path = "0.2.1"
pretty_assertions = "1.4.1"
rand = "0.10.0"
strip-ansi-escapes = "0.2.1"
11 changes: 11 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ Do not output `.shared.details` in the JSON output.

Do not output `.shared.summary` in the JSON output.

<a id="color" name="color"></a>
### `--color`

* _Default:_ `auto`.
* _Choices:_
- `auto`: Detect if the output is a TTY and render colors accordingly
- `always`: Always render colors
- `never`: Never render colors

Whether to show colors.

<a id="option-h" name="option-h"></a><a id="help" name="help"></a>
### `--help`

Expand Down
6 changes: 5 additions & 1 deletion exports/completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ _pdu() {

case "${cmd}" in
pdu)
opts="-b -H -q -d -w -m -s -p -h -V --json-input --json-output --bytes-format --detect-links --dedupe-links --deduplicate-hardlinks --top-down --align-right --quantity --depth --max-depth --width --total-width --column-width --min-ratio --no-sort --no-errors --silent-errors --progress --threads --omit-json-shared-details --omit-json-shared-summary --help --version [FILES]..."
opts="-b -H -q -d -w -m -s -p -h -V --json-input --json-output --bytes-format --detect-links --dedupe-links --deduplicate-hardlinks --top-down --align-right --quantity --depth --max-depth --width --total-width --column-width --min-ratio --no-sort --no-errors --silent-errors --progress --threads --omit-json-shared-details --omit-json-shared-summary --color --help --version [FILES]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
Expand Down Expand Up @@ -85,6 +85,10 @@ _pdu() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--color)
COMPREPLY=($(compgen -W "auto always never" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
Expand Down
1 change: 1 addition & 0 deletions exports/completion.elv
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ set edit:completion:arg-completer[pdu] = {|@words|
cand -m 'Minimal size proportion required to appear'
cand --min-ratio 'Minimal size proportion required to appear'
cand --threads 'Set the maximum number of threads to spawn. Could be either "auto", "max", or a positive integer'
cand --color 'Whether to show colors'
cand --json-input 'Read JSON data from stdin'
cand --json-output 'Print JSON data instead of an ASCII chart'
cand -H 'Detect and subtract the sizes of hardlinks from their parent directory totals'
Expand Down
3 changes: 3 additions & 0 deletions exports/completion.fish
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ complete -c pdu -s w -l total-width -l width -d 'Width of the visualization' -r
complete -c pdu -l column-width -d 'Maximum widths of the tree column and width of the bar column' -r
complete -c pdu -s m -l min-ratio -d 'Minimal size proportion required to appear' -r
complete -c pdu -l threads -d 'Set the maximum number of threads to spawn. Could be either "auto", "max", or a positive integer' -r
complete -c pdu -l color -d 'Whether to show colors' -r -f -a "auto\t'Detect if the output is a TTY and render colors accordingly'
always\t'Always render colors'
never\t'Never render colors'"
complete -c pdu -l json-input -d 'Read JSON data from stdin'
complete -c pdu -l json-output -d 'Print JSON data instead of an ASCII chart'
complete -c pdu -s H -l deduplicate-hardlinks -l detect-links -l dedupe-links -d 'Detect and subtract the sizes of hardlinks from their parent directory totals'
Expand Down
1 change: 1 addition & 0 deletions exports/completion.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Register-ArgumentCompleter -Native -CommandName 'pdu' -ScriptBlock {
[CompletionResult]::new('-m', '-m', [CompletionResultType]::ParameterName, 'Minimal size proportion required to appear')
[CompletionResult]::new('--min-ratio', '--min-ratio', [CompletionResultType]::ParameterName, 'Minimal size proportion required to appear')
[CompletionResult]::new('--threads', '--threads', [CompletionResultType]::ParameterName, 'Set the maximum number of threads to spawn. Could be either "auto", "max", or a positive integer')
[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'Whether to show colors')
[CompletionResult]::new('--json-input', '--json-input', [CompletionResultType]::ParameterName, 'Read JSON data from stdin')
[CompletionResult]::new('--json-output', '--json-output', [CompletionResultType]::ParameterName, 'Print JSON data instead of an ASCII chart')
[CompletionResult]::new('-H', '-H ', [CompletionResultType]::ParameterName, 'Detect and subtract the sizes of hardlinks from their parent directory totals')
Expand Down
3 changes: 3 additions & 0 deletions exports/completion.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ block-count\:"Count numbers of blocks"))' \
'-m+[Minimal size proportion required to appear]:MIN_RATIO:_default' \
'--min-ratio=[Minimal size proportion required to appear]:MIN_RATIO:_default' \
'--threads=[Set the maximum number of threads to spawn. Could be either "auto", "max", or a positive integer]:THREADS:_default' \
'--color=[Whether to show colors]:COLOR:((auto\:"Detect if the output is a TTY and render colors accordingly"
always\:"Always render colors"
never\:"Never render colors"))' \
'(-q --quantity -H --deduplicate-hardlinks)--json-input[Read JSON data from stdin]' \
'--json-output[Print JSON data instead of an ASCII chart]' \
'-H[Detect and subtract the sizes of hardlinks from their parent directory totals]' \
Expand Down
10 changes: 10 additions & 0 deletions exports/long.help
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ Options:
--omit-json-shared-summary
Do not output `.shared.summary` in the JSON output

--color <COLOR>
Whether to show colors

Possible values:
- auto: Detect if the output is a TTY and render colors accordingly
- always: Always render colors
- never: Never render colors

[default: auto]

-h, --help
Print help (see a summary with '-h')

Expand Down
2 changes: 2 additions & 0 deletions exports/short.help
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Options:
Do not output `.shared.details` in the JSON output
--omit-json-shared-summary
Do not output `.shared.summary` in the JSON output
--color <COLOR>
Whether to show colors [default: auto] [possible values: auto, always, never]
-h, --help
Print help (see more with '--help')
-V, --version
Expand Down
16 changes: 14 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ pub mod sub;
pub use sub::Sub;

use crate::{
args::{Args, Quantity, Threads},
args::{Args, ColorWhen, Quantity, Threads},
bytes_format::BytesFormat,
get_size::{GetApparentSize, GetSize},
hardlink,
json_data::{JsonData, JsonDataBody, JsonShared, JsonTree},
ls_colors::LsColors,
reporter::{ErrorOnlyReporter, ErrorReport, ProgressAndErrorReporter, ProgressReport},
runtime_error::RuntimeError,
size,
Expand All @@ -16,7 +17,10 @@ use crate::{
use clap::Parser;
use hdd::any_path_is_in_hdd;
use pipe_trait::Pipe;
use std::{io::stdin, time::Duration};
use std::{
io::{stdin, stdout, IsTerminal},
time::Duration,
};
use sub::JsonOutputParam;
use sysinfo::Disks;

Expand Down Expand Up @@ -86,6 +90,7 @@ impl App {
column_width_distribution,
direction,
bar_alignment,
coloring: None,
};

let JsonShared { details, summary } = shared;
Expand Down Expand Up @@ -169,6 +174,12 @@ impl App {
ErrorReport::TEXT
};

let color = match self.args.color {
ColorWhen::Always => Some(LsColors::from_env()),
ColorWhen::Never => None,
ColorWhen::Auto => stdout().is_terminal().then(LsColors::from_env),
};

trait GetSizeUtils: GetSize<Size: size::Size> {
const INSTANCE: Self;
const QUANTITY: Quantity;
Expand Down Expand Up @@ -307,6 +318,7 @@ impl App {
max_depth,
min_ratio,
no_sort,
color,
}
.run(),
)*} };
Expand Down
72 changes: 70 additions & 2 deletions src/app/sub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ use crate::{
get_size::GetSize,
hardlink::{DeduplicateSharedSize, HardlinkIgnorant, RecordHardlinks},
json_data::{BinaryVersion, JsonData, JsonDataBody, JsonShared, JsonTree, SchemaVersion},
ls_colors::LsColors,
os_string_display::OsStringDisplay,
reporter::ParallelReporter,
runtime_error::RuntimeError,
size,
status_board::GLOBAL_STATUS_BOARD,
visualizer::{BarAlignment, ColumnWidthDistribution, Direction, Visualizer},
visualizer::{BarAlignment, Color, Coloring, ColumnWidthDistribution, Direction, Visualizer},
};
use pipe_trait::Pipe;
use serde::Serialize;
use std::{io::stdout, iter::once, path::PathBuf};
use std::{
collections::HashMap,
ffi::OsString,
io::stdout,
iter::once,
path::{Path, PathBuf},
};

/// The sub program of the main application.
pub struct Sub<Size, SizeGetter, HardlinksHandler, Report>
Expand Down Expand Up @@ -49,6 +56,8 @@ where
pub min_ratio: Fraction,
/// Preserve order of entries.
pub no_sort: bool,
/// Whether to color the output.
pub color: Option<LsColors>,
}

impl<Size, SizeGetter, HardlinksHandler, Report> Sub<Size, SizeGetter, HardlinksHandler, Report>
Expand All @@ -74,6 +83,7 @@ where
reporter,
min_ratio,
no_sort,
color,
} = self;

let max_depth = max_depth.get();
Expand All @@ -98,6 +108,7 @@ where
files: vec![".".into()],
hardlinks_handler,
reporter,
color,
..self
}
.run();
Expand Down Expand Up @@ -187,12 +198,19 @@ where
.or(deduplication_result);
}

let coloring: Option<Coloring> = color.map(|ls_colors| {
let mut map = HashMap::new();
build_coloring_map(&data_tree, &mut Vec::new(), &mut map);
Coloring::new(ls_colors, map)
});

let visualizer = Visualizer {
data_tree: &data_tree,
bytes_format,
direction,
bar_alignment,
column_width_distribution,
coloring: coloring.as_ref(),
};

print!("{visualizer}"); // visualizer already ends with "\n", println! isn't needed here.
Expand Down Expand Up @@ -262,5 +280,55 @@ where
}
}

/// Recursively walk a pruned [`DataTree`] and build a map of path-component vectors to [`Color`] values.
///
/// The `path_stack` argument is a reusable buffer of path components representing the current
/// ancestor chain. Each recursive call pushes the node's name and pops it on return, so no
/// cloning occurs during traversal — only at leaf insertions.
/// Leaf nodes (files or childless directories after pruning) are added to the map.
/// Nodes with children are skipped because the [`Visualizer`] uses the children count to
/// determine their color at render time.
fn build_coloring_map(
node: &DataTree<OsStringDisplay, impl size::Size>,
path_stack: &mut Vec<OsString>,
map: &mut HashMap<Vec<OsString>, Color>,
) {
path_stack.push(node.name().as_os_str().to_os_string());
if node.children().is_empty() {
let color = file_color(&path_stack.iter().collect::<PathBuf>());
map.insert(path_stack.clone(), color);
} else {
for child in node.children() {
build_coloring_map(child, path_stack, map);
}
}
path_stack.pop();
}

fn file_color(path: &Path) -> Color {
if path.is_symlink() {
Color::Symlink
} else if path.is_dir() {
Color::Directory
} else if is_executable(path) {
Color::Executable
} else {
Color::Normal
}
}

#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
path.metadata()
.map(|stats| stats.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}

#[cfg(not(unix))]
fn is_executable(_path: &Path) -> bool {
false
}

#[cfg(unix)]
mod unix_ext;
Loading
Loading