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
-

-
- |
- |
+ |
+ |
+ |
+ |
| Quick start |
-All features |
+Edit mode |
+Notifications |
+Services |
@@ -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
# ============================================================