← back to Desktop Sorter
desktop-sorter: 3-day Desktop sweep into category folders + daily launchd job
f1d8959eaf232a63cee94ae15b0270fa9b693a95 · 2026-06-26 13:08:23 -0700 · Steve Abrams
Files touched
A .gitignoreA README.mdA sort-desktop.sh
Diff
commit f1d8959eaf232a63cee94ae15b0270fa9b693a95
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 26 13:08:23 2026 -0700
desktop-sorter: 3-day Desktop sweep into category folders + daily launchd job
---
.gitignore | 9 ++++++
README.md | 31 ++++++++++++++++++++
sort-desktop.sh | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 131 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..909502d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+logs/
+*.log
+.DS_Store
+node_modules/
+.env*
+tmp/
+dist/
+build/
+.next/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b182aca
--- /dev/null
+++ b/README.md
@@ -0,0 +1,31 @@
+# desktop-sorter
+
+Keeps loose items on the Desktop for **3 days**, then files them into category
+subfolders by type. Runs daily at **9:00 AM** via launchd.
+
+## Rules
+- **Age:** an item is swept once its last-modified time (mtime) is ≥ 3 days old.
+ Anything you touched in the last 3 days stays on the Desktop.
+- **Always excluded:** `.app` bundles, symlinks/aliases, dotfiles, and the
+ category buckets themselves.
+- **Categories (Desktop subfolders):**
+ `Images Videos Audio Documents Data Web Scripts Folders Misc`
+- **Reversible:** every move is appended to `logs/sort-YYYY-MM-DD.log`
+ (`<file> -> <Category>/`), so any move can be undone by reading the log.
+
+## Usage
+```sh
+DRY_RUN=1 ./sort-desktop.sh # preview, moves nothing
+./sort-desktop.sh # live run
+AGE_DAYS=7 ./sort-desktop.sh # override the 3-day threshold
+```
+
+## launchd
+- Job: `com.steve.desktop-sorter`
+- Plist: `~/Library/LaunchAgents/com.steve.desktop-sorter.plist`
+- Reload after edits:
+ ```sh
+ launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.steve.desktop-sorter.plist 2>/dev/null
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.steve.desktop-sorter.plist
+ ```
+- Run now: `launchctl kickstart -k gui/$(id -u)/com.steve.desktop-sorter`
diff --git a/sort-desktop.sh b/sort-desktop.sh
new file mode 100755
index 0000000..bdcf304
--- /dev/null
+++ b/sort-desktop.sh
@@ -0,0 +1,91 @@
+#!/bin/bash
+# Desktop sorter — keep loose items on the Desktop for 3 days, then file them
+# into category subfolders by type. Reversible (every move is logged).
+#
+# Excludes ALWAYS: .app bundles, symlinks/aliases, dotfiles, and the category
+# folders themselves (so it never re-sweeps its own buckets).
+#
+# Usage:
+# ./sort-desktop.sh # live run
+# DRY_RUN=1 ./sort-desktop.sh # preview only, moves nothing
+# AGE_DAYS=7 ./sort-desktop.sh # override the 3-day threshold
+set -euo pipefail
+
+DESKTOP="$HOME/Desktop"
+LOG_DIR="$HOME/Projects/desktop-sorter/logs"
+mkdir -p "$LOG_DIR"
+LOG="$LOG_DIR/sort-$(date +%Y-%m-%d).log"
+
+DRY_RUN="${DRY_RUN:-0}"
+AGE_DAYS="${AGE_DAYS:-3}"
+CUTOFF=$(( $(date +%s) - AGE_DAYS * 86400 ))
+
+# Category buckets — also the exclusion set (never re-sweep our own folders).
+CATEGORIES=(Images Videos Audio Documents Data Web Scripts Folders Misc)
+
+is_category() {
+ local name="$1" c
+ for c in "${CATEGORIES[@]}"; do [ "$name" = "$c" ] && return 0; done
+ return 1
+}
+
+category_for_ext() {
+ case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
+ png|jpg|jpeg|gif|heic|webp|tiff|tif|bmp|svg) echo Images ;;
+ mov|mp4|m4v|avi|mkv|webm) echo Videos ;;
+ m4a|aifc|aiff|mp3|wav|aac|flac) echo Audio ;;
+ md|txt|pdf|csv|tsv|xlsx|xls|doc|docx|ppt|pptx|rtf|pages|numbers|key) echo Documents ;;
+ json|ics|xml|yaml|yml|plist|bak|log) echo Data ;;
+ html|htm|css|js|mjs) echo Web ;;
+ command|sh|zsh|py|rb|pl|bash) echo Scripts ;;
+ *) echo Misc ;;
+ esac
+}
+
+moved=0
+shopt -s nullglob
+cd "$DESKTOP"
+
+for item in *; do
+ case "$item" in .*) continue ;; esac # skip dotfiles (belt + suspenders)
+ [ -L "$item" ] && continue # never touch symlinks/aliases
+ case "$item" in *.app) continue ;; esac # never touch app bundles
+ is_category "$item" && continue # skip our own buckets
+
+ mtime=$(stat -f %m "$item")
+ [ "$mtime" -ge "$CUTOFF" ] && continue # younger than AGE_DAYS → keep on Desktop
+
+ if [ -d "$item" ]; then
+ dest="Folders"
+ elif [ "$item" = "${item##*.}" ]; then
+ dest="Misc" # no extension
+ else
+ dest="$(category_for_ext "${item##*.}")"
+ fi
+
+ mkdir -p "$DESKTOP/$dest"
+ target="$DESKTOP/$dest/$item"
+ if [ -e "$target" ]; then # collision-safe rename
+ ts=$(date +%H%M%S)
+ if [ "$item" = "${item##*.}" ] || [ -d "$item" ]; then
+ target="$DESKTOP/$dest/${item}-${ts}"
+ else
+ target="$DESKTOP/$dest/${item%.*}-${ts}.${item##*.}"
+ fi
+ fi
+
+ if [ "$DRY_RUN" = "1" ]; then
+ printf '[DRY] %s -> %s/\n' "$item" "$dest"
+ else
+ mv "$item" "$target"
+ printf '%s %s -> %s/\n' "$(date '+%F %T')" "$item" "$dest" >> "$LOG"
+ moved=$((moved + 1))
+ fi
+done
+
+if [ "$DRY_RUN" = "1" ]; then
+ printf '\nDry run complete — nothing moved (AGE_DAYS=%s).\n' "$AGE_DAYS"
+else
+ printf '%s Done. Moved %s item(s) (AGE_DAYS=%s).\n' "$(date '+%F %T')" "$moved" "$AGE_DAYS" >> "$LOG"
+ printf 'Moved %s item(s). Log: %s\n' "$moved" "$LOG"
+fi
(oldest)
·
back to Desktop Sorter
·
Add MANIFEST mode for parallel first-sweep driver 9107482 →