diff --git a/CLAUDE.md b/CLAUDE.md index 2493d9b..77cf50e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Overview -`systab` is a single-file Bash script that provides a cron/at/batch-like interface for systemd user timers. It creates, manages, and cleans up systemd `.service` and `.timer` unit files in `~/.config/systemd/user/`. Managed units are tagged with a `# SYSTAB_MANAGED` marker comment and a `# SYSTAB_ID=` short ID for human-friendly identification. +`systab` is a single-file Bash script that provides a cron/at/batch-like interface for systemd user timers. It creates, manages, and cleans up systemd `.service` and `.timer` unit files in `~/.config/systemd/user/`. Managed units are tagged with a `# SYSTAB_MANAGED` marker comment. Unit filenames use a 6-char hex ID (e.g., `systab_a1b2c3.timer`) which doubles as the human-facing job identifier. ## Running @@ -26,7 +26,7 @@ The script has two modes controlled by CLI flags: - `-S`: Show timer status via `systemctl`, including short IDs. - `-C`: Interactively clean up elapsed one-time timers (removes unit files from disk). -Key functions: `parse_time` (time spec → OnCalendar), `create_job` (generates unit files), `edit_jobs` (crontab-style edit with diff-and-apply), `get_managed_services`/`get_managed_timers` (find tagged units), `ensure_job_id` (auto-assign IDs to legacy jobs), `clean_jobs` (remove elapsed one-time timers). +Key functions: `parse_time` (time spec → OnCalendar), `create_job` (generates unit files), `edit_jobs` (crontab-style edit with diff-and-apply), `get_managed_services`/`get_managed_timers` (find tagged units), `clean_jobs` (remove elapsed one-time timers). ## Testing diff --git a/systab b/systab index b3f9882..cf0ae64 100755 --- a/systab +++ b/systab @@ -113,62 +113,14 @@ is_recurring() { esac } -# Generate unique job name -generate_job_name() { - local timestamp rand - timestamp=$(date +%Y%m%d_%H%M%S) - rand=$(od -An -tx2 -N2 /dev/urandom | tr -d ' ') - echo "${SCRIPT_NAME}_${timestamp}_${rand}" -} - -# Generate a random 6-char hex short ID -generate_short_id() { +# Generate a random 6-char hex ID +generate_id() { od -An -tx1 -N3 /dev/urandom | tr -d ' \n' } -# Extract SYSTAB_ID from a job's timer file; empty if missing -get_job_id() { - local job_name="$1" - local timer_file="$SYSTEMD_USER_DIR/${job_name}.timer" - if [[ -f "$timer_file" ]]; then - sed -n 's/^# SYSTAB_ID=//p' "$timer_file" - fi -} - -# Find job name by short ID; empty if not found -get_job_by_id() { - local target_id="$1" - local job - while IFS= read -r job; do - [[ -z "$job" ]] && continue - local jid - jid=$(get_job_id "$job") - if [[ "$jid" == "$target_id" ]]; then - echo "$job" - return - fi - done < <(get_managed_timers) -} - -# Ensure a managed job has a SYSTAB_ID; assign one if missing -ensure_job_id() { - local job_name="$1" - local existing_id - existing_id=$(get_job_id "$job_name") - if [[ -n "$existing_id" ]]; then - echo "$existing_id" - return - fi - local new_id - new_id=$(generate_short_id) - # Insert SYSTAB_ID line after the MARKER in both files - for ext in timer service; do - local unit_file="$SYSTEMD_USER_DIR/${job_name}.${ext}" - if [[ -f "$unit_file" ]]; then - sed -i "s/^$MARKER$/&\n# SYSTAB_ID=$new_id/" "$unit_file" - fi - done - echo "$new_id" +# Extract short ID from job name (systab_) +job_id() { + echo "${1#"${SCRIPT_NAME}"_}" } # Get all managed service files @@ -216,7 +168,7 @@ get_managed_timers() { # Create systemd service and timer files create_job() { local job_name command_to_run time_spec - job_name=$(generate_job_name) + job_name="${SCRIPT_NAME}_$(generate_id)" time_spec=$(parse_time "$opt_time") # Determine command to run @@ -238,15 +190,10 @@ create_job() { # Create systemd user directory if needed mkdir -p "$SYSTEMD_USER_DIR" - # Generate short ID for human-friendly display - local short_id - short_id=$(generate_short_id) - # Create service file local service_file="$SYSTEMD_USER_DIR/${job_name}.service" cat > "$service_file" < "$timer_file" < "$SYSTEMD_USER_DIR/${job_name}.service" < "$SYSTEMD_USER_DIR/${job_name}.timer" < "$temp_file" @@ -436,18 +379,6 @@ HEADER # "new" lines are collected separately since there can be multiple declare -A orig_jobs edited_jobs declare -a new_jobs=() - declare -A id_to_jobname - - # Build id -> jobname mapping from current managed timers - local job - while IFS= read -r job; do - [[ -z "$job" ]] && continue - local jid - jid=$(get_job_id "$job") - if [[ -n "$jid" ]]; then - id_to_jobname["$jid"]="$job" - fi - done < <(get_managed_timers) # Parse a crontab line into id, sched, cmd (split on first two whitespace runs) parse_crontab_line() { @@ -485,13 +416,10 @@ HEADER # Deletions: IDs in original but not in edited for id in "${!orig_jobs[@]}"; do if [[ -z "${edited_jobs[$id]+x}" ]]; then - local jname="${id_to_jobname[$id]:-}" - if [[ -n "$jname" ]]; then - remove_job "$jname" - echo "Deleted: $id ($jname)" - deleted=$((deleted + 1)) - needs_reload=true - fi + remove_job "${SCRIPT_NAME}_${id}" + echo "Deleted: $id" + deleted=$((deleted + 1)) + needs_reload=true fi done @@ -517,11 +445,7 @@ HEADER # Updates: IDs in both but with changed values for id in "${!edited_jobs[@]}"; do if [[ -n "${orig_jobs[$id]+x}" && "${orig_jobs[$id]}" != "${edited_jobs[$id]}" ]]; then - local jname="${id_to_jobname[$id]:-}" - if [[ -z "$jname" ]]; then - warn "Cannot find job for ID '$id', skipping update" - continue - fi + local jname="${SCRIPT_NAME}_${id}" local old_entry="${orig_jobs[$id]}" new_entry="${edited_jobs[$id]}" local old_sched new_sched old_cmd new_cmd @@ -552,7 +476,7 @@ HEADER fi systemctl --user restart "${jname}.timer" 2>/dev/null || true - echo "Updated: $id ($jname)" + echo "Updated: $id" updated=$((updated + 1)) needs_reload=true fi @@ -590,13 +514,7 @@ show_status() { local timer_file="$SYSTEMD_USER_DIR/${job}.timer" local service_file="$SYSTEMD_USER_DIR/${job}.service" - local short_id - short_id=$(get_job_id "$job") - if [[ -n "$short_id" ]]; then - echo "Job: $job (ID: $short_id)" - else - echo "Job: $job" - fi + echo "Job: $(job_id "$job")" # Get schedule from timer file if [[ -f "$timer_file" ]]; then @@ -690,13 +608,7 @@ list_logs() { echo "" for job in "${job_list[@]}"; do - local short_id - short_id=$(get_job_id "$job") - if [[ -n "$short_id" ]]; then - echo "=== Logs for $job (ID: $short_id) ===" - else - echo "=== Logs for $job ===" - fi + echo "=== Logs for $(job_id "$job") ===" # Show timer status if systemctl --user is-active "${job}.timer" &>/dev/null; then