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>
|
|
@ -27,11 +27,24 @@ fi
|
|||
echo "Running tests..."
|
||||
if output=$(./test.sh 2>&1); then
|
||||
echo "$output"
|
||||
count=$(grep -oP '\d+ passed' <<< "$output" | tail -1)
|
||||
write_badge "${count:-passing}" "brightgreen"
|
||||
test_count=$(grep -oP '\d+(?= passed)' <<< "$output" | tail -1)
|
||||
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"
|
||||
|
|
|
|||
16
README.md
|
|
@ -15,18 +15,16 @@ 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
|
||||
|
||||
<p align="center"><img src="demo/editmode.png" alt="Edit mode"></p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="25%"><img src="demo/quickstart.gif" alt="Quick start demo"></td>
|
||||
<td width="25%"><img src="demo/editmode.gif" alt="Edit mode demo"></td>
|
||||
<td width="25%"><img src="demo/notifications.gif" alt="Notifications demo"></td>
|
||||
<td width="25%"><img src="demo/services.gif" alt="Services demo"></td>
|
||||
<td width="50%"><img src="demo/quickstart.gif" alt="Quick start demo"></td>
|
||||
<td width="50%"><img src="demo/all-features.gif" alt="All features demo"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b>Quick start</b></td>
|
||||
<td align="center"><b>Edit mode</b></td>
|
||||
<td align="center"><b>Notifications</b></td>
|
||||
<td align="center"><b>Services</b></td>
|
||||
<td align="center"><b>All features</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
|
@ -184,7 +182,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`, `-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`, `-X`, `-S`, and `-L`. Names must be unique and cannot contain whitespace, pipes, or colons.
|
||||
|
||||
## How it works
|
||||
|
||||
|
|
@ -210,6 +208,7 @@ Job Creation:
|
|||
Management (accept hex ID or name):
|
||||
-D <id|name> Disable a job
|
||||
-E <id|name> Enable a disabled job
|
||||
-X <id|name> Delete a job (stop, disable, and remove unit files)
|
||||
-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)
|
||||
|
|
@ -221,6 +220,7 @@ Management (accept hex ID or name):
|
|||
## Future feature ideas
|
||||
|
||||
- [ ] `-R` flag to restart / reload
|
||||
- [x] `-X` flag to delete
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "tests",
|
||||
"message": "88 passed",
|
||||
"message": "127 passed",
|
||||
"color": "brightgreen"
|
||||
}
|
||||
|
|
|
|||
BIN
demo/all-features.gif
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
193
demo/all-features.tape
Normal 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
|
||||
|
Before Width: | Height: | Size: 1.2 MiB |
BIN
demo/editmode.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
|
|
@ -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
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 656 KiB |
|
|
@ -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
|
||||
|
Before Width: | Height: | Size: 948 KiB After Width: | Height: | Size: 1,018 KiB |
|
|
@ -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
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.1 MiB |
|
|
@ -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
|
|
@ -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 ]]
|
||||
30
systab
|
|
@ -23,6 +23,7 @@ opt_clean=false
|
|||
opt_status=false
|
||||
opt_disable=""
|
||||
opt_enable=""
|
||||
opt_delete=""
|
||||
opt_filter=""
|
||||
opt_output=""
|
||||
opt_name=""
|
||||
|
|
@ -48,6 +49,7 @@ Job Creation Options:
|
|||
Management Options (accept hex ID or name):
|
||||
-D <id|name> Disable a job
|
||||
-E <id|name> Enable a disabled job
|
||||
-X <id|name> Delete a job (stop, disable, and remove unit files)
|
||||
-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)
|
||||
|
|
@ -89,6 +91,10 @@ EXAMPLES:
|
|||
$SCRIPT_NAME -D <id>
|
||||
$SCRIPT_NAME -E backup
|
||||
|
||||
# Delete a job permanently
|
||||
$SCRIPT_NAME -X <id>
|
||||
$SCRIPT_NAME -X backup
|
||||
|
||||
# View logs for backup jobs
|
||||
$SCRIPT_NAME -L backup
|
||||
|
||||
|
|
@ -333,6 +339,20 @@ 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"
|
||||
}
|
||||
|
||||
# Get all managed unit files of a given type (service or timer)
|
||||
getManagedUnits() {
|
||||
local ext="$1"
|
||||
|
|
@ -1185,7 +1205,7 @@ cleanJobs() {
|
|||
|
||||
# Parse command-line options
|
||||
parseOptions() {
|
||||
while getopts "t:sc:f:n:im:oD:E:elLSCh" opt; do
|
||||
while getopts "t:sc:f:n:im:oD:E:X:elLSCh" opt; do
|
||||
case $opt in
|
||||
t) opt_time="$OPTARG" ;;
|
||||
s) opt_service=true ;;
|
||||
|
|
@ -1208,6 +1228,7 @@ parseOptions() {
|
|||
fi ;;
|
||||
D) opt_disable="$OPTARG" ;;
|
||||
E) opt_enable="$OPTARG" ;;
|
||||
X) opt_delete="$OPTARG" ;;
|
||||
e) opt_edit=true ;;
|
||||
l) opt_print_crontab=true ;;
|
||||
L) opt_list=true ;;
|
||||
|
|
@ -1250,6 +1271,7 @@ 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))
|
||||
$opt_edit && manage_count=$((manage_count + 1))
|
||||
$opt_print_crontab && manage_count=$((manage_count + 1))
|
||||
$opt_list && manage_count=$((manage_count + 1))
|
||||
|
|
@ -1257,11 +1279,11 @@ parseOptions() {
|
|||
$opt_clean && manage_count=$((manage_count + 1))
|
||||
|
||||
if [[ $manage_count -gt 1 ]]; then
|
||||
error "Options -D, -E, -e, -l, -L, -S, and -C are mutually exclusive"
|
||||
error "Options -D, -E, -X, -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, -e, -l, -L, -S, and -C cannot be used with job creation options"
|
||||
error "Management options -D, -E, -X, -e, -l, -L, -S, and -C cannot be used with job creation options"
|
||||
fi
|
||||
|
||||
if [[ -n "$opt_command" && -n "$opt_file" ]]; then
|
||||
|
|
@ -1290,6 +1312,8 @@ main() {
|
|||
toggleJobById "$opt_disable" disable
|
||||
elif [[ -n "$opt_enable" ]]; then
|
||||
toggleJobById "$opt_enable" enable
|
||||
elif [[ -n "$opt_delete" ]]; then
|
||||
deleteJob "$opt_delete"
|
||||
elif $opt_edit; then
|
||||
editJobs
|
||||
elif $opt_print_crontab; then
|
||||
|
|
|
|||
52
test.sh
|
|
@ -188,6 +188,58 @@ 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
|
||||
# ============================================================
|
||||
|
|
|
|||