Repository navigation
Photos become JPEG, screenshots PNG — and automations actually run - #90
Conversation
New installs convert photos to JPEG and other images (including screenshots) to PNG. Existing settings.toml files keep the image default they already saved. Automations now watch one folder per rule — the real macOS screenshot location, the screen-recording folder, or an explicit path — and can copy the result to the clipboard.
PNG is now a no-op under the images default, so conversion tests use BMP. Screenshot watches on catch-all folders only match screenshot names. HEIC assertions run only when the engine is present.
0b40856 to
cd1a6d5
Compare
| } else { | ||
| self.submitted.insert(path); | ||
| } |
There was a problem hiding this comment.
🔴 Screen recordings skipped on shared Desktop
When both rules watch Desktop, drain_ready marks new recordings submitted under the screenshot rule. The recording rule then skips them, so recordings never convert.
Learn more
The watcher shares submitted and seen across all rules. The screenshot rule comes first in example_automations. When both rules resolve to Desktop, it marks a new .mov submitted after deciding that it does not match. The recording rule then skips that path before checking whether it matches. This also suppresses other rules that share a directory.
Example: On a Mac using Desktop for both captures, Screen Recording 1.mov appears. The screenshot rule ignores it but inserts its path into submitted; the recording rule never queues MP4 conversion.
Recommended fix: Track per-rule processing independently, or evaluate all matching rules for each new stable path before marking it processed. Test two enabled rules watching one folder with different file types.
Was this helpful? React with 👍 or 👎 to provide feedback.
| _automations: if cfg!(not(test)) { | ||
| Some(crate::automation::watch(cx)) | ||
| } else { |
There was a problem hiding this comment.
🔴 Automations stop when the window closes
With no active jobs, closing the last window ends the AppState watcher. last_window_closed quits the app without considering enabled automations, so later captures go unconverted.
Learn more
Automations poll only while the AppState task exists. last_window_closed calls cx.quit() when there are no windows and no active conversions. The tray icon cannot currently keep a usable popover open, so closing the main window ends the only process watching folders.
Example: An idle user closes convt, then saves Screenshot 1.heic on Desktop. No process remains to notice or convert it to PNG.
Recommended fix: Decide and implement an app lifetime for enabled automations, such as continuing to run after the last window closes with an explicit Quit path. Cover closing the last window while watching and then creating a new capture.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if matches_rule(rule, &path, &dir) { | ||
| self.submitted.insert(path.clone()); | ||
| if let Some(to) = format_by_id(&rule.to) { | ||
| self.submitted.insert(path.with_extension(to.extension())); | ||
| } | ||
| ready.push((index, path)); |
There was a problem hiding this comment.
🔴 Failed automation files never retry
When poll_automations cannot queue a file, drain_ready has already marked its path submitted. Activating a license or restoring conversion support cannot retry that file.
Learn more
The watcher records a path as submitted before poll_automations verifies that its target is reachable or that convert accepts it. On subsequent polls, the submitted check skips the path without inspecting why it did not convert. This makes a temporary license or engine failure permanent for that file.
Example: A screenshot arrives while the trial has expired. Its PNG conversion is rejected. After the user activates a license, the screenshot remains unconverted despite continuing polls.
Recommended fix: Separate detection from successful submission. Acknowledge the file only after a job is accepted, and provide a controlled retry path for temporary failures without repeating the error every second.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if let Some(dir) = video_dir() | ||
| .map(|p| p.join("Captures")) | ||
| .filter(|p| p.is_dir()) | ||
| { | ||
| return Some(dir); | ||
| } | ||
| desktop_dir() |
There was a problem hiding this comment.
🔴 Custom macOS recording folder ignored
When macOS uses a custom capture location without a recording-specific key, recording_dir watches Desktop instead. Recordings saved in the configured capture folder never convert.
Learn more
The screenshot rule resolves the macOS com.apple.screencapture location setting in screenshot_location. The recording rule probes separate recording keys, then falls back to a Captures directory or Desktop without checking the screenshot location. A Mac that stores both captures under its configured location consequently watches different folders for the two rules.
Example: com.apple.screencapture location is ~/Work/Captures and none of the recording-specific keys is present. A new screen recording lands in ~/Work/Captures; the recording rule polls Desktop.
Recommended fix: On macOS, fall back to the configured capture location before generic Captures/Desktop paths, while preserving any explicit recording directory override.
Was this helpful? React with 👍 or 👎 to provide feedback.
| for (index, rule) in rules.iter().enumerate() { | ||
| if !rule.is_watched() { | ||
| continue; |
There was a problem hiding this comment.
🟡 Re-enabled rules convert older files
If a file arrives while a rule is off, drain_ready retains its old directory snapshot. Re-enabling the rule treats that existing file as new and converts it.
Learn more
The watcher primes a directory only once in drain_ready. Disabling a rule leaves that snapshot in WatchState. Re-enabling it resumes from the older snapshot, not from the folder contents at enable time.
Example: Disable the screenshot rule, save Screenshot 2.heic, then re-enable it. The next two polls convert the old file even though it was already in the folder at activation.
Recommended fix: Reprime the affected rule when it becomes enabled again. Keep priming per rule so other rules watching the same directory remain unaffected.
Was this helpful? React with 👍 or 👎 to provide feedback.
| for (path, len) in entries { | ||
| if self.submitted.contains(&path) { | ||
| self.seen.insert(path, len); | ||
| continue; | ||
| } |
| fn is_too_broad(dir: &Path) -> bool { | ||
| dir.parent().is_none() || home_dir().is_some_and(|home| same_path(dir, &home)) |
| let copy = rule.copies_to_clipboard(); | ||
| match self.convert(std::slice::from_ref(&path), to, &Options::default(), cx) { |
|
| pub struct WatchState { | ||
| primed: HashSet<PathBuf>, | ||
| seen: HashMap<PathBuf, u64>, | ||
| submitted: HashSet<PathBuf>, |
There was a problem hiding this comment.
Rules interfere with each other
When both default rules watch Desktop, they share seen and submitted. For a new screenshot, the screenshot rule records its size. The recording rule then treats that same observation as stable and marks the nonmatching image submitted, so the screenshot never converts. A new recording can instead be submitted during its first poll, before its size settles.
Keep file observations separate from rule checks, and test both rules together. Confidence: 5/5.
| if let Some(dir) = video_dir() | ||
| .map(|p| p.join("Captures")) | ||
| .filter(|p| p.is_dir()) | ||
| { | ||
| return Some(dir); | ||
| } | ||
| desktop_dir() |
There was a problem hiding this comment.
Custom-folder recordings are missed
If recordings land in a custom screenshot folder and no separate video-location key is set, recording_dir() watches Captures or Desktop instead. It skips the promised screenshot-folder fallback, so those recordings never convert automatically.
Read the screenshot location before falling back to Desktop, while still allowing recordings when screenshots target the clipboard. Confidence: 4/5.
| if this | ||
| .update(cx, |state, cx| state.poll_automations(cx)) |
There was a problem hiding this comment.
watch() runs folder discovery and the full directory scan inside the UI update every second. On macOS, discovery also waits for defaults processes, and drawing rules calls the same lookup through source_line(). A large or slow watched folder can repeatedly stall the window.
Move these checks to background work and let drawing read the latest saved status. Confidence: 5/5.
Knowledge Base Used: Desktop application workflows
| if self.primed.insert(dir.clone()) { | ||
| for (path, len) in &entries { | ||
| self.seen.insert(path.clone(), *len); | ||
| self.submitted.insert(path.clone()); | ||
| } | ||
| continue; |
There was a problem hiding this comment.
Re-enabling converts old files
A folder is snapshotted only once for the app's lifetime. Turning a rule off and back on does not reset primed, so files created while it was off are converted after re-enabling it. That breaks the stated behavior of leaving existing files alone when a rule is turned on.
Take a fresh snapshot when a rule becomes enabled. Confidence: 5/5.
| if self.submitted.contains(&path) { | ||
| self.seen.insert(path, len); | ||
| continue; |
There was a problem hiding this comment.
Reused filenames stop converting
submitted remembers each pathname forever, and the skip branch ignores later changes to that file. After a watched file is deleted and recreated—or replaced with a new capture using the same name—it never converts again until the app restarts.
Remove vanished entries and track file changes so a new file at an old path gets a fresh stability check. This also keeps seen and submitted from growing forever. Confidence: 5/5.
| if matches_rule(rule, &path, &dir) { | ||
| self.submitted.insert(path.clone()); | ||
| if let Some(to) = format_by_id(&rule.to) { | ||
| self.submitted.insert(path.with_extension(to.extension())); | ||
| } | ||
| ready.push((index, path)); |
There was a problem hiding this comment.
drain_ready() marks a file submitted before poll_automations() checks whether it can queue the conversion. If the license blocks work, a document pack is temporarily locked, or the target is unavailable, the file stays skipped forever. No job is created, so Activity offers no retry for that file.
Keep it pending until a job is accepted, with a delay between repeated attempts. Confidence: 5/5.
Knowledge Base Used: Desktop application workflows
| let outcome = match &entry.status { | ||
| Status::Done(outputs) => { | ||
| batch.done += 1; | ||
| if self.automation_copies.remove(&entry.id) { |
There was a problem hiding this comment.
Cancelled jobs never leave automation_copies. The new cleanup runs for success and failure only, so each cancelled copy-enabled automation keeps an unused job ID for the rest of the session.
Remove the ID for every finished job, then use the saved result of that removal to decide whether to copy successful outputs. Confidence: 5/5.
| fn is_too_broad(dir: &Path) -> bool { | ||
| dir.parent().is_none() || home_dir().is_some_and(|home| same_path(dir, &home)) |
There was a problem hiding this comment.
Folder links bypass safeguards
The root-folder check uses the supplied path rather than its resolved destination. An explicit folder such as ~/watch-root that links to / passes this check and gets watched. A link to Desktop with a different name also bypasses is_catch_all_folder(), allowing ordinary photos to convert and copy.
Resolve folder links before applying these checks, and compare shared folders by their actual paths. Confidence: 5/5.
| rule.detail = if copy { | ||
| "copy to clipboard".into() | ||
| } else { | ||
| "save beside original".into() |
There was a problem hiding this comment.
Turning off copying sets detail to “save beside original,” but automated conversions use convert(), which honors the global output_dir. With a custom output folder, the popover tells users the wrong place to find their result.
Either make this rule explicitly save beside the input or describe the configured output location instead. Confidence: 5/5.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Spikk’s feedback, which Leo agreed with: Add files was sending every image to WebP, and automations did not really cover screenshots vs screen recordings (especially when Cmd+Shift+4 copies to the clipboard but recordings still land on the Desktop).
Image defaults
New installs split stills instead of one “Images → WebP” chip:
A file that is already the target (PNG→PNG, JPEG→JPEG) opens Quick convert, same as before.
Existing
settings.tomlfiles are left alone. An older file that only hasimages = "webp"keeps WebP for both kinds. Only a missing file, or a defaults section that never savedimages, gets the new split.First-run’s last step, the Activity chips, and the desktop test skill all say JPEG/PNG now.
Activity defaults: Photos to JPEG, Images to PNG
Automations
Rules are no longer display-only.
/are refused.com.apple.screencapture location, and the watch is skipped iftargetis clipboard (nothing lands on disk). Recordings try a video-location key, then the screenshot folder, then Desktop.Automations: screenshot PNG with copy, recordings MP4, resolved folders
Tests never open the real Desktop; they set
Automation.folder(orCONVT_SCREENSHOT_DIR/CONVT_RECORDING_DIR).Legacy “Exports” placeholders without an explicit watch kind stay idle, so turning this on does not start converting
~/Movies/Exports.PR #68
Based on
main. UI changes stay small (intro copy, resolved path, a checkbox) so they can sit on the redesign. #68 is not merged yet — this should be rebased onto it once it lands.Out of scope
Homebrew install and the HEIC→WebP orientation report from the same note are not in this PR. Orientation is already applied on HEIC decode; changing the default away from WebP may be enough for the case Spikk hit.
Verification
cargo test -p convt-app: 120 passedcargo clippy -p convt-app --all-targets -- -D warnings: cleancargo fmt -p convt-app -- --check: cleanTo show artifacts inline, enable in settings.