Skip to content

feat: migrate color handling to colored_text crate#114

Merged
seapagan merged 7 commits intomainfrom
feat/colored-text-migration
Apr 4, 2026
Merged

feat: migrate color handling to colored_text crate#114
seapagan merged 7 commits intomainfrom
feat/colored-text-migration

Conversation

@seapagan
Copy link
Copy Markdown
Owner

@seapagan seapagan commented Apr 4, 2026

This migrates lsplus from inline_colorization to colored_text for colour output, and adds explicit no-color support.

It also fixes the short-view rendering regressions discovered during the migration by restoring the old aligned behavior and then removing prettytable from short mode entirely.

Key changes:

  • replace inline_colorization crate with colored_text 0.4.1
  • add -N/--no-color and config no_color support
  • keep captured and piped output plain by default
  • restore short colored alignment and move short rendering to a manual renderer
  • keep long format on prettytable for now

Summary by CodeRabbit

  • New Features

    • Added -N / --no-color CLI flag to disable styled/coloured output
    • Added no_color configuration option and support for the NO_COLOR environment variable
  • Behaviour Change

    • Styled/coloured output is now enabled automatically only for interactive terminals; piped/redirected/captured output is plain by default
  • Documentation

    • Updated docs and README to describe the new flag, config option and default terminal detection behavior

seapagan added 3 commits April 4, 2026 16:27
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 676a6ef3-1330-4219-911c-f3ab7265f0b7

📥 Commits

Reviewing files that changed from the base of the PR and between 282fffb and 97c1291.

📒 Files selected for processing (4)
  • src/utils/file.rs
  • src/utils/render.rs
  • tests/crate/file.rs
  • tests/integration.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/crate/file.rs
  • src/utils/file.rs

📝 Walkthrough

Walkthrough

Replaces inline_colorization with colored_text, adds -N/--no-color CLI flag and no_color config, configures global color mode at startup, extends FileInfo with styling metadata, and refactors rendering and file-styling code to use colored_text and dimming semantics.

Changes

Cohort / File(s) Summary
Dependency
Cargo.toml
Swapped dependency: inline_colorization = "0.1.6"colored_text = "0.4.1".
Docs
README.md, docs/src/config.md, docs/src/usage.md
Documented new -N/--no-color flag, no_color config key, and behaviour: styled output enabled only for TTY, plain output for piped/captured streams; added config example.
CLI & Params
src/cli.rs, src/structs.rs, src/lib.rs
Added --no-color (-N) to Flags; added no_color: bool to Params (merged from flags
App flow
src/app.rs
Calls utils::color::configure_color_output(&params) after merging params so global color mode is set before rendering.
Color module
src/utils.rs, src/utils/color.rs
Added pub mod color; new helpers color_mode_for(&Params) -> ColorMode and configure_color_output(&Params) to set global ColorizeConfig.
File styling
src/utils/file.rs
Replaced inline ANSI approach with colored_text APIs; centralized dimming via dimmed flag; introduced format_symlink_display_name_with_dim, colorize_name_by_metadata(..., dimmed), name_style_by_metadata, plain_text, and executable detection; populate FileInfo.short_name, name_style, dimmed.
Rendering
src/utils/render.rs
Rewrote long/short rendering to use colored_text and StyledText; replaced prettytable short-table construction with render_short_format_lines producing padded String lines; added print_table/print_short_lines helpers and plain-width computations.
Tests (unit & integration)
tests/... (many files)
Updated tests to use colored_text and new config flow: added ColorModeGuard/with_color_output_enabled/has_ansi helpers; updated assertions for plain vs coloured output; added tests for color_mode_for; updated test fixtures to include no_color where applicable.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI
    participant App
    participant ColorConfig
    participant Renderer
    participant Output

    User->>CLI: invoke lsplus (args, env, config)
    CLI->>App: run_with_flags(args, config)
    App->>App: Params::merge(flags, config)
    App->>ColorConfig: configure_color_output(&params)
    ColorConfig->>ColorConfig: color_mode_for(params) → ColorMode::Never/Auto
    ColorConfig->>ColorConfig: ColorizeConfig::set_color_mode(...)
    App->>Renderer: display_long_format(...) / display_short_format(...)
    Renderer->>Renderer: build FileInfo (short_name,name_style,dimmed) and StyledText
    Renderer->>Output: print lines (coloured if ColorMode permits)
    Output->>User: visible/plain output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Colours hop, then softly hide,

When pipes are full, they step aside.
A -N carrot stills the light,
Terminals glow, but files stay white.
I nibble code and stamp my paw—hip, hooray! 🌈

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: migration from inline_colorization to colored_text crate, which is the primary objective across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/colored-text-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production
Copy link
Copy Markdown

codacy-production bot commented Apr 4, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 35 complexity · 14 duplication

Metric Results
Complexity 35
Duplication 14

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

@seapagan seapagan self-assigned this Apr 4, 2026
@seapagan seapagan added the enhancement New feature or request label Apr 4, 2026
@seapagan seapagan marked this pull request as ready for review April 4, 2026 16:06
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
tests/integration.rs (1)

36-38: Duplicate has_ansi helper function.

This function is identical to the one in tests/crate/file.rs. Consider extracting it to a shared test utilities module to avoid duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration.rs` around lines 36 - 38, The helper function has_ansi is
duplicated (also present in tests/crate/file.rs); extract it into a shared test
utilities module (e.g., tests/utils.rs or tests/common/mod.rs), move the
has_ansi(text: &str) -> bool implementation there, export it (pub) and update
both callers (tests/integration.rs and tests/crate/file.rs) to use the shared
symbol (crate::tests::utils::has_ansi or appropriate path), then remove the
duplicate definitions from the individual test files.
src/utils/render.rs (2)

116-126: Potential inefficiency: check_display_name called multiple times per item.

In the short format rendering path, check_display_name is called in multiple functions for the same FileInfo:

  • plain_short_cell_content (line 118)
  • short_cell_parts (line 134)
  • Indirectly via short_column_widths and render_short_row

For large directories, this could add up. Consider caching the result or restructuring to compute it once per item.

♻️ Suggested optimisation
-fn short_cell_parts(info: &FileInfo) -> (String, String) {
+fn short_cell_parts(info: &FileInfo, checked_name: &str) -> (String, String) {
     let prefix = info
         .item_icon
         .as_ref()
         .map(|icon| format!("{} ", icon))
         .unwrap_or_default();
-    let display_name = check_display_name(info);
-    let name = if display_name == info.display_name {
+    let name = if checked_name == info.display_name {
         info.short_name.clone()
     } else {
-        strip_str(&display_name)
+        strip_str(checked_name)
     };
     (prefix, name)
 }

Then compute check_display_name once at the row rendering level and pass it through.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/render.rs` around lines 116 - 126, The code calls
check_display_name repeatedly for the same FileInfo (e.g., in
plain_short_cell_content, short_cell_parts, short_column_widths, and
render_short_row); compute check_display_name once per FileInfo at the row
rendering level (for example in render_short_row or the caller that iterates
files) and thread that computed display_name through the helper functions
instead of calling check_display_name again—update plain_short_cell_content and
short_cell_parts to accept a display_name (&str or String) parameter (or return
a tuple that includes it), and change callers (short_column_widths,
render_short_row, etc.) to pass the cached value so each FileInfo’s display name
is determined only once.

143-155: Column width calculation iterates all rows multiple times.

short_column_widths calls plain_short_cell_content for each cell, which in turn calls short_cell_parts and check_display_name. Combined with the earlier call in short_cell_width (via short_column_count), this means each cell's content is computed twice.

For typical directory listings this is likely negligible, but for very large directories it could be noticeable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/render.rs` around lines 143 - 155, The code recomputes each cell's
display string twice because short_column_widths calls plain_short_cell_content
(which calls short_cell_parts and check_display_name) and earlier
short_column_count/short_cell_width also compute it; fix by computing each
cell's plain short content once and reusing it: in the pipeline, either change
short_column_count/short_cell_width to return/store a Vec<Vec<String>> of
plain_short_cell_content (or a single shared Vec of widths) instead of just
counts, or add a new helper that builds a Vec<Vec<String>> of
plain_short_cell_content by iterating rows once and then have
short_column_widths use those precomputed strings to compute visible_text_width;
update references to short_column_count/short_cell_width and short_column_widths
accordingly and remove duplicate plain_short_cell_content calls (affecting
functions short_cell_parts and check_display_name usage).
src/utils/file.rs (2)

291-321: Conditional symlink formatting logic is correct but could be simplified.

The logic correctly handles ignored symlinks by using format_symlink_display_name_with_dim with dimmed=true. However, both branches call the same underlying function with different dimmed values. Consider simplifying:

♻️ Optional simplification
     let (display_name, short_name, name_style) = if metadata.is_symlink() {
         (
-            if ignored {
-                format_symlink_display_name_with_dim(
-                    &safe_file_name,
-                    path,
-                    fs::read_link(path),
-                    params,
-                    true,
-                )
-            } else {
-                format_symlink_display_name(
-                    &safe_file_name,
-                    path,
-                    fs::read_link(path),
-                    params,
-                )
-            },
+            format_symlink_display_name_with_dim(
+                &safe_file_name,
+                path,
+                fs::read_link(path),
+                params,
+                ignored,
+            ),
             format!("{safe_file_name}{}", symlink_short_suffix(params)),
             NameStyle::Symlink,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/file.rs` around lines 291 - 321, The symlink branch duplicates
calls that only differ by a dimmed flag; replace the if metadata.is_symlink()
conditional body to call format_symlink_display_name_with_dim(&safe_file_name,
path, fs::read_link(path), params, ignored) (using ignored as the dimmed flag)
for the display_name, keep the same short_name via format!("{safe_file_name}{}",
symlink_short_suffix(params)), and NameStyle::Symlink; leave the non-symlink
branch unchanged (functions: format_symlink_display_name_with_dim,
format_symlink_display_name, fs::read_link, symlink_short_suffix,
NameStyle::Symlink, colorize_name_by_metadata, name_style_by_metadata).

421-439: name_style_by_metadata duplicates logic from colorize_name_by_metadata.

Both functions determine the file type (symlink, directory, executable, plain) using nearly identical conditional logic. This duplication could lead to inconsistencies if one is updated without the other.

Consider refactoring to have one function call the other, or extract the type determination into a shared helper:

♻️ Suggested refactor to reduce duplication
fn name_style_by_metadata(metadata: &fs::Metadata) -> NameStyle {
    if metadata.is_symlink() {
        NameStyle::Symlink
    } else if metadata.is_dir() {
        NameStyle::Directory
    } else if is_executable(metadata) {
        NameStyle::Executable
    } else {
        NameStyle::Plain
    }
}

fn is_executable(metadata: &fs::Metadata) -> bool {
    #[cfg(unix)]
    { metadata.permissions().mode() & 0o111 != 0 }
    #[cfg(windows)]
    { false }
}

fn colorize_name_by_metadata(
    safe_name: &str,
    metadata: &fs::Metadata,
    dimmed: bool,
) -> String {
    let styled = match name_style_by_metadata(metadata) {
        NameStyle::Symlink => safe_name.cyan(),
        NameStyle::Directory => safe_name.blue(),
        NameStyle::Executable => safe_name.green().bold(),
        NameStyle::Plain => StyledText::plain(safe_name),
    };
    apply_dim(styled, dimmed).to_string()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/file.rs` around lines 421 - 439, name_style_by_metadata duplicates
file-type/executable detection logic present in colorize_name_by_metadata;
extract the executable check into a shared helper (e.g., is_executable(metadata:
&fs::Metadata) -> bool with #[cfg(unix)] and #[cfg(windows)] branches) and then
simplify name_style_by_metadata to use that helper (returning
NameStyle::Symlink, Directory, Executable, or Plain), and update
colorize_name_by_metadata to call name_style_by_metadata(metadata) and match on
its result to pick styling; this removes duplicated conditional logic and
centralizes platform-specific executable detection.
tests/crate/file.rs (1)

73-79: Hardcoded ANSI escape sequences may be fragile.

The assertions use raw ANSI sequences like "\u{1b}[34m.\u{1b}[0m" which tightly couple the test to the exact output format of colored_text. If the library changes its reset sequence or uses different SGR codes, these tests will break.

Consider using the has_ansi helper combined with content checks, or extract the expected sequences into named constants for easier maintenance:

const BLUE_DOT: &str = "\u{1b}[34m.\u{1b}[0m";
assert_eq!(check_display_name(&dot), BLUE_DOT);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/crate/file.rs` around lines 73 - 79, The tests assert exact ANSI escape
sequences for colored output in with_color_output_enabled() using
basic_info(...) and check_display_name(...); replace these fragile assertions by
either (a) using a helper like has_ansi(&s) to assert that the string contains
ANSI sequences and separately assert the visible content (e.g., "." or ".."), or
(b) extract the expected sequence into a named constant (e.g., BLUE_DOT) and use
that constant in assert_eq! to centralize the value; update the two assertions
for dot and dotdot accordingly so tests no longer depend on inline raw SGR
bytes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/crate/render.rs`:
- Around line 43-68: Tests mutate global ColorizeConfig without synchronization;
make those mutations serial by introducing a global Mutex and have
ColorModeGuard hold the lock while it saves/restores state. Add a static lock
(e.g., once_cell::sync::Lazy<Mutex<()>> or parking_lot::Mutex) and change
ColorModeGuard to store the MutexGuard alongside the previous ColorMode so
acquiring the lock happens in ColorModeGuard::set and the guard is dropped in
Drop (releasing the lock) after restoring the previous mode; update
with_color_output_enabled to create the ColorModeGuard as before so the lock is
held for the test duration. Ensure references to ColorModeGuard, ColorizeConfig,
with_color_output_enabled and has_ansi are used to locate the changes.

In `@tests/integration.rs`:
- Around line 360-361: The test
test_gitignore_flag_dims_ignored_entries_in_short_output currently asserts no
ANSI codes (assert!(!has_ansi(...))) which contradicts its intent to verify
dimming; either rename the test to reflect plain/captured output (e.g.,
test_gitignore_flag_output_is_plain_when_captured) or change the test to force
colour and capture raw output: use the runner helper run_and_capture_raw (or
equivalent) and pass the --color=always flag, then assert that ignored_line
contains ANSI dim codes (using has_ansi or a specific dim-sequence check) while
visible_line shows expected colouring; update assertions and test name
accordingly.

---

Nitpick comments:
In `@src/utils/file.rs`:
- Around line 291-321: The symlink branch duplicates calls that only differ by a
dimmed flag; replace the if metadata.is_symlink() conditional body to call
format_symlink_display_name_with_dim(&safe_file_name, path, fs::read_link(path),
params, ignored) (using ignored as the dimmed flag) for the display_name, keep
the same short_name via format!("{safe_file_name}{}",
symlink_short_suffix(params)), and NameStyle::Symlink; leave the non-symlink
branch unchanged (functions: format_symlink_display_name_with_dim,
format_symlink_display_name, fs::read_link, symlink_short_suffix,
NameStyle::Symlink, colorize_name_by_metadata, name_style_by_metadata).
- Around line 421-439: name_style_by_metadata duplicates file-type/executable
detection logic present in colorize_name_by_metadata; extract the executable
check into a shared helper (e.g., is_executable(metadata: &fs::Metadata) -> bool
with #[cfg(unix)] and #[cfg(windows)] branches) and then simplify
name_style_by_metadata to use that helper (returning NameStyle::Symlink,
Directory, Executable, or Plain), and update colorize_name_by_metadata to call
name_style_by_metadata(metadata) and match on its result to pick styling; this
removes duplicated conditional logic and centralizes platform-specific
executable detection.

In `@src/utils/render.rs`:
- Around line 116-126: The code calls check_display_name repeatedly for the same
FileInfo (e.g., in plain_short_cell_content, short_cell_parts,
short_column_widths, and render_short_row); compute check_display_name once per
FileInfo at the row rendering level (for example in render_short_row or the
caller that iterates files) and thread that computed display_name through the
helper functions instead of calling check_display_name again—update
plain_short_cell_content and short_cell_parts to accept a display_name (&str or
String) parameter (or return a tuple that includes it), and change callers
(short_column_widths, render_short_row, etc.) to pass the cached value so each
FileInfo’s display name is determined only once.
- Around line 143-155: The code recomputes each cell's display string twice
because short_column_widths calls plain_short_cell_content (which calls
short_cell_parts and check_display_name) and earlier
short_column_count/short_cell_width also compute it; fix by computing each
cell's plain short content once and reusing it: in the pipeline, either change
short_column_count/short_cell_width to return/store a Vec<Vec<String>> of
plain_short_cell_content (or a single shared Vec of widths) instead of just
counts, or add a new helper that builds a Vec<Vec<String>> of
plain_short_cell_content by iterating rows once and then have
short_column_widths use those precomputed strings to compute visible_text_width;
update references to short_column_count/short_cell_width and short_column_widths
accordingly and remove duplicate plain_short_cell_content calls (affecting
functions short_cell_parts and check_display_name usage).

In `@tests/crate/file.rs`:
- Around line 73-79: The tests assert exact ANSI escape sequences for colored
output in with_color_output_enabled() using basic_info(...) and
check_display_name(...); replace these fragile assertions by either (a) using a
helper like has_ansi(&s) to assert that the string contains ANSI sequences and
separately assert the visible content (e.g., "." or ".."), or (b) extract the
expected sequence into a named constant (e.g., BLUE_DOT) and use that constant
in assert_eq! to centralize the value; update the two assertions for dot and
dotdot accordingly so tests no longer depend on inline raw SGR bytes.

In `@tests/integration.rs`:
- Around line 36-38: The helper function has_ansi is duplicated (also present in
tests/crate/file.rs); extract it into a shared test utilities module (e.g.,
tests/utils.rs or tests/common/mod.rs), move the has_ansi(text: &str) -> bool
implementation there, export it (pub) and update both callers
(tests/integration.rs and tests/crate/file.rs) to use the shared symbol
(crate::tests::utils::has_ansi or appropriate path), then remove the duplicate
definitions from the individual test files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8c11aff4-0e43-4be1-8552-e4c1bf450022

📥 Commits

Reviewing files that changed from the base of the PR and between e848d13 and 282fffb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • Cargo.toml
  • README.md
  • docs/src/config.md
  • docs/src/usage.md
  • src/app.rs
  • src/cli.rs
  • src/lib.rs
  • src/structs.rs
  • src/utils.rs
  • src/utils/color.rs
  • src/utils/file.rs
  • src/utils/render.rs
  • tests/crate/app.rs
  • tests/crate/cli.rs
  • tests/crate/file.rs
  • tests/crate/render.rs
  • tests/crate/settings.rs
  • tests/integration.rs
  • tests/seams.rs
  • tests/structs.rs

Signed-off-by: Grant Ramsay <seapagan@gmail.com>
@seapagan seapagan changed the title feat: migrate color handling to colored_text feat: migrate color handling to colored_text crate Apr 4, 2026
seapagan added 3 commits April 4, 2026 18:21
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
@seapagan seapagan merged commit 9996688 into main Apr 4, 2026
5 checks passed
@seapagan seapagan deleted the feat/colored-text-migration branch April 4, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant