Skip to content

Add ability to edit in Photoshop and copy path of current image to clipboard#10

Merged
AlanRockefeller merged 3 commits intomainfrom
test
Nov 20, 2025
Merged

Add ability to edit in Photoshop and copy path of current image to clipboard#10
AlanRockefeller merged 3 commits intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Nov 16, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Filter images by filename text
    • Windows drag-and-drop support for images
    • Edit images directly in Photoshop
    • Copy image file paths to clipboard
    • New keyboard shortcuts for quick actions
  • UI Improvements

    • Real-time status messages for user actions
    • Enhanced stack summary display
    • Refined theme switching behavior
    • New filter management options in the Actions menu

This commit introduces new features:
- Pressing 'E' now opens the current image in Adobe Photoshop.
- Pressing 'Ctrl+C' copies the path of the current image to the clipboard.
- The application's status bar now displays messages for actions like copying to clipboard or launching Photoshop.

Changes include:
- Updated aststack/faststack/config.py to include a 'photoshop' section for executable path and arguments.
- Modified aststack/faststack/ui/keystrokes.py to map 'E' to 'edit_in_photoshop' and 'Ctrl+C' to 'copy_path_to_clipboard'.
- Implemented edit_in_photoshop, copy_path_to_clipboard, and update_status_message methods in aststack/faststack/app.py.
- Enhanced aststack/faststack/ui/provider.py by adding statusMessageChanged signal and statusMessage property to UIState.
- Updated aststack/faststack/qml/Main.qml to display the uiState.statusMessage in the footer.
- Removed the unused aststack/faststack/ui/state.py file.
Replaced the lambda function in update_status_message with a nested function to avoid a SyntaxError caused by an assignment within the lambda.
This commit addresses two UI issues:
1.  The Key Bindings dialog now uses a ScrollView to ensure all text is visible, and the key bindings list has been updated.
2.  The Settings dialog now includes a field for selecting the Photoshop executable path.

Changes include:
- Updated aststack/faststack/qml/Main.qml to wrap the Key Bindings text in a ScrollView and to load the Photoshop path into the Settings dialog.
- Modified aststack/faststack/qml/SettingsDialog.qml to include UI elements for Photoshop path selection.
- Added get_photoshop_path and set_photoshop_path methods to aststack/faststack/app.py.
- Exposed the new methods to QML through aststack/faststack/ui/provider.py.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 16, 2025

Walkthrough

Adds filtering capability with apply/clear filter slots, drag-and-drop support via Windows HDROP, Photoshop and clipboard integration, status messaging, and improved UI state synchronization through new signals and properties across controller, configuration, QML, provider, and keystroke modules.

Changes

Cohort / File(s) Summary
Filtering workflow
faststack/faststack/app.py, faststack/faststack/qml/Main.qml, faststack/faststack/ui/keystrokes.py
Added apply_filter, get_filter_string, and clear_filter slots to AppController; refresh_image_list now applies active filter; Main.qml integrates FilterDialog with filter actions in menu; keystroke mappings adjusted.
Photoshop and clipboard integration
faststack/faststack/app.py, faststack/faststack/config.py, faststack/faststack/qml/Main.qml, faststack/faststack/qml/SettingsDialog.qml, faststack/faststack/ui/provider.py, faststack/faststack/ui/keystrokes.py
AppController adds edit_in_photoshop and copy_path_to_clipboard slots; config.py includes Photoshop exe/args section; SettingsDialog exposes Photoshop path field with validation; provider exposes Photoshop path getters/setters; keystroke E and C mappings added.
Drag-and-drop enhancement
faststack/faststack/app.py
Added make_hdrop utility and start_drag_current_image slot for Windows drag-and-drop support with CF_HDROP payload and thumbnail preview.
Theme and UI state management
faststack/faststack/app.py, faststack/faststack/qml/Main.qml, faststack/faststack/ui/provider.py
AppController set_theme now updates ui_state.theme and emits single themeChanged signal; Main.qml theme binding refactored to use uiState.theme property; provider adds theme property with notify signal and getter/setter slots.
Status messaging
faststack/faststack/app.py, faststack/faststack/qml/Main.qml, faststack/faststack/ui/provider.py
AppController adds update_status_message helper; Main.qml displays statusMessageLabel bound to uiState.statusMessage; provider introduces statusMessage property and statusMessageChanged signal.
Stack and metadata utilities
faststack/faststack/app.py, faststack/faststack/qml/Main.qml
AppController adds public get_stack_summary and is_stacked methods; clear_all_stacks now emits dataChanged; Main.qml shows uiState.stackSummary with fallback text.
General state signaling and sync
faststack/faststack/app.py, faststack/faststack/ui/provider.py
New dataChanged signal in AppController emitted after load and filter operations; sync_ui_state explicitly emits index/image/metadata signals; provider converts get_stack_summary to reactive Property with notify.
Keystroke dispatch refactoring
faststack/faststack/ui/keystrokes.py
Keybinder constructor now accepts generic controller instead of main_window; key_map stores method name strings; new _call helper dispatches to main_window then controller with logging fallback; modifier_key_map added for modifier combinations.
Context exposure
faststack/faststack/app.py, faststack/faststack/qml/Main.qml
Main function exposes controller via QML context property; Main.qml binds uiState and controller for direct access.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant QML as QML UI
    participant Ctrl as AppController
    participant State as UIState
    participant Model as Image Model

    User->>QML: Opens Filter Dialog
    User->>QML: Enters filter string, clicks OK
    QML->>Ctrl: apply_filter(filter_string)
    Ctrl->>Ctrl: refresh_image_list() with active filter
    Ctrl->>Model: Update image_files
    Ctrl->>State: emit dataChanged()
    Ctrl->>Ctrl: Reset current index, sync_ui_state()
    State->>QML: dataChanged signal
    QML->>QML: Update image list display

    User->>QML: Clicks "Clear Filename Filter"
    QML->>Ctrl: clear_filter()
    Ctrl->>Ctrl: Disable filter
    Ctrl->>Ctrl: refresh_image_list() (unfiltered)
    Ctrl->>Model: Update image_files (all)
    Ctrl->>State: emit dataChanged()
    Ctrl->>Ctrl: Adjust index, sync_ui_state()
    State->>QML: dataChanged signal
    QML->>QML: Display all images
Loading
sequenceDiagram
    participant User as User
    participant QML as QML UI
    participant Ctrl as AppController
    participant Config as Config
    participant Sys as System/Photoshop

    User->>QML: Opens Settings Dialog
    QML->>QML: Displays current Photoshop path
    User->>QML: Clicks Browse for Photoshop path
    QML->>Ctrl: open_file_dialog()
    Ctrl->>Sys: File picker dialog
    Sys->>QML: Returns selected path
    QML->>QML: Updates photoshopPath field
    QML->>Ctrl: check_path_exists(path)
    QML->>QML: Shows checkmark if valid
    User->>QML: Clicks OK in Settings
    QML->>Ctrl: set_photoshop_path(path)
    Ctrl->>Config: Persist Photoshop exe path
    
    User->>QML: Selects image, presses E
    QML->>Ctrl: edit_in_photoshop()
    Ctrl->>Config: Retrieve Photoshop exe and args
    Ctrl->>Sys: Launch Photoshop with image file
    Ctrl->>QML: update_status_message("Opening in Photoshop...")
    QML->>User: Display status message
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • faststack/faststack/app.py — Large number of new slots and signals with intertwined filter logic, drag-and-drop Windows-specific code (make_hdrop), Photoshop process launching, clipboard operations, and refactored sync_ui_state emissions; verify signal emissions don't cause UI feedback loops.
  • faststack/faststack/ui/keystrokes.py — Refactored dispatch mechanism with new _call helper fallback logic and modifier key handling; ensure method name string lookups are robust and don't silently fail.
  • faststack/faststack/qml/Main.qml — Substantial rework of theme binding (uiState.theme property vs. get_theme method), new FilterDialog integration, status message display, and menu action additions; verify QML bindings update correctly without circular updates.
  • faststack/faststack/ui/provider.py — Multiple new signals and properties (theme, statusMessage, photoshop_path), and conversion of get_stack_summary from method to reactive Property; ensure all notify signals are correctly wired and don't cause redundant updates.
  • Cross-module consistency — Verify that changes in app.py (new signals/slots), provider.py (new properties/signals), and QML (new bindings) are correctly synchronized and that the controller is properly exposed to QML context without lifecycle issues.

Possibly related PRs

  • Release v0.3 — more improvements #4 — Modifies the UI theme signaling path with themeChanged notification and AppController.set_theme/UIState theme handling, directly related to theme property changes in this PR.
  • Release v0.2 — more improvements #3 — Modifies the same UI/controller surfaces with theme and Photoshop-path getters/setters, UIState properties, and QML bindings across Main.qml, SettingsDialog.qml, and provider.py.
  • Version 0.1 #1 — Introduces the core AppController, UIState, and provider infrastructure extended by this PR; changes build on the Version 0.1 foundation established in that PR.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the two main user-facing features added (Photoshop editing and clipboard path copying), which are clearly implemented across the changes shown in the summary.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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

Tip

📝 Customizable high-level summaries are now available!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide custom instructions to shape the summary (bullet lists, tables, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example:

"Create a concise high-level summary as a bullet-point list. Then include a Markdown table showing lines added and removed by each contributing author."


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

🧹 Nitpick comments (3)
faststack/faststack/qml/SettingsDialog.qml (1)

56-78: Consider extracting a reusable component for path configuration.

The Photoshop Path section (lines 56-78) duplicates the pattern from the Helicon Path section (lines 32-54). Consider creating a reusable QML component to reduce duplication.

Example structure:

// PathConfigRow.qml
GridLayout {
    property string label: ""
    property string value: ""
    property var onValueChanged: function(v) {}
    
    Label { text: label }
    TextField {
        Layout.fillWidth: true
        text: value
        onTextChanged: onValueChanged(text)
    }
    RowLayout {
        Button {
            text: "Browse..."
            onClicked: {
                var path = uiState.open_file_dialog()
                if (path) value = path
            }
        }
        Label {
            text: ""
            color: "lightgreen"
            visible: uiState.check_path_exists(value)
        }
    }
}
faststack/faststack/ui/provider.py (1)

42-44: Consider using logging.exception for error logging.

When logging within an exception handler, logging.exception automatically includes the traceback, which aids debugging.

 except (ValueError, IndexError) as e:
-    log.error(f"Invalid image ID requested from QML: {id}. Error: {e}")
+    log.exception(f"Invalid image ID requested from QML: {id}")

Based on static analysis hints.

faststack/faststack/app.py (1)

443-444: Commented-out code should be removed.

The os.remove(tmp_path) line is commented out with a note to keep the temporary file. Either remove the commented code or document why the temporary file should be kept.

-                # os.remove(tmp_path)
-                log.info(f"Keeping temporary file: {tmp_path}")
+                log.debug(f"Temporary file retained for Helicon: {tmp_path}")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c7c43e and ea433ef.

📒 Files selected for processing (6)
  • faststack/faststack/app.py (15 hunks)
  • faststack/faststack/config.py (1 hunks)
  • faststack/faststack/qml/Main.qml (7 hunks)
  • faststack/faststack/qml/SettingsDialog.qml (3 hunks)
  • faststack/faststack/ui/keystrokes.py (1 hunks)
  • faststack/faststack/ui/provider.py (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
faststack/faststack/app.py (7)
faststack/faststack/ui/provider.py (16)
  • ImageProvider (13-45)
  • UIState (48-260)
  • theme (73-74)
  • theme (77-82)
  • get_theme (233-235)
  • currentIndex (116-117)
  • imageCount (120-121)
  • currentFilename (128-129)
  • isFlagged (132-133)
  • isRejected (136-137)
  • stackInfoText (148-149)
  • get_photoshop_path (201-202)
  • set_photoshop_path (205-206)
  • statusMessage (162-163)
  • statusMessage (166-169)
  • get_stack_summary (152-159)
faststack/faststack/ui/keystrokes.py (1)
  • Keybinder (7-88)
faststack/faststack/imaging/prefetch.py (1)
  • update_prefetch (36-56)
faststack/faststack/io/indexer.py (1)
  • find_images (19-54)
faststack/faststack/io/sidecar.py (2)
  • get_metadata (85-87)
  • save (54-83)
faststack/faststack/config.py (2)
  • save (53-61)
  • get (63-64)
faststack/faststack/io/watcher.py (1)
  • start (49-62)
faststack/faststack/ui/provider.py (2)
faststack/faststack/models.py (1)
  • DecodedImage (33-42)
faststack/faststack/app.py (4)
  • get_photoshop_path (464-465)
  • set_photoshop_path (467-469)
  • get_theme (498-499)
  • set_theme (501-511)
🪛 Ruff (0.14.4)
faststack/faststack/app.py

40-40: Redefinition of unused ImageProvider from line 14

Remove definition: ImageProvider

(F811)


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

(S603)


610-610: Do not catch blind exception: Exception

(BLE001)


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

Replace with exception

(TRY400)


670-670: Comment contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF003)

faststack/faststack/ui/provider.py

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

Replace with exception

(TRY400)

🔇 Additional comments (21)
faststack/faststack/ui/keystrokes.py (4)

8-15: LGTM! Clean refactoring to support flexible dispatch.

The change from main_window to controller with the dispatch pattern allows for better separation of concerns and makes the keybinding system more flexible.


48-61: LGTM! Proper fallback chain for method dispatch.

The _call method correctly implements a fallback chain: QML root first (for UI sync), then controller, with appropriate logging for missing methods.


81-86: Good fallback for non-standard keyboard layouts.

The text-based bracket key fallback is a thoughtful addition for handling keyboards where bracket keycodes might not match Qt.Key_BracketLeft/Right.


69-72: The original review comment is based on a misunderstanding of Qt's modifier key API and can be disregarded.

The bitwise AND operation is the correct and standard pattern for checking modifier keys in Qt/PyQt. In Qt, event.modifiers() returns a combined set of all currently pressed modifiers. The bitwise AND check (event.modifiers() & mapped_modifier) correctly tests whether a specific modifier flag is set, even when other modifiers are also pressed. Using strict equality (==) would actually break the code—for example, if the user pressed Ctrl+Shift, the equality check would fail because event.modifiers() would represent both modifiers combined. The current implementation at line 70 is correct and follows Qt best practices.

Likely an incorrect or invalid review comment.

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

71-82: LGTM! Proper Qt property implementation for theme.

The theme property correctly implements the Qt property pattern with getter, setter, and change notification signal.


161-169: LGTM! Status message property follows Qt patterns.

The statusMessage property is well-implemented with proper change detection and signal emission.


200-206: LGTM! Consistent delegation pattern for Photoshop path.

The get/set_photoshop_path slots follow the same pattern as the existing helicon_path methods, maintaining consistency.

faststack/faststack/qml/Main.qml (4)

18-34: LGTM! Improved theme binding with reactive properties.

Migrating from get_theme() method calls to uiState.theme property binding enables reactive updates and eliminates manual synchronization.


112-118: LGTM! Clean status message display implementation.

The status message label with conditional visibility provides good user feedback without cluttering the UI when empty.


250-276: LGTM! Comprehensive keyboard shortcuts documentation.

The about dialog now documents all keyboard shortcuts including the new Photoshop (E) and clipboard (Ctrl+C) actions.


186-187: FilterDialog component verified and properly implemented.

The FilterDialog component is correctly defined inline at lines 303-307 in Main.qml with the id filterDialog and includes the necessary onAccepted signal handler. The reference at line 186 (filterDialog.open()) is valid and the component is properly accessible.

faststack/faststack/app.py (9)

44-58: LGTM! Correct Windows HDROP implementation.

The make_hdrop function properly constructs a CF_HDROP payload with the DROPFILES header structure required for Windows drag-and-drop operations.


63-63: LGTM! General data change signal for UI synchronization.

Adding a dataChanged signal provides a way to notify the UI of broad state changes without multiple specific signals.


120-153: LGTM! Well-structured filtering implementation.

The filtering workflow correctly:

  • Validates input
  • Updates internal state
  • Refreshes the image list
  • Emits signals
  • Resets navigation
  • Triggers prefetch

218-228: LGTM! Filter applied during image list refresh.

The filter is correctly applied during refresh_image_list, ensuring consistency whenever the image list is rebuilt.


464-469: LGTM! Consistent pattern for Photoshop path configuration.

The get/set methods follow the same pattern as the existing Helicon path methods.


501-511: LGTM! Proper theme state synchronization.

The method now updates both the UI state property and persists the configuration, ensuring consistency across the application.


615-623: LGTM! Clean clipboard integration.

The clipboard copy functionality is straightforward and includes appropriate user feedback via status messages.


625-634: LGTM! Useful status message helper with timeout.

The status message helper with automatic clearing provides good UX without cluttering the interface.


638-677: LGTM! Comprehensive drag-and-drop implementation.

The drag-and-drop implementation:

  • Validates file existence
  • Handles Windows-specific HDROP format
  • Falls back to standard URLs for other platforms
  • Includes a thumbnail preview
  • Logs appropriately
faststack/faststack/config.py (1)

22-25: Remove the concern—"Photoshop 2026" is the current version as of November 2025.

The latest Adobe Photoshop desktop version is 27.0 (Photoshop 2026), and the path C:\Program Files\Adobe\Adobe Photoshop 2026\ matches Adobe's documented typical Windows installation path format. The hardcoded version in the config is accurate and aligns with the current target version.

from faststack.imaging.cache import ByteLRUCache, get_decoded_image_size
from faststack.imaging.prefetch import Prefetcher
from faststack.ui.provider import ImageProvider, UIState
from faststack.ui.provider import ImageProvider
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove duplicate import.

ImageProvider is imported twice: once on line 14 and again on line 40.

 from faststack.imaging.prefetch import Prefetcher
-from faststack.ui.provider import ImageProvider
 from faststack.ui.keystrokes import Keybinder

Based on static analysis hints.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from faststack.ui.provider import ImageProvider
from faststack.imaging.prefetch import Prefetcher
from faststack.ui.keystrokes import Keybinder
🧰 Tools
🪛 Ruff (0.14.4)

40-40: Redefinition of unused ImageProvider from line 14

Remove definition: ImageProvider

(F811)

🤖 Prompt for AI Agents
In faststack/faststack/app.py around line 40, there is a duplicate import of
ImageProvider (already imported on line 14); remove the redundant import
statement at line 40 so ImageProvider is imported only once, then run the
linter/tests to confirm no unused-import or import-order issues remain.

Comment on lines +586 to +612
@Slot()
def edit_in_photoshop(self):
if not self.image_files:
self.update_status_message("No image to edit.")
return

current_image_path = self.image_files[self.current_index].path
photoshop_exe = config.get('photoshop', 'exe')
photoshop_args = config.get('photoshop', 'args')

if not photoshop_exe or not Path(photoshop_exe).exists():
self.update_status_message("Photoshop executable not configured or not found.")
log.error(f"Photoshop executable not found: {photoshop_exe}")
return

try:
command = [photoshop_exe]
if photoshop_args:
command.extend(photoshop_args.split())
command.append(str(current_image_path))

subprocess.Popen(command)
self.update_status_message(f"Opened {current_image_path.name} in Photoshop.")
log.info(f"Launched Photoshop with: {command}")
except Exception as e:
self.update_status_message(f"Failed to open in Photoshop: {e}")
log.error(f"Error launching Photoshop: {e}")
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

Security: Validate Photoshop executable path before execution.

The code executes the configured Photoshop path without validation. Consider adding signature verification or restricting to known installation paths.

Also, replace the broad Exception catch with specific exception types, and use logging.exception to include traceback:

         try:
             command = [photoshop_exe]
             if photoshop_args:
                 command.extend(photoshop_args.split())
             command.append(str(current_image_path))
             
             subprocess.Popen(command)
             self.update_status_message(f"Opened {current_image_path.name} in Photoshop.")
             log.info(f"Launched Photoshop with: {command}")
-        except Exception as e:
+        except (OSError, subprocess.SubprocessError) as e:
             self.update_status_message(f"Failed to open in Photoshop: {e}")
-            log.error(f"Error launching Photoshop: {e}")
+            log.exception("Error launching Photoshop")

Based on static analysis hints.

🧰 Tools
🪛 Ruff (0.14.4)

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

(S603)


610-610: Do not catch blind exception: Exception

(BLE001)


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

Replace with exception

(TRY400)

@AlanRockefeller
Copy link
Copy Markdown
Owner Author

@coderabbitai What improvements could be made?

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 17, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the main controller file to analyze Photoshop integration and other features
cat faststack/faststack/app.py

Length of output: 28381


🏁 Script executed:

#!/bin/bash
# Read the config file to see Photoshop configuration
cat faststack/faststack/config.py

Length of output: 2797


🏁 Script executed:

#!/bin/bash
# Read the keystrokes handler to understand keyboard integration
cat faststack/faststack/ui/keystrokes.py

Length of output: 2839


🏁 Script executed:

#!/bin/bash
# Read the UI provider to understand state management
cat faststack/faststack/ui/provider.py

Length of output: 8207


🏁 Script executed:

#!/bin/bash
# Check if there are any tests for the new functionality
fd -e py test

Length of output: 123


🏁 Script executed:

#!/bin/bash
# Read the Main.qml to understand UI integration
cat faststack/faststack/qml/Main.qml

Length of output: 10448


🏁 Script executed:

#!/bin/bash
# Check for any FilterDialog component
fd FilterDialog

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for any TODO or FIXME comments in the changed files
rg -n "TODO|FIXME|XXX|HACK" faststack/faststack/app.py faststack/faststack/config.py faststack/faststack/ui/keystrokes.py faststack/faststack/ui/provider.py

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there's a requirements.txt or setup.py to understand dependencies
fd -g "requirements*.txt" -g "setup.py" -g "pyproject.toml"

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Find dependency files correctly
fd -t f "requirements.txt" -o fd -t f "setup.py" -o fd -t f "pyproject.toml"

Length of output: 123


🏁 Script executed:

#!/bin/bash
# Look for FilterDialog component in QML files
fd -e qml | xargs grep -l "FilterDialog"

Length of output: 32


🏁 Script executed:

#!/bin/bash
# Check if FilterDialog.qml exists
cat faststack/faststack/qml/FilterDialog.qml 2>/dev/null || echo "FilterDialog.qml not found"

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Check the project structure
ls -la faststack/faststack/qml/

Length of output: 306


🏁 Script executed:

#!/bin/bash
# Look for any README or documentation
fd -t f README

Length of output: 19


🏁 Script executed:

#!/bin/bash
# Find dependency files
find . -name "requirements.txt" -o -name "setup.py" -o -name "pyproject.toml" | head -5

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Read the README to understand the project better
cat faststack/README.md

Length of output: 1711


🏁 Script executed:

#!/bin/bash
# Check what Components.qml contains to understand UI structure
cat faststack/faststack/qml/Components.qml

Length of output: 2949


🏁 Script executed:

#!/bin/bash
# Look for any existing documentation on configuration
rg -n "photoshop|Photoshop" faststack/README.md

Length of output: 0


@AlanRockefeller I've analyzed your PR and identified several areas for improvement. Here are my recommendations organized by priority:

🚨 Critical Issues

1. Missing FilterDialog.qml Component

Main.qml references FilterDialog but the file doesn't exist in faststack/faststack/qml/:

FilterDialog {
    id: filterDialog
    onAccepted: {
        controller.apply_filter(filterString)
    }
}

Fix: Create FilterDialog.qml with appropriate UI controls for entering filter text.

2. Property Name Mismatch

Main.qml uses uiState.stackSummary but provider.py defines it as get_stack_summary:

# In provider.py, should be:
@Property(str, notify=statusMessageChanged)
def stackSummary(self):  # Changed from get_stack_summary
    # ... implementation

⚠️ Security & Platform Issues

3. Unsafe Subprocess Execution

In edit_in_photoshop(), using unsanitized args with shell execution:

@Slot()
def edit_in_photoshop(self):
    # Current: photoshop_args.split() is unsafe
    command = [photoshop_exe]
    if photoshop_args:
        command.extend(photoshop_args.split())  # ⚠️ Vulnerable to injection
    
    # Better approach:
    import shlex
    if photoshop_args:
        command.extend(shlex.split(photoshop_args))
    
    # Or use subprocess.run with security options:
    subprocess.run(command, check=False, shell=False, timeout=5)

4. Hardcoded Windows Paths

Config has Windows-only default paths that will fail on macOS/Linux:

DEFAULT_CONFIG = {
    "photoshop": {
        "exe": "C:\\Program Files\\Adobe\\Adobe Photoshop 2026\\Photoshop.exe",  # ⚠️ Windows-only
    }
}

# Better:
import platform
DEFAULT_CONFIG = {
    "photoshop": {
        "exe": _get_default_photoshop_path(),
    }
}

def _get_default_photoshop_path():
    if platform.system() == "Windows":
        return "C:\\Program Files\\Adobe\\Adobe Photoshop 2026\\Photoshop.exe"
    elif platform.system() == "Darwin":  # macOS
        return "/Applications/Adobe Photoshop 2026/Adobe Photoshop 2026.app/Contents/MacOS/Adobe Photoshop 2026"
    return ""  # Linux

5. Platform-Specific Code Without Guards

make_hdrop() and drag-drop logic is Windows-specific but not clearly guarded:

def start_drag_current_image(self):
    # Add platform check
    if sys.platform.startswith("win"):
        hdrop = make_hdrop([str(file_path)])
        # Windows-specific MIME types
    else:
        # Already have fallback for non-Windows
        mime_data.setUrls([QUrl.fromLocalFile(str(file_path))])

🧪 Testing & Quality

6. No Tests for New Features

Add tests for:

  • Filtering functionality (apply_filter, clear_filter)
  • Photoshop integration (mock subprocess)
  • Clipboard operations
  • Drag-and-drop MIME data generation
  • Status message display and timeout

Example test structure:

# tests/test_filtering.py
def test_apply_filter_reduces_image_list():
    controller = AppController(...)
    controller.apply_filter("IMG")
    assert len(controller.image_files) < original_count

def test_clear_filter_restores_all_images():
    controller.apply_filter("test")
    controller.clear_filter()
    assert len(controller.image_files) == original_count

7. Missing Input Validation

Add validation for:

  • File paths before subprocess execution
  • Filter strings (SQL injection if you add database support)
  • Executable path existence before launching
@Slot()
def edit_in_photoshop(self):
    if not self.image_files:
        self.update_status_message("No image to edit.")
        return
    
    photoshop_exe = config.get('photoshop', 'exe')
    
    # Add validation
    if not photoshop_exe:
        self.update_status_message("Photoshop path not configured.")
        return
    
    exe_path = Path(photoshop_exe)
    if not exe_path.exists():
        self.update_status_message(f"Photoshop not found at: {photoshop_exe}")
        log.error(f"Photoshop executable not found: {photoshop_exe}")
        return
    
    # Verify file is executable (Unix)
    if not sys.platform.startswith("win") and not os.access(exe_path, os.X_OK):
        self.update_status_message("Photoshop executable lacks execute permission.")
        return

📚 Documentation

8. Update README

The README doesn't mention new features:

## Features (add these)

- **Photoshop Integration:** Edit current image in Photoshop (E key)
- **Clipboard Support:** Copy image path to clipboard (Ctrl+C)
- **Image Filtering:** Filter images by filename
- **Drag & Drop:** Drag images to external applications
- **Theme Support:** Toggle between light and dark themes

## Keyboard Shortcuts (add these)
- `E`: Edit in Photoshop
- `Ctrl+C`: Copy image path to clipboard
- `C`: Clear all stacks

9. Add Docstrings

Many new methods lack docstrings:

@Slot()
def copy_path_to_clipboard(self):
    """
    Copies the full path of the current image to the system clipboard.
    
    Displays a status message confirming the action.
    """
    if not self.image_files:
        self.update_status_message("No image path to copy.")
        return
    # ... rest of implementation

🔧 Code Quality

10. Keybinding Conflict

The C key has conflicting uses:

# In keystrokes.py
self.key_map = {
    Qt.Key_C: "clear_all_stacks",  # C alone
}

self.modifier_key_map = {
    (Qt.Key_C, Qt.ControlModifier): "copy_path_to_clipboard",  # Ctrl+C
}

# Consider using different key for clear_all_stacks, e.g.:
Qt.Key_Delete: "clear_all_stacks",  # or Shift+C

11. Magic Numbers

Hardcoded timeout and dimensions:

def update_status_message(self, message: str, timeout: int = 3000):
    # Better: make configurable
    timeout = config.getint('ui', 'status_message_timeout_ms', fallback=3000)

# In make_hdrop
pFiles = 20  # Add comment explaining this is DROPFILES struct size

12. Error Handling for External Apps

Improve error handling when launching external applications:

@Slot()
def edit_in_photoshop(self):
    try:
        subprocess.Popen(command, 
                        stderr=subprocess.PIPE,
                        stdout=subprocess.PIPE)
        self.update_status_message(f"Opened {current_image_path.name} in Photoshop.")
    except FileNotFoundError:
        self.update_status_message("Photoshop executable not found.")
        log.error(f"Photoshop not found: {photoshop_exe}")
    except PermissionError:
        self.update_status_message("Permission denied launching Photoshop.")
        log.error(f"Permission denied: {photoshop_exe}")
    except Exception as e:
        self.update_status_message(f"Failed to open in Photoshop: {type(e).__name__}")
        log.error(f"Error launching Photoshop: {e}", exc_info=True)

🎨 UI/UX Improvements

13. Filter Persistence

Consider persisting filter state in sidecar:

# In SidecarManager
self.sidecar.data.last_filter = self._filter_string

14. Keyboard Shortcut Hints

Show active filter in UI footer:

Label {
    text: ` | Filter: ${controller.get_filter_string() || 'None'}`
    visible: controller.get_filter_string() !== ""
    color: "yellow"
}

15. Configurable Status Timeout

Make status message timeout configurable per message:

def update_status_message(self, message: str, timeout: int = None):
    if timeout is None:
        timeout = config.getint('ui', 'status_timeout_ms', fallback=3000)

🏗️ Architecture

16. Signal Consolidation

Multiple places emit dataChanged and sync_ui_state(). Consider:

def _invalidate_ui(self, sync=True):
    """Central method to invalidate cache and optionally sync UI."""
    self._metadata_cache_index = (-1, -1)
    self.dataChanged.emit()
    if sync:
        self.sync_ui_state()

# Then use:
self._invalidate_ui(sync=True)  # Instead of manual calls

17. Configuration Validation

Add configuration validation on startup:

def validate_config(self):
    """Validates critical configuration paths and settings."""
    issues = []
    
    photoshop_exe = config.get('photoshop', 'exe')
    if photoshop_exe and not Path(photoshop_exe).exists():
        issues.append(f"Photoshop not found: {photoshop_exe}")
    
    helicon_exe = config.get('helicon', 'exe')
    if helicon_exe and not Path(helicon_exe).exists():
        issues.append(f"Helicon Focus not found: {helicon_exe}")
    
    if issues:
        log.warning("Configuration issues:\n" + "\n".join(issues))
    
    return issues

Would you like me to create an issue to track any of these improvements, or would you prefer to address them in this PR?

@AlanRockefeller AlanRockefeller merged commit 1128d98 into main Nov 20, 2025
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Dec 8, 2025
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