Add -X delete operation, demo tape tests, and consolidate demos

- Add -X <id|name> to stop, disable, and remove a job's unit files;
  mutually exclusive with all other management options; 13 new tests
- Add demo/test-tapes.sh to verify all VHS tape commands run cleanly;
  wired into pre-commit hook alongside test.sh (combined badge count)
- Rename demo job names to *_home variants to avoid clashing with real
  user jobs; add per-tape preflight cleanup via -X
- Consolidate four demo GIFs into quickstart + all-features + editmode
  screenshot; remove notifications/services tapes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthias Johnson 2026-03-02 01:02:13 -07:00
parent 16404fb596
commit 8e45f7917c
17 changed files with 472 additions and 420 deletions

BIN
demo/all-features.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

193
demo/all-features.tape Normal file
View file

@ -0,0 +1,193 @@
# systab — All Features
# A complete walkthrough: timers, services, status, logs,
# disable/enable, list mode, notifications, and edit mode.
Output demo/all-features.gif
Set Shell bash
Set Width 1200
Set Height 700
Set FontSize 16
Set TypingSpeed 50ms
Sleep 1s
# Clean up any pre-existing jobs with names used in this demo
Hide
Type "for n in healthcheck_home monitor_home backup_home health2_home; do systab -X \"$n\" 2>/dev/null || true; done"
Enter
Sleep 1s
Show
# ── Job Creation ──────────────────────────────────────────────
Hide
Type "./demo/note.sh 'Recurring timer with a name'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -t 'every 5 minutes' -n healthcheck_home -c 'echo health check OK'"
Sleep 500ms
Enter
Sleep 2s
Hide
Type "./demo/note.sh 'One-time reminder in 30 minutes'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -t 'in 30 minutes' -c 'echo reminder: meeting soon'"
Sleep 500ms
Enter
Sleep 2s
Hide
Type "./demo/note.sh 'Persistent service — starts on login, auto-restarts on failure'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -s -n monitor_home -c 'sleep 3600'"
Sleep 500ms
Enter
Sleep 2s
# ── Status ────────────────────────────────────────────────────
Hide
Type "./demo/note.sh 'Status of all jobs and services'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -S"
Sleep 500ms
Enter
Sleep 3s
# ── Logs ──────────────────────────────────────────────────────
Hide
Type "./demo/note.sh 'Viewing logs for a specific job'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -L healthcheck_home"
Sleep 500ms
Enter
Sleep 2s
# ── Disable / Enable ──────────────────────────────────────────
Hide
Type "./demo/note.sh 'Disabling a job by name'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -D healthcheck_home"
Sleep 500ms
Enter
Sleep 1s
Type "systab -S healthcheck_home"
Sleep 500ms
Enter
Sleep 2s
Hide
Type "./demo/note.sh 'Re-enabling the job'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -E healthcheck_home"
Sleep 500ms
Enter
Sleep 2s
# ── Notifications ─────────────────────────────────────────────
Hide
Type "./demo/note.sh 'Add desktop notification with -i (fires on job completion)'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -t 'daily' -n backup_home -c '/home/user/backup.sh' -i"
Sleep 500ms
Enter
Sleep 2s
# ── List mode ─────────────────────────────────────────────────
Hide
Type "./demo/note.sh 'List all jobs in crontab format with -l'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -l"
Sleep 500ms
Enter
Sleep 2s
# ── Edit mode ─────────────────────────────────────────────────
Hide
Type "./demo/note.sh 'Edit mode — manage jobs in your $EDITOR'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "EDITOR=nano systab -e"
Sleep 500ms
Enter
Sleep 3s
# Navigate to end of file in nano and add a new job with flags
Ctrl+V
Sleep 500ms
Down 5
Sleep 500ms
Type "new:n=health2_home,i,e=admin@example.com | hourly | curl -s https://example.com/health"
Sleep 1s
Enter
Sleep 500ms
Ctrl+O
Sleep 500ms
Enter
Sleep 500ms
Ctrl+X
Sleep 3s
# Show updated status
Hide
Type "./demo/note.sh 'Status after edit — new job added with flags'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -S"
Sleep 500ms
Enter
Sleep 3s
# ── Clean ─────────────────────────────────────────────────────
Hide
Type "./demo/note.sh 'Clean up completed one-time timers'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -C"
Sleep 500ms
Enter
Sleep 2s
Sleep 2s

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

BIN
demo/editmode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View file

@ -1,107 +1,39 @@
# systab — Edit Mode with Notification Flags
# Demonstrates the crontab-style editor with :flags syntax.
# systab — Edit Mode Screenshot
# Creates representative jobs, opens edit mode, captures a screenshot.
Output demo/editmode.gif
Set Shell bash
Set Width 1200
Set Height 600
Set Height 500
Set FontSize 16
Set TypingSpeed 50ms
Set TypingSpeed 0
Sleep 1s
# First create some jobs to edit
# Clean up any pre-existing jobs with names used in this demo
Hide
Type "./demo/note.sh 'Creating some jobs to work with'"
Type "for n in healthcheck_home backup_home syncthing_home cleanup_home; do systab -X \"$n\" 2>/dev/null || true; done"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -t 'every 5 minutes' -c 'echo ping'"
# Create representative jobs silently
Type "systab -t 'every 5 minutes' -n healthcheck_home -c 'curl -s https://example.com/health' -i"
Enter
Sleep 2s
Type "systab -t daily -c '/home/user/backup.sh' -i"
Type "systab -t 'daily' -n backup_home -c '/home/user/backup.sh' -i"
Enter
Sleep 2s
# List jobs in crontab format without opening editor
Hide
Type "./demo/note.sh 'Listing jobs in crontab format (-l)'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -l"
Sleep 500ms
Type "systab -s -n syncthing_home -c '/usr/bin/syncthing --no-browser'"
Enter
Sleep 2s
# Open edit mode with EDITOR=nano for visibility
Hide
Type "./demo/note.sh 'Opening edit mode — add notifications, create and modify jobs'"
Type "systab -t 'weekly' -n cleanup_home -c 'find /tmp -mtime +7 -delete'"
Enter
Sleep 500ms
Sleep 2s
Show
Sleep 1s
Type "EDITOR=nano systab -e"
Sleep 500ms
Enter
Sleep 3s
# In nano: navigate to the end of the file
# Ctrl+V = page down in nano, then Down to reach last line
Ctrl+V
Sleep 500ms
Down 5
Sleep 500ms
# Add a new job line with notification flags
Type "new:n=health,i,e=admin@example.com | hourly | curl -s https://example.com/health"
Sleep 1s
Enter
Screenshot demo/editmode.png
Sleep 1s
# Save and exit nano
Ctrl+O
Sleep 500ms
Enter
Sleep 500ms
Ctrl+X
Sleep 3s
# Show the result
Hide
Type "./demo/note.sh 'Changes applied — checking status'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -S | less"
Sleep 500ms
Enter
Sleep 2s
Type "/Job:"
Enter
Sleep 3s
Type "q"
Sleep 1s
# Re-open edit mode to show flags are persisted
Hide
Type "./demo/note.sh 'Re-opening edit mode — flags are persisted'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "EDITOR=nano systab -e"
Sleep 500ms
Enter
Sleep 3s
# Just view and exit without changes
Ctrl+X
Sleep 2s
Sleep 2s

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 KiB

View file

@ -1,96 +0,0 @@
# systab — Notifications
# Shows status-aware desktop and email notifications.
Output demo/notifications.gif
Set Shell bash
Set Width 1200
Set Height 600
Set FontSize 16
Set TypingSpeed 50ms
Sleep 1s
# Desktop notification — success case
Hide
Type "./demo/note.sh 'Desktop notification on success'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -t 'in 1 minute' -c 'echo success' -i"
Sleep 500ms
Enter
Sleep 2s
# Desktop notification — failure case
Hide
Type "./demo/note.sh 'Desktop notification on failure'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -t 'in 1 minute' -c 'exit 1' -i"
Sleep 500ms
Enter
Sleep 2s
# Email notification
Hide
Type "./demo/note.sh 'Email notification via sendmail'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -t 'in 1 minute' -c 'echo test' -m user@example.com"
Sleep 500ms
Enter
Sleep 2s
# Both notifications
Hide
Type "./demo/note.sh 'Both desktop and email notifications'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -t 'every day at 9am' -n backup -c '/home/user/backup.sh' -i -m admin@example.com"
Sleep 500ms
Enter
Sleep 2s
# Show the generated service file to see ExecStopPost lines
Hide
Type "./demo/note.sh 'Inspecting generated service file'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "cat ~/.config/systemd/user/systab_*.service | less"
Sleep 500ms
Enter
Sleep 2s
Type "/ExecStopPost"
Enter
Sleep 3s
Type "q"
Sleep 1s
# Check status
Hide
Type "./demo/note.sh 'Checking status of all jobs'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -S | less"
Sleep 500ms
Enter
Sleep 2s
Type "/Job:"
Enter
Sleep 3s
Type "q"
Sleep 1s
Sleep 2s

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 KiB

After

Width:  |  Height:  |  Size: 1,018 KiB

Before After
Before After

View file

@ -1,5 +1,5 @@
# systab — Quick Start
# Creates a few jobs, checks status, views logs, then cleans up.
# Create a timer, create a service, then disable the service via edit mode.
Output demo/quickstart.gif
Set Shell bash
@ -11,113 +11,87 @@ Set TypingSpeed 50ms
Sleep 1s
# Create a recurring job
# Clean up any pre-existing jobs with names used in this demo
Hide
Type "./demo/note.sh 'Creating a recurring health check (every 5 minutes)'"
Type "for n in healthcheck_home monitor_home; do systab -X \"$n\" 2>/dev/null || true; done"
Enter
Sleep 1s
Show
# Create a recurring timer
Hide
Type "./demo/note.sh 'Create a recurring timer'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -t 'every 5 minutes' -n healthcheck -c 'echo health check OK'"
Type "systab -t 'every 5 minutes' -n healthcheck_home -c 'echo health check OK'"
Sleep 500ms
Enter
Sleep 2s
# Create a one-time job
# Create a persistent service
Hide
Type "./demo/note.sh 'Creating a one-time reminder (30 minutes from now)'"
Type "./demo/note.sh 'Create a persistent service'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -t 'in 30 minutes' -c 'echo reminder: meeting soon'"
Type "systab -s -n monitor_home -c 'sleep 3600'"
Sleep 500ms
Enter
Sleep 2s
# Check status of all jobs
# Both visible in status
Hide
Type "./demo/note.sh 'Checking status of all jobs'"
Type "./demo/note.sh 'Both jobs visible in status'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -S | less"
Type "systab -S"
Sleep 500ms
Enter
Sleep 2s
Type "/Job:"
# Open edit mode and disable the service by commenting out its line
Hide
Type "./demo/note.sh 'Edit mode — disable the monitor service'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "EDITOR=nano systab -e"
Sleep 500ms
Enter
Sleep 3s
Type "q"
Sleep 1s
# View logs
Hide
Type "./demo/note.sh 'Viewing job logs'"
Enter
# In nano: search for "monitor_home", jump to start of that line, prefix with '#'
Ctrl+W
Sleep 500ms
Show
Type "monitor_home"
Enter
Sleep 1s
Type "systab -L | less"
Ctrl+A
Sleep 500ms
Type "#"
Sleep 500ms
Ctrl+O
Sleep 500ms
Enter
Sleep 2s
Type "/Logs for"
Enter
Sleep 500ms
Ctrl+X
Sleep 3s
Type "q"
Sleep 1s
# Disable a job (uses the first job ID from status)
# Verify the service is now disabled
Hide
Type "./demo/note.sh 'Disabling a job'"
Type "./demo/note.sh 'Monitor service is now disabled'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -D healthcheck"
Sleep 500ms
Enter
Sleep 2s
# Show it's disabled
Hide
Type "./demo/note.sh 'Verifying job is disabled'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -S | less"
Sleep 500ms
Enter
Sleep 2s
Type "/Disabled"
Enter
Sleep 3s
Type "q"
Sleep 1s
# Enable it
Hide
Type "./demo/note.sh 'Enabling the disabled job'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -E healthcheck"
Sleep 500ms
Enter
Sleep 2s
# Clean up completed one-time jobs
Hide
Type "./demo/note.sh 'Cleaning up completed one-time jobs'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -C"
Type "systab -S"
Sleep 500ms
Enter
Sleep 2s

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

View file

@ -1,159 +0,0 @@
# systab — Persistent Services
# Creates a managed service (no timer), checks status, disables/enables,
# inspects via edit mode, then cleans up.
Output demo/services.gif
Set Shell bash
Set Width 1200
Set Height 600
Set FontSize 16
Set TypingSpeed 50ms
Sleep 1s
# Create a persistent service
Hide
Type "./demo/note.sh 'Creating a persistent service job (runs on login, auto-restarts)'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -s -n monitor -c 'sleep 3600'"
Sleep 500ms
Enter
Sleep 2s
# Check status — should show Type: Service, Active (running)
Hide
Type "./demo/note.sh 'Checking status — service is running'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -S monitor"
Sleep 500ms
Enter
Sleep 2s
# Inspect the generated unit file
Hide
Type "./demo/note.sh 'Inspecting the generated unit file'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "cat ~/.config/systemd/user/systab_*.service | less"
Sleep 500ms
Enter
Sleep 2s
Type "/SYSTAB_TYPE"
Enter
Sleep 2s
Type "q"
Sleep 1s
# View logs
Hide
Type "./demo/note.sh 'Viewing service logs'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -L monitor"
Sleep 500ms
Enter
Sleep 2s
# Disable the service
Hide
Type "./demo/note.sh 'Disabling the service (stops it)'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -D monitor"
Sleep 500ms
Enter
Sleep 2s
# Verify disabled
Hide
Type "./demo/note.sh 'Verifying service is disabled/stopped'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -S monitor"
Sleep 500ms
Enter
Sleep 2s
# Re-enable
Hide
Type "./demo/note.sh 'Re-enabling the service'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -E monitor"
Sleep 500ms
Enter
Sleep 2s
# Open edit mode — service appears with 'service' in schedule column
Hide
Type "./demo/note.sh 'Service jobs appear in edit mode with schedule = service'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "EDITOR=nano systab -e"
Sleep 500ms
Enter
Sleep 3s
# Just view and exit
Ctrl+X
Sleep 2s
# Also show a service created via edit mode
Hide
Type "./demo/note.sh 'Creating a service via edit mode: new:s | service | cmd'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "EDITOR=nano systab -e"
Sleep 500ms
Enter
Sleep 3s
Ctrl+V
Sleep 500ms
Down 5
Sleep 500ms
Type "new:s,n=watcher | service | sleep 7200"
Sleep 1s
Enter
Ctrl+O
Sleep 500ms
Enter
Sleep 500ms
Ctrl+X
Sleep 3s
# Final status
Hide
Type "./demo/note.sh 'Both services visible in status'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -S"
Sleep 500ms
Enter
Sleep 3s
Sleep 2s

119
demo/test-tapes.sh Executable file
View file

@ -0,0 +1,119 @@
#!/usr/bin/env bash
# test-tapes.sh — Verify all systab commands in VHS tape files run correctly.
# Greps Type "systab ..." and Type "EDITOR=... systab ..." lines from *.tape
# files and runs them in order per tape, reporting pass/fail.
#
# Run from the project root: ./demo/test-tapes.sh
set -uo pipefail
# Make ./systab callable as 'systab', matching how tapes reference it
PATH="$PWD:$PATH"
export PATH
TAPE_DIR="demo"
SYSTEMD_USER_DIR="${HOME}/.config/systemd/user"
passed=0
failed=0
total=0
tape_job_ids=()
if [[ -t 1 ]]; then
GREEN=$'\033[32m' RED=$'\033[31m' BOLD=$'\033[1m' RESET=$'\033[0m'
else
GREEN="" RED="" BOLD="" RESET=""
fi
pass() {
echo "${GREEN}[PASS]${RESET} $1"
passed=$((passed + 1))
total=$((total + 1))
}
fail() {
echo "${RED}[FAIL]${RESET} $1$2"
failed=$((failed + 1))
total=$((total + 1))
}
# Stop, disable, and remove all unit files created during tape tests
cleanup_tape_jobs() {
[[ ${#tape_job_ids[@]} -eq 0 ]] && return
for id in "${tape_job_ids[@]}"; do
[[ -z "$id" ]] && continue
for ext in service timer; do
local f="$SYSTEMD_USER_DIR/systab_${id}.${ext}"
[[ -f "$f" ]] || continue
systemctl --user stop "systab_${id}.${ext}" 2>/dev/null || true
systemctl --user disable "systab_${id}.${ext}" 2>/dev/null || true
rm -f "$f"
done
done
systemctl --user daemon-reload 2>/dev/null || true
tape_job_ids=()
}
trap cleanup_tape_jobs EXIT
# Collect any job IDs from command output into tape_job_ids
collect_ids() {
local id
while IFS= read -r id; do
[[ -n "$id" ]] && tape_job_ids+=("$id")
done < <(sed -n 's/^\(Job\|Service\) created: \([0-9a-f]\{6\}\).*$/\2/p' <<< "$1")
}
# Prepare a tape command for test execution:
# - EDITOR=nano → EDITOR=cat (non-interactive; cat exits 0, no file changes)
# - strip trailing " | less" (we capture stdout directly)
normalize() {
local cmd="$1"
cmd="${cmd//EDITOR=nano/EDITOR=cat}"
cmd="${cmd% | less}"
echo "$cmd"
}
# Run all systab commands from one tape file in order
run_tape() {
local tape="$1"
local tape_name
tape_name=$(basename "$tape" .tape)
echo ""
echo "${BOLD}=== $tape_name ===${RESET}"
cleanup_tape_jobs # each tape starts with a clean slate
local raw cmd output exit_code
while IFS= read -r raw; do
cmd=$(normalize "$raw")
exit_code=0
# Pipe /dev/null for commands that read stdin interactively
if [[ "$cmd" == *"systab -C"* ]] || [[ "$cmd" == *"systab -e"* ]]; then
output=$(eval "$cmd" < /dev/null 2>&1) || exit_code=$?
else
output=$(eval "$cmd" 2>&1) || exit_code=$?
fi
collect_ids "$output"
if [[ $exit_code -eq 0 ]]; then
pass "$tape_name: $raw"
else
fail "$tape_name: $raw" "exit $exit_code: ${output:0:120}"
fi
done < <(grep -E '^Type "(systab |EDITOR=)' "$tape" | sed 's/^Type "//; s/"$//')
}
echo "${BOLD}Testing systab commands from VHS tape files...${RESET}"
for tape in "$TAPE_DIR"/*.tape; do
run_tape "$tape"
done
echo ""
echo "---"
echo "${BOLD}${total} tape commands: ${GREEN}${passed} passed${RESET}, ${RED}${failed} failed${RESET}"
[[ $failed -eq 0 ]]