diff --git a/.githooks/pre-commit b/.githooks/pre-commit index c56f7af..39e69cc 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -27,24 +27,11 @@ fi echo "Running tests..." if output=$(./test.sh 2>&1); then echo "$output" - test_count=$(grep -oP '\d+(?= passed)' <<< "$output" | tail -1) + count=$(grep -oP '\d+ passed' <<< "$output" | tail -1) + write_badge "${count:-passing}" "brightgreen" else echo "$output" write_badge "failing" "red" echo "Tests failed — commit blocked." exit 1 fi - -echo "Running demo tape tests..." -if tape_output=$(./demo/test-tapes.sh 2>&1); then - echo "$tape_output" - tape_count=$(grep -oP '\d+(?= passed)' <<< "$tape_output" | tail -1) -else - echo "$tape_output" - write_badge "failing" "red" - echo "Demo tape tests failed — commit blocked." - exit 1 -fi - -total_count=$(( ${test_count:-0} + ${tape_count:-0} )) -write_badge "${total_count} passed" "brightgreen" diff --git a/README.md b/README.md index 80d067f..1ba3a74 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,18 @@ Because you want to use systemd, but miss the ease of ~crontab~`systab -e`! - 📋 access the logs of any job - 💪 enable and disable timers and services -

Edit mode

- - - + + + + - + + +
Quick start demoAll features demoQuick start demoEdit mode demoNotifications demoServices demo
Quick startAll featuresEdit modeNotificationsServices
@@ -182,7 +184,7 @@ systab -C ### Job IDs and names -Each job gets a 6-character hex ID (e.g., `a1b2c3`) displayed on creation and in status output. You can also assign a human-readable name with `-n` at creation time. Names can be used interchangeably with hex IDs in `-D`, `-E`, `-X`, `-R`, `-S`, and `-L`. Names must be unique and cannot contain whitespace, pipes, or colons. +Each job gets a 6-character hex ID (e.g., `a1b2c3`) displayed on creation and in status output. You can also assign a human-readable name with `-n` at creation time. Names can be used interchangeably with hex IDs in `-D`, `-E`, `-S`, and `-L`. Names must be unique and cannot contain whitespace, pipes, or colons. ## How it works @@ -208,8 +210,6 @@ Job Creation: Management (accept hex ID or name): -D Disable a job -E Enable a disabled job - -X Delete a job (stop, disable, and remove unit files) - -R Restart a job (resets timer countdown / restarts service process) -e Edit jobs in crontab-like format -l Print jobs in crontab-like format to stdout -L [id|name] [filter] List job logs (optionally for a specific job and/or filtered) @@ -220,8 +220,7 @@ Management (accept hex ID or name): ## Future feature ideas -- [x] `-R` flag to restart / reload -- [x] `-X` flag to delete +- [ ] `-R` flag to restart / reload ## License @@ -239,15 +238,6 @@ After cloning, enable the pre-commit hook (runs ShellCheck + tests): git config core.hooksPath .githooks ``` -Common tasks via [`just`](https://github.com/casey/just): - -```bash -just check # lint + unit tests + tape tests -just test # unit tests only -just lint # ShellCheck only -just record # re-record demo GIFs with VHS -``` - ## FAQ diff --git a/badges/tests.json b/badges/tests.json index b571791..3e4522a 100644 --- a/badges/tests.json +++ b/badges/tests.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "tests", - "message": "131 passed", + "message": "88 passed", "color": "brightgreen" } diff --git a/demo/all-features.gif b/demo/all-features.gif deleted file mode 100644 index 6bb5ab0..0000000 Binary files a/demo/all-features.gif and /dev/null differ diff --git a/demo/all-features.tape b/demo/all-features.tape deleted file mode 100644 index da64117..0000000 --- a/demo/all-features.tape +++ /dev/null @@ -1,206 +0,0 @@ -# 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 "systab -X healthcheck_home 2>/dev/null; systab -X monitor_home 2>/dev/null; systab -X backup_home 2>/dev/null; systab -X health2_home 2>/dev/null; true" -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 'uptime'" -Sleep 500ms -Enter -Sleep 2s - -Hide -Type "./demo/note.sh 'One-time reminder with desktop notification'" -Enter -Sleep 500ms -Show -Sleep 1s -Type "systab -t 'in 30 minutes' -c 'echo reminder: meeting soon' -i" -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 'ping -c 1 localhost'" -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 - -# ── Restart ─────────────────────────────────────────────────── - -Hide -Type "./demo/note.sh 'Restart a job — resets the timer countdown'" -Enter -Sleep 500ms -Show -Sleep 1s -Type "systab -R 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 diff --git a/demo/editmode.gif b/demo/editmode.gif new file mode 100644 index 0000000..5b9a644 Binary files /dev/null and b/demo/editmode.gif differ diff --git a/demo/editmode.png b/demo/editmode.png deleted file mode 100644 index df075e8..0000000 Binary files a/demo/editmode.png and /dev/null differ diff --git a/demo/editmode.tape b/demo/editmode.tape new file mode 100644 index 0000000..8ddf4e9 --- /dev/null +++ b/demo/editmode.tape @@ -0,0 +1,107 @@ +# systab — Edit Mode with Notification Flags +# Demonstrates the crontab-style editor with :flags syntax. + +Output demo/editmode.gif +Set Shell bash +Set Width 1200 +Set Height 600 +Set FontSize 16 + +Set TypingSpeed 50ms + +Sleep 1s + +# First create some jobs to edit +Hide +Type "./demo/note.sh 'Creating some jobs to work with'" +Enter +Sleep 500ms +Show +Sleep 1s +Type "systab -t 'every 5 minutes' -c 'echo ping'" +Enter +Sleep 2s +Type "systab -t daily -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 +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'" +Enter +Sleep 500ms +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 + +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 diff --git a/demo/notifications.gif b/demo/notifications.gif new file mode 100644 index 0000000..0c5d060 Binary files /dev/null and b/demo/notifications.gif differ diff --git a/demo/notifications.tape b/demo/notifications.tape new file mode 100644 index 0000000..bd14cc6 --- /dev/null +++ b/demo/notifications.tape @@ -0,0 +1,96 @@ +# 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 diff --git a/demo/quickstart.gif b/demo/quickstart.gif index 5f27161..f815788 100644 Binary files a/demo/quickstart.gif and b/demo/quickstart.gif differ diff --git a/demo/quickstart.tape b/demo/quickstart.tape index 1fc4f12..05f3bd5 100644 --- a/demo/quickstart.tape +++ b/demo/quickstart.tape @@ -1,5 +1,5 @@ # systab — Quick Start -# Create a timer, create a service, then use edit mode to add a new job. +# Creates a few jobs, checks status, views logs, then cleans up. Output demo/quickstart.gif Set Shell bash @@ -11,87 +11,113 @@ Set TypingSpeed 50ms Sleep 1s -# Clean up any pre-existing jobs with names used in this demo +# Create a recurring job Hide -Type "systab -X healthcheck_home 2>/dev/null; systab -X monitor_home 2>/dev/null; systab -X diskcheck_home 2>/dev/null; true" -Enter -Sleep 1s -Show - -# Create a recurring timer -Hide -Type "./demo/note.sh 'Create a recurring timer'" +Type "./demo/note.sh 'Creating a recurring health check (every 5 minutes)'" Enter Sleep 500ms Show Sleep 1s -Type "systab -t 'every 5 minutes' -n healthcheck_home -c 'uptime'" +Type "systab -t 'every 5 minutes' -n healthcheck -c 'echo health check OK'" Sleep 500ms Enter Sleep 2s -# Create a persistent service +# Create a one-time job Hide -Type "./demo/note.sh 'Create a persistent service'" +Type "./demo/note.sh 'Creating a one-time reminder (30 minutes from now)'" Enter Sleep 500ms Show Sleep 1s -Type "systab -s -n monitor_home -c 'ping -c 1 localhost'" +Type "systab -t 'in 30 minutes' -c 'echo reminder: meeting soon'" Sleep 500ms Enter Sleep 2s -# Both visible in status +# Check status of all jobs Hide -Type "./demo/note.sh 'Both jobs visible in status'" +Type "./demo/note.sh 'Checking status of all jobs'" Enter Sleep 500ms Show Sleep 1s -Type "systab -S" +Type "systab -S | less" Sleep 500ms Enter Sleep 2s - -# Open edit mode: screenshot then add a new job -Hide -Type "./demo/note.sh 'Edit mode — add a new job'" -Enter -Sleep 500ms -Show -Sleep 1s -Type "EDITOR=nano systab -e" -Sleep 500ms +Type "/Job:" Enter Sleep 3s +Type "q" +Sleep 1s -Screenshot demo/editmode.png - -# Navigate to end of file in nano and add a new job -Ctrl+V -Sleep 500ms -Down 5 -Sleep 500ms -Type "new:n=diskcheck_home | daily | df -h" -Enter -Sleep 500ms - -Ctrl+O -Sleep 500ms -Enter -Sleep 500ms -Ctrl+X -Sleep 3s - -# Show updated status +# View logs Hide -Type "./demo/note.sh 'Status after edit — new job added'" +Type "./demo/note.sh 'Viewing job logs'" Enter Sleep 500ms Show Sleep 1s -Type "systab -S" +Type "systab -L | less" +Sleep 500ms +Enter +Sleep 2s +Type "/Logs for" +Enter +Sleep 3s +Type "q" +Sleep 1s + +# Disable a job (uses the first job ID from status) +Hide +Type "./demo/note.sh 'Disabling a job'" +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" Sleep 500ms Enter Sleep 2s diff --git a/demo/services.gif b/demo/services.gif new file mode 100644 index 0000000..c4fd41e Binary files /dev/null and b/demo/services.gif differ diff --git a/demo/services.tape b/demo/services.tape new file mode 100644 index 0000000..8d57873 --- /dev/null +++ b/demo/services.tape @@ -0,0 +1,159 @@ +# 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 diff --git a/demo/test-tapes.sh b/demo/test-tapes.sh deleted file mode 100755 index c605bbf..0000000 --- a/demo/test-tapes.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash -# test-tapes.sh — Verify all systab commands in VHS tape files run correctly. -# Reads each tape line-by-line, tracking Hide/Show blocks. Only runs -# Type "systab ..." and Type "EDITOR=..." lines that are in visible (Show) blocks. -# -# 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 # clean up IDs from previous tape - - # Pre-clean named jobs from this tape to handle leftovers from failed runs - local name - while IFS= read -r name; do - systab -X "$name" 2>/dev/null || true - done < <(grep -E '^Type "systab .*-n ' "$tape" | grep -oP '(?<=-n )\w+') - - local line raw cmd output exit_code in_hide=false - while IFS= read -r line; do - # Track Hide/Show blocks — skip commands in hidden sections - [[ "$line" == "Hide" ]] && { in_hide=true; continue; } - [[ "$line" == "Show" ]] && { in_hide=false; continue; } - $in_hide && continue - - # Only process visible Type lines with systab or EDITOR= commands - [[ "$line" == 'Type "systab '* ]] || [[ "$line" == 'Type "EDITOR='* ]] || continue - - raw="${line#Type \"}" - raw="${raw%\"}" - 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 < "$tape" -} - -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 ]] diff --git a/justfile b/justfile deleted file mode 100644 index 7cb9146..0000000 --- a/justfile +++ /dev/null @@ -1,25 +0,0 @@ -# systab task runner — `just` to list all recipes - -# List available recipes -default: - @just --list - -# Run ShellCheck linter -lint: - shellcheck systab - -# Run unit tests -test: - ./test.sh - -# Run demo tape command tests (no VHS required) -tape-test: - ./demo/test-tapes.sh - -# Run all checks: lint + unit tests + tape tests -check: lint test tape-test - -# Record demo GIFs with VHS -record: - vhs demo/quickstart.tape - vhs demo/all-features.tape diff --git a/systab b/systab index 0c5fb6e..e21273f 100755 --- a/systab +++ b/systab @@ -23,8 +23,6 @@ opt_clean=false opt_status=false opt_disable="" opt_enable="" -opt_delete="" -opt_restart="" opt_filter="" opt_output="" opt_name="" @@ -50,8 +48,6 @@ Job Creation Options: Management Options (accept hex ID or name): -D Disable a job -E Enable a disabled job - -X Delete a job (stop, disable, and remove unit files) - -R Restart a job (resets timer countdown / restarts service process) -e Edit jobs in crontab-like format -l Print jobs in crontab-like format to stdout -L [id|name] [filter] List job logs (optionally for a specific job and/or filtered) @@ -93,14 +89,6 @@ EXAMPLES: $SCRIPT_NAME -D $SCRIPT_NAME -E backup - # Delete a job permanently - $SCRIPT_NAME -X - $SCRIPT_NAME -X backup - - # Restart a job - $SCRIPT_NAME -R - $SCRIPT_NAME -R healthcheck_home - # View logs for backup jobs $SCRIPT_NAME -L backup @@ -345,40 +333,6 @@ toggleJobById() { fi } -# Delete a job (stop, disable, remove unit files) by hex ID or name -deleteJob() { - local input="$1" - validateJobId "$input" - local id="$_resolved_id" - local name - name=$(getJobName "$SYSTEMD_USER_DIR/${_job_name}.service") - local label - label=$(formatJobId "$id" "$name") - removeJob "$_job_name" - systemctl --user daemon-reload - echo "Deleted: $label" -} - -# Restart a job by hex ID or name -restartJob() { - local input="$1" - validateJobId "$input" - local id="$_resolved_id" - local name - name=$(getJobName "$SYSTEMD_USER_DIR/${_job_name}.service") - local label - label=$(formatJobId "$id" "$name") - if ! isJobEnabled "$_job_name"; then - error "Job is disabled, enable it first with -E: $label" - fi - if isJobService "$_job_name"; then - systemctl --user restart "${_job_name}.service" 2>/dev/null || true - else - systemctl --user restart "${_job_name}.timer" 2>/dev/null || true - fi - echo "Restarted: $label" -} - # Get all managed unit files of a given type (service or timer) getManagedUnits() { local ext="$1" @@ -1231,7 +1185,7 @@ cleanJobs() { # Parse command-line options parseOptions() { - while getopts "t:sc:f:n:im:oD:E:X:R:elLSCh" opt; do + while getopts "t:sc:f:n:im:oD:E:elLSCh" opt; do case $opt in t) opt_time="$OPTARG" ;; s) opt_service=true ;; @@ -1254,8 +1208,6 @@ parseOptions() { fi ;; D) opt_disable="$OPTARG" ;; E) opt_enable="$OPTARG" ;; - X) opt_delete="$OPTARG" ;; - R) opt_restart="$OPTARG" ;; e) opt_edit=true ;; l) opt_print_crontab=true ;; L) opt_list=true ;; @@ -1298,8 +1250,6 @@ parseOptions() { local manage_count=0 [[ -n "$opt_disable" ]] && manage_count=$((manage_count + 1)) [[ -n "$opt_enable" ]] && manage_count=$((manage_count + 1)) - [[ -n "$opt_delete" ]] && manage_count=$((manage_count + 1)) - [[ -n "$opt_restart" ]] && manage_count=$((manage_count + 1)) $opt_edit && manage_count=$((manage_count + 1)) $opt_print_crontab && manage_count=$((manage_count + 1)) $opt_list && manage_count=$((manage_count + 1)) @@ -1307,11 +1257,11 @@ parseOptions() { $opt_clean && manage_count=$((manage_count + 1)) if [[ $manage_count -gt 1 ]]; then - error "Options -D, -E, -X, -R, -e, -l, -L, -S, and -C are mutually exclusive" + error "Options -D, -E, -e, -l, -L, -S, and -C are mutually exclusive" fi if [[ $manage_count -gt 0 ]] && { [[ -n "$opt_time$opt_command$opt_file" ]] || $opt_service; }; then - error "Management options -D, -E, -X, -R, -e, -l, -L, -S, and -C cannot be used with job creation options" + error "Management options -D, -E, -e, -l, -L, -S, and -C cannot be used with job creation options" fi if [[ -n "$opt_command" && -n "$opt_file" ]]; then @@ -1340,10 +1290,6 @@ main() { toggleJobById "$opt_disable" disable elif [[ -n "$opt_enable" ]]; then toggleJobById "$opt_enable" enable - elif [[ -n "$opt_delete" ]]; then - deleteJob "$opt_delete" - elif [[ -n "$opt_restart" ]]; then - restartJob "$opt_restart" elif $opt_edit; then editJobs elif $opt_print_crontab; then diff --git a/test.sh b/test.sh index 8672148..972d94e 100755 --- a/test.sh +++ b/test.sh @@ -188,58 +188,6 @@ assert_output "enable job" "Enabled:" $SYSTAB -E "$id_recurring" assert_output "enable already enabled" "Already enabled:" $SYSTAB -E "$id_recurring" -# ============================================================ -# Delete (-X) -# ============================================================ - -echo "" -echo "${BOLD}--- Delete (-X) ---${RESET}" - -assert_output "create timer job for deletion" "Job created:" $SYSTAB -t "every 30 minutes" -c "echo delete_test" -extract_id; id_del=$_extracted_id - -assert_output "delete timer job by ID" "Deleted:" $SYSTAB -X "$id_del" -assert_last_output_contains "delete output contains job ID" "$id_del" - -if [[ ! -f "$SYSTEMD_USER_DIR/systab_${id_del}.service" ]]; then - pass "service file removed after delete" -else - fail "service file removed after delete" "file still exists" -fi -if [[ ! -f "$SYSTEMD_USER_DIR/systab_${id_del}.timer" ]]; then - pass "timer file removed after delete" -else - fail "timer file removed after delete" "file still exists" -fi - -assert_output "create named job for deletion" "Job created:" $SYSTAB -t "every 30 minutes" -c "echo named_delete_test" -n deltarget -extract_id; id_del_named=$_extracted_id - -assert_output "delete named job by name" "Deleted:" $SYSTAB -X deltarget -assert_last_output_contains "delete-by-name output shows name" "(deltarget)" - -if [[ ! -f "$SYSTEMD_USER_DIR/systab_${id_del_named}.service" ]]; then - pass "named job service file removed after delete" -else - fail "named job service file removed after delete" "file still exists" -fi - -assert_output "create service job for deletion" "Service created:" $SYSTAB -s -c "sleep 3600" -extract_id; id_del_svc=$_extracted_id - -assert_output "delete service job by ID" "Deleted:" $SYSTAB -X "$id_del_svc" - -if [[ ! -f "$SYSTEMD_USER_DIR/systab_${id_del_svc}.service" ]]; then - pass "service-only job file removed after delete" -else - fail "service-only job file removed after delete" "file still exists" -fi - -assert_failure "delete nonexistent job fails" $SYSTAB -X "zzzzzz" -assert_failure "-X and -D are mutually exclusive" $SYSTAB -X "$id_recurring" -D "$id_recurring" -assert_failure "-X and -E are mutually exclusive" $SYSTAB -X "$id_recurring" -E "$id_recurring" -assert_failure "-X cannot be combined with job creation" $SYSTAB -X "$id_recurring" -t daily -c "echo test" - # ============================================================ # Notifications # ============================================================ @@ -502,26 +450,6 @@ assert_failure "-s and -i are mutually exclusive" $SYSTAB -s -i -c "echo test" assert_failure "-s and -m are mutually exclusive" $SYSTAB -s -m user@example.com -c "echo test" assert_failure "-s and -o are mutually exclusive" $SYSTAB -s -o -c "echo test" -# ============================================================ -# Restart (-R) -# ============================================================ - -echo "" -echo "${BOLD}--- Restart (-R) ---${RESET}" - -assert_output "restart timer job by ID" "Restarted:" $SYSTAB -R "$id_recurring" -assert_output "restart timer job by name" "Restarted:" $SYSTAB -R mytest -assert_last_output_contains "restart by name shows name" "(mytest)" -assert_output "restart service job" "Restarted:" $SYSTAB -R "$id_svc" - -$SYSTAB -D "$id_recurring" -assert_failure "restart disabled job fails" $SYSTAB -R "$id_recurring" -$SYSTAB -E "$id_recurring" - -assert_failure "restart nonexistent job fails" $SYSTAB -R "zzzzzz" -assert_failure "-R and -D are mutually exclusive" $SYSTAB -R "$id_recurring" -D "$id_recurring" -assert_failure "-R cannot be combined with job creation" $SYSTAB -R "$id_recurring" -t daily -c "echo test" - # ============================================================ # Clean # ============================================================