Skip to content

v1.1#17

Closed
AlanRockefeller wants to merge 3 commits intomainfrom
test
Closed

v1.1#17
AlanRockefeller wants to merge 3 commits intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Nov 22, 2025

Summary by CodeRabbit

Release Notes - Version 1.2

  • New Features

    • Built-in Image Editor with exposure, contrast, saturation, white balance, crop, and rotation controls
    • Quick Auto White Balance with auto-save and undo support (A key)
    • Batch drag-and-drop support (B key)
    • Jump-to-image dialog (G key)
    • Dark/Light theme toggle
    • Filter dialog for filename-based image filtering
    • Floating menu bar overlay with File, View, Actions, and Help menus
    • Recycle bin for deleted images with undo restoration
    • Comprehensive keyboard shortcuts system
  • UI/UX Improvements

    • Enhanced status footer with image index, stack/batch info, and color mode controls
    • Improved settings dialog with cache and prefetch management
    • Theme-aware UI components with Material design
    • Better image navigation with directional prefetch optimization
  • Documentation

    • Updated README with latest features and version 1.2
    • Added ChangeLog documenting all releases
    • Added MIT License file

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 22, 2025

Walkthrough

This PR introduces FastStack, a comprehensive ultra-fast JPG/RAW image viewer with caching and focus stacking support. It adds a complete application comprising Python backend modules for image processing (JPEG decoding, caching, prefetching with ICC color management), file I/O (scanning, RAW pairing, metadata sidecars, filesystem watching), integration with Helicon Focus and Photoshop, and a QML-based UI. A second update adds an image editor with exposure, contrast, saturation, white balance, cropping, and auto white balance functionality.

Changes

Cohort / File(s) Change Summary
Project Metadata & Configuration
faststack.working-menus/pyproject.toml, faststack.working-menus/requirements.txt, faststack.working-menus/LICENSE, faststack.working-menus/README.md, faststack.working-menus/ChangeLog.md
Added project configuration with PEP 517 build system, Python dependencies, MIT license, and comprehensive documentation. faststack/pyproject.toml updated to version 1.2 with editor additions; removed typer dependency from faststack.egg-info/requires.txt.
Package Metadata
faststack.working-menus/faststack.egg-info/*, faststack/faststack.egg-info/PKG-INFO
Added egg-info metadata: PKG-INFO, SOURCES.txt, entry_points.txt (console script faststack → faststack.app:cli), requires.txt with versioned dependencies, dependency_links.txt, and top_level.txt.
Core Models & Configuration
faststack/models.py, faststack/config.py, faststack/logging_setup.py
Introduced data structures (ImageFile, EntryMetadata, Sidecar, DecodedImage) for image metadata and state. Added AppConfig class for INI-based configuration persistence and get_app_data_dir/setup_logging utilities for application initialization.
Image Processing & Caching
faststack/imaging/jpeg.py, faststack/imaging/cache.py, faststack/imaging/prefetch.py, faststack/imaging/editor.py
Implemented JPEG decoding with PyTurboJPEG/Pillow fallback, byte-aware LRU cache (ByteLRUCache), and background prefetching with generation-based invalidation. Added ImageEditor supporting exposure, brightness, contrast, saturation, white balance, cropping, rotation, and sharpness adjustments with ICC color management and saturation compensation.
File I/O & Metadata
faststack/io/indexer.py, faststack/io/sidecar.py, faststack/io/watcher.py, faststack/io/helicon.py, faststack/io/executable_validator.py
Added directory scanning with JPG/RAW pairing logic, JSON-based sidecar metadata persistence (SidecarManager), filesystem event watching (Watcher with watchdog), Helicon Focus integration (launch_helicon_focus), and secure executable path validation.
Application Controller & UI State
faststack/app.py, faststack/ui/provider.py, faststack/ui/keystrokes.py
Introduced AppController orchestrating image loading, filtering, caching, prefetching, stack/batch management, metadata toggling, Photoshop/Helicon integration, drag-and-drop, and undo history. Added UIState QObject exposing UI properties/signals to QML and ImageProvider for image rendering. Implemented Keybinder for keyboard event dispatching.
QML UI Components
faststack/qml/Main.qml, faststack/qml/Components.qml, faststack/qml/SettingsDialog.qml, faststack/qml/JumpToImageDialog.qml, faststack/qml/FilterDialog.qml, faststack/qml/ImageEditorDialog.qml*
Built ApplicationWindow with menu bar, dialogs for settings/jump-to/filter/editor, and main image viewer (Components.qml) supporting zoom/pan with mouse interaction and debounced resize notifications. ImageEditorDialog added with parameter sliders for brightness, contrast, saturation, white balance, exposure, shadows, highlights, clarity, sharpness, rotation, and effects.
Tests
faststack/tests/test_cache.py, faststack/tests/test_executable_validator.py, faststack/tests/test_pairing.py, faststack/tests/test_sidecar.py
Added unit tests for ByteLRUCache eviction, executable validation logic, RAW-JPG pairing heuristics, and SidecarManager load/save workflows.
Documentation Updates
faststack/README.md, faststack/ChangeLog.md
Updated version to 1.2 with new Image Editor features, Quick Auto White Balance (A), and revised keyboard shortcuts.
Backup Files
faststack/qml/*.bak, faststack/qml/*.old, faststack/qml/*.new
Legacy/alternate QML component versions retained during development.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant QML as QML UI
    participant Controller as AppController
    participant Indexer as Image IO
    participant Cache as ByteLRUCache
    participant Prefetcher
    participant Sidecar as SidecarManager
    
    User->>QML: Open directory
    QML->>Controller: load(directory)
    Controller->>Indexer: find_images(dir)
    Indexer-->>Controller: [ImageFile, ...]
    Controller->>Sidecar: load()
    Sidecar-->>Controller: Sidecar
    Controller->>Controller: _load_image_at(0)
    Controller->>Cache: Decode & cache image
    Controller->>Prefetcher: update_prefetch(0)
    Prefetcher-->>Cache: Async decode adjacent
    Controller->>QML: currentIndexChanged, imageCountChanged
    QML-->>User: Display image
    
    User->>QML: Press next image
    QML->>Controller: next_image()
    Controller->>Controller: current_index++
    Controller->>Prefetcher: update_prefetch(idx, is_navigation=true)
    Controller->>QML: currentIndexChanged, metadataChanged
    QML-->>User: Display next image
Loading
sequenceDiagram
    actor User
    participant QML as ImageEditorDialog
    participant Controller as AppController
    participant Editor as ImageEditor
    participant Cache as ByteLRUCache
    
    User->>QML: Open Editor
    QML->>Controller: load_image_for_editing()
    Controller->>Editor: load_image(current_path)
    Editor-->>Controller: preview loaded
    QML->>User: Show editor sliders
    
    User->>QML: Adjust brightness
    QML->>Controller: set_edit_parameter("brightness", value)
    Controller->>Editor: set_edit_param("brightness", value)
    Controller->>Editor: get_preview_data()
    Editor-->>Controller: preview image
    Controller->>Cache: Cache preview
    Controller->>QML: currentEditedImageDataChanged
    QML-->>User: Show edited preview
    
    User->>QML: Save Edited Image
    QML->>Controller: save_edited_image()
    Controller->>Editor: save_image()
    Editor-->>Controller: saved path
    Controller->>Controller: reload image list & cache
    Controller->>QML: imageListChanged
    QML-->>User: Display saved image
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150+ minutes

Key areas requiring detailed review:

  • AppController (faststack/app.py): Massive class (~2000+ lines) coordinating image loading, filtering, caching, stacking, batching, Photoshop/Helicon integration, drag-and-drop, undo history, and metadata persistence. Requires careful validation of state machine consistency, signal/slot correctness, and integration points.
  • Image Processing Pipeline (imaging/jpeg.py, cache.py, prefetch.py, editor.py): Complex multi-threaded prefetching with generation-based invalidation, ICC color management transforms, TurboJPEG fallbacks, and image editing operations. Verify memory safety, error handling, and color space correctness.
  • QML/PySide6 Integration (ui/provider.py, qml/Main.qml): Extensive two-way binding between Python state and QML. Verify property synchronization, signal connections, and null-safety guards throughout.
  • File I/O & Metadata (io/sidecar.py, io/watcher.py, io/indexer.py): JSON persistence, filesystem event handling, and RAW pairing heuristics. Check atomic writes, race conditions, and robustness to concurrent modifications.
  • ImageEditor Workflow: New crop/rotation/adjustment pipeline with undo support. Verify parameter ranges, preview generation correctness, and file save logic.
  • Helicon Focus & Photoshop Integration (io/helicon.py, app.py): External process launching, argument parsing, and file selection logic. Validate executable validation, safe argument construction, and metadata updates post-launch.
  • Keyboard & Event Handling (ui/keystrokes.py, Components.qml): Key binding dispatch, drag initiation, mouse events. Verify event propagation and action dispatch correctness.

Possibly related PRs

  • Release v0.5 — more improvements #7: Overlaps on JPEG scaling, prefetcher mmap handling, watcher events, AppController zoom logic, and Components.qml resize.
  • Test #14: Related through prefetch/ICC caching, TurboJPEG decode paths, and Helicon JPG fallback changes.
  • Release v0.4 — more improvements #6: Shared prefetching/caching design, preload-all-images flow with progress, and AppController/UIState/Prefetcher integration patterns.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.37% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'v1.1' is vague and does not clearly describe the substantive changes in the pull request, which include an image editor, auto white balance, enhanced batch display, bug fixes, and version updates. Use a descriptive title that summarizes the main changes, such as 'Add image editor with auto white balance and enhance batch display' or 'Release v1.1: Image editor, auto white balance, and UI improvements'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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: 32

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
faststack/README.md (1)

12-12: Fix typo in main README feature list.

Same as the working-menus README, this should read “high-performance decoding”.

-- **Instant Navigation:** Sub-10ms next/previous image switching, high peformance decoding via `PyTurboJPEG`.
+- **Instant Navigation:** Sub-10ms next/previous image switching, high-performance decoding via `PyTurboJPEG`.
faststack/faststack/qml/Main.qml (1)

424-424: Inconsistent help text for remove from batch/stack.

Line 424 states "X or S: Remove current image from batch/stack", but based on the keystrokes mapping, S is bound to toggle_stack_membership, not removal. Only X should be listed here.

Apply this diff:

-                      "&nbsp;&nbsp;X or S: Remove current image from batch/stack<br><br>" +
+                      "&nbsp;&nbsp;X: Remove current image from batch/stack<br><br>" +
faststack/faststack/app.py (1)

318-335: Fix type annotation and implementation of AppController.get_preview_data()

Verification confirms a real type mismatch: AppController.get_preview_data() at line 1643 declares return type Optional[bytes] with @Slot(result=bytes), but its implementation returns a DecodedImage object from self.image_editor.get_preview_data().

  • ImageEditor.get_preview_data() (editor.py:215) returns Optional[DecodedImage] and constructs DecodedImage(buffer=..., width=..., height=..., bytes_per_line=...).
  • AppController.get_preview_data() (app.py:1643) receives this DecodedImage and returns it directly as if it were bytes, which will fail if QML calls this slot expecting byte data.

The get_decoded_image() method (lines 318-335) itself is correct—it calls self.image_editor.get_preview_data() directly and properly returns DecodedImage.

Required fix at app.py:1643: Either change the return type and @Slot result to DecodedImage, or serialize the DecodedImage to bytes and update the implementation accordingly to match the declared return type.

🧹 Nitpick comments (45)
faststack.working-menus/ChangeLog.md (2)

100-105: Consider more formal verb choice (LanguageTool suggestion).

The phrasing "Fixed an issue where panning..." could use slightly more formal wording, though this is optional. LanguageTool suggests reconsidering the verb usage for style consistency.


116-122: Use hyphen for compound adjective (grammar fix).

The phrase "full res image" should be hyphenated as "full-res image" when used as a compound adjective modifying "image". Additionally, "Make it use" would be more formal as a task statement.

Apply this diff for improved grammar:

 ### Todo
 
-Make it use the full res image when zooming in
+Make it use the full-res image when zooming in
 When multiple stacks are selected, call Helicon multiple times
faststack.working-menus/faststack.egg-info/PKG-INFO (1)

3-25: Avoid committing stale PKG-INFO or clarify its role vs the 1.1 release.

This metadata advertises Version: 1.0 and a “Version 1.0 - November 21, 2025” description while the main project is moving to 1.1. If this egg-info isn’t actually used for distribution, consider removing it from VCS; if it is used, it should be aligned with the main package version to avoid confusing installers/tools.

faststack.working-menus/faststack/logging_setup.py (1)

21-37: Guard against adding duplicate file handlers on repeated setup_logging() calls.

If setup_logging() is called more than once in a process (e.g., tests, re-init), you’ll get duplicate log lines because a new RotatingFileHandler is always added to the root logger. It’s cheap to guard against this by checking for an existing handler targeting the same log file.

-    root_logger = logging.getLogger()
-    # Set log level based on debug flag
-    root_logger.setLevel(logging.DEBUG if debug else logging.INFO)
-    root_logger.addHandler(handler)
+    root_logger = logging.getLogger()
+    # Set log level based on debug flag
+    root_logger.setLevel(logging.DEBUG if debug else logging.INFO)
+
+    # Avoid adding duplicate rotating file handlers for the same log file
+    if not any(
+        isinstance(h, logging.handlers.RotatingFileHandler)
+        and getattr(h, "baseFilename", None) == str(log_file)
+        for h in root_logger.handlers
+    ):
+        root_logger.addHandler(handler)

</blockquote></details>
<details>
<summary>faststack.working-menus/faststack/qml/Main.qml.bak (2)</summary><blockquote>

`120-130`: **Fix window drag logic to avoid exaggerated movement (likely copied into real Main.qml).**

`onPositionChanged` always computes `delta` from the original press position but accumulates it on every move, so the window moves too far as the mouse moves. Either update `lastMousePos` each time or compute absolute positioning instead of incremental updates.

```diff
-            onPositionChanged: function(mouse) {
-                var delta = Qt.point(mouse.x - lastMousePos.x, mouse.y - lastMousePos.y)
-                root.x += delta.x
-                root.y += delta.y
-            }
+            onPositionChanged: function(mouse) {
+                var delta = Qt.point(mouse.x - lastMousePos.x, mouse.y - lastMousePos.y)
+                root.x += delta.x
+                root.y += delta.y
+                lastMousePos = Qt.point(mouse.x, mouse.y)
+            }

Even though this is a .bak file, it’s worth fixing if you ever copy/paste from it.


241-259: Update about dialog key-bindings text to match current shortcuts.

The help text still says G: Toggle Grid View (not implemented), while the READMEs describe G as “Go to image #” and you now have a JumpToImageDialog. To avoid confusing users, either update this dialog to the current bindings or clearly mark it as legacy if it’s only kept as a backup.

faststack/faststack/imaging/editor.py (2)

82-87: Replace print + bare except Exception with proper logging and narrower error handling.

load_image() and save_image() swallow all exceptions and only print, which makes real issues hard to diagnose and can hide programming errors. Since the project now has centralized logging, it’s better to log exceptions via a module logger and (optionally) narrow the exception types.

+import logging
@@
-from faststack.models import DecodedImage
-from PySide6.QtGui import QImage
+from faststack.models import DecodedImage
+from PySide6.QtGui import QImage
+
+logger = logging.getLogger(__name__)
@@
-        except Exception as e:
-            print(f"Error loading image for editing: {e}")
+        except Exception:
+            # Log full stack trace; return False so callers can handle gracefully
+            logger.exception("Error loading image for editing: %s", filepath)
             self.original_image = None
             self._preview_image = None
             return False
@@
-            try:
+            try:
@@
-            except Exception as e:
-                print(f"Warning: Could not preserve EXIF data: {e}")
+            except Exception:
+                logger.exception("Warning: Could not preserve EXIF data for %s", original_path)
                 # Fall back to saving without EXIF if there's an issue
                 final_img.save(original_path, format='JPEG', quality=95)
@@
-        except Exception as e:
-            print(f"Failed to save edited image or backup: {e}")
+        except Exception:
+            logger.exception("Failed to save edited image or backup for %s", original_path)
             return None

You can further narrow the caught exceptions later if needed, but this at least gets failures into your log instead of stdout.

Also applies to: 270-293


245-285: Confirm save_image() is only used for JPEGs, or guard against non-JPEG input.

save_image() always writes format='JPEG' back to original_path, regardless of the original extension. That’s fine if the editor is strictly for JPGs, but if it ever receives PNG/other formats, you’ll silently convert them to JPEG while keeping the old extension.

Consider either:

  • Asserting/validating that original_path.suffix is a JPEG extension before saving, or
  • Deriving the save format from the original file type instead of hard-coding JPEG.

This will make the behavior explicit and avoid surprising conversions if usage expands beyond JPG.

faststack.working-menus/faststack/imaging/prefetch.py (3)

129-241: Prefetch window and scheduling look sound, but consider current‑index prioritization.

The asymmetric window computation and scheduled set per generation look correct and avoid re‑submitting tasks. However, submit_task’s priority flag is never used here, so the currently viewed image isn’t explicitly prioritized over neighbors from this code path. If you rely on the prefetcher itself (rather than an external caller) to prioritize the current index, consider calling submit_task(current_index, self.generation, priority=True) or similar when appropriate.


87-128: Saturation compensation logic is correct; optional micro‑optimizations.

The shape assertions and RGB reshaping are consistent with how arr, w, h, and bytes_per_line are constructed in this module, and the math for desaturation is correct. If you ever profile this path and find it hot, you could eliminate one reshape and the extra temporary from astype(np.float32) by reusing buffers and in‑place operations, but it’s not essential right now.


272-460: Running verification scripts to inspect the cache implementation:

Add thread-safety synchronization to cache operations and consider futures cleanup for stale generations.

The cachetools.LRUCache classes are not thread-safe, and access from multiple threads must be properly synchronized with a suitable lock object. Since ByteLRUCache extends this unsafe base class and cache_put (bound to __setitem__) is called directly from worker threads in _decode_and_cache, concurrent writes risk cache corruption.

Recommendation:

  • Wrap cache operations with a threading lock: add self.cache_lock = threading.Lock() to Prefetcher.__init__, then use with self.cache_lock: self.cache_put(...) at line 453.
  • For futures cleanup: consider clearing self.futures[index] in _decode_and_cache after detecting a stale generation (lines 273–276, 441–443) to allow prompt re-submission of the current image on zoom/mode changes, rather than waiting for the old future to complete.
faststack.working-menus/patch (2)

40-117: Edit application flow is clear; consider optional bounds‑checking and logging.

The edit pipeline (crop → brightness → contrast → saturation → white balance) is straightforward and deterministic, and using PNG for previews avoids cumulative JPEG artifacts. You rely on the UI to keep normalized parameters in the intended ranges; if you expect any external callers, you might want to clamp brightness/contrast/saturation to [-1, 1] and white_balance_* to the expected range, and consider logging when load_image fails instead of printing to stdout.


336-407: Controller↔editor wiring looks consistent; minor UX enhancements possible.

  • Loading the current image into ImageEditor and calling reset_edit_parameters() when the current image changes is a good reset point.
  • Slots set_edit_parameter and set_crop_selection_normalized correctly keep the Python editor state, previews, and QML properties in sync.

Optional tweaks:

  • In set_crop_selection_normalized, you might also set hasActiveCropSelection via the model to ensure consistency if other code paths manage it.
  • In save_or_discard_edits, consider logging when save_changes is true but save_image() fails, so failures surface beyond the UI.
faststack/ChangeLog.md (1)

3-31: Verify 1.1.0 release notes match actual shortcuts and behavior

The 1.1.0 notes (E to open editor, Ctrl+S to save, A for auto WB, B for batch toggle) look clear, but they’re tightly coupled to UI logic and key handling elsewhere. Please double-check that:

  • The documented keys match what Main.qml / controller actually bind.
  • Auto-WB and editor behavior (auto-save, undo, backup naming) match what’s implemented.

This keeps the ChangeLog trustworthy for users.

faststack.working-menus/faststack/benchmark_decode.py (1)

1-20: Make benchmark script safe to import and path-configurable

Right now the benchmark runs on import and hard‑codes a Windows‑specific path, which makes accidental imports brittle and limits reuse.

Consider refactoring to something like:

import argparse

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("image", type=Path)
    parser.add_argument("--iterations", type=int, default=20)
    args = parser.parse_args()

    print(f"TurboJPEG available: {TURBO_AVAILABLE}")
    start = time.perf_counter()
    for _ in range(args.iterations):
        with open(args.image, "rb") as f:
            with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mmapped:
                jpeg_bytes = mmapped[:]
        decode_jpeg_resized(jpeg_bytes, 1920, 1080)
    elapsed = time.perf_counter() - start
    print(f"Average time (with mmap): {elapsed/args.iterations*1000:.1f}ms")

if __name__ == "__main__":
    main()

This keeps the benchmark useful without impacting the package if imported.

faststack.working-menus/faststack/tests/test_pairing.py (1)

36-74: Pairing tests cover key scenarios; consider minor style cleanups

The tests exercise:

  • Directory scan ordering and pairing (test_find_images).
  • _find_raw_pair behavior for perfect match, out‑of‑range delta, “closest of many”, and empty candidates.

This lines up well with the indexer implementation and should catch regressions in RAW–JPG pairing.

For Ruff’s E702 warnings and readability, you might optionally split the semicolon‑separated statements, e.g.:

jpg_stat = MagicMock()
jpg_stat.st_mtime = 1000.0

instead of keeping them on one line.

faststack.working-menus/faststack/models.py (1)

7-44: Unify Optional/union style and confirm sidecar metadata fields

Two small points here:

  • Type hints currently use Optional[...] and Dict/List, while other modules (e.g. _find_raw_pair) are already on the Path | None and builtin dict[...]/list[...] style. To keep Ruff (RUF013) and style consistent, consider:
from __future__ import annotations  # if not already at module top elsewhere

class ImageFile:
    path: Path
    raw_pair: Path | None = None
    ...

class EntryMetadata:
    stack_id: int | None = None
    stacked_date: str | None = None
    ...
  • Tests and example sidecar JSON refer to flag and reject fields on EntryMetadata. If those are still part of the sidecar schema, they should probably be added here; if they’ve been removed in v2, the tests/loader should be updated accordingly.
faststack.working-menus/faststack/tests/test_executable_validator.py (1)

40-41: Clean up unused lambda args/variables and consider asserting on log warnings.

  • Lines 40, 62, and 123: the lambda self: ... assignments for __str__ trigger Ruff’s ARG005 (“unused lambda argument”). If you want to keep this approach, prefer lambda _self: ... or, better, configure the MagicMock directly:
mock_path_instance.__str__.return_value = photoshop_path

This avoids the unused-parameter warning and aligns with how MagicMock is typically customized.

  • Lines 66 and 127: error is unpacked but never used. If you just need to keep the tuple shape, rename to _error or _ to satisfy Ruff (RUF059) and document intent.

  • For test_suspicious_path_with_traversal, the docstring mentions that suspicious paths are “flagged”, but the test only checks is_valid and not that a warning was actually logged. If you want stronger coverage, you could use caplog to assert that a warning about a suspicious path was emitted.

These changes are small but will keep the test suite cleaner and closer to what static analysis and the docstrings imply.

Also applies to: 62-63, 66-66, 123-124, 127-127

faststack.working-menus/faststack/imaging/cache.py (1)

10-45: Align get_decoded_image_size docstring with actual DecodedImage usage and consider logging on the fallback path.

get_decoded_image_size() currently documents “decoded image tuple (buffer, qimage)” but in practice it operates on DecodedImage instances (imported from faststack.models). That mismatch can be confusing when someone reads the docstring or reuses this helper.

Consider tightening this up:

def get_decoded_image_size(item) -> int:
    """Return the size in bytes of a DecodedImage buffer."""
    from faststack.models import DecodedImage

    if isinstance(item, DecodedImage):
        ...
    else:
        # Log unexpected type so we notice misuses
        log.warning("ByteLRUCache received non-DecodedImage item of type %s", type(item))
        return 1

This makes the expected type explicit and turns the “should not happen” path into something that’s observable when it does happen.

faststack.working-menus/faststack/qml/JumpToImageDialog.qml (1)

32-37: Guard the Ok button against invalid/empty input so the dialog doesn’t silently close without jumping.

Right now onAccepted only calls controller.jump_to_image(...) when the parsed value is within [1, maxImageCount], but the dialog still closes even if the field is empty or out of range. From a user’s perspective that looks like “Ok” did nothing.

A simple way to tighten this up is to tie the Ok button’s enabled state to the validator:

  • If you’re using the default standardButtons, you can add a footer with an explicit Ok Button:
footer: DialogButtonBox {
    standardButtons: Dialog.Cancel
    Button {
        text: "OK"
        enabled: imageNumberField.acceptableInput
        onClicked: jumpDialog.accept()
    }
}

or, if you stick with standardButtons, you can bind enabled on the DialogButtonBox’s Ok button via component.onCompleted lookup.

That way, “Ok” is only clickable when the input passes IntValidator, and you avoid the “dialog closes but nothing happens” UX trap.

Also applies to: 54-72

faststack.working-menus/faststack/qml/Components.qml (1)

86-123: Drag-outside detection is tied to loupeView’s size; consider anchoring to the Window for robustness.

The drag-and-drop trigger checks:

var globalPos = mapToItem(null, mouse.x, mouse.y)
if (globalPos.x < 0 || globalPos.y < 0 ||
    globalPos.x > loupeView.width || globalPos.y > loupeView.height) {
    isDraggingOutside = true
    controller.start_drag_current_image()
}

This works as long as loupeView fills the top-level window starting at (0,0). If layout changes (padding, nested containers, multi-window), loupeView.width/height may no longer match the window’s bounds, and the “outside” check will misfire.

If you want this behavior to be resilient, you might:

  • Map to the actual Window item instead of null and compare with that window’s width/height, or
  • Expose the window size via uiState or a root id (e.g., rootWindow) and compare against that.

That keeps the drag-outside heuristic correct even if the viewer layout evolves.

faststack.working-menus/faststack/config.py (1)

58-67: Use logging.exception on save failures to capture stack traces.

In save(), IO failures are logged with log.error, which drops the traceback:

except IOError as e:
    log.error(f"Failed to save config to {self.config_path}: {e}")

Switching to log.exception here will retain the same message but also record the stack trace, which is usually what you want when config persistence fails:

except IOError:
    log.exception("Failed to save config to %s", self.config_path)

That should satisfy the Ruff hint and make diagnosing config write problems easier.

faststack.working-menus/faststack/io/helicon.py (2)

70-70: Remove redundant exception object from log.exception call.

The log.exception() method automatically includes exception information, so explicitly passing {e} is redundant.

Apply this diff:

-                log.exception(f"Invalid helicon args format: {e}")
+                log.exception("Invalid helicon args format")

86-91: Remove redundant exception objects from log.exception calls.

The log.exception() method automatically captures and logs exception information, so explicitly including {e} in the message is redundant.

Apply this diff:

-        log.exception(f"Failed to launch Helicon Focus: {e}")
+        log.exception("Failed to launch Helicon Focus")
         return False, None
-    except (IOError, PermissionError) as e:
-        log.exception(f"Failed to create temporary file for Helicon Focus: {e}")
+    except (IOError, PermissionError):
+        log.exception("Failed to create temporary file for Helicon Focus")
         return False, None
faststack.working-menus/faststack/io/indexer.py (1)

64-84: Remove unused jpg_path parameter.

The jpg_path parameter in _find_raw_pair is never used in the function body. It can be removed to simplify the signature.

Apply this diff:

 def _find_raw_pair(
-    jpg_path: Path,
     jpg_stat: os.stat_result,
     potential_raws: List[Tuple[Path, os.stat_result]]
 ) -> Path | None:
     """Finds the best RAW pair for a JPG from a list of candidates."""

And update the call site:

     image_files: List[ImageFile] = []
     for jpg_path, jpg_stat in jpgs:
-        raw_pair = _find_raw_pair(jpg_path, jpg_stat, raws.get(jpg_path.stem, []))
+        raw_pair = _find_raw_pair(jpg_stat, raws.get(jpg_path.stem, []))
         image_files.append(ImageFile(
faststack.working-menus/faststack/io/executable_validator.py (1)

106-112: Consider restructuring exception handling for clarity.

The return True statement could be moved to an else block for clearer control flow, though the current implementation is functionally correct.

Apply this diff:

 def _is_subpath(path: Path, parent: Path) -> bool:
     """Check if path is a subpath of parent."""
     try:
         path.resolve().relative_to(parent.resolve())
-        return True
     except (ValueError, RuntimeError):
         return False
+    else:
+        return True
faststack.working-menus/faststack/io/sidecar.py (2)

57-60: Use log.exception instead of log.error in exception handler.

When logging within an exception handler, log.exception() is preferred over log.error() as it automatically includes the stack trace, which aids debugging.

Apply this diff:

         except (json.JSONDecodeError, TypeError) as e:
-            log.error(f"Failed to load or parse sidecar file {self.path}: {e}")
+            log.exception(f"Failed to load or parse sidecar file {self.path}")
             # Consider backing up the corrupted file here
             return Sidecar()

87-89: Use log.exception instead of log.error in exception handler.

When logging within an exception handler, log.exception() is preferred as it automatically includes the stack trace.

Apply this diff:

         except (IOError, TypeError) as e:
-            log.error(f"Failed to save sidecar file {self.path}: {e}")
+            log.exception(f"Failed to save sidecar file {self.path}")
faststack.working-menus/faststack/qml/SettingsDialog.qml (2)

18-25: Cache usage Timer behaves correctly; consider eager refresh on open

The Timer wiring looks good and stops when the dialog closes. If you want to avoid showing 0.00 GB for the first second, you could also refresh cacheUsage immediately when the dialog becomes visible:

     onVisibleChanged: {
         cacheUsageTimer.running = visible
         if (visible) {
+            cacheUsage = uiState.get_cache_usage_gb()
             controller.dialog_opened()
         } else {
             controller.dialog_closed()
         }
     }

Also applies to: 164-171


59-73: Path existence checks may be a bit aggressive

visible: uiState.check_path_exists(heliconPathField.text) / photoshopPathField.text re-run the check on every text change. If check_path_exists() is more than a cheap local stat call, consider debouncing (e.g., checking on onEditingFinished or via a short Timer) to avoid blocking the UI while typing.

Also applies to: 83-97

faststack.working-menus/faststack/imaging/jpeg.py (1)

29-30: Simplify logging.exception calls to match Ruff’s TRY401 guidance

logging.exception() already logs the active exception and stack trace; interpolating e into the message is redundant and triggers TRY401. You can keep the context while simplifying the calls:

-    except Exception as e:
-        log.exception(f"PyTurboJPEG failed to decode image: {e}. Trying Pillow.")
+    except Exception:
+        log.exception("PyTurboJPEG failed to decode image. Trying Pillow.")

Apply the same pattern to the other logging.exception sites in this module (Pillow also failed..., thumbnail, resized path) to quiet TRY401 and keep logs cleaner.

Also applies to: 38-39, 66-67, 75-76, 129-130, 147-148

faststack/faststack/qml/ImageEditorDialog.qml.old (1)

1-196: Consider dropping this .qml.old snapshot from the repo

This appears to be an older copy of the Image Editor dialog, with the real implementation now in ImageEditorDialog.qml.new. Since .qml.old won’t be loaded by the engine and largely duplicates logic, it’s probably better to delete it (or move it to docs/notes) to avoid confusion about which editor QML is authoritative.

faststack/faststack/qml/ImageEditorDialog.qml.new (1)

136-218: Slider wiring looks solid; optionally guard against negative slidersPressedCount

The editSlider component’s backend binding and reversal logic look consistent, and the updated visuals are nice. To make the global slidersPressedCount a bit more defensive, you could avoid ever letting it go negative in case of unexpected press/release sequences:

-                onPressedChanged: {
-                    if (pressed) editDialog.slidersPressedCount++; else editDialog.slidersPressedCount--;
-                }
+                onPressedChanged: {
+                    if (pressed) {
+                        editDialog.slidersPressedCount++;
+                    } else if (editDialog.slidersPressedCount > 0) {
+                        editDialog.slidersPressedCount--;
+                    }
+                }
faststack.working-menus/faststack/ui/provider.py (1)

28-36: Silence Ruff ARG002 by marking unused requestImage parameters

size and requestedSize are unused but required by QQuickImageProvider.requestImage’s signature. To keep the override correct and appease Ruff, consider renaming them to underscore-prefixed names.

-    def requestImage(self, id: str, size: object, requestedSize: object) -> QImage:
+    def requestImage(self, id: str, _size: object, _requestedSize: object) -> QImage:
faststack/faststack/ui/provider.py (2)

87-140: Editor-related state wiring is coherent; consider minor cleanup

The added editor signals/fields (_is_editor_open, crop, tone/contrast/color params, etc.) line up with the QML keys and give a clear, one-to-one bridge between backend edits and UI.

Two small follow‑ups to consider:

  • default_directory_changed is declared but never used by any property; either wire it to a @Property or drop it.
  • reset_editor_state() resets all edit parameters and cropping, but not anySliderPressed or isEditorOpen. If you ever reuse it outside the “editor just closed” path, it might be worth explicitly clearing those too.
     def reset_editor_state(self):
         """Resets all UI state variables for the editor."""
+        self.anySliderPressed = False
         self.brightness = 0.0
         ...
         self.clarity = 0.0

28-61: Mirror Ruff fixes from working-menus provider here as well

Same pattern as the working-menus ImageProvider: size/requestedSize are unused but required by the Qt override, and the invalid ID handler would benefit from log.exception for full tracebacks.

-    def requestImage(self, id: str, size: object, requestedSize: object) -> QImage:
+    def requestImage(self, id: str, _size: object, _requestedSize: object) -> QImage:
...
-        except (ValueError, IndexError) as e:
-            log.error(f"Invalid image ID requested from QML: {id}. Error: {e}")
+        except (ValueError, IndexError):
+            log.exception("Invalid image ID requested from QML: %s", id)
faststack/faststack/app.py (3)

624-642: reset_editor_state integration: consider clearing editor-open state as well

reset_edit_parameters() uses ui_state.reset_editor_state() to reset sliders/crop, and save_edited_image() calls reset_edit_parameters() after saving. That’s good for visual consistency.

If the intended UX after a full save is “editor remains open but parameters snap back to neutral”, current behavior is fine. If instead you want saving to conceptually “finish editing”, consider also toggling ui_state.isEditorOpen off here or at the caller, so get_decoded_image stops returning the editor preview and normal cache behavior resumes immediately.

Not strictly required, but worth confirming UX.


1188-1192: Avoid redundant import time inside delete_current_image

time is already imported at module level. The inner import time inside delete_current_image is redundant and can be dropped:

-            if deleted_files:
-                import time
-                timestamp = time.time()
+            if deleted_files:
+                timestamp = time.time()
                 self.delete_history.append((jpg_path, raw_path))
                 self.undo_history.append(("delete", (jpg_path, raw_path), timestamp))

Minor, but it keeps imports centralized.


1919-1955: QML import-path setup is reasonable; watch for environment interactions

The new environment and import-path setup:

  • Sets QML2_IMPORT_PATH relative to __file__.
  • Adds PySide6’s qml directory, qrc:/qt-project.org/imports, your own qml directory, and Qt5Compat into engine.addImportPath.

This should help resolve Qt5Compat.GraphicalEffects and custom imports across platforms. Just be mindful if callers also set QML2_IMPORT_PATH before launching FastStack—your override will win. If that’s a concern, you might want to append to any existing value instead of blindly replacing it.

faststack.working-menus/faststack/app.py (4)

16-45: Duplicate ImageProvider import

ImageProvider is imported twice (once with UIState at Line 16 and again alone at Line 43). The second import is redundant and triggers Ruff F811.

You can safely drop the second import:

-from faststack.ui.provider import ImageProvider

643-678: Clean up f-strings without interpolation and unused locals in batch/stack helpers

There are a couple of small Ruff issues in this block:

  • self.update_status_message(f"Removed from batch")
  • self.update_status_message(f"Removed from stack")

These are plain strings; the f prefix is unnecessary (F541). Also, in _get_batch_info, the loop index i and the count_in_batch / pos_in_batch locals are never used (B007/F841).

Suggested minimal clean-up:

-                self.update_status_message(f"Removed from batch")
+                self.update_status_message("Removed from batch")
...
-                    self.update_status_message(f"Removed from stack")
+                    self.update_status_message("Removed from stack")

And in _get_batch_info:

-        for i, (start, end) in enumerate(self.batches):
+        for start, end in self.batches:
             if start <= index <= end:
-                count_in_batch = end - start + 1
-                pos_in_batch = index - start + 1
                 info = "In Batch"
                 break

These changes are behavior‑preserving and quiet the lints.


907-914: Prefer log.exception in exception handlers for better diagnostics

The OSError/ValueError handlers currently use log.error, which drops the traceback:

except OSError as e:
    log.error("Error deleting temporary file %s: %s", tmp_path, e)

Since these paths are rare and debugging them benefits from full context, consider log.exception, which includes the stack trace while keeping the message:

-        except OSError as e:
-            log.error("Error deleting temporary file %s: %s", tmp_path, e)
+        except OSError:
+            log.exception("Error deleting temporary file %s", tmp_path)

Likewise for delete/restore failures and invalid Photoshop args. Not required for correctness, but improves supportability.

Also applies to: 1120-1127, 1292-1299, 1351-1358


1506-1531: Tighten _get_batch_info implementation

_get_batch_info currently computes count_in_batch and pos_in_batch but only returns "In Batch", and Ruff flags the unused locals and loop index. Since you’re not showing position/count in the footer anymore, you can simplify:

-    def _get_batch_info(self, index: int) -> str:
-        """Get batch info for the given index."""
-        info = ""
-        for i, (start, end) in enumerate(self.batches):
-            if start <= index <= end:
-                count_in_batch = end - start + 1
-                pos_in_batch = index - start + 1
-                info = "In Batch"
-                break
+    def _get_batch_info(self, index: int) -> str:
+        """Get batch info for the given index."""
+        info = ""
+        for start, end in self.batches:
+            if start <= index <= end:
+                info = "In Batch"
+                break

Behavior stays identical, and the lints go away.

faststack/patch (2)

21-155: ImageEditor core logic is reasonable, but consider stronger clamping and error reporting

The editing pipeline (crop → brightness/contrast/saturation → white balance BY/MG → PNG preview / JPEG save) looks coherent and matches the normalized [-1.0, 1.0] UI model.

A couple of non‑blocking improvements:

  • Clamp the normalized parameters defensively in set_edit_param (e.g., to [-1.0, 1.0]) so misuse from non‑QML callers can’t drive brightness/contrast factors far outside [0, 2].
  • Replace bare print in load_image / save_image error paths with logging so failures are visible in the same log pipeline as the rest of the app.

Example for clamping:

     def set_edit_param(self, key: str, value: float) -> bool:
         """Update a single edit parameter (normalized -1.0 to 1.0)."""
-        if key in self.current_edits and key != 'crop_box':
-            self.current_edits[key] = value
+        if key in self.current_edits and key != 'crop_box':
+            clamped = max(-1.0, min(1.0, value))
+            self.current_edits[key] = clamped
             return True

607-745: QML edit/crop UX is generally solid; verify crop normalization vs image letterboxing

The main QML changes wire together:

  • E key toggling edit mode with a save/discard prompt when edits_pending is true.
  • ENTER key to either execute crop (via app_controller.execute_crop()) or confirm the save/discard dialog.
  • Conditional image source based on appModel.isEditing, switching to the edited provider.
  • Mouse-based crop selection normalized against mainImage.width/height and sent to Python via set_crop_selection_normalized.
  • A red rectangle overlay that tracks the selection, plus a clear MessageDialog for save/discard.

Two edge cases to double‑check in practice:

  1. Letterboxing: If mainImage uses fillMode: Image.PreserveAspectFit (or similar), the actual drawn image may be letterboxed within mainImage.width/height. Normalizing mouse.x/mouse.y by the full mainImage size will include the black bars, so crop coordinates won’t map 1:1 to the underlying image pixels. You may need to account for the visible image rect (subtracting margins) before normalizing.

  2. Edited source refresh: This ties back to the provider comment – if caching prevents requestPixmap from being called on every currentEditedImageDataChanged, the user will see stale edited previews despite the QML wiring being correct.

Functionally the flow is well thought out; just verify these two behaviors at runtime.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a76d5ea and 83bf949.

📒 Files selected for processing (53)
  • faststack.working-menus/ChangeLog.md (1 hunks)
  • faststack.working-menus/LICENSE (1 hunks)
  • faststack.working-menus/README.md (1 hunks)
  • faststack.working-menus/faststack.egg-info/PKG-INFO (1 hunks)
  • faststack.working-menus/faststack.egg-info/SOURCES.txt (1 hunks)
  • faststack.working-menus/faststack.egg-info/dependency_links.txt (1 hunks)
  • faststack.working-menus/faststack.egg-info/entry_points.txt (1 hunks)
  • faststack.working-menus/faststack.egg-info/requires.txt (1 hunks)
  • faststack.working-menus/faststack.egg-info/top_level.txt (1 hunks)
  • faststack.working-menus/faststack/app.py (1 hunks)
  • faststack.working-menus/faststack/benchmark_decode.py (1 hunks)
  • faststack.working-menus/faststack/config.py (1 hunks)
  • faststack.working-menus/faststack/imaging/cache.py (1 hunks)
  • faststack.working-menus/faststack/imaging/jpeg.py (1 hunks)
  • faststack.working-menus/faststack/imaging/prefetch.py (1 hunks)
  • faststack.working-menus/faststack/io/executable_validator.py (1 hunks)
  • faststack.working-menus/faststack/io/helicon.py (1 hunks)
  • faststack.working-menus/faststack/io/indexer.py (1 hunks)
  • faststack.working-menus/faststack/io/sidecar.py (1 hunks)
  • faststack.working-menus/faststack/io/watcher.py (1 hunks)
  • faststack.working-menus/faststack/logging_setup.py (1 hunks)
  • faststack.working-menus/faststack/models.py (1 hunks)
  • faststack.working-menus/faststack/qml/Components.qml (1 hunks)
  • faststack.working-menus/faststack/qml/FilterDialog.qml (1 hunks)
  • faststack.working-menus/faststack/qml/JumpToImageDialog.qml (1 hunks)
  • faststack.working-menus/faststack/qml/Main.qml (1 hunks)
  • faststack.working-menus/faststack/qml/Main.qml.bak (1 hunks)
  • faststack.working-menus/faststack/qml/SettingsDialog.qml (1 hunks)
  • faststack.working-menus/faststack/tests/test_cache.py (1 hunks)
  • faststack.working-menus/faststack/tests/test_executable_validator.py (1 hunks)
  • faststack.working-menus/faststack/tests/test_pairing.py (1 hunks)
  • faststack.working-menus/faststack/tests/test_sidecar.py (1 hunks)
  • faststack.working-menus/faststack/ui/keystrokes.py (1 hunks)
  • faststack.working-menus/faststack/ui/keystrokes.py.bak (1 hunks)
  • faststack.working-menus/faststack/ui/provider.py (1 hunks)
  • faststack.working-menus/faststack/ui/provider.py.bak (1 hunks)
  • faststack.working-menus/patch (1 hunks)
  • faststack.working-menus/pyproject.toml (1 hunks)
  • faststack.working-menus/requirements.txt (1 hunks)
  • faststack/ChangeLog.md (1 hunks)
  • faststack/README.md (4 hunks)
  • faststack/faststack.egg-info/PKG-INFO (2 hunks)
  • faststack/faststack/app.py (18 hunks)
  • faststack/faststack/imaging/editor.py (1 hunks)
  • faststack/faststack/qml/Components.qml (1 hunks)
  • faststack/faststack/qml/ImageEditorDialog.qml (1 hunks)
  • faststack/faststack/qml/ImageEditorDialog.qml.new (1 hunks)
  • faststack/faststack/qml/ImageEditorDialog.qml.old (1 hunks)
  • faststack/faststack/qml/Main.qml (7 hunks)
  • faststack/faststack/ui/keystrokes.py (1 hunks)
  • faststack/faststack/ui/provider.py (3 hunks)
  • faststack/patch (1 hunks)
  • faststack/pyproject.toml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
faststack.working-menus/faststack/imaging/cache.py (1)
faststack.working-menus/faststack/models.py (1)
  • DecodedImage (35-44)
faststack.working-menus/faststack/ui/keystrokes.py (1)
faststack/faststack/ui/keystrokes.py (3)
  • Keybinder (7-110)
  • _call (61-74)
  • handle_key_press (76-110)
faststack.working-menus/faststack/tests/test_cache.py (1)
faststack.working-menus/faststack/imaging/cache.py (1)
  • ByteLRUCache (10-28)
faststack.working-menus/faststack/io/helicon.py (2)
faststack.working-menus/faststack/io/executable_validator.py (1)
  • validate_executable_path (23-95)
faststack.working-menus/faststack/config.py (1)
  • get (68-69)
faststack.working-menus/faststack/config.py (1)
faststack.working-menus/faststack/logging_setup.py (1)
  • get_app_data_dir (8-13)
faststack/faststack/imaging/editor.py (2)
faststack.working-menus/faststack/models.py (1)
  • DecodedImage (35-44)
faststack/faststack/app.py (2)
  • get_preview_data (1643-1648)
  • set_crop_box (1663-1668)
faststack.working-menus/faststack/imaging/prefetch.py (4)
faststack.working-menus/faststack/models.py (2)
  • ImageFile (8-12)
  • DecodedImage (35-44)
faststack.working-menus/faststack/imaging/jpeg.py (2)
  • decode_jpeg_rgb (22-40)
  • decode_jpeg_resized (99-149)
faststack.working-menus/faststack/config.py (1)
  • get (68-69)
faststack.working-menus/faststack/app.py (2)
  • get_display_info (183-186)
  • shutdown (1224-1259)
faststack.working-menus/faststack/benchmark_decode.py (1)
faststack.working-menus/faststack/imaging/jpeg.py (1)
  • decode_jpeg_resized (99-149)
faststack.working-menus/faststack/tests/test_pairing.py (1)
faststack.working-menus/faststack/io/indexer.py (2)
  • find_images (20-62)
  • _find_raw_pair (64-84)
faststack.working-menus/faststack/io/sidecar.py (3)
faststack.working-menus/faststack/models.py (2)
  • Sidecar (27-32)
  • EntryMetadata (15-23)
faststack.working-menus/faststack/app.py (1)
  • load (269-282)
faststack.working-menus/faststack/io/watcher.py (3)
  • stop (64-70)
  • start (49-62)
  • is_alive (72-74)
faststack.working-menus/faststack/ui/provider.py (3)
faststack.working-menus/faststack/models.py (1)
  • DecodedImage (35-44)
faststack.working-menus/faststack/app.py (4)
  • get_decoded_image (313-370)
  • get_current_metadata (519-550)
  • get_color_mode (987-989)
  • get_saturation_factor (1025-1027)
faststack.working-menus/faststack/config.py (1)
  • get (68-69)
🪛 LanguageTool
faststack.working-menus/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 1.0 - November 21, 2025 # By Alan Rockefeller Ultra-fast, cach...

(MISSING_COMMA_AFTER_YEAR)


[grammar] ~12-~12: Ensure spelling is correct
Context: ...0ms next/previous image switching, high peformance decoding via PyTurboJPEG. - **Zoom & ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

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 1.1 - November 22, 2025 # By Alan Rockefeller Ultra-fast, cach...

(MISSING_COMMA_AFTER_YEAR)

faststack.working-menus/ChangeLog.md

[style] ~103-~103: Consider using a different verb for a more formal wording.
Context: ...he JPEG scaling factor calculation. - Fixed an issue where panning the image was no...

(FIX_RESOLVE)


[grammar] ~118-~118: Use a hyphen to join words.
Context: ...sion 0.4 ### Todo Make it use the full res image when zooming in When multiple ...

(QB_NEW_EN_HYPHEN)

🪛 Ruff (0.14.5)
faststack.working-menus/faststack/io/helicon.py

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

(TRY401)


77-77: subprocess call: check for execution of untrusted input

(S603)


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

(TRY300)


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

(TRY401)


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

(TRY401)

faststack.working-menus/faststack/config.py

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

Replace with exception

(TRY400)

faststack.working-menus/faststack/tests/test_executable_validator.py

40-40: Unused lambda argument: self

(ARG005)


62-62: Unused lambda argument: self

(ARG005)


66-66: Unpacked variable error is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


123-123: Unused lambda argument: self

(ARG005)


127-127: Unpacked variable error is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

faststack/faststack/imaging/editor.py

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

(TRY300)


82-82: Do not catch blind exception: Exception

(BLE001)


285-285: Do not catch blind exception: Exception

(BLE001)


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

(TRY300)


291-291: Do not catch blind exception: Exception

(BLE001)

faststack.working-menus/faststack/io/executable_validator.py

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

(TRY300)

faststack.working-menus/faststack/imaging/jpeg.py

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

(TRY401)


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

(TRY401)


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

(TRY300)


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

(TRY401)


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

(TRY401)


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

(TRY401)


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

(TRY401)

faststack.working-menus/faststack/imaging/prefetch.py

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

(TRY300)

faststack.working-menus/faststack/tests/test_sidecar.py

14-14: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

faststack.working-menus/faststack/io/indexer.py

39-39: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


65-65: Unused function argument: jpg_path

(ARG001)

faststack.working-menus/faststack/app.py

43-43: Redefinition of unused ImageProvider from line 16

Remove definition: ImageProvider

(F811)


364-364: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


644-644: f-string without any placeholders

Remove extraneous f prefix

(F541)


678-678: f-string without any placeholders

Remove extraneous f prefix

(F541)


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

Replace with exception

(TRY400)


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

Replace with exception

(TRY400)


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

Replace with exception

(TRY400)


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

Replace with exception

(TRY400)


1366-1366: subprocess call: check for execution of untrusted input

(S603)


1522-1522: Loop control variable i not used within loop body

Rename unused i to _i

(B007)


1524-1524: Local variable count_in_batch is assigned to but never used

Remove assignment to unused variable count_in_batch

(F841)


1525-1525: Local variable pos_in_batch is assigned to but never used

Remove assignment to unused variable pos_in_batch

(F841)

faststack.working-menus/faststack/tests/test_pairing.py

55-55: Multiple statements on one line (semicolon)

(E702)


58-58: Multiple statements on one line (semicolon)

(E702)


58-58: Multiple statements on one line (semicolon)

(E702)


63-63: Multiple statements on one line (semicolon)

(E702)


63-63: Multiple statements on one line (semicolon)

(E702)


68-68: Multiple statements on one line (semicolon)

(E702)


68-68: Multiple statements on one line (semicolon)

(E702)


69-69: Multiple statements on one line (semicolon)

(E702)


69-69: Multiple statements on one line (semicolon)

(E702)

faststack/faststack/app.py

137-137: Undefined name Tuple

(F821)


655-655: f-string without any placeholders

Remove extraneous f prefix

(F541)


689-689: f-string without any placeholders

Remove extraneous f prefix

(F541)


1818-1818: Local variable b_diff is assigned to but never used

Remove assignment to unused variable b_diff

(F841)


1881-1881: Loop control variable i not used within loop body

Rename unused i to _i

(B007)

faststack.working-menus/faststack/io/sidecar.py

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

Replace with exception

(TRY400)


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

Replace with exception

(TRY400)

faststack.working-menus/faststack/ui/provider.py

28-28: Unused method argument: size

(ARG002)


28-28: Unused method argument: requestedSize

(ARG002)


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

Replace with exception

(TRY400)

@@ -0,0 +1 @@

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Verify that generated build artifacts are excluded from version control.

The egg-info/ directory contains auto-generated Python packaging metadata and should typically not be committed to version control. These directories are created during package installation and should be excluded via .gitignore.

Please verify that your .gitignore file includes entries to exclude build artifacts:

If egg-info directories are present in the repository, consider removing them with git rm -r --cached faststack.working-menus/faststack.egg-info/ and updating .gitignore.


🏁 Script executed:

#!/bin/bash
# Description: Verify that egg-info and other build artifacts are excluded from version control

# Check if .gitignore exists and contains egg-info exclusions
if [ -f .gitignore ]; then
  echo "=== Checking .gitignore for build artifact exclusions ==="
  grep -E '(egg-info|build|dist|\.eggs)' .gitignore || echo "WARNING: No egg-info or build artifact exclusions found"
else
  echo "WARNING: .gitignore file not found"
fi

# List all egg-info directories currently in the repository
echo ""
echo "=== Egg-info directories in repository ==="
fd -t d 'egg-info$' || echo "No egg-info directories found"

Length of output: 188


Add egg-info/ to .gitignore and remove committed build artifacts.

The egg-info/ directories are auto-generated Python packaging metadata that should not be committed to version control. Your .gitignore currently excludes dist/ and build/ but is missing egg-info.

Remove the committed directories and update .gitignore:

egg-info
*.egg-info/
.eggs/

Then run:

git rm -r --cached faststack/faststack.egg-info/ faststack.working-menus/faststack.egg-info/
🤖 Prompt for AI Agents
faststack.working-menus/faststack.egg-info/dependency_links.txt around lines 1
to 1: the repo contains committed egg-info build artifacts that should be
ignored; update .gitignore to add entries for egg-info, *.egg-info/, and .eggs/
and then remove the committed egg-info directories from the repository by
removing them from the git index (e.g., use git rm -r --cached on
faststack/faststack.egg-info/ and faststack.working-menus/faststack.egg-info/),
then commit the updated .gitignore and the removal so the generated metadata is
no longer tracked.

Comment on lines +1 to +2
[console_scripts]
faststack = faststack.app:main
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Confirm alignment between this entry point and pyproject/CLI design.

This wires the faststack console script to faststack.app:main, while your pyproject.toml (per summary) also defines a faststack entry pointing to faststack.app:cli. Having two different targets for the same script name across metadata sources can be confusing.

Decide whether faststack should launch the GUI (main) or CLI (cli) and keep that consistent in all packaging metadata (pyproject, setup.cfg, egg‑info generation, etc.).


🏁 Script executed:

#!/bin/bash
# Check for other console script definitions for 'faststack'
rg -n "console_scripts" -C3
rg -n "faststack.app:cli" -C3
rg -n "faststack.app:main" -C3

Length of output: 1328


Resolve the faststack entry point mismatch between pyproject.toml and egg-info.

faststack.working-menus/pyproject.toml:31 defines faststack = "faststack.app:cli", while faststack.working-menus/faststack.egg-info/entry_points.txt:2 defines faststack = faststack.app:main. These must align. The egg-info file is auto-generated from pyproject.toml during package installation; verify which target is correct and ensure pyproject.toml reflects it, so the next build regenerates entry_points.txt consistently.

🤖 Prompt for AI Agents
In faststack.working-menus/faststack.egg-info/entry_points.txt lines 1-2, the
console script is defined as faststack = faststack.app:main but pyproject.toml
defines faststack = "faststack.app:cli"; pick the correct callable (either
app:cli or app:main), update pyproject.toml to match that target, then
rebuild/reinstall the package so setuptools regenerates
faststack.egg-info/entry_points.txt with the same entry point; verify by running
the installed console script and adjust the callable name in the code if
necessary.

Comment on lines +1 to +8
PySide6<7.0,>=6.0
PyTurboJPEG<2.0,>=1.8
numpy<3.0,>=2.0
cachetools<6.0,>=5.0
watchdog<5.0,>=4.0
typer<1.0,>=0.12
Pillow<11.0,>=10.0
pytest<9.0,>=8.0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Pinned dependency ranges look coherent; validate against your target environments.

The version constraints for PySide6, PyTurboJPEG, numpy, Pillow, etc., are modern and consistent with the features used in this PR. Just ensure:

  • These ranges match what’s declared in pyproject.toml/other packaging files.
  • Your supported platforms (Python version, OS) are tested against at least one combination in these ranges, especially numpy/Pillow + PySide6.

No changes strictly required here; this is more of a deployment sanity check.


🏁 Script executed:

#!/bin/bash
# Compare requires.txt with pyproject.toml for drift
fd pyproject.toml -x cat {}
cat faststack.working-menus/faststack.egg-info/requires.txt

Length of output: 1710


Dependency drift detected: typer is declared in requires.txt but missing from pyproject.toml.

The requires.txt includes typer<1.0,>=0.12, which does not appear in either pyproject.toml configuration (version 1.0 or 1.1). This mismatch must be resolved:

  • If typer is a genuine runtime dependency, add it to pyproject.toml dependencies.
  • If typer is not needed, regenerate requires.txt from pyproject.toml to ensure consistency.

Verify whether typer is actually used in the codebase, then align the two configuration sources.

🤖 Prompt for AI Agents
In faststack.working-menus/faststack.egg-info/requires.txt around lines 1-8,
requires.txt lists "typer<1.0,>=0.12" but pyproject.toml (both v1.0 and v1.1)
lacks this dependency; inspect the codebase to confirm whether typer is actually
used and then either add typer with the matching constraint to the
pyproject.toml [project] dependencies (if it is a runtime dependency) or
remove/omit typer from requires.txt and regenerate the egg-info requires list
from pyproject.toml (if it is not needed), ensuring both files stay consistent.

Comment on lines +334 to +367
# Cache miss: need to decode synchronously to ensure correct image displays
if _debug_mode:
decode_start = time.perf_counter()
log.info("Cache miss for index %d (gen: %d). Blocking decode.", index, display_gen)

# Submit with priority=True to cancel pending prefetch tasks and free up workers
future = self.prefetcher.submit_task(index, self.prefetcher.generation, priority=True)
if future:
try:
# Wait for decode to complete (blocking but fast for JPEGs)
result = future.result(timeout=5.0) # 5 second timeout as safety
if result:
decoded_index, decoded_display_gen = result
cache_key = f"{decoded_index}_{decoded_display_gen}"
if cache_key in self.image_cache:
decoded = self.image_cache[cache_key]
with self._last_image_lock:
self.last_displayed_image = decoded
if _debug_mode:
elapsed = time.perf_counter() - decode_start
log.info("Decoded image %d in %.3fs", index, elapsed)
return decoded
except concurrent.futures.TimeoutError:
log.exception("Timeout decoding image at index %d", index)
with self._last_image_lock:
return self.last_displayed_image
except concurrent.futures.CancelledError:
log.warning("Decode cancelled for index %d", index)
with self._last_image_lock:
return self.last_displayed_image
except Exception as e:
log.exception("Error decoding image at index %d", index)
with self._last_image_lock:
return self.last_displayed_image
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Drop unused exception variable in get_decoded_image

In the generic exception handler, e is never used:

except Exception as e:
    log.exception("Error decoding image at index %d", index)

Given log.exception already captures the traceback, you can drop the variable to satisfy Ruff F841 and slightly simplify:

-            except Exception as e:
+            except Exception:
                 log.exception("Error decoding image at index %d", index)
                 with self._last_image_lock:
                     return self.last_displayed_image
🧰 Tools
🪛 Ruff (0.14.5)

364-364: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

🤖 Prompt for AI Agents
In faststack.working-menus/faststack/app.py around lines 334 to 367, the generic
exception handler captures an unused variable `e`, triggering a lint F841;
change the handler from `except Exception as e:` to `except Exception:` and
leave the body unchanged (keep the log.exception call and the
with-self._last_image_lock return) so the traceback is still logged but the
unused variable is removed.

Comment thread faststack.working-menus/faststack/imaging/jpeg.py
Comment thread faststack/faststack/app.py
Comment thread faststack/faststack/qml/Components.qml Outdated
Comment thread faststack/patch Outdated
Comment on lines +295 to +332
# --------------------------------------------------------------------------
# Image Provider Setup
- self.image_provider = ImageProvider(self.app_controller.app_state_model)
+ # Use a single provider for both original and edited images
+ self.image_provider = ImageProvider(self.app_controller.app_state_model, is_edited_provider=False)
self.engine.addImageProvider("imageProvider", self.image_provider)
+
+ # Separate provider for the live-edited image preview
+ self.edited_image_provider = ImageProvider(self.app_controller.app_state_model, is_edited_provider=True)
+ self.engine.addImageProvider("editedImageProvider", self.edited_image_provider)

# --------------------------------------------------------------------------
# Engine Context Setup
self.engine.rootContext().setContextProperty("appModel", self.app_controller.app_state_model)
+
+ # --------------------------------------------------------------------------
+ # Connect signals for edit mode to manage image source
+ self.app_controller.app_state_model.isEditingChanged.connect(self._handle_editing_toggle)
+ # Connect data change signal to the edited provider
+ self.app_controller.app_state_model.currentEditedImageDataChanged.connect(
+ lambda: self.edited_image_provider.image_data = self.app_controller.app_state_model.current_edited_image_data
+ )

# Load the QML file
qml_file = Path(__file__).parent / "qml" / "main.qml"
@@ -74,6 +86,14 @@
if self.engine.rootObjects():
self.engine.rootObjects()[0].show()

+ def _handle_editing_toggle(self):
+ """Update the image provider source when editing mode changes."""
+ is_editing = self.app_controller.app_state_model.is_editing
+
+ # If starting edit mode, ensure the initial edited data is generated
+ if is_editing:
+ # Trigger the edited image data update to show the initial (unmodified) preview
+ self.app_controller.app_state_model.current_edited_image_data = self.app_controller.image_editor.get_preview_data()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

_handle_editing_toggle uses a non‑existent is_editing attribute

In the app wrapper:

def _handle_editing_toggle(self):
    """Update the image provider source when editing mode changes."""
    is_editing = self.app_controller.app_state_model.is_editing

    if is_editing:
        self.app_controller.app_state_model.current_edited_image_data = \
            self.app_controller.image_editor.get_preview_data()

AppStateModel exposes isEditing (property) and the backing field _is_editing, but not is_editing. Accessing is_editing will raise AttributeError when the isEditingChanged signal fires.

Also, you don’t actually use is_editing for branching beyond the if check, so you can read the public property directly:

     def _handle_editing_toggle(self):
         """Update the image provider source when editing mode changes."""
-        is_editing = self.app_controller.app_state_model.is_editing
-        
-        # If starting edit mode, ensure the initial edited data is generated
-        if is_editing:
+        # If starting edit mode, ensure the initial edited data is generated
+        if self.app_controller.app_state_model.isEditing:
             # Trigger the edited image data update to show the initial (unmodified) preview
             self.app_controller.app_state_model.current_edited_image_data = (
                 self.app_controller.image_editor.get_preview_data()
             )
🤖 Prompt for AI Agents
In faststack/patch around lines 295 to 332, _handle_editing_toggle reads a
non-existent attribute is_editing on app_controller.app_state_model which will
raise AttributeError; change the code to read the public property
(app_controller.app_state_model.isEditing) instead and use that value for the
conditional (or eliminate the temporary and test the property directly),
ensuring you set current_edited_image_data via
app_controller.app_state_model.current_edited_image_data when editing begins.

Comment thread faststack/patch Outdated
Comment on lines +442 to +564
+ # Editing parameters (normalized to -1.0 to 1.0)
+ self._edits_pending = False
+ self._brightness = 0.0
+ self._contrast = 0.0
+ self._saturation = 0.0
+ self._white_balance_by = 0.0
+ self._white_balance_mg = 0.0
+ self._has_active_crop_selection = False
+ self._current_edited_image_data = None # Holds image data bytes for QML provider
+ self._current_aspect_ratio_index = 0
+ self._aspect_ratio_names = [r["name"] for r in self.app_controller.get_aspect_ratios()]
+
@Property(str, notify=currentImagePathChanged)
def current_image_path(self):
return str(self._current_image_file.path) if self._current_image_file else ""
@@ -176,3 +187,94 @@
if self._is_cropping != value:
self._is_cropping = value
self.isCroppingChanged.emit()
+
+ @Property(bool, notify=editsPendingChanged)
+ def edits_pending(self):
+ return self._edits_pending
+
+ @edits_pending.setter
+ def edits_pending(self, value: bool):
+ if self._edits_pending != value:
+ self._edits_pending = value
+ self.editsPendingChanged.emit()
+
+ @Property(float, notify=brightnessChanged)
+ def brightness(self):
+ return self._brightness
+
+ @brightness.setter
+ def brightness(self, value: float):
+ if self._brightness != value:
+ self._brightness = value
+ self.brightnessChanged.emit()
+
+ @Property(float, notify=contrastChanged)
+ def contrast(self):
+ return self._contrast
+
+ @contrast.setter
+ def contrast(self, value: float):
+ if self._contrast != value:
+ self._contrast = value
+ self.contrastChanged.emit()
+
+ @Property(float, notify=saturationChanged)
+ def saturation(self):
+ return self._saturation
+
+ @saturation.setter
+ def saturation(self, value: float):
+ if self._saturation != value:
+ self._saturation = value
+ self.saturationChanged.emit()
+
+ @Property(float, notify=whiteBalanceBYChanged)
+ def whiteBalanceBY(self):
+ return self._white_balance_by
+
+ @whiteBalanceBY.setter
+ def whiteBalanceBY(self, value: float):
+ if self._white_balance_by != value:
+ self._white_balance_by = value
+ self.whiteBalanceBYChanged.emit()
+
+ @Property(float, notify=whiteBalanceMGChanged)
+ def whiteBalanceMG(self):
+ return self._white_balance_mg
+
+ @whiteBalanceMG.setter
+ def whiteBalanceMG(self, value: float):
+ if self._white_balance_mg != value:
+ self._white_balance_mg = value
+ self.whiteBalanceMGChanged.emit()
+
+ @Property(bool, notify=hasActiveCropSelectionChanged)
+ def hasActiveCropSelection(self):
+ return self._has_active_crop_selection
+
+ @hasActiveCropSelection.setter
+ def hasActiveCropSelection(self, value: bool):
+ if self._has_active_crop_selection != value:
+ self._has_active_crop_selection = value
+ self.hasActiveCropSelectionChanged.emit()
+
+ @Property(QByteArray, notify=currentEditedImageDataChanged)
+ def current_edited_image_data(self):
+ return self._current_edited_image_data
+
+ @current_edited_image_data.setter
+ def current_edited_image_data(self, value: Optional[bytes]):
+ data = QByteArray(value) if value is not None else QByteArray()
+ if self._current_edited_image_data != data:
+ self._current_edited_image_data = data
+ self.currentEditedImageDataChanged.emit()
+
+ @Property("QStringList", notify=aspectRatioNamesChanged)
+ def aspectRatioNames(self):
+ return self._aspect_ratio_names
+
+ @Property(int, notify=currentAspectRatioIndexChanged)
+ def currentAspectRatioIndex(self):
+ return self._current_aspect_ratio_index
+
+ @currentAspectRatioIndex.setter
+ def currentAspectRatioIndex(self, value: int):
+ if self._current_aspect_ratio_index != value:
+ self._current_aspect_ratio_index = value
+ self.currentAspectRatioIndexChanged.emit()
+
+ @Slot()
+ def reset_edit_parameters(self):
+ self.brightness = self.contrast = self.saturation = 0.0
+ self.whiteBalanceBY = self.whiteBalanceMG = 0.0
+ self.edits_pending = False
+ self.isCropping = False
+ self.hasActiveCropSelection = False
--- a/faststack/ui/provider.py
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

reset_edit_parameters only resets UI state, not the underlying ImageEditor

reset_edit_parameters lives on AppStateModel:

@Slot()
def reset_edit_parameters(self):
    self.brightness = self.contrast = self.saturation = 0.0
    self.whiteBalanceBY = self.whiteBalanceMG = 0.0
    self.edits_pending = False
    self.isCropping = False
    self.hasActiveCropSelection = False

This correctly resets the model properties and clears the “edits pending” flag, but it never resets ImageEditor.current_edits or regenerates a fresh preview. As a result:

  • The “Reset All Edits” button in ImageEditorDialog.qml will visually reset sliders, but the underlying preview and eventual saved image stay with the old edits.
  • On a pure “UI reset”, the user might think they’re back to the original, but the backend still holds modifications.

To keep UI and backend in sync, route the reset through ImageEditor and update the preview:

    @Slot()
    def reset_edit_parameters(self):
-        self.brightness = self.contrast = self.saturation = 0.0
-        self.whiteBalanceBY = self.whiteBalanceMG = 0.0
-        self.edits_pending = False
-        self.isCropping = False
-        self.hasActiveCropSelection = False
+        # Reset UI-side properties
+        self.brightness = self.contrast = self.saturation = 0.0
+        self.whiteBalanceBY = self.whiteBalanceMG = 0.0
+        self.edits_pending = False
+        self.isCropping = False
+        self.hasActiveCropSelection = False
+
+        # Reset backend editor state and preview
+        editor = self.app_controller.image_editor
+        editor.current_edits = editor._initial_edits()
+        self.current_edited_image_data = editor.get_preview_data()

(Or better, expose a public ImageEditor.reset_edits() helper instead of touching _initial_edits() directly.)

🤖 Prompt for AI Agents
In faststack/patch around lines 442-564, reset_edit_parameters only clears UI
properties but does not reset the ImageEditor backend; update this method to
call into the ImageEditor to clear its edits and regenerate the preview (either
call a new public ImageEditor.reset_edits() helper or an existing method that
clears ImageEditor.current_edits and re-renders the preview), then update
current_edited_image_data (emit currentEditedImageDataChanged) with the fresh
image bytes and ensure edits_pending is set to False so UI and backend remain in
sync.

Comment thread faststack/patch Outdated
Comment on lines +567 to +583
class ImageProvider(QQuickImageProvider):
"""Custom image provider for QML to retrieve images from the cache/disk."""

- def __init__(self, app_state_model: "AppStateModel"):
+ def __init__(self, app_state_model: "AppStateModel", is_edited_provider: bool):
super().__init__(QQuickImageProvider.Pixmap)
self.app_state_model = app_state_model
self.app_controller = self.app_state_model.app_controller
+ self.is_edited_provider = is_edited_provider
self.current_path: Optional[Path] = None
self.current_data: Optional[DecodedImage] = None
self.last_load_time: float = 0
+
+ # For edited provider, we store the raw byte data
+ self.image_data: QByteArray = QByteArray()
+ self.edited_pixmap: QPixmap = QPixmap()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Edited image provider: confirm refresh behavior and handle empty QSize

The edited ImageProvider branch:

if self.is_edited_provider and not self.image_data.isEmpty():
    self.edited_pixmap.loadFromData(self.image_data, "PNG")
    # Scale the pixmap to the requested size
    return self.edited_pixmap.scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)

Two things to watch:

  1. Refresh semantics: QML’s Image caches image:// URLs by default. Your edited source is a constant:

    source: appModel.isEditing
            ? "image://editedImageProvider/currentEdit"
            : "image://imageProvider/" + appModel.current_image_path

    Since the URL doesn’t change when currentEditedImageDataChanged fires, QML may keep returning the cached pixmap and never re‑call requestPixmap, so live slider changes won’t be visible.

    Common fixes:

    • Set cache: false on the Image showing the edited content, or
    • Append a changing token (e.g., an incrementing generation) to the id so the URL changes when current_edited_image_data changes.
  2. Zero size handling: When sourceSize is not set, the size passed to requestPixmap can be (0, 0). In that case scaled(size, ...) produces a null pixmap. Typical provider implementations return the original pixmap and optionally set size instead of scaling when the requested size is empty.

A defensive pattern:

if self.is_edited_provider and not self.image_data.isEmpty():
    if not self.edited_pixmap.loadFromData(self.image_data, "PNG"):
        return QPixmap()
    if size.width() > 0 and size.height() > 0:
        return self.edited_pixmap.scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
    return self.edited_pixmap

I'll verify the review comment by examining the codebase for the concerns raised about QML caching and size handling.


🏁 Script executed:

#!/bin/bash
# Find the requestPixmap implementation in the ImageProvider class
rg -nA 30 "def requestPixmap" faststack/patch --type py

Length of output: 1214


🏁 Script executed:

#!/bin/bash
# Search for the edited image provider URL in QML files
rg -nC 5 "editedImageProvider" --type qml

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if Image elements have cache:false or sourceSize set
rg -nC 5 "image://editedImageProvider\|cache:\s*false\|sourceSize" --type qml

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for any size parameter checks in requestPixmap or related methods
rg -nC 3 "size\.width\(\)|size\.height\(\)|isEmpty\(\)" faststack/patch --type py

Length of output: 443


🏁 Script executed:

#!/bin/bash
# Find where currentEditedImageDataChanged signal is emitted
rg -nC 3 "currentEditedImageDataChanged" faststack/patch --type py

Length of output: 1397


🏁 Script executed:

#!/bin/bash
# Check for cache invalidation or URL token generation logic
rg -nC 5 "cache|generation|token|refresh|invalidate" faststack/patch --type py | head -100

Length of output: 542


🏁 Script executed:

#!/bin/bash
# Find the QML Image element that uses editedImageProvider
fd -e qml | xargs rg -l "Image\|editedImageProvider"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get full context of requestPixmap implementation
rg -nA 50 "class ImageProvider" faststack/patch --type py | grep -A 50 "requestPixmap"

Length of output: 1363


🏁 Script executed:

#!/bin/bash
# Check the actual behavior of scaled() with (0,0) size
rg -nB 5 -A 10 "\.scaled\(size" faststack/patch --type py

Length of output: 764


🏁 Script executed:

#!/bin/bash
# Look for the Image element in QML that shows edited content
fd -e qml -x rg -l "current_edited_image_data\|currentEditedImageData\|editedImageProvider"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find all QML Image declarations to understand the structure
fd -e qml -x grep -n "Image\s*{" 

Length of output: 29


🏁 Script executed:

#!/bin/bash
# Search for Image sourceSize configuration in QML
fd -e qml -x grep -n "sourceSize"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for the complete requestPixmap method signature and body
ast-grep --pattern 'def requestPixmap($_, $_) {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find all QML Image sources that use image protocol
fd -e qml -x rg -n "source:\s*\"image://"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there's any cache handling or URL parameter modification
rg -nC 10 "is_edited_provider" faststack/patch --type py

Length of output: 3441


🏁 Script executed:

#!/bin/bash
# Search for current_edited_image_data or similar edited data handling
rg -nC 5 "current_edit\|edited.*data\|editedImage" faststack/patch --type py

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for the Image element source binding more carefully
fd -e qml -x rg -B 5 -A 5 "editedImageProvider"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if sourceSize is set on any Image element
fd -e qml -x rg -B 3 -A 3 "sourceSize"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find where the edited pixmap is set/updated
rg -nC 5 "self.edited_pixmap\|editedPixmap" faststack/patch --type py

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for Image cache property settings
fd -e qml -x rg -B 2 -A 2 "cache:\s*"

Length of output: 481


🏁 Script executed:

#!/bin/bash
# Examine the structure around line 567-583 more carefully
sed -n '550,620p' faststack/patch

Length of output: 2827


🏁 Script executed:

#!/bin/bash
# Look for the complete ImageProvider implementation
sed -n '567,700p' faststack/patch

Length of output: 5518


🏁 Script executed:

#!/bin/bash
# Find where QML loads the ImageProvider or Image element
fd -e qml | head -20

Length of output: 519


🏁 Script executed:

#!/bin/bash
# Check what files reference ImageProvider in QML context
rg -n "ImageProvider\|imageProvider\|editedImageProvider" --type qml

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for the Image element that displays the edited content
rg -nB 10 -A 10 "Image.*id\|Image.*source" --type qml | head -60

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find QML files that might contain the Image element
fd -e qml

Length of output: 519


🏁 Script executed:

#!/bin/bash
# Check the actual requestPixmap implementation for size handling
sed -n '600,750p' faststack/patch | rg -A 40 "def requestPixmap"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find all methods that handle size parameter
rg -nC 8 "def requestPixmap|def .*size.*:" faststack/patch --type py | head -80

Length of output: 771


🏁 Script executed:

#!/bin/bash
# Get the full class definition including requestPixmap
python3 << 'EOF'
with open('faststack/patch', 'r') as f:
    lines = f.readlines()
    in_image_provider = False
    in_request_pixmap = False
    indent_level = 0
    
    for i, line in enumerate(lines[566:750], start=567):
        if 'class ImageProvider' in line:
            in_image_provider = True
        if in_image_provider:
            if 'def requestPixmap' in line:
                in_request_pixmap = True
            if in_request_pixmap:
                print(f"{i}: {line}", end='')
                if line.strip() and not line[0].isspace() and i > 600:
                    break
        elif 'def requestPixmap' in line:
            in_request_pixmap = True
            print(f"{i}: {line}", end='')
            indent_level = len(line) - len(line.lstrip())
        elif in_request_pixmap:
            print(f"{i}: {line}", end='')
            if line.strip() and len(line) - len(line.lstrip()) <= indent_level and line.strip():
                if not line.strip().startswith('#'):
                    break
EOF

Length of output: 897


🏁 Script executed:

#!/bin/bash
# Simpler approach - show the entire ImageProvider class from line 567 onwards
sed -n '567,750p' faststack/patch

Length of output: 7430


🏁 Script executed:

#!/bin/bash
# Look for the requestPixmap method implementation
grep -n "def requestPixmap" faststack/patch

Length of output: 55


I need to verify one more detail about Qt's sourceSize behavior and whether the zero-size concern is still valid with the current configuration.


🏁 Script executed:

#!/bin/bash
# Verify the exact sourceSize configuration in context
sed -n '100,150p' faststack/patch | grep -B 5 -A 5 "sourceSize"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there are any other Image elements using editedImageProvider without sourceSize
grep -n "editedImageProvider\|Image {" faststack/patch | head -40

Length of output: 161


🏁 Script executed:

#!/bin/bash
# Look for the complete Image element definition with all properties
sed -n '105,130p' faststack/patch

Length of output: 1036


🏁 Script executed:

#!/bin/bash
# Search for any dynamic sourceSize or potential (0,0) scenarios
rg -n "sourceSize|width|height" faststack/patch | grep -i "source\|edit\|image" | head -30

Length of output: 596


🏁 Script executed:

#!/bin/bash
# Check if mainImage is guaranteed to have non-zero dimensions
sed -n '1,200p' faststack/patch | grep -B 3 -A 3 "id.*mainImage\|anchors.fill"

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Look for the parent Rectangle or layout that contains mainImage
sed -n '50,150p' faststack/patch | grep -B 10 "id: mainImage"

Length of output: 0



🏁 Script executed:

#!/bin/bash
# Find the Image element definition with id: mainImage
sed -n '600,750p' faststack/patch | grep -B 20 "id: mainImage\|image://editedImageProvider"

Length of output: 783


🏁 Script executed:

#!/bin/bash
# Get the Image element with all its properties
rg -n "Image\s*\{" faststack/patch -A 30 | grep -A 30 "mainImage\|editedImageProvider"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search around line 650-660 for full Image element
sed -n '640,680p' faststack/patch

Length of output: 1551


🏁 Script executed:

#!/bin/bash
# Check the parent RowLayout that contains mainImage
sed -n '630,660p' faststack/patch

Length of output: 1195


Add defensive size checks to handle edge cases in requestPixmap

The caching concern is already addressed: the Image element has cache: false set, so the constant URL won't cause stale pixmaps.

However, the zero-size handling still needs attention. The requestPixmap implementation should validate the size parameter before scaling, since scaled(QSize(0, 0), ...) returns a null pixmap. While sourceSize is currently bound to mainImage dimensions, this provides only runtime protection if the binding exists.

Add explicit size validation in faststack/ui/provider.py at the edited image provider branch (around line 592-595):

if self.is_edited_provider and not self.image_data.isEmpty():
    if not self.edited_pixmap.loadFromData(self.image_data, "PNG"):
        return QPixmap()
    if size.width() > 0 and size.height() > 0:
        return self.edited_pixmap.scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
    return self.edited_pixmap

This defensive pattern ensures the provider returns a valid pixmap even if sourceSize is removed or the Image component initializes before dimensions are available.

🤖 Prompt for AI Agents
In faststack/patch around lines 567 to 583, the edited-image branch in
requestPixmap must defensively handle zero-size requests: check that
self.is_edited_provider and self.image_data is not empty, attempt to load the
edited_pixmap from self.image_data (return an empty QPixmap on load failure),
then if size.width() > 0 and size.height() > 0 return edited_pixmap.scaled(size,
Qt.KeepAspectRatio, Qt.SmoothTransformation), otherwise return the unscaled
edited_pixmap; this prevents scaled(QSize(0,0)) from producing a null pixmap
when the QML Image sourceSize is unavailable.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
faststack/faststack/imaging/jpeg.py (1)

117-141: Incorrect max_dim calculation leads to suboptimal scaling.

Line 123 uses max(width, height) to determine the TurboJPEG scaling factor, but this is incorrect for non-square target dimensions. When the target is, say, 100×200 and the image is 1000×1000, passing max_dim=200 may result in a 200×200 decode that exceeds the 100-pixel width constraint. Although the Pillow thumbnail at lines 134–138 compensates, this adds unnecessary overhead.

The correct approach is to choose max_dim based on which dimension (width or height) is the limiting factor after accounting for aspect ratios.

Apply this diff to calculate the correct max_dim:

             img_width, img_height, _, _ = jpeg_decoder.decode_header(jpeg_bytes)
             
-            # Calculate best scaling factor for TurboJPEG (supports 1/8, 1/4, 1/2, etc.)
-            scale_factor = _get_turbojpeg_scaling_factor(img_width, img_height, max(width, height))
+            # Determine which dimension is the limiting factor
+            if img_width * height > img_height * width:
+                # Image is wider relative to target box; width is the constraint
+                max_dim = width
+            else:
+                # Image is taller relative to target box; height is the constraint
+                max_dim = height
+            
+            scale_factor = _get_turbojpeg_scaling_factor(img_width, img_height, max_dim)

This ensures TurboJPEG decodes as close as possible to the final size, minimizing the need for the subsequent Pillow resize.

♻️ Duplicate comments (3)
faststack/faststack/app.py (3)

10-10: Import Tuple so the undo_history type hint and Ruff F821 are satisfied

self.undo_history: List[Tuple[str, Any, float]] uses Tuple but it isn’t imported from typing, which Ruff flags as F821 and can also confuse tooling.

Minimal fix:

-from typing import Optional, List, Dict, Any
+from typing import Optional, List, Dict, Any, Tuple

Also applies to: 137-138


1188-1192: Auto white balance undo still looks for _backup filenames, which won’t match the editor’s -backup pattern

The unified undo_delete now handles "auto_white_balance" actions, but the backup lookup still hardcodes an underscore suffix:

elif action_type == "auto_white_balance":
    filepath, saved_path = action_data
    filepath_obj = Path(filepath)
    backup_path = filepath_obj.parent / f"{filepath_obj.stem}_backup{filepath_obj.suffix}"
...
    if backup_path.exists():
        filepath_obj.unlink()
        backup_path.rename(filepath_obj)

Per the ImageEditor.save_image() implementation (in faststack/imaging/editor.py), backups are created using a -backup suffix with possible numbering (e.g. name-backup.jpg, name-backup2.jpg, etc.), not _backup. As a result, undo for auto white balance will almost always report “Backup not found” even when a valid backup exists.

A minimal fix that matches the documented pattern and supports numbered backups:

-        filepath, saved_path = action_data
-        filepath_obj = Path(filepath)
-        backup_path = filepath_obj.parent / f"{filepath_obj.stem}_backup{filepath_obj.suffix}"
+        filepath, saved_path = action_data
+        filepath_obj = Path(filepath)
+        # Look for backups like "<stem>-backup[<n>].ext"
+        candidates = sorted(
+            filepath_obj.parent.glob(f"{filepath_obj.stem}-backup*{filepath_obj.suffix}")
+        )
+        backup_path = candidates[-1] if candidates else None
...
-        try:
-            if backup_path.exists():
+        try:
+            if backup_path and backup_path.exists():
                 filepath_obj.unlink()
                 backup_path.rename(filepath_obj)
                 ...
-            else:
+            else:
                 self.update_status_message("Backup not found")
                 log.warning("Backup not found at %s", backup_path)
                 self.undo_history.append(("auto_white_balance", (filepath, saved_path), timestamp))

This should make Ctrl+Z reliably undo Quick Auto White Balance operations.

Also applies to: 1217-1308


1715-1858: Auto white balance logic is mathematically consistent; just drop the unused b_diff

The new auto_white_balance implementation correctly:

  • Uses the original full-resolution image for statistics.
  • Computes channel means and a grey-world target.
  • Derives BY/MG shifts using the two-equation system consistent with the editor’s documented WB model.
  • Scales/clamps values into the editor’s expected [-1, 1] range and updates both editor parameters and UIState, then triggers a refresh.

One small cleanup: b_diff is computed but never used, which Ruff flags as F841:

r_diff = r_mean - grey_target
g_diff = g_mean - grey_target
b_diff = b_mean - grey_target  # unused

You can safely remove b_diff for now:

-        r_diff = r_mean - grey_target
-        g_diff = g_mean - grey_target
-        b_diff = b_mean - grey_target
+        r_diff = r_mean - grey_target
+        g_diff = g_mean - grey_target

If you later extend the system to incorporate the blue equation explicitly, you can reintroduce it with a corresponding adjustment.

🧹 Nitpick comments (4)
faststack/README.md (1)

12-25: Fix minor typo in feature description

Line 12 has a small typo: "high-peformance""high-performance". Everything else in this section reads clearly.

faststack/faststack/qml/Main.qml (2)

224-241: “Open Folder…” menu item currently has no user-visible effect

The File → “Open Folder…” action only logs "Open folder triggered" and closes the menu; it doesn’t open a directory dialog or change the viewed folder.

If this is intended as a functional entry point, consider wiring it to the existing folder-selection flow (e.g., a controller slot that opens a QFileDialog and reloads images), or add a TODO so it’s clear this is placeholder behavior.


596-616: Guard Ctrl+S save against key auto‑repeat

In the key handler, E ignores auto-repeat, but the Ctrl+S path doesn’t:

else if (event.key === Qt.Key_S && (event.modifiers & Qt.ControlModifier)) {
    if (uiState.isEditorOpen) {
        controller.save_edited_image()
        event.accepted = true
    }
}

Holding Ctrl+S could therefore trigger multiple saves. Mirroring the !event.isAutoRepeat guard here would make the behavior more robust:

else if (event.key === Qt.Key_S &&
         (event.modifiers & Qt.ControlModifier) &&
         !event.isAutoRepeat) {
    ...
}
faststack/faststack/app.py (1)

666-699: Avoid double-saving sidecar stacks when removing from a stack

In remove_from_batch_or_stack, the stack-removal path updates and saves the sidecar twice—once before updating self.stacks, and again afterwards:

                    self.sidecar.data.stacks = self.stacks  # BEFORE self.stacks is replaced
                    self.sidecar.save()
...
            if stack_modified:
                self.stacks = new_stacks
                self.sidecar.data.stacks = self.stacks
                self.sidecar.save()

The first save writes the old stack list and is immediately overwritten by the second save. This adds unnecessary disk I/O and makes the comment misleading.

You can safely drop the earlier assignment/save and only persist after self.stacks is updated:

-                    self.sidecar.data.stacks = self.stacks  # Update sidecar BEFORE self.stacks is replaced
-                    self.sidecar.save()
...
             if stack_modified:
                 self.stacks = new_stacks
                 self.sidecar.data.stacks = self.stacks
                 self.sidecar.save()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83bf949 and c31ee7a.

📒 Files selected for processing (11)
  • faststack/ChangeLog.md (1 hunks)
  • faststack/README.md (4 hunks)
  • faststack/faststack.egg-info/PKG-INFO (2 hunks)
  • faststack/faststack.egg-info/requires.txt (0 hunks)
  • faststack/faststack/app.py (18 hunks)
  • faststack/faststack/imaging/jpeg.py (4 hunks)
  • faststack/faststack/io/helicon.py (1 hunks)
  • faststack/faststack/qml/Components.qml (2 hunks)
  • faststack/faststack/qml/Main.qml (8 hunks)
  • faststack/pyproject.toml (1 hunks)
  • faststack/requirements.txt (0 hunks)
💤 Files with no reviewable changes (2)
  • faststack/requirements.txt
  • faststack/faststack.egg-info/requires.txt
🚧 Files skipped from review as they are similar to previous changes (4)
  • faststack/faststack/qml/Components.qml
  • faststack/pyproject.toml
  • faststack/faststack.egg-info/PKG-INFO
  • faststack/ChangeLog.md
🧰 Additional context used
🪛 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 1.2 - November 22, 2025 # By Alan Rockefeller Ultra-fast, cach...

(MISSING_COMMA_AFTER_YEAR)


[grammar] ~12-~12: Ensure spelling is correct
Context: ...0ms next/previous image switching, high-peformance decoding via PyTurboJPEG. - **Zoom & ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 Ruff (0.14.5)
faststack/faststack/app.py

137-137: Undefined name Tuple

(F821)


655-655: f-string without any placeholders

Remove extraneous f prefix

(F541)


689-689: f-string without any placeholders

Remove extraneous f prefix

(F541)


1815-1815: Local variable b_diff is assigned to but never used

Remove assignment to unused variable b_diff

(F841)

🔇 Additional comments (13)
faststack/faststack/imaging/jpeg.py (5)

12-29: LGTM: Robust PyTurboJPEG initialization with proper fallback.

The nested try-except-else structure correctly handles both import failures and initialization failures, setting the TURBO_AVAILABLE flag appropriately in each case. The logging provides good visibility into which decoder is being used.


31-49: LGTM: Proper color space handling and robust fallback.

The use of pixel_format=TJPF_RGB with flags=0 ensures correct YCbCr→RGB conversion. The exception handling appropriately falls back to Pillow when TurboJPEG fails.


52-87: LGTM: Well-structured thumbnail generation.

The function properly leverages TurboJPEG's hardware-accelerated scaling and includes a defensive Pillow resize step (lines 71-74) if the decoded thumbnail still exceeds max_dim. The fallback path is sound.


90-107: LGTM: Efficient scaling factor selection.

The function correctly identifies the largest TurboJPEG scaling factor that keeps both dimensions within max_dim. Sorting in descending order and returning early optimizes the common case.


143-160: LGTM: Smart resampling selection in Pillow fallback.

The dynamic choice of resampling filter based on scale factor (BILINEAR for >4× downscales, LANCZOS otherwise) balances performance and quality effectively.

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

22-24: Docstring now correctly documents the tuple return contract

The updated return-value description matches the actual (success: bool, tmp_path: Optional[Path]) behavior across all branches; no further changes needed here.

faststack/faststack/app.py (6)

329-334: Editor live preview integration in get_decoded_image and get_preview_data looks coherent

The early-return in get_decoded_image when the editor is open for the current file, plus the get_preview_data slot that delegates to ImageEditor.get_preview_data(), provides a clean path for showing live edits without fighting the prefetch/cache pipeline:

if self.ui_state.isEditorOpen and self.image_editor.original_image and \
        str(self.image_editor.current_filepath) == str(self.image_files[index].path):
    preview_data = self.image_editor.get_preview_data()
    if preview_data:
        return preview_data

and

@Slot(result=DecodedImage)
def get_preview_data(self) -> Optional[DecodedImage]:
    return self.image_editor.get_preview_data()

The logic is sound; assuming QML calls this via controller.get_preview_data() for previews, this should integrate well with the existing ImageProvider.

Also applies to: 1614-1659


708-885: Batch/stack toggle helpers are a nice UX improvement

The new toggle_batch_membership and toggle_stack_membership helpers provide intuitive “one-key” add/remove behavior, correctly:

  • Detecting membership in existing ranges.
  • Splitting ranges or merging/normalizing overlapping/adjacent ranges.
  • Updating sidecar, cache indices, summary signals, and UI sync.

The algorithms look correct for edge cases (single-element ranges, adjacency, overlaps) and should behave well as the number of batches/stacks grows.


1399-1429: Photoshop integration’s backup-stripping and RAW preference logic looks solid

The new edit_in_photoshop flow correctly:

  • Strips -backup, -backup2, or -backup-1 style suffixes from the JPG stem via re.sub(r'-backup(-?\d+)?$', '', original_stem).
  • Prefers an existing image_file.raw_pair when available.
  • Falls back to searching for a RAW file using RAW_EXTENSIONS and the original (non-backup) stem.
  • Only uses the JPG when no RAW is found, and logs that choice.

This should behave well for edited/backup outputs while still honoring RAW-first editing where possible.


1524-1534: Status message helper is a good centralization of transient UI messages

update_status_message centralizes status text updates and timed clearing, and you’ve already threaded it into many new paths (batch/stack actions, deletes, Photoshop, auto WB, etc.). This should make future UX tweaks (timeouts, styling) much simpler.


1552-1611: Drag-and-drop now correctly filters non-existent files and marks only successfully dragged items as uploaded

In start_drag_current_image:

  • You compute existing_indices from the collected batch/current indices before building file_paths, ensuring you never drag missing files.
  • After a successful drag (Copy or Move), you loop over existing_indices to mark only those items as uploaded, then clear all batches and refresh metadata/UI.

This avoids errors on stale paths and keeps the “uploaded” metadata consistent with what was actually dragged.


1908-1952: QML import path setup for PySide6 looks appropriate

The additional QML import paths in main():

engine.addImportPath(os.path.join(os.path.dirname(PySide6.__file__), "qml"))
engine.addImportPath("qrc:/qt-project.org/imports")
engine.addImportPath(os.path.join(os.path.dirname(__file__), "qml"))
engine.addImportPath(os.path.join(os.path.dirname(PySide6.__file__), "qml", "Qt5Compat"))

combined with os.environ["QML2_IMPORT_PATH"] pointing at your local qml directory, should make it much easier for PySide6 to resolve both your own QML and QtQuick/Qt5Compat modules without relying on environment-specific configuration.

faststack/faststack/qml/Main.qml (1)

245-255: Fix only the filter dialog binding; settings dialog bindings are correct

The settings dialog methods (get_helicon_path(), get_photoshop_path(), get_cache_size(), get_prefetch_radius(), get_default_directory()) are already correctly exposed via @Slot wrappers on UIState in faststack/faststack/ui/provider.py (lines 310–366). The QML code at lines 245–255 is correct as written.

However, the filter dialog at line 845 has an issue: uiState.applyFilter(filterString) does not exist on UIState. The method apply_filter() exists only on AppController.

Fix required at Main.qml line 845:

-            if (uiState) uiState.applyFilter(filterString)
+            if (controller) controller.apply_filter(filterString)

Likely an incorrect or invalid review comment.

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