← back to Desktop Sorter
sort-desktop.sh
95 lines
#!/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 [ "${MANIFEST:-0}" = "1" ]; then
# Emit NUL-delimited src + final target for an external parallel driver.
printf '%s\0%s\0' "$DESKTOP/$item" "$target"
elif [ "$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