Skip to content

Version 0.8#13

Merged
AlanRockefeller merged 4 commits intomainfrom
test
Nov 21, 2025
Merged

Version 0.8#13
AlanRockefeller merged 4 commits intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Nov 20, 2025

  • Backspace key now deletes images (in addition to Delete key). Control-Z restores.
  • Photoshop integration now automatically uses RAW files when available, falling back to JPG.
  • We now have some new color modes in the view menu to make the images in your monitor reflect reality. ICC profile mode works best on my system - try it if the
    images are over-saturated - or turn down the saturation in saturation mode. Test it out by loading an image in Faststack and Photoshop or another image viewer an
    d make sure the colors look the same.

Summary by CodeRabbit

  • New Features

    • Delete/Undo: Delete/Backspace removes images; Ctrl+Z restores; recycle bin with prompt to empty on exit
    • Color management: View menu modes (none, saturation, ICC), saturation slider/percentage, and saturation reset
    • Photoshop: Edit now prefers RAW when available
    • Ctrl+0 resets zoom/pan; Key Bindings dialog enlarged and updated with File Management
  • Chores

    • Release bumped to version 0.8 and CLI entry with optional --debug flag

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 20, 2025

Walkthrough

Bumps project to 0.8.0; adds delete-with-undo (recycle bin) and keyboard shortcuts; introduces three color modes (none / saturation / icc) with saturation control and ICC handling in prefetch; prefers RAW files for Photoshop edits; adds debug/timing logging and CLI entrypoint.

Changes

Cohort / File(s) Summary
Version & Metadata
faststack/pyproject.toml, faststack/faststack.egg-info/PKG-INFO
Project version bumped 0.7 → 0.8; script entrypoint changed to faststack.app:cli; PKG-INFO Version field updated.
Docs & Release Notes
faststack/README.md, faststack/ChangeLog.md
README and ChangeLog updated for 0.8: new features, shortcuts (Delete/Backspace, Ctrl+Z, Ctrl+0), Photoshop RAW preference, and changelog entries.
Core app controller
faststack/faststack/app.py
Adds recycle bin dir, delete_history, empty_recycle_bin/undo_delete/delete_current_image slots; color API slots (get/set_color_mode, get/set_saturation_factor); edits prefer RAW for Photoshop, safer process invocation; debug flag, timing/logging, prefetch/display readiness and last_displayed_image handling.
Configuration
faststack/faststack/config.py
Adds color section to default config with keys: mode, saturation_factor, monitor_icc_path.
Imaging prefetch & color mgmt
faststack/faststack/imaging/prefetch.py
Adds SRGB_PROFILE, get_monitor_profile(), apply_saturation_compensation(); implements ICC conversion path and saturation branch in decode/prefetch; enhanced scheduling, cancellation, and generation-aware caching.
UI provider & state
faststack/faststack/ui/provider.py
Adds UIState signals/properties colorMode and saturationFactor; respects config.color.mode when constructing QColorSpace and logs ICC decisions; null-safety guards for empty image lists.
QML interface
faststack/faststack/qml/Main.qml
Adds saturation slider (visible in saturation mode), View menu color-mode group (none/saturation/icc), enlarges Key Bindings dialog and documents File Management shortcuts.
Keybindings
faststack/faststack/ui/keystrokes.py
Maps Delete/Backspace → delete_current_image; Ctrl+Z → undo_delete.
I/O instrumentation
faststack/faststack/io/indexer.py, faststack/faststack/io/sidecar.py
Adds timing instrumentation and conditional debug logging around image discovery and sidecar JSON loads.
Logging setup
faststack/faststack/logging_setup.py
setup_logging(debug: bool = False) added; debug flag controls root logger and imaging-related log levels.
CLI entry
faststack/faststack/app.py, faststack/pyproject.toml
Adds cli() entrypoint and main(..., debug=...) CLI wiring; pyproject script updated to call cli.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as QML UI
    participant Ctrl as AppController
    participant FS as FileSystem
    participant Cache as Prefetcher/Cache

    User->>UI: Press Delete / Backspace
    UI->>Ctrl: delete_current_image()
    Ctrl->>FS: move current file -> recycle_bin_dir
    Ctrl->>Ctrl: append to delete_history
    Ctrl->>Cache: invalidate/refresh prefetch
    Ctrl->>UI: update display & status

    User->>UI: Press Ctrl+Z
    UI->>Ctrl: undo_delete()
    Ctrl->>FS: move file back from recycle_bin_dir
    Ctrl->>Ctrl: update delete_history
    Ctrl->>Cache: invalidate/refresh prefetch
    Ctrl->>UI: update display & status
Loading
sequenceDiagram
    participant User
    participant UI as QML UI
    participant Ctrl as AppController
    participant Pref as Prefetcher
    participant Config as Config

    User->>UI: Select color mode
    UI->>Ctrl: set_color_mode(mode)
    Ctrl->>Config: persist color.mode
    Ctrl->>Pref: reset prefetch / clear caches
    Pref->>Pref: choose decode path based on mode
    alt saturation
        Pref->>Pref: apply_saturation_compensation(factor)
    else icc
        Pref->>Pref: load monitor profile & ImageCms conversion
    end
    Pref->>UI: supply updated image buffers
Loading
sequenceDiagram
    participant User
    participant UI as QML UI
    participant Ctrl as AppController
    participant FS as FileSystem
    participant PS as Photoshop

    User->>UI: Edit in Photoshop (E)
    UI->>Ctrl: edit_in_photoshop()
    Ctrl->>FS: check for RAW alongside image
    alt RAW found
        Ctrl->>Ctrl: choose RAW file
    else
        Ctrl->>Ctrl: fall back to JPEG
    end
    Ctrl->>Ctrl: validate Photoshop executable, build args safely
    Ctrl->>PS: launch process (no shell)
    Ctrl->>UI: show status or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing close review:
    • Recycle-bin file moves, undo atomicity and shutdown cleanup (faststack/faststack/app.py).
    • ICC profile loading, ImageCms usage, numpy buffer handling and byte/stride correctness (faststack/faststack/imaging/prefetch.py).
    • Saturation algorithm correctness and performance impacts on prefetch/worker threads (prefetch.py).
    • Signal/property synchronization between Python and QML for colorMode/saturationFactor (faststack/faststack/ui/provider.py, faststack/faststack/qml/Main.qml).
    • Photoshop invocation safety, argument parsing, and path validation (faststack/faststack/app.py).

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.70% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Version 0.8' directly corresponds to the version bump implemented across multiple files and accurately represents the primary change in this release.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test

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.

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: 0

🧹 Nitpick comments (8)
faststack/faststack/imaging/prefetch.py (4)

19-35: Cache monitor ICC profile instead of reloading it for every decode

get_monitor_profile re-reads the config and constructs a new ImageCmsProfile on every ICC decode, and logs a warning on every call when monitor_icc_path is unset. Caching the profile by path would reduce overhead and log spam, while still allowing the path to change at runtime.

You can also narrow the bare except Exception here to the concrete error types you expect (e.g., OSError, ImageCms.PyCMSError) so unexpected failures are not silently downgraded.

-SRGB_PROFILE = ImageCms.createProfile("sRGB")
-
-def get_monitor_profile():
-    """Dynamically load monitor ICC profile based on current config."""
-    try:
-        monitor_icc_path = config.get('color', 'monitor_icc_path', fallback="").strip()
-        if monitor_icc_path:
-            profile = ImageCms.ImageCmsProfile(monitor_icc_path)
-            log.debug(f"Loaded monitor ICC profile: {monitor_icc_path}")
-            return profile
-        else:
-            log.warning("ICC mode enabled but no monitor_icc_path configured")
-            return None
-    except Exception as e:
-        log.warning(f"Failed to load monitor ICC profile: {e}")
-        return None
+SRGB_PROFILE = ImageCms.createProfile("sRGB")
+_MONITOR_PROFILE_CACHE: dict[str, Optional[ImageCms.ImageCmsProfile]] = {}
+
+def get_monitor_profile() -> Optional[ImageCms.ImageCmsProfile]:
+    """Dynamically load monitor ICC profile based on current config."""
+    monitor_icc_path = config.get('color', 'monitor_icc_path', fallback="").strip()
+    if not monitor_icc_path:
+        log.warning("ICC mode enabled but no monitor_icc_path configured")
+        return None
+
+    cached = _MONITOR_PROFILE_CACHE.get(monitor_icc_path)
+    if cached is not None:
+        return cached
+
+    try:
+        profile = ImageCms.ImageCmsProfile(monitor_icc_path)
+    except OSError as e:
+        log.warning(f"Failed to load monitor ICC profile '{monitor_icc_path}': {e}")
+        profile = None
+
+    _MONITOR_PROFILE_CACHE[monitor_icc_path] = profile
+    if profile is not None:
+        log.debug(f"Loaded monitor ICC profile: {monitor_icc_path}")
+    return profile

38-72: Add a cheap shape assertion to guard saturation compensation assumptions

The saturation math looks good and is nicely localized, but it assumes arr.size == height * bytes_per_line. If the upstream decode pipeline ever changes (e.g., padded rows), this will silently reshape incorrectly.

A lightweight assertion here would make mismatches fail fast and easier to debug.

-    # Treat the buffer as [height, bytes_per_line]
-    buf2d = arr.reshape((height, bytes_per_line))
+    # Treat the buffer as [height, bytes_per_line]
+    assert arr.size == height * bytes_per_line, (
+        f"Unexpected buffer size for saturation compensation: "
+        f"{arr.size} != {height} * {bytes_per_line}"
+    )
+    buf2d = arr.reshape((height, bytes_per_line))

157-163: Consider narrowing bare Exception catches to expected error types

In both the embedded ICC profile parsing (Lines 157–163) and the saturation compensation block (Lines 211–216), the code catches Exception and continues. That’s safe for robustness, but it can also hide programming errors such as MemoryError or KeyboardInterrupt.

If possible, tighten these to the concrete exceptions you expect (e.g., OSError, ImageCms.PyCMSError, ValueError) and let truly unexpected failures surface.

Also applies to: 211-216


168-175: Minor: drop unnecessary f-string in debug log

log.debug(f"Converting image from source to monitor profile") doesn’t interpolate any variables, so the f prefix is unnecessary and flagged by Ruff (F541).

-                    log.debug(f"Converting image from source to monitor profile")
+                    log.debug("Converting image from source to monitor profile")
faststack/faststack/app.py (4)

523-579: Normalize color-mode casing and align saturation-factor semantics

Two minor consistency points in the color management slots:

  1. set_color_mode (Lines 529–537) expects exact lowercase 'none' | 'saturation' | 'icc', but callers might pass 'ICC' or 'Saturation', and Prefetcher itself lowercases config.get('color', 'mode', ...). Normalizing the input before validation will make this more robust.

  2. apply_saturation_compensation’s docstring allows factor > 1.0 for extra saturation, but set_saturation_factor clamps to [0.0, 1.0], so the UI can only reduce or keep saturation, not increase it. That matches the PR description (“allows reducing saturation”), but it’s slightly at odds with the helper’s docstring.

    @Slot(str)
    def set_color_mode(self, mode: str):
        """Sets color management mode and clears cache to force re-decode."""
-        if mode not in ['none', 'saturation', 'icc']:
+        mode = mode.lower()
+        if mode not in ['none', 'saturation', 'icc']:
             log.error(f"Invalid color mode: {mode}")
             return

If you intend saturation-only reduction in the UI, consider updating the saturation helper’s docstring to say 0.0–1.0 rather than suggesting >1.0 is supported.


641-697: Tighten delete/undo bookkeeping and exception logging

The recycle-bin–based delete/undo flow is a nice UX improvement. A couple of edge cases are worth tightening:

  • delete_history.append((jpg_path, raw_path)) is recorded even if neither file actually existed/moved (e.g., a race where the file was deleted externally). undo_delete will then no-op while reporting “Restored: ”. Tracking only files that were actually moved, or storing that information alongside the paths, would keep history truthful.
  • On partial failures (e.g., JPG move succeeds but RAW move fails), the exception path does not add an entry to delete_history, so the successfully moved file can’t be undone. Depending on how likely that is in your environment, you may want to record at least what was moved so the user can still undo that part.

Also, for the except OSError as e: blocks in both delete_current_image and undo_delete, using log.exception(...) instead of log.error(...) will capture a stack trace, which is helpful if filesystem errors show up in the wild.

-        except OSError as e:
-            self.update_status_message(f"Delete failed: {e}")
-            log.error(f"Failed to delete image: {e}")
+        except OSError as e:
+            self.update_status_message(f"Delete failed: {e}")
+            log.exception(f"Failed to delete image: {e}")
-        except OSError as e:
-            self.update_status_message(f"Undo failed: {e}")
-            log.error(f"Failed to restore image: {e}")
-            # Put it back in history if it failed
-            self.delete_history.append((jpg_path, raw_path))
+        except OSError as e:
+            self.update_status_message(f"Undo failed: {e}")
+            log.exception(f"Failed to restore image: {e}")
+            # Put it back in history if it failed
+            self.delete_history.append((jpg_path, raw_path))

Also applies to: 698-749


750-767: Keep delete_history in sync when emptying the recycle bin and log errors with tracebacks

The shutdown prompt + empty_recycle_bin behavior is good, but currently:

  • empty_recycle_bin removes the directory with shutil.rmtree without clearing self.delete_history. After a successful empty, undo_delete can still pop entries that no longer map to real files, leading to confusing “Restored: ” messages.
  • The except OSError in empty_recycle_bin uses log.error, which drops the traceback (Ruff TRY400).

Clearing the history on successful empty and switching to log.exception will keep internal state consistent and improve diagnosability.

    def empty_recycle_bin(self):
        """Permanently deletes all files in the recycle bin."""
        if not self.recycle_bin_dir.exists():
            return
        
        try:
            import shutil
-            shutil.rmtree(self.recycle_bin_dir)
-            log.info("Emptied recycle bin")
-        except OSError as e:
-            log.error(f"Failed to empty recycle bin: {e}")
+            shutil.rmtree(self.recycle_bin_dir)
+            self.delete_history.clear()
+            log.info("Emptied recycle bin and cleared delete history")
+        except OSError as e:
+            log.exception(f"Failed to empty recycle bin: {e}")

Also applies to: 778-789


796-806: Simplify Photoshop error handling; avoid unreachable FileNotFoundError block

The enhanced Photoshop integration looks solid: prefer RAW when available, validate the executable, build a safe argv list, and disable shell=True. One small cleanup:

  • FileNotFoundError is a subclass of OSError, so the second except FileNotFoundError block (Lines 862–864) is currently unreachable because the earlier except (OSError, subprocess.SubprocessError) already catches it.

You can either merge the messages into a single block or put the FileNotFoundError handler first if you want a special message.

-        except (OSError, subprocess.SubprocessError) as e:
-            self.update_status_message(f"Failed to open in Photoshop: {e}")
-            log.exception(f"Error launching Photoshop: {e}")
-        except FileNotFoundError as e:
-            self.update_status_message(f"Photoshop executable not found: {e}")
-            log.exception(f"Photoshop executable not found: {e}")
+        except FileNotFoundError as e:
+            self.update_status_message(f"Photoshop executable not found: {e}")
+            log.exception(f"Photoshop executable not found: {e}")
+        except (OSError, subprocess.SubprocessError) as e:
+            self.update_status_message(f"Failed to open in Photoshop: {e}")
+            log.exception(f"Error launching Photoshop: {e}")

Also applies to: 832-842, 846-856, 859-864

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e78ae48 and 2d9ada1.

📒 Files selected for processing (10)
  • faststack/ChangeLog.md (1 hunks)
  • faststack/README.md (3 hunks)
  • faststack/faststack.egg-info/PKG-INFO (2 hunks)
  • faststack/faststack/app.py (5 hunks)
  • faststack/faststack/config.py (1 hunks)
  • faststack/faststack/imaging/prefetch.py (2 hunks)
  • faststack/faststack/qml/Main.qml (3 hunks)
  • faststack/faststack/ui/keystrokes.py (1 hunks)
  • faststack/faststack/ui/provider.py (4 hunks)
  • faststack/pyproject.toml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
faststack/faststack/imaging/prefetch.py (3)
faststack/faststack/models.py (1)
  • DecodedImage (33-42)
faststack/faststack/imaging/jpeg.py (2)
  • decode_jpeg_rgb (22-40)
  • decode_jpeg_resized (99-149)
faststack/faststack/config.py (1)
  • get (68-69)
faststack/faststack/ui/provider.py (2)
faststack/faststack/config.py (1)
  • get (68-69)
faststack/faststack/app.py (2)
  • get_color_mode (524-526)
  • get_saturation_factor (558-560)
faststack/faststack/app.py (3)
faststack/faststack/config.py (3)
  • get (68-69)
  • save (58-66)
  • getfloat (74-75)
faststack/faststack/io/sidecar.py (1)
  • save (54-83)
faststack/faststack/imaging/prefetch.py (3)
  • cancel_all (244-250)
  • update_prefetch (95-115)
  • shutdown (252-256)
🪛 LanguageTool
faststack/README.md

[style] ~3-~3: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ...FastStack # Version 0.8 - November 20, 2025 # By Alan Rockefeller Ultra-fast, cach...

(MISSING_COMMA_AFTER_YEAR)

🪛 Ruff (0.14.5)
faststack/faststack/imaging/prefetch.py

33-33: Do not catch blind exception: Exception

(BLE001)


161-161: Do not catch blind exception: Exception

(BLE001)


169-169: f-string without any placeholders

Remove extraneous f prefix

(F541)


215-215: Do not catch blind exception: Exception

(BLE001)


233-233: Consider moving this statement to an else block

(TRY300)

faststack/faststack/app.py

657-657: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


696-696: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


746-746: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


788-788: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🔇 Additional comments (14)
faststack/pyproject.toml (1)

8-8: LGTM!

The version bump to 0.8 is consistent with the PR objectives and aligns with changes documented in the ChangeLog.

faststack/ChangeLog.md (1)

3-8: LGTM!

The changelog entries accurately document the new features introduced in version 0.8, including delete/undo functionality, enhanced Photoshop integration, and color management modes.

faststack/faststack.egg-info/PKG-INFO (1)

3-3: LGTM!

The package metadata version updates are consistent with the version bump across all other files.

Also applies to: 24-24

faststack/faststack/ui/keystrokes.py (2)

42-43: LGTM!

The addition of Delete and Backspace key bindings for image deletion follows the existing key mapping pattern and aligns with the PR objectives.


49-49: LGTM!

The Ctrl+Z binding for undo functionality follows the established modifier key mapping pattern and provides standard undo behavior.

faststack/README.md (1)

3-3: LGTM!

The README updates comprehensively document the new features in version 0.8, including the enhanced Photoshop integration with RAW file preference, delete/undo functionality, and additional keyboard shortcuts.

Also applies to: 20-20, 25-25, 50-54

faststack/faststack/config.py (1)

26-30: LGTM!

The new color configuration section is well-structured with clear defaults and inline documentation. The saturation factor default of 0.85 provides a reasonable starting point for color compensation.

faststack/faststack/qml/Main.qml (3)

118-150: LGTM!

The saturation slider implementation is well-integrated with conditional visibility based on color mode. The use of onMoved rather than onValueChanged is appropriate here as it only triggers when the user actively adjusts the slider, preventing feedback loops.


220-247: LGTM!

The color mode menu implementation correctly uses ActionGroup with exclusive: true to ensure only one color mode is active at a time. The checkable actions with bidirectional state binding (checked vs. uiState.colorMode) provide proper visual feedback.


323-357: LGTM!

The Key Bindings dialog updates appropriately document the new file management features (Delete and Ctrl+Z) and the increased size (500x600) provides better readability for the expanded content.

faststack/faststack/ui/provider.py (2)

85-86: LGTM!

The new signals and properties for color mode and saturation factor correctly expose the color management configuration to the QML UI layer, following the established pattern for other UI state properties.

Also applies to: 202-210


47-58: I found a potential API discrepancy. Let me verify the correct syntax for PySide6 6.10.

QColorSpace API usage is correct.

PySide6 includes QColorSpace.NamedColorSpace for predefined color spaces, and the constructor accepts namedColorSpace parameters of type NamedColorSpace. The code correctly uses QColorSpace(namedColorSpace) with QColorSpace.NamedColorSpace.SRgb. The ICC mode logic is sound: skipping sRGB application when pixels are already in monitor space. No changes needed.

faststack/faststack/imaging/prefetch.py (1)

196-217: Color-mode decode pipeline and generation re-check look consistent

The branching on color_mode (icc vs standard + optional saturation) and the generation re-check before caching integrate cleanly with the existing prefetcher design:

  • ICC mode: resize before profile conversion, honor embedded profiles when present, and fall back to sRGB with clear logging.
  • Non-ICC: reuse the existing decode_jpeg_resized path, with an optional in-place saturation pass when color_mode == "saturation".
  • The second if self.generation != local_generation guard before cache_put correctly drops stale decodes.

I don’t see functional issues here; this is a solid extension of the existing pipeline.

Also applies to: 218-233

faststack/faststack/app.py (1)

114-116: No exclusion logic needed—find_images does not recursively scan subdirectories.

The original concern assumed find_images performs recursive directory traversal. However, the implementation uses os.scandir(directory) with only a top-level file check (if entry.is_file()), meaning it scans only immediate children of the provided directory. Subdirectories like "image recycle bin" are never entered, so files within it will not appear in the image list. The recycle bin implementation is safe as currently written.

Likely an incorrect or invalid review comment.

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: 0

🧹 Nitpick comments (4)
faststack/faststack/app.py (4)

246-291: Consider exception handling improvements and document blocking behavior.

The blocking decode approach with fallback to last_displayed_image is a good UX improvement. However:

  1. Exception handling: Lines 282, 288, and 289 should use logging.exception() instead of logging.error() to include traceback information for debugging.
  2. Broad exception catch: Line 287 catches bare Exception - consider being more specific or add a comment justifying the broad catch.
  3. Timeout value: The 5-second timeout may be insufficient for large RAW files in color management modes. Consider making this configurable or increasing it.

Apply this diff to improve exception logging:

             except concurrent.futures.TimeoutError:
-                log.error(f"Timeout decoding image at index {index}")
+                log.exception(f"Timeout decoding image at index {index}")
                 return self.last_displayed_image
             except concurrent.futures.CancelledError:
                 log.warning(f"Decode cancelled for index {index}")
                 return self.last_displayed_image
             except Exception as e:
-                log.error(f"Error decoding image at index {index}: {e}")
+                log.exception(f"Error decoding image at index {index}: {e}")
                 return self.last_displayed_image

663-719: Improve exception logging in delete operation.

The delete implementation is solid with proper directory creation, file moving, and state updates. However:

  1. Exception logging: Lines 679 and 718 should use logging.exception() instead of logging.error() to capture full traceback.
  2. Partial failure handling: If JPG moves successfully but RAW fails, the operation is half-complete. Consider wrapping both moves in a transaction-like structure or documenting this behavior.
  3. Race condition: Between checking jpg_path.exists() (line 685) and calling rename() (line 687), the file could be deleted externally. Consider catching FileNotFoundError separately.

Apply this diff to improve exception logging:

         except OSError as e:
             self.update_status_message(f"Delete failed: {e}")
-            log.error(f"Failed to delete image: {e}")
+            log.exception(f"Failed to delete image: {e}")

720-771: Improve exception logging and consider partial restore handling.

The undo implementation correctly restores files and re-adds to history on failure. However:

  1. Exception logging: Line 768 should use logging.exception() to capture full traceback.
  2. Partial restore: If JPG restores successfully but RAW fails, the state is inconsistent. Consider handling this more explicitly.

Apply this diff:

         except OSError as e:
             self.update_status_message(f"Undo failed: {e}")
-            log.error(f"Failed to restore image: {e}")
+            log.exception(f"Failed to restore image: {e}")
             # Put it back in history if it failed
             self.delete_history.append((jpg_path, raw_path))

800-811: Improve exception logging in empty_recycle_bin.

The implementation correctly uses shutil.rmtree for recursive deletion. Line 810 should use logging.exception() instead of logging.error() to capture full traceback for debugging.

Apply this diff:

         except OSError as e:
-            log.error(f"Failed to empty recycle bin: {e}")
+            log.exception(f"Failed to empty recycle bin: {e}")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d9ada1 and d24a486.

📒 Files selected for processing (4)
  • faststack/faststack/app.py (13 hunks)
  • faststack/faststack/io/indexer.py (3 hunks)
  • faststack/faststack/io/sidecar.py (2 hunks)
  • faststack/faststack/logging_setup.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
faststack/faststack/io/sidecar.py (1)
faststack/faststack/models.py (2)
  • Sidecar (25-30)
  • EntryMetadata (15-21)
faststack/faststack/app.py (5)
faststack/faststack/models.py (1)
  • DecodedImage (33-42)
faststack/faststack/imaging/prefetch.py (4)
  • submit_task (117-128)
  • cancel_all (244-250)
  • update_prefetch (95-115)
  • shutdown (252-256)
faststack/faststack/config.py (3)
  • get (68-69)
  • save (58-66)
  • getfloat (74-75)
faststack/faststack/io/sidecar.py (1)
  • save (63-92)
faststack/faststack/logging_setup.py (1)
  • setup_logging (15-46)
🪛 Ruff (0.14.5)
faststack/faststack/app.py

282-282: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


287-287: Do not catch blind exception: Exception

(BLE001)


288-288: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


679-679: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


718-718: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


768-768: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


810-810: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


989-989: Do not perform function call typer.Argument in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🔇 Additional comments (9)
faststack/faststack/logging_setup.py (1)

15-45: LGTM! Clean debug mode implementation.

The conditional logging configuration properly reduces noise in production while enabling detailed debugging when needed. The docstring clearly documents the new parameter.

faststack/faststack/io/sidecar.py (1)

33-42: LGTM! Consistent timing instrumentation.

The timing measurement is appropriately scoped to the json.load() operation. Same initialization order concern as noted in indexer.py regarding _debug_mode.

faststack/faststack/app.py (6)

64-66: Global debug flag is acceptable for this use case.

The global _debug_mode flag is set early in main() and provides a simple mechanism for debug instrumentation across modules. Ensure other modules import this only after initialization (already addressed in other file reviews).


102-102: LGTM! Clean state management additions.

The new state attributes support the delete/undo feature and image caching improvements. The last_displayed_image cache prevents grey squares on decode failures.

Also applies to: 119-121


545-601: LGTM! Robust color management implementation.

The color mode and saturation factor slots include proper validation, cache invalidation, and conditional refreshing. The optimization to only refresh images when in saturation mode (line 593) is well-considered.


818-828: LGTM! Clean RAW-first preference logic.

The updated logic correctly prefers RAW files for Photoshop editing, falling back to JPG if RAW is unavailable. The logging clearly indicates which file type is being used.


854-864: Excellent security-conscious argument parsing.

The use of shlex.split() with posix=(os.name != 'nt') properly handles quoted arguments and platform differences. This is much safer than manual string splitting and pairs well with shell=False on line 873.


988-1060: Clean debug mode integration in main().

The timing instrumentation provides useful performance insights during startup. The debug flag is properly initialized early (line 994) before any module imports that depend on _debug_mode.

Note: Static analysis flagged line 989 for performing typer.Argument() call in function defaults. This is a minor style issue with typer but doesn't affect functionality.

faststack/faststack/io/indexer.py (1)

55-59: The initialization concern is not valid - _debug_mode has a safe default.

The variable _debug_mode is initialized at module level in faststack/app.py:65 with a default value of False. This eliminates any risk of AttributeError when the function-level import occurs, regardless of when find_images() is called. The pattern is defensive and correct: even if executed before main() sets _debug_mode = debug, the function will safely use the False default. No changes are required.

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: 0

🧹 Nitpick comments (9)
faststack/faststack/io/indexer.py (2)

40-40: Consider using logging.exception for better debugging.

While logging.error works, logging.exception would include the stack trace, which can be valuable for diagnosing directory scanning issues.

Based on static analysis hints.

Apply this diff:

-        log.error("Error scanning directory %s: %s", directory, e)
+        log.exception("Error scanning directory %s", directory)

55-61: Simplify the conditional logging logic.

The code logs two similar messages when DEBUG is enabled (lines 60 and 61), which creates redundancy. Consider consolidating these into a single message that includes timing only when DEBUG is enabled.

Apply this diff:

     elapsed = time.perf_counter() - t_start
     paired_count = sum(1 for im in image_files if im.raw_pair)
     
-    # Log timing info if DEBUG level is enabled
-    if log.isEnabledFor(logging.DEBUG):
-        log.info("find_images: found %d images in %.3fs", len(image_files), elapsed)
-    log.info("Found %d JPG files and paired %d with RAWs.", len(image_files), paired_count)
+    if log.isEnabledFor(logging.DEBUG):
+        log.info("Found %d JPG files and paired %d with RAWs in %.3fs", len(image_files), paired_count, elapsed)
+    else:
+        log.info("Found %d JPG files and paired %d with RAWs.", len(image_files), paired_count)
faststack/faststack/imaging/prefetch.py (2)

19-57: LGTM: ICC profile management with caching

The implementation properly caches monitor ICC profiles to minimize I/O overhead and log spam. Error handling is comprehensive.

Consider this minor refactor suggested by static analysis to improve code structure:

     try:
         profile = ImageCms.ImageCmsProfile(monitor_icc_path)
         log.debug("Loaded monitor ICC profile: %s", monitor_icc_path)
         _monitor_profile_cache[monitor_icc_path] = profile
-        return profile
     except (OSError, ImageCms.PyCMSError) as e:
         log.warning("Failed to load monitor ICC profile from %s: %s", monitor_icc_path, e)
         _monitor_profile_cache[monitor_icc_path] = None
-        return None
+    
+    return _monitor_profile_cache[monitor_icc_path]

290-292: Consider using logging.exception for better error diagnostics.

While catching broad exceptions is appropriate for robustness in the decode path, using logging.exception would include the stack trace, which is valuable for diagnosing decode failures.

Based on static analysis hints.

Apply this diff:

         except Exception as e:
-            log.error("Error decoding image %s at index %d: %s", image_file.path, index, e)
+            log.exception("Error decoding image %s at index %d", image_file.path, index)
faststack/faststack/app.py (5)

276-321: LGTM: Blocking decode prevents gray squares

The refactored implementation correctly blocks on decode when a cache miss occurs, ensuring the correct image is displayed. The fallback to last_displayed_image on errors prevents gray squares during transient failures. The 5-second timeout is reasonable for JPEG decoding.

Consider using logging.exception for lines 312 and 318 to include stack traces in error logs, which aids debugging:

             except concurrent.futures.TimeoutError:
-                log.error("Timeout decoding image at index %d", index)
+                log.exception("Timeout decoding image at index %d", index)
                 return self.last_displayed_image
             except concurrent.futures.CancelledError:
                 log.warning("Decode cancelled for index %d", index)
                 return self.last_displayed_image
             except Exception as e:
-                log.error("Error decoding image at index %d: %s", index, e)
+                log.exception("Error decoding image at index %d", index)
                 return self.last_displayed_image

Based on static analysis hints.


700-760: LGTM: Delete implementation with recycle bin

The delete operation safely moves files to a recycle bin and properly updates application state. The implementation correctly handles missing files and provides clear status feedback.

Remove the redundant exception parameter from logging.exception (line 759) as suggested by static analysis:

         except OSError as e:
             self.update_status_message(f"Delete failed: {e}")
-            log.exception(f"Failed to delete image: {e}")
+            log.exception("Failed to delete image")

762-815: LGTM: Undo implementation with history restoration

The undo operation correctly restores files and navigates to the restored image. The re-addition to history on failure (line 814) prevents data loss if the restoration fails partway through.

Remove the redundant exception parameter from logging.exception (line 812) as suggested by static analysis:

         except OSError as e:
             self.update_status_message(f"Undo failed: {e}")
-            log.exception(f"Failed to restore image: {e}")
+            log.exception("Failed to restore image")
             # Put it back in history if it failed
             self.delete_history.append((jpg_path, raw_path))

816-856: LGTM: Shutdown with recycle bin prompt

The shutdown sequence appropriately prompts users to empty the recycle bin, defaulting to "No" for safety. The implementation uses shutil.rmtree for efficient directory removal.

Remove the redundant exception parameter from logging.exception (line 855):

         except OSError as e:
-            log.exception(f"Failed to empty recycle bin: {e}")
+            log.exception("Failed to empty recycle bin")

857-932: LGTM: Enhanced Photoshop integration preferring RAW files

The implementation correctly prefers RAW files when available (as per PR objectives) and demonstrates excellent security practices:

  • Path validation before execution
  • Safe argument parsing with shlex
  • No shell execution (shell=False)
  • Proper file descriptor management (close_fds=True)

Remove redundant exception parameters from logging.exception calls (lines 928, 931):

         except FileNotFoundError as e:
             self.update_status_message(f"Photoshop executable not found: {e}")
-            log.exception(f"Photoshop executable not found: {e}")
+            log.exception("Photoshop executable not found")
         except (OSError, subprocess.SubprocessError) as e:
             self.update_status_message(f"Failed to open in Photoshop: {e}")
-            log.exception(f"Error launching Photoshop: {e}")
+            log.exception("Error launching Photoshop")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d24a486 and 9ac0596.

📒 Files selected for processing (6)
  • faststack/faststack/app.py (34 hunks)
  • faststack/faststack/imaging/prefetch.py (4 hunks)
  • faststack/faststack/io/indexer.py (5 hunks)
  • faststack/faststack/qml/Main.qml (4 hunks)
  • faststack/faststack/ui/provider.py (5 hunks)
  • faststack/pyproject.toml (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • faststack/faststack/qml/Main.qml
🧰 Additional context used
🧬 Code graph analysis (3)
faststack/faststack/imaging/prefetch.py (3)
faststack/faststack/models.py (2)
  • ImageFile (8-12)
  • DecodedImage (33-42)
faststack/faststack/imaging/jpeg.py (2)
  • decode_jpeg_rgb (22-40)
  • decode_jpeg_resized (99-149)
faststack/faststack/config.py (1)
  • get (68-69)
faststack/faststack/ui/provider.py (2)
faststack/faststack/config.py (1)
  • get (68-69)
faststack/faststack/app.py (3)
  • get_current_metadata (366-393)
  • get_color_mode (582-584)
  • get_saturation_factor (617-619)
faststack/faststack/app.py (7)
faststack/faststack/models.py (1)
  • DecodedImage (33-42)
faststack/faststack/imaging/prefetch.py (4)
  • cancel_all (307-314)
  • update_prefetch (129-170)
  • submit_task (172-183)
  • shutdown (316-320)
faststack/faststack/ui/provider.py (10)
  • currentIndex (142-143)
  • imageCount (146-147)
  • currentFilename (154-157)
  • isFlagged (160-163)
  • isRejected (166-169)
  • stackInfoText (184-187)
  • preloadProgress (131-132)
  • preloadProgress (135-138)
  • isPreloading (121-122)
  • isPreloading (125-128)
faststack/faststack/io/watcher.py (1)
  • start (49-62)
faststack/faststack/config.py (3)
  • save (58-66)
  • get (68-69)
  • getfloat (74-75)
faststack/faststack/io/sidecar.py (1)
  • save (63-92)
faststack/faststack/logging_setup.py (1)
  • setup_logging (15-46)
🪛 Ruff (0.14.5)
faststack/faststack/imaging/prefetch.py

52-52: Consider moving this statement to an else block

(TRY300)


288-288: Consider moving this statement to an else block

(TRY300)


290-290: Do not catch blind exception: Exception

(BLE001)


291-291: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

faststack/faststack/app.py

312-312: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


317-317: Do not catch blind exception: Exception

(BLE001)


318-318: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


513-513: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


716-716: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


759-759: Redundant exception object included in logging.exception call

(TRY401)


812-812: Redundant exception object included in logging.exception call

(TRY401)


855-855: Redundant exception object included in logging.exception call

(TRY401)


907-907: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


928-928: Redundant exception object included in logging.exception call

(TRY401)


931-931: Redundant exception object included in logging.exception call

(TRY401)

faststack/faststack/io/indexer.py

40-40: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🔇 Additional comments (23)
faststack/pyproject.toml (2)

8-8: LGTM: Version bump to 0.8

The version increment aligns with the PR objectives and changes introduced.


31-31: LGTM: CLI entrypoint updated

The entrypoint change from main to cli aligns with the new argument parsing structure introduced in app.py.

faststack/faststack/io/indexer.py (1)

22-23: LGTM: Timing instrumentation added

The timing setup follows a standard pattern for performance monitoring.

faststack/faststack/ui/provider.py (4)

47-58: LGTM: ICC mode color space handling

The logic correctly skips Qt's sRGB color space assignment when ICC mode is active, since pixels are already in monitor space. The expanded exception handling for ValueError improves robustness.


85-86: LGTM: New signals for color management

The new signals follow standard Qt patterns and support the color management UI bindings.


155-187: LGTM: Null safety guards added

The defensive checks prevent crashes when image_files is empty, returning safe defaults. This improves robustness across all metadata properties.


214-222: LGTM: Color management properties exposed to QML

The new properties correctly expose color mode and saturation factor settings to the UI layer, following standard Qt property patterns.

faststack/faststack/imaging/prefetch.py (6)

59-100: LGTM: Saturation compensation algorithm

The implementation efficiently applies saturation adjustment in-place using vectorized NumPy operations. The algorithm correctly interpolates between grayscale and original RGB values, with proper bounds checking and handling of buffer padding.


117-122: LGTM: Adaptive prefetch radius attributes

The attributes support a smart optimization: starting with a small prefetch radius during startup to reduce cache thrashing, then expanding once the user begins navigation.


129-170: LGTM: Enhanced prefetch with adaptive radius and deduplication

The implementation intelligently manages prefetch radius expansion and prevents duplicate task submission. The deduplication logic correctly tracks scheduled indices per generation.


274-288: LGTM: Generation-aware caching

The re-check of generation before caching correctly prevents stale results from being cached after the prefetch has moved on.


295-314: LGTM: Updated helper methods

The optional radius parameter in _is_in_prefetch_range supports the adaptive radius feature, and clearing _scheduled in cancel_all ensures proper cleanup.


194-272: Verify fallback value discrepancy for saturation_factor

Config defines saturation_factor default as "0.85" (config.py line 28), but prefetch.py line 268 uses fallback="1.0". Since 1.0 means no saturation compensation and 0.85 means 15% reduction, this discrepancy means if config.get fails, the code silently uses no compensation instead of the configured intent. Confirm this is intentional (safe fallback to no-op) or should match the configured default.

The exception handling for saturation compensation (lines 270-271) is appropriate: catching ValueError (invalid config value) and AssertionError (buffer dimension mismatch) are specific to the actual failure modes, and logging a warning while continuing is correct since the image remains usable without compensation applied.

faststack/faststack/app.py (10)

64-65: LGTM: Global debug flag

The module-level debug flag provides a simple mechanism for conditional debug instrumentation across the application.


87-88: LGTM: Display readiness tracking

These attributes support the deferred prefetch optimization, preventing cache invalidation before the display size stabilizes.


120-124: LGTM: Delete/undo state management

The recycle bin and delete history structures properly support the delete-with-undo feature introduced in this version.


189-210: LGTM: Display resize handling with deferred prefetch

The implementation correctly defers prefetch until the display size stabilizes on first resize, then processes any pending prefetch request. This optimization reduces cache thrashing during startup.


231-242: LGTM: Centralized prefetch deferral logic

The _do_prefetch helper properly encapsulates the logic for deferring prefetch until display is ready, improving code consistency across navigation and filter operations.


351-361: LGTM: Navigation tracking for adaptive prefetch

Passing is_navigation=True to _do_prefetch enables the adaptive radius expansion feature in the prefetcher.


581-638: LGTM: Color mode and saturation factor controls

The implementations properly validate inputs, persist configuration, and intelligently manage cache invalidation. The optimization to only refresh when in saturation mode (line 630) prevents unnecessary work when adjusting the slider in other modes.


413-486: LGTM: Parameterized logging improvements

The conversion from f-strings to parameterized logging (using % formatting) follows Python logging best practices and improves performance by deferring string interpolation until the message is actually logged.


1033-1104: LGTM: Enhanced main() with debug mode and timing

The addition of the debug parameter and startup timing instrumentation provides valuable performance insights. The handling of image_dir as a parameter supports the new CLI entrypoint while maintaining backward compatibility with default directory configuration.


1106-1115: LGTM: New CLI entrypoint with argparse

The new cli() function provides a clean command-line interface using argparse, aligning with the pyproject.toml entrypoint change and eliminating the typer dependency.

@AlanRockefeller AlanRockefeller merged commit 796f1e0 into main Nov 21, 2025
1 check passed
This was referenced Nov 21, 2025
Merged
Closed
Merged
@coderabbitai coderabbitai Bot mentioned this pull request Dec 8, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Jan 1, 2026
This was referenced Jan 29, 2026
Merged
@coderabbitai coderabbitai Bot mentioned this pull request Mar 13, 2026
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