-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathwatchthis.sh
More file actions
executable file
·128 lines (112 loc) · 4.65 KB
/
Copy pathwatchthis.sh
File metadata and controls
executable file
·128 lines (112 loc) · 4.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#!/bin/bash
#
# watchthis.sh — the harness.
#
# Triggered by launchd when a file lands in WATCH_DIR. Waits for the file to
# finish writing, claims it into a per-run working dir, finalizes that dir, then
# calls your HANDLER. You should not need to edit this file: put your pipeline in
# the handler (see watchthis.conf and examples/).
#
# Pattern: settle-wait -> claim (processing/) -> finalize (done/) -> handler -> notify.
#
# Part of termpaper.dev — terminal tools for managing Claude Code.
set -uo pipefail
# --- locate self + load config -------------------------------------------------
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONF="${WATCHTHIS_CONF:-$SELF_DIR/watchthis.conf}"
# shellcheck disable=SC1090
[ -r "$CONF" ] && . "$CONF" || { echo "watchthis: cannot read config at $CONF" >&2; exit 1; }
# launchd provides no HOME/PATH. Pin them. (HOME is needed before the conf
# expands $HOME, so we set a fallback first, then re-source if it changed.)
export HOME="${HOME:-/Users/$(whoami)}"
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
ROOT="$HOME/$NAME"
INBOX="$WATCH_DIR"
PROC="$ROOT/processing"
DONE="$ROOT/done"
LOGDIR="$ROOT/logs"
LOCKDIR="$ROOT/.lock.d"
mkdir -p "$INBOX" "$PROC" "$DONE" "$LOGDIR"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOGDIR/process.log"; }
notify() {
# $1 = banner text. Optional $2 = phrase to SPEAK aloud.
# Speak synchronously: a backgrounded `say` gets killed when this launchd job
# exits (process-group teardown) before it makes a sound. Foreground finishes
# speaking first. macOS gates osascript banners unreliably, so voice is the
# dependable channel for the high-signal events.
/usr/bin/osascript -e "display notification \"$1\" with title \"$NAME\" sound name \"Glass\"" 2>/dev/null || true
[ -n "${2:-}" ] && /usr/bin/say "$2" >/dev/null 2>&1 || true
}
# Single-instance lock. mkdir is atomic; WatchPaths can fire repeatedly during a
# copy, and a long handler must not overlap with the next trigger.
if ! mkdir "$LOCKDIR" 2>/dev/null; then
log "another run holds the lock; exiting"
exit 0
fi
trap 'rmdir "$LOCKDIR" 2>/dev/null' EXIT
# Wait until a file's size has stopped changing (finished writing/copying).
wait_for_settle() {
local f="$1" last=-1 cur
for _ in $(seq 1 "${SETTLE_TRIES:-60}"); do
cur=$(stat -f%z "$f" 2>/dev/null || echo 0)
if [ "$cur" = "$last" ] && [ "$cur" -gt 0 ]; then return 0; fi
last="$cur"; sleep "${SETTLE_SECS:-2}"
done
return 0
}
# Collect matching files in the inbox. Split FILE_GLOBS into literal patterns
# with globbing OFF (so the patterns themselves don't expand against the cwd and
# vanish under nullglob), then apply each pattern to the inbox with nullglob ON.
shopt -s nullglob
files=()
set -f
patterns=( $FILE_GLOBS )
set +f
for g in "${patterns[@]}"; do
for f in "$INBOX"/$g; do [ -e "$f" ] && files+=("$f"); done
done
[ ${#files[@]} -eq 0 ] && { log "no matching files in inbox; exiting"; exit 0; }
for src_in in "${files[@]}"; do
[ -e "$src_in" ] || continue
base="$(basename "$src_in")"
log "found: $base"
wait_for_settle "$src_in"
# Claim it into processing/ so the watcher won't re-trigger on it.
stamp="$(date '+%Y%m%d-%H%M%S')"
slug="$(echo "${base%.*}" | tr ' :' '__' | tr -cd '[:alnum:]_-' | cut -c1-40)"
work="$PROC/${stamp}_${slug}"
mkdir -p "$work"
ext="${base##*.}"
if ! mv "$src_in" "$work/source.$ext" 2>/dev/null; then
log "could not claim $base"; rmdir "$work" 2>/dev/null; continue
fi
log "claimed -> $work"
notify "Started: $base" "$NOTIFY_START"
# Finalize the run into done/ BEFORE the handler runs, so any path the handler
# hands off (to a reviewer, an inbox, etc.) points at where artifacts actually
# live. Moving after handoff is the classic bug: the handoff references an
# already-empty processing/ path.
finalwork="$DONE/${stamp}_${slug}"
if mv "$work" "$finalwork" 2>/dev/null; then
work="$finalwork"
log "finalized -> $work"
else
log "could not finalize move to done/; referencing in-place $work"
fi
src="$work/source.$ext"
# Hand off to the user's pipeline. The handler does the real work and decides
# what to do with the output. It gets the source file and the run dir.
if [ -x "$HANDLER" ]; then
log "handler: $HANDLER"
if ! "$HANDLER" "$src" "$work" >> "$LOGDIR/process.log" 2>&1; then
log "handler exited non-zero (run still completed)"
fi
elif [ -f "$HANDLER" ]; then
log "handler not executable: $HANDLER (chmod +x it)"
else
log "no handler at $HANDLER — set HANDLER in watchthis.conf (see examples/)"
fi
notify "Done: $base" "$NOTIFY_DONE"
log "done: $base"
done
log "run complete"