Skip to content

fix: resolve 33 confirmed issues from scan audit#57

Merged
leszek3737 merged 3 commits into
mainfrom
fix55
May 25, 2026
Merged

fix: resolve 33 confirmed issues from scan audit#57
leszek3737 merged 3 commits into
mainfrom
fix55

Conversation

@leszek3737
Copy link
Copy Markdown
Owner

@leszek3737 leszek3737 commented May 25, 2026

Summary

Refine file search, sorting, viewer image preview, and panel behavior while tightening filesystem watcher safety and configuration handling, and expand tests to cover new helpers and edge cases.

New Features:

  • Introduce an asynchronous image preview loader with background chafa rendering and main-loop integration.
  • Add helper APIs for toggling active panels, restoring previous modes, pushing shell history entries, and managing overwrite flags on pending actions.

Bug Fixes:

  • Fix a TOCTOU vulnerability in recursive directory deletion by avoiding cross-root canonicalization checks during traversal.
  • Correct watcher rename timeout handling so stale "rename-from" entries only emit delete events if the pending state still matches.
  • Prevent image preview race conditions by associating loader results with the originating viewer path and cancelling outdated loaders.
  • Avoid creating path indexes or removing entries for non-existent paths in panels, and ensure watcher-driven removes respect the path index.
  • Ensure selection-based operations prefer selected entries using an explicit selected_count instead of inferring from selection vectors.
  • Harden environment variable expansion for brace-style variables, correctly handling empty and invalid names.
  • Avoid double-adding or overgrowing the shell command history, and ignore empty commands via a shared helper.
  • Ensure dialogs and search/menu modes reliably restore the previous app mode via a dedicated helper, including in tests.
  • Fix log rotation tests and constants by expressing max log size in MiB and using the renamed constant consistently.
  • Ensure progress dialogs and pickers/search modes handle empty or edge-case states gracefully without leaving the app in an inconsistent mode.

Enhancements:

  • Refactor case-insensitive substring search into a shared helper reused by filename and content search patterns.
  • Unify file search recursion into a single cancellable implementation via a context object, simplifying cancel checks.
  • Rework entry sorting to use direct comparators instead of precomputed key tuples and introduce case-aware name/extension comparison helpers.
  • Migrate the legacy byte-based natural sort implementation into test-only code while keeping behavior coverage via new tests.
  • Extract shared time formatting into a reusable function used by both generic time formatting and file entry display timestamps.
  • Optimize panel line and status bar string construction to preallocate buffers and avoid intermediate format! allocations.
  • Refine input dialog rendering to compute grapheme widths on the fly, simplifying scroll and cursor column calculations.
  • Expose and reuse panel error update logic and path index rebuilding helpers across reader and watcher sync code.
  • Refine dialog and menu handling to use the structured Progress dialog variant and centralized mode restoration logic.

Tests:

  • Consolidate file color tests into a macro-based suite and add coverage for truncate_name with empty input.
  • Add tests for the legacy natural sort behavior, image preview loader race-guard semantics, and viewer rendering changes.
  • Extend key event, pickers, search, history, and miscellaneous tests to use shared helpers and to assert correct mode restoration and no-op behavior on empty state transitions.
  • Update dialog, mouse, and job-runner tests to cover the new structured Progress dialog variant and overwrite handling via PendingAction methods.
  • Add configuration-loading tests using an isolated environment provider to verify behavior when no config file is present.
  • Strengthen comparison and selection tests to validate confirm dialogs, selection precedence, and path choice behavior.

- Fala 1: Magic numbers, doc comments, redundant ops, deprecated constants
- Fala 2: Extract shared helpers, unify duplicate functions, DRY patterns
- Fala 3: Ghost test assertions, production code in tests, isolated config test
- Fala 4: Zero-alloc render paths, iterator-based dialogs, repeat_n padding
- Fala 5: O(1) path_index lookups, zero-alloc sort comparators, O(n) dedup
- Fala 6: TOCTOU watcher fix, async image preview, unconditional cleanup
- Review: Image preview race guard, loader cleanup, DRY fallback, tests

957 tests pass, zero clippy warnings.
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 25, 2026

Reviewer's Guide

Refactors multiple subsystems (search, sorting, viewer image preview, dialogs, shell/history, panel indexing, config loading, watcher sync, deletion safety, and tests) to remove duplication, improve performance/robustness, and tighten API shapes while fixing race conditions and TOCTOU-style issues surfaced by a scan audit.

Sequence diagram for asynchronous image preview loading and race guard

sequenceDiagram
    participant Main as run_app
    participant VS as viewer::ViewerState
    participant IPL as viewer::ImagePreviewLoader
    participant Chafa as run_chafa

    Main->>Main: start_image_preview_if_needed(viewer_state, image_preview_loader, terminal_size)
    alt VS.view_mode == ViewMode::Image && VS.needs_image_preview()
        opt existing loader
            Main->>IPL: cancel()
            Main->>Main: image_preview_loader.take()
        end
        Main->>IPL: ImagePreviewLoader::start(VS.file_path.clone(), cw, ch)
        activate IPL
        IPL->>Chafa: run_chafa(path, width, height)
        Chafa-->>IPL: Text
        IPL-->>Main: tx.send((width, height, text))
        deactivate IPL
    end

    loop main loop
        Main->>Main: poll_image_preview(&mut viewer_state, &mut image_preview_loader)
        alt loader.try_recv() == Ok((w, h, text))
            Main->>VS: compare vs.file_path == loader.file_path
            alt paths match
                Main->>VS: set_image_preview(w, h, text)
                Main->>Main: image_preview_loader = None
            else paths differ
                Main->>Main: image_preview_loader = None (discard stale preview)
            end
        else Disconnected
            Main->>Main: image_preview_loader = None
        end
    end
Loading

File-Level Changes

Change Details Files
Deduplicate and harden search logic and case-insensitive matching, and simplify cancellation handling in file search.
  • Extract a shared contains_case_insensitive helper used by both CompiledPattern and content search paths to remove duplicated implementations.
  • Unify cancellable and non-cancellable file search recursion into a single search_files_recursive that consults an optional AtomicBool via FileSearchContext::is_cancelled.
  • Adjust FileSearch call sites to construct FileSearchContext with or without a cancel flag and update usages of the cancellation check within recursion.
src/ops/search.rs
Replace sort-by-key allocations with comparator-based sorting and centralize case-insensitive name/extension comparisons.
  • Switch sort_entries to use sort_by with cmp_group, cmp_name, and cmp_ext comparators instead of sort_by_cached_key and tuple key builders.
  • Remove the various *sort_key helper types/functions and implement cmp_group, cmp_name (with stable case-insensitive tie-breaking), and cmp_ext as the canonical comparison operations.
  • Keep natural sort modes using existing key-based approach but remove redundant low-level natsort comparator from production code, moving legacy natsort into tests-only module for regression coverage.
src/ops/sorting.rs
src/ops/natsort.rs
Introduce asynchronous image preview loading with race guards and clean integration into the main event loop.
  • Factor out run_chafa to encapsulate chafa command invocation and ANSI parsing for image previews.
  • Add ImagePreviewLoader struct that spawns a worker thread to render previews, exposes cancel and try_recv, and carries the file_path for race detection.
  • Refactor ViewerState image preview API into needs_image_preview, set_image_preview, and image_content_size to separate layout decisions from async loading.
  • Wire new image preview loader into run_app main loop via poll_image_preview and start_image_preview_if_needed, ensuring loaders are cancelled on viewer close or path change and that stale loader results are discarded if file_path mismatches the current viewer.
  • Extend viewer tests to cover loader path tracking and basic race-condition guard expectations.
src/ui/viewer.rs
src/main.rs
src/ui/panels/tests.rs
Tighten dialog, mode, and pending action handling to reduce duplication and make state transitions safer.
  • Change DialogKind::Progress to a struct variant with named fields, and update all creation sites, pattern matches, rendering, and tests accordingly.
  • Add PendingAction::set_overwrite and reuse it from both keyboard and mouse overwrite-confirm flows, removing duplicated helper functions in mouse and dialog input modules.
  • Introduce restore_prev_mode helper in types and replace direct prev_mode.take() pattern with this helper in multiple mode-transition sites (search clear, menu, viewer, mouse menu, etc.).
src/app/types.rs
src/app/job_runner.rs
src/input/mouse.rs
src/input/dialogs.rs
src/input/mode_dispatch.rs
src/render.rs
src/tests/dialogs.rs
Improve panel/file listing robustness and watcher integration, especially around path_index and error handling.
  • Extract rebuild_path_index and use it from ensure_path_index and remove_entry to keep path_index consistent with unfiltered_entries.
  • Change ensure_path_index to early-return only when index is non-empty, otherwise rebuild from unfiltered_entries.
  • Update remove_entry to always ensure a populated path_index, skip work when the path is not indexed, and rebuild the index after removal instead of conditionally clearing it.
  • Adjust watcher_sync apply_watcher_remove/full_refresh_panel to rely on path_index existence and to keep last_error in sync with reader::read_directory errors via update_panel_read_errors (now pub(crate)).
src/fs/reader.rs
src/app/panel_ops.rs
src/app/watcher_sync.rs
Make filesystem deletion safer by simplifying recursive delete invariants while maintaining TOCTOU protections at the top level.
  • Change delete_dir_recursive_with_cancel to call delete_dir_contents with the canonical root but have delete_dir_contents_impl operate from the current path downward without re-canonicalizing or performing repeated lexical_path_starts_with checks.
  • Remove the per-level root/path canonicalization and path-prefix checks from delete_dir_contents_impl, relying instead on the initial canonicalization/guard at the top-level delete_dir_recursive_with_cancel.
  • Preserve depth limiting, symlink refusal, and cancellation checks to avoid unsafe recursion or unbounded traversal.
src/ops/file_ops.rs
Normalize shell command history handling and add helper to deduplicate pushes with a max length cap, updating tests.
  • Introduce shell::push_history that skips empty commands, deduplicates consecutive identical entries, and enforces MAX_HISTORY cap, and call it from run_shell_command.
  • Refactor history tests to use push_history instead of reimplementing the logic inline, and add a cap test using the helper.
  • Tighten command-line execution to move out command_line.text, reset cursor, and keep draft/history index consistent while reusing shell history logic.
src/app/shell.rs
src/tests/history.rs
src/input/command_line.rs
Harden config loading and environment handling, enabling isolated testing of configuration search paths.
  • Introduce EnvProvider trait-based config loading via load_settings_with_env and read_config_raw_with_env, with default implementations delegating to ProcessEnv.
  • Adjust config load path to use config_file_path_with_env, enabling tests to control XDG_CONFIG_HOME/HOME resolution.
  • Add IsolatedEnv implementation and update config_load_missing_file_ok test to verify behavior against a temporary directory instead of the real environment.
src/app/config.rs
src/tests/misc.rs
Refine selection, active panel toggling, and selection-derived path computation to avoid subtle bugs with selection count and '..' entries.
  • Add ActivePanel::toggle and use it across keyboard and menu navigation paths when switching/swap panels, removing inline matches.
  • Update selected_or_current_paths to short-circuit when selected_count == 0, and to use a shared current_entry_fallback that excludes '..'.
  • Ensure tests for selection-derived paths and selection-based behavior explicitly set selected_count where needed.
src/app/types.rs
src/main.rs
src/input/menu_actions.rs
src/app/panel_ops.rs
src/tests/selection.rs
Optimize various UI code paths for allocations and Unicode handling, and clean up deprecated/theme/test code.
  • Replace repeated String::repeat-based padding with pre-sized String plus extend(repeat_n) in panels status formatting and entry line formatting to avoid intermediate allocations.
  • Simplify render_input_dialog grapheme handling by streaming over graphemes twice instead of maintaining explicit grapheme arrays and cumulative width vectors, while correctly computing cursor positioning and scroll offset.
  • Remove deprecated Theme constant aliases in favor of accessor methods, reducing API surface and scan warnings.
  • Refactor tests to use macros and helpers: introduce test_file_color macro for file color tests, move truncate_name tests near other width tests and add an empty-string case, and introduce populate_panel helper to DRY test panel setup.
  • Tighten debug logger constants (MIB and MAX_LOG_SIZE_BYTES) and update rotation tests accordingly.
src/ui/panels/mod.rs
src/ui/dialogs.rs
src/ui/theme.rs
src/ui/panels/tests.rs
src/tests/helpers.rs
src/ui/viewer.rs
src/app/debug_log.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements several performance optimizations and architectural refactorings, including the introduction of asynchronous image previews using chafa and the transition of DialogKind::Progress to a struct variant. Significant optimizations were made to UI rendering string allocations, path deduplication logic, and file listing operations via a path index. Review feedback identifies a high-severity security risk where the removal of path validation in recursive deletions exposes the system to directory traversal attacks. Further feedback notes efficiency issues in bulk file entry removals and suggests short-circuiting logic for case-insensitive sorting tie-breakers.

I am having trouble creating individual review comments. Click here to see my feedback.

src/ops/file_ops.rs (635-646)

security-high high

The removal of the path validation logic significantly weakens the safety of recursive deletions. This check ensured that the canonical path of every subdirectory being processed remained within the intended root directory. Removing it makes the function vulnerable to symlink-based directory traversal attacks or race conditions (TOCTOU), where a directory is replaced by a symlink to an external location during the deletion process. If the goal was to improve performance by avoiding repeated canonicalization, consider a more efficient way to track the allowed path boundaries rather than removing the safety guard entirely.

src/fs/reader.rs (285-294)

medium

This implementation is inefficient for bulk removals. Calling rebuild_path_index (which is $O(N)$ and involves multiple allocations) inside remove_entry makes repeated removals $O(N^2)$. Furthermore, the call to ensure_path_index at line 285 is redundant because the index is unconditionally rebuilt at line 294. Consider updating the path_index incrementally or deferring the rebuild until after a batch of operations is complete.

src/ops/sorting.rs (147-148)

medium

The tie-breaker logic for case-insensitive sorting is inefficient. chars().all(|c| !c.is_uppercase()) scans the entire string even if an uppercase character is found early. Using !any(|c| c.is_uppercase()) would allow for short-circuiting. Additionally, since this is only evaluated when cmp_ignore_case returns Equal, the strings are already known to be identical except for case, making a full scan unnecessary.

        let a_lower = !a.chars().any(|c| c.is_uppercase());
        let b_lower = !b.chars().any(|c| c.is_uppercase());

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • The refactor of delete_dir_contents_impl drops the canonical-root boundary check (the previous lexical_path_starts_with(root, &canonical) guard); it would be safer to keep an equivalent root confinement check to prevent recursive deletion from ever escaping the originally requested directory in the presence of races or unusual filesystem setups.
  • run_chafa in viewer.rs and update_panel_read_errors in panel_ops are now pub/pub(crate) helpers but appear to be used only internally; consider tightening their visibility (e.g. pub(crate)/fn) if you don't intend them as part of the public API surface.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The refactor of `delete_dir_contents_impl` drops the canonical-root boundary check (the previous `lexical_path_starts_with(root, &canonical)` guard); it would be safer to keep an equivalent root confinement check to prevent recursive deletion from ever escaping the originally requested directory in the presence of races or unusual filesystem setups.
- `run_chafa` in `viewer.rs` and `update_panel_read_errors` in `panel_ops` are now `pub`/`pub(crate)` helpers but appear to be used only internally; consider tightening their visibility (e.g. `pub(crate)`/`fn`) if you don't intend them as part of the public API surface.

## Individual Comments

### Comment 1
<location path="src/ui/panels/mod.rs" line_range="422-425" />
<code_context>

     let padding = available_name_width.saturating_sub(name_actual_width);
-    format!("{marker}{name}{}{}", " ".repeat(padding), suffix.text)
+    let mut s = String::with_capacity(1 + name.len() + padding + suffix.text.len());
+    s.push(marker);
+    s.push_str(&name);
+    s.extend(std::iter::repeat_n(' ', padding));
+    s.push_str(&suffix.text);
+    s
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `std::iter::repeat_n` here is likely to fail on stable Rust; prefer `repeat().take()` or a helper.

`std::iter::repeat_n` isn’t in stable `std` and will fail to compile there. Instead, use something like:

```rust
s.extend(std::iter::repeat(' ').take(padding));
```

or keep the previous `
</issue_to_address>

### Comment 2
<location path="src/ui/panels/mod.rs" line_range="599-601" />
<code_context>
     let info_line_width = UnicodeWidthStr::width(info_line.as_str());
     let padding = remaining.saturating_sub(info_line_width);
-    let full_text = format!("{info_line}{}{right_info}", " ".repeat(padding));
+    let mut full_text = String::with_capacity(info_line.len() + padding + right_info.len());
+    full_text.push_str(&info_line);
+    full_text.extend(std::iter::repeat_n(' ', padding));
+    full_text.push_str(&right_info);

</code_context>
<issue_to_address>
**issue (bug_risk):** Same `repeat_n` compilation issue in status bar construction as in `format_entry_line`.

Please update this to use a stable approach (and match whatever you do in `format_entry_line`), for example:

```rust
full_text.extend(std::iter::repeat(' ').take(padding));
```

or build and reuse a `String` of spaces.
</issue_to_address>

### Comment 3
<location path="src/tests/history.rs" line_range="5-12" />
<code_context>

     #[test]
     fn test_dialog_kind_progress() {
-        let dialog = DialogKind::Progress("Copying...".to_string(), 0.5, true);
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for push_history ignoring empty or whitespace-only commands

The tests cover duplicates and max-length capping, but don’t assert the behavior for empty/whitespace-only commands, which are currently ignored via `cmd.trim().is_empty()`. Please add tests that:
- Call `push_history` with only whitespace and with an empty string on a fresh `AppState`, asserting `command_history` remains empty.
- Optionally, push a valid command, then a whitespace-only command, and assert the length and last entry are unchanged.

This will lock in the intended “no blank commands in history” behavior.
</issue_to_address>

### Comment 4
<location path="src/tests/selection.rs" line_range="151" />
<code_context>
+fn selected_or_current_paths_uses_selection_when_present() {
</code_context>
<issue_to_address>
**suggestion (testing):** Consider tests for selected_or_current_paths when there is no selection or only ".."

The current test only covers the `selected_count > 0` path. To exercise the rest of the logic, please add tests for:

1. `selected_count == 0` with a valid current entry (not `".."`): expect a single path for the current entry.
2. `selected_count == 0` with the current entry `".."`: expect an empty vec.
3. (If representable) `selected_count > 0` where all selected entries are `".."`, to validate the fallback to `current_entry_fallback` and the `".."` filter.

This will verify that the function never returns `".."` and falls back correctly when the selection is effectively empty.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/ui/panels/mod.rs
Comment on lines +422 to +425
let mut s = String::with_capacity(1 + name.len() + padding + suffix.text.len());
s.push(marker);
s.push_str(&name);
s.extend(std::iter::repeat_n(' ', padding));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Using std::iter::repeat_n here is likely to fail on stable Rust; prefer repeat().take() or a helper.

std::iter::repeat_n isn’t in stable std and will fail to compile there. Instead, use something like:

s.extend(std::iter::repeat(' ').take(padding));

or keep the previous `

Comment thread src/ui/panels/mod.rs
Comment on lines +599 to +601
let mut full_text = String::with_capacity(info_line.len() + padding + right_info.len());
full_text.push_str(&info_line);
full_text.extend(std::iter::repeat_n(' ', padding));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Same repeat_n compilation issue in status bar construction as in format_entry_line.

Please update this to use a stable approach (and match whatever you do in format_entry_line), for example:

full_text.extend(std::iter::repeat(' ').take(padding));

or build and reuse a String of spaces.

Comment thread src/tests/history.rs
Comment on lines 5 to 12
#[test]
fn history_dedup_consecutive() {
let tmp = tempfile::tempdir().unwrap();
let mut state = AppState::default();
state.left_panel.set_path(tmp.path().to_path_buf());
state.command_history.push_back("echo hi".to_string());
let cmd = "echo hi";
if state.command_history.back().is_none_or(|l| l != cmd) {
state.command_history.push_back(cmd.to_string());
}
shell::push_history(&mut state, "echo hi");
shell::push_history(&mut state, "echo hi");
assert_eq!(state.command_history.len(), 1);
assert_eq!(state.command_history[0], "echo hi");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add tests for push_history ignoring empty or whitespace-only commands

The tests cover duplicates and max-length capping, but don’t assert the behavior for empty/whitespace-only commands, which are currently ignored via cmd.trim().is_empty(). Please add tests that:

  • Call push_history with only whitespace and with an empty string on a fresh AppState, asserting command_history remains empty.
  • Optionally, push a valid command, then a whitespace-only command, and assert the length and last entry are unchanged.

This will lock in the intended “no blank commands in history” behavior.

Comment thread src/tests/selection.rs
state.left_panel.selected_count = 2;

let paths = selected_or_current_paths(&state);
assert_eq!(paths.len(), 2);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Consider tests for selected_or_current_paths when there is no selection or only ".."

The current test only covers the selected_count > 0 path. To exercise the rest of the logic, please add tests for:

  1. selected_count == 0 with a valid current entry (not ".."): expect a single path for the current entry.
  2. selected_count == 0 with the current entry "..": expect an empty vec.
  3. (If representable) selected_count > 0 where all selected entries are "..", to validate the fallback to current_entry_fallback and the ".." filter.

This will verify that the function never returns ".." and falls back correctly when the selection is effectively empty.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 25, 2026

Greptile Summary

This PR resolves 33 confirmed scan-audit issues across six passes, touching 36 files. The changes span naming improvements (magic numbers, deprecated constants), DRY refactoring (shared helpers, unified duplicate functions), ghost-assertion fixes in tests, zero-alloc render paths, O(1) data-structure lookups, and a genuine TOCTOU fix in the filesystem watcher.

  • Core logic improvements: DialogKind::Progress converted from an anonymous tuple to a named-field struct; ActivePanel::toggle(), PendingAction::set_overwrite(), and restore_prev_mode() extracted to eliminate repeated match arms; format_system_time deduplicated across two call sites.
  • Performance: Sorting changed from sort_by_cached_key (allocates per-element key strings) to zero-alloc sort_by comparators; render_input_dialog and format_entry_line replaced Vec + format! with iterator-based string building; path_index used for O(1) entry lookup in set_unfiltered_selection and watcher removal.
  • Async image preview: ViewerState::prepare_image_preview (blocking chafa call on the render thread) replaced by ImagePreviewLoader (background thread) with a file-path race guard in the poll loop.

Confidence Score: 3/5

Mostly safe to merge; the watcher TOCTOU fix, zero-alloc paths, and DRY consolidations are all correct. Two behavioral changes in src/main.rs deserve a second look before merging.

The selected_or_current_paths function now gates entirely on selected_count being accurate rather than iterating actual entries — the companion test required an explicit selected_count = 2 patch to stay green, confirming the new coupling. Separately, recover_terminal_state and launch_editor now delete the terminal-state marker before confirming the resume succeeded, meaning a failed restore leaves the terminal with no recovery path on next startup. Neither issue blocks most use cases, but both are subtle behavioral shifts introduced alongside otherwise clean refactoring.

src/main.rs — selected_or_current_paths counter dependency and unconditional state-file deletion; src/ui/viewer.rs — ImagePreviewLoader cancel semantics vs. live subprocesses.

Important Files Changed

Filename Overview
src/main.rs Two changes warrant attention: selected_or_current_paths now uses selected_count as an early-exit gate (silently trusts the counter), and recover_terminal_state/launch_editor now delete the state file unconditionally before confirming resume success.
src/ui/viewer.rs Adds async ImagePreviewLoader backed by a background thread running chafa; cancel flag prevents sending stale results, but the subprocess cannot be killed mid-run, so rapid navigation can stack live chafa processes.
src/app/types.rs Converts DialogKind::Progress tuple variant to named-field struct, adds ActivePanel::toggle() and PendingAction::set_overwrite() helpers, extracts format_system_time and restore_prev_mode to reduce duplication, and adds O(1) path_index fast path to set_unfiltered_selection.
src/ops/sorting.rs Replaces sort_by_cached_key with zero-allocation sort_by comparators using cmp_group, cmp_name, and cmp_ext; sort semantics verified equivalent to the old key-tuple approach including case-insensitive tiebreak and btime-present grouping.
src/fs/watcher.rs TOCTOU fix: stale "rename-from" entries are now only emitted as Deleted events (and removed from pending) if the pending map still holds the same path+time pair, preventing spurious delete events from racing rename completions.
src/ops/file_ops.rs Removed per-directory canonical path check in delete_dir_contents_impl; replaced by fs::symlink_metadata checks that refuse to recurse into symlinked directories — equivalent protection without the allocation cost of repeated canonicalization.
src/ui/dialogs.rs Refactors render_input_dialog to avoid allocating a grapheme Vec and cumulative-width Vec; uses iterator chains to compute cursor_display and visible substring in a single pass, with verified-equivalent scrolling semantics for wide characters.
src/app/config.rs Extracts load_settings_with_env / read_config_raw_with_env accepting an EnvProvider trait for testability, allowing config_load_missing_file_ok to use an isolated temp directory instead of the real $HOME.
src/fs/reader.rs Extracts rebuild_path_index helper used by both ensure_path_index and remove_entry; remove_entry now eagerly rebuilds the index after retain instead of lazily invalidating it.
src/ops/natsort.rs Legacy byte-level natsort function (previously #[cfg(test)] pub) moved into a private legacy submodule scoped to the test module, eliminating the production-visible test helper while keeping the compatibility tests intact.
src/ui/theme.rs Removes all #[deprecated] color constants that have been superseded by method-based accessors; no callers remain since they were already deprecated.

Sequence Diagram

sequenceDiagram
    participant EL as Event Loop
    participant PVL as poll_viewer_loader
    participant PIP as poll_image_preview
    participant SIPN as start_image_preview_if_needed
    participant IPL as ImagePreviewLoader (thread)
    participant Chafa as chafa process

    EL->>PVL: file loaded?
    PVL-->>EL: "dirty=true (ViewerState ready)"
    EL->>PIP: result ready?
    PIP-->>EL: false (loader not started yet)
    EL->>SIPN: needs_image_preview?
    SIPN->>IPL: ImagePreviewLoader::start(path, w, h)
    IPL->>Chafa: Command::output() [blocking]
    note over IPL,Chafa: cancel flag checked before and after, not during
    Chafa-->>IPL: ANSI text
    IPL-->>EL: tx.send(w, h, text)

    EL->>PIP: try_recv()
    PIP->>PIP: "guard: loader.file_path == vs.file_path?"
    PIP-->>EL: "dirty=true, sets cached_image_text"
    EL->>SIPN: needs_image_preview? (cached_image_size matches, false)
    EL->>EL: terminal.draw() with image text

    note over EL,SIPN: On rapid navigation: old loader cancelled, new loader started, but old chafa still runs
Loading

Comments Outside Diff (2)

  1. src/main.rs, line 313-327 (link)

    P2 State file removed before confirming successful resume

    remove_file(&terminal_state_file) is now attempted unconditionally — before the result of resume_terminal_stdout() is propagated via resume_result?. If the resume fails (leaving the terminal in raw/TUI mode), the marker file is already gone. The next lc startup won't find the file and won't attempt a second restoration, potentially leaving the terminal broken with no recovery path.

    The same pattern was applied to launch_editor. If this is intentional ("always clean up regardless"), consider logging the decision or documenting that a failed resume is treated as unrecoverable so future maintainers don't re-add the guard.

  2. src/ui/viewer.rs, line 2990-2995 (link)

    P2 chafa subprocess cannot be cancelled mid-run; rapid navigation stacks processes

    The cancel flag is only checked at the very start of the thread and after run_chafa returns. run_chafa calls Command::output() which blocks until chafa exits. If the user navigates quickly through image files, each navigation calls start_image_preview_if_needed which cancels the old ImagePreviewLoader and starts a new one, but the old chafa subprocess continues running until it finishes — since cancelling the AtomicBool has no effect on an already-blocking Command::output(). Under rapid navigation, several chafa processes can pile up concurrently.

    This is benign for typical usage (chafa is fast), but worth a comment explaining the trade-off. Alternatively, Command::spawn() + Child::wait()-with-cancel or a separate kill call would allow actually terminating the process on cancel.

Reviews (1): Last reviewed commit: "fix: resolve 33 confirmed issues from sc..." | Re-trigger Greptile

Comment thread src/main.rs
Comment on lines 925 to +950
}

fn selected_or_current_paths(state: &AppState) -> Vec<std::path::PathBuf> {
let selected: Vec<std::path::PathBuf> = state
.active_panel()
let panel = state.active_panel();

let current_entry_fallback = || {
panel
.current_entry()
.filter(|entry| entry.name != "..")
.map(|entry| vec![entry.path.clone()])
.unwrap_or_default()
};

if panel.selected_count == 0 {
return current_entry_fallback();
}

let selected: Vec<std::path::PathBuf> = panel
.selected_entries()
.into_iter()
.filter(|entry| entry.name != "..")
.map(|entry| entry.path.clone())
.collect();

if selected.is_empty() {
state
.active_panel()
.current_entry()
.filter(|entry| entry.name != "..")
.map(|entry| vec![entry.path.clone()])
.unwrap_or_default()
} else {
selected
return current_entry_fallback();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 selected_or_current_paths now silently depends on selected_count accuracy

The function now short-circuits at if panel.selected_count == 0 { return current_entry_fallback(); } before ever calling selected_entries(). This is a semantic shift: previously the function checked actual entry state; now it trusts the counter. The companion test fix in src/tests/selection.rs had to manually add state.left_panel.selected_count = 2 to keep the test passing, which confirms the new coupling.

If any code path sets entry.selected = true without incrementing selected_count (or decrements the counter without clearing the field), this function will silently fall back to the cursor entry and silently skip the selection — with no error or warning.

- history: empty command, whitespace command, whitespace after valid
- selection: no-selection returns current, dotdot current returns empty,
  all-dotdot-selected falls back to current entry
@leszek3737 leszek3737 merged commit 06925e7 into main May 25, 2026
5 checks passed
@leszek3737 leszek3737 deleted the fix55 branch May 25, 2026 04:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant