[object Object]

← 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

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 →