#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' Usage: tools/watch_prompt_image_folder.sh --folder DIR --target TMUX_TARGET [options] Watch a folder for prompt/image pairs and notify a selected Byobu/tmux Codex pane using tmux send-keys. Prompt and image files are paired by basename: atlas_case_001.txt + atlas_case_001.png atlas_case_002.prompt + atlas_case_002.jpg Required: --folder DIR Folder to watch. --target TARGET tmux target pane, for example session:1.0. Options: --notes FILE Notes/output file to mention in the Codex message. Default: DIR/prompt-learning.md --state FILE Seen-state file. Default: DIR/.sxcp_watch_seen --once Scan once and exit. --dry-run Print tmux send-keys commands instead of sending them. --poll-interval SEC Poll/fallback interval in seconds. Default: 2 --stable-delay SEC Seconds to wait before accepting a new image. Default: 1 --prompt-exts CSV Prompt extensions. Default: txt,prompt --image-exts CSV Image extensions. Default: png,jpg,jpeg,webp -h, --help Show this help. Inside Byobu/tmux, get the current target with: tmux display-message -p '#S:#I.#P' EOF } folder="" target="" notes="" state="" once=0 dry_run=0 poll_interval=2 stable_delay=1 prompt_exts_csv="txt,prompt" image_exts_csv="png,jpg,jpeg,webp" while [[ $# -gt 0 ]]; do case "$1" in --folder) folder="${2:-}" shift 2 ;; --target) target="${2:-}" shift 2 ;; --notes) notes="${2:-}" shift 2 ;; --state) state="${2:-}" shift 2 ;; --once) once=1 shift ;; --dry-run) dry_run=1 shift ;; --poll-interval) poll_interval="${2:-}" shift 2 ;; --stable-delay) stable_delay="${2:-}" shift 2 ;; --prompt-exts) prompt_exts_csv="${2:-}" shift 2 ;; --image-exts) image_exts_csv="${2:-}" shift 2 ;; -h|--help) usage exit 0 ;; *) echo "unknown argument: $1" >&2 usage >&2 exit 2 ;; esac done if [[ -z "$folder" || -z "$target" ]]; then echo "--folder and --target are required" >&2 usage >&2 exit 2 fi if [[ ! -d "$folder" ]]; then echo "folder does not exist: $folder" >&2 exit 2 fi folder="$(cd "$folder" && pwd -P)" if [[ -z "$notes" ]]; then notes="$folder/prompt-learning.md" fi if [[ -z "$state" ]]; then state="$folder/.sxcp_watch_seen" fi if [[ "$notes" != /* ]]; then notes="$PWD/$notes" fi if [[ "$state" != /* ]]; then state="$PWD/$state" fi mkdir -p "$(dirname "$state")" touch "$state" IFS=',' read -r -a prompt_exts <<< "$prompt_exts_csv" IFS=',' read -r -a image_exts <<< "$image_exts_csv" shell_quote() { printf '%q' "$1" } state_key() { local prompt_path="$1" local image_path="$2" printf '%s | %s\n' "$prompt_path" "$image_path" } is_seen() { local key="$1" grep -Fxq -- "$key" "$state" } mark_seen() { local key="$1" printf '%s\n' "$key" >> "$state" } file_size() { wc -c < "$1" | tr -d '[:space:]' } wait_for_stable_image() { local image_path="$1" if [[ "$dry_run" -eq 1 || "$stable_delay" == "0" ]]; then return 0 fi local before local after before="$(file_size "$image_path")" sleep "$stable_delay" after="$(file_size "$image_path")" [[ "$before" == "$after" ]] } find_image_for_prompt() { local prompt_path="$1" local filename local stem local ext local candidate filename="$(basename "$prompt_path")" stem="${filename%.*}" for ext in "${image_exts[@]}"; do candidate="$folder/$stem.$ext" if [[ -f "$candidate" ]]; then printf '%s\n' "$candidate" return 0 fi candidate="$folder/$stem.${ext^^}" if [[ -f "$candidate" ]]; then printf '%s\n' "$candidate" return 0 fi done return 1 } notify_codex() { local prompt_path="$1" local image_path="$2" local message message="New atlas sample ready: prompt=$prompt_path image=$image_path. Analyze it and append prompt-learning notes to $notes." if [[ "$dry_run" -eq 1 ]]; then printf 'tmux send-keys -t %s %s Enter\n' "$(shell_quote "$target")" "$(shell_quote "$message")" return 0 fi tmux send-keys -t "$target" "$message" Enter } scan_once() { local notified=0 local ext local prompt_path local image_path local key for ext in "${prompt_exts[@]}"; do while IFS= read -r -d '' prompt_path; do if ! image_path="$(find_image_for_prompt "$prompt_path")"; then continue fi key="$(state_key "$prompt_path" "$image_path")" if is_seen "$key"; then continue fi if ! wait_for_stable_image "$image_path"; then continue fi notify_codex "$prompt_path" "$image_path" mark_seen "$key" notified=$((notified + 1)) done < <(find "$folder" -maxdepth 1 -type f \( -iname "*.$ext" \) -print0) done if [[ "$notified" -eq 0 && "$once" -eq 1 ]]; then echo "no new prompt/image pairs in $folder" fi return 0 } if [[ "$once" -eq 1 ]]; then scan_once exit 0 fi scan_once if command -v inotifywait >/dev/null 2>&1; then while inotifywait -qq -e close_write,create,moved_to "$folder"; do scan_once done else echo "inotifywait not found; polling $folder every $poll_interval seconds" >&2 while true; do sleep "$poll_interval" scan_once done fi