# wallp Merge Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Merge `multiwal.sh` (interactive setter) and `qar-lastwall.sh` (restorer) into one flag-driven bash script `wallp` with per-output partial updates, theme persistence, and a config file.

**Architecture:** Single bash script `wallp`. Pure logic (conf parse, arg parse, `~` expansion, output mapping, theme resolution) lives in functions. Side-effecting actions (swaybg, wal, qarma, notify-send, filesystem writes) are isolated in thin functions. The script runs `main "$@"` only when executed directly, so bats tests can source it and exercise the functions with PATH stubs and a temp `$HOME`. Persistence is wallp-owned under `~/.config/wallp/`; a `~/.cache/wal/wpaper` symlink is maintained for rofi.

**Tech Stack:** bash, swaybg, pywal (`wal`), qarma, notify-send. Tests: bats 1.5.0 (installed), PATH stubs, temp `$HOME`. No bats-support/-assert helper libs — use core `run`/`$status`/`$output` + `[ ]`.

---

## Conventions

- Tests live in `tests/wallp.bats`. A shared `setup()` creates a temp `$HOME`, a stub `PATH` dir with stub `swaybg/wal/qarma/notify-send/pkill/kill` (each logs its call to `$HOME/calls.log` and exits 0), then `source`s `wallp`. `teardown()` removes the temp dir.
- `wallp` ends with:
  ```bash
  if [ "${BASH_SOURCE[0]}" = "$0" ]; then main "$@"; fi
  ```
  so sourcing it in bats does not run `main`.
- Functions reference paths via `$HOME` at call time (no hardcoded `/home/danix`), so relocating `$HOME` in tests works.
- Run the suite with `bats tests/wallp.bats`. Commit after each task.

## File Structure

- Create: `wallp` — the merged script (all logic + `main`).
- Create: `wallp.conf.example` — shipped example config.
- Create: `tests/wallp.bats` — bats test file with shared setup/teardown.
- Modify: `/home/danix/bin/wal.sh` — trim wal/wal2/wpaper logic (final task; outside repo).
- Delete (after wallp verified): `multiwal.sh`, `qar-lastwall.sh` (final task).

---

## Task 1: bats setup/teardown + sourceable wallp

**Files:**
- Create: `wallp` (minimal sourceable stub)
- Create: `tests/wallp.bats`

- [ ] **Step 1: Create minimal sourceable `wallp`**

Create `wallp`:

```bash
#!/bin/bash
# wallp — unified wallpaper manager. See docs/superpowers/specs/.

set -u

main() {
  return 0
}

if [ "${BASH_SOURCE[0]}" = "$0" ]; then main "$@"; fi
```

- [ ] **Step 2: Create `tests/wallp.bats` with shared setup + one sanity test**

Create `tests/wallp.bats`:

```bash
#!/usr/bin/env bats

setup() {
  SCRIPT_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
  TMP_HOME="$(mktemp -d)"
  export HOME="$TMP_HOME"
  STUB_DIR="$TMP_HOME/stubs"
  mkdir -p "$STUB_DIR"
  export PATH="$STUB_DIR:$PATH"
  local bin
  for bin in swaybg wal qarma notify-send pkill kill; do
    cat > "$STUB_DIR/$bin" <<EOF
#!/bin/bash
echo "$bin \$*" >> "$TMP_HOME/calls.log"
exit 0
EOF
    chmod +x "$STUB_DIR/$bin"
  done
  source "$SCRIPT_DIR/wallp"
}

teardown() {
  rm -rf "$TMP_HOME"
}

@test "harness sanity" {
  [ "x" = "x" ]
}
```

- [ ] **Step 3: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `1 test, 0 failures`.

- [ ] **Step 4: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "test: bats setup/teardown + sourceable wallp stub"
```

---

## Task 2: `~` expansion helper

**Files:**
- Modify: `wallp` (add `expand_tilde`)
- Test: `tests/wallp.bats`

- [ ] **Step 1: Write failing tests**

Append to `tests/wallp.bats`:

```bash
@test "expand_tilde expands leading tilde" {
  run expand_tilde '~/pics/a.png'
  [ "$status" -eq 0 ]
  [ "$output" = "$HOME/pics/a.png" ]
}

@test "expand_tilde leaves absolute path untouched" {
  run expand_tilde '/abs/b.png'
  [ "$output" = "/abs/b.png" ]
}
```

- [ ] **Step 2: Run, verify fail**

Run: `bats tests/wallp.bats`
Expected: FAIL — `expand_tilde: command not found`.

- [ ] **Step 3: Implement**

Add to `wallp` above `main`:

```bash
# Expand a leading ~ or ~/ to $HOME. Leaves other paths unchanged.
expand_tilde() {
  case "$1" in
    "~") printf '%s\n' "$HOME" ;;
    "~/"*) printf '%s\n' "$HOME/${1#\~/}" ;;
    *) printf '%s\n' "$1" ;;
  esac
}
```

- [ ] **Step 4: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `0 failures`.

- [ ] **Step 5: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "feat: expand_tilde helper"
```

---

## Task 3: Config parsing

Parses `key=value` lines (ignore blanks + `#`), expands `~` in path values, sets globals `CONF_THEME CONF_OUTPUT_H CONF_OUTPUT_V CONF_DEFAULT_H CONF_DEFAULT_V`.

**Files:**
- Modify: `wallp` (add `parse_conf`)
- Test: `tests/wallp.bats`

- [ ] **Step 1: Write failing test**

Append to `tests/wallp.bats`:

```bash
@test "parse_conf reads keys and expands tilde in paths" {
  conf="$HOME/c.conf"
  printf '%s\n' \
    '# comment' '' \
    'THEME=mytheme' \
    'OUTPUT_H=DP-1' \
    'OUTPUT_V=DP-3' \
    'DEFAULT_H=~/p/h.png' \
    'DEFAULT_V=/abs/v.png' > "$conf"
  parse_conf "$conf"
  [ "$CONF_THEME" = "mytheme" ]
  [ "$CONF_OUTPUT_H" = "DP-1" ]
  [ "$CONF_OUTPUT_V" = "DP-3" ]
  [ "$CONF_DEFAULT_H" = "$HOME/p/h.png" ]
  [ "$CONF_DEFAULT_V" = "/abs/v.png" ]
}
```

- [ ] **Step 2: Run, verify fail**

Run: `bats tests/wallp.bats`
Expected: FAIL — `parse_conf: command not found`.

- [ ] **Step 3: Implement**

Add to `wallp`:

```bash
# Parse key=value conf into CONF_* globals. Ignores blanks and #-comments.
# Expands ~ in DEFAULT_* path values. Does not validate (see require_conf_keys).
CONF_THEME="" CONF_OUTPUT_H="" CONF_OUTPUT_V="" CONF_DEFAULT_H="" CONF_DEFAULT_V=""
parse_conf() {
  local file="$1" line key val
  while IFS= read -r line || [ -n "$line" ]; do
    case "$line" in ''|'#'*) continue ;; esac
    key="${line%%=*}"
    val="${line#*=}"
    case "$key" in
      THEME)     CONF_THEME="$val" ;;
      OUTPUT_H)  CONF_OUTPUT_H="$val" ;;
      OUTPUT_V)  CONF_OUTPUT_V="$val" ;;
      DEFAULT_H) CONF_DEFAULT_H="$(expand_tilde "$val")" ;;
      DEFAULT_V) CONF_DEFAULT_V="$(expand_tilde "$val")" ;;
    esac
  done < "$file"
}
```

- [ ] **Step 4: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `0 failures`.

- [ ] **Step 5: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "feat: parse_conf key=value config loader"
```

---

## Task 4: Conf bootstrap + required-key validation

`load_conf`: if conf missing → write template, message, return 10 (caller exits 0); else parse + validate required path/output keys (return 1 on miss). THEME falls back to `sexy-splurge` if empty.

**Files:**
- Modify: `wallp` (add `conf_path`, `write_conf_template`, `require_conf_keys`, `load_conf`)
- Test: `tests/wallp.bats`

- [ ] **Step 1: Write failing tests**

```bash
@test "load_conf bootstraps template when conf missing" {
  run load_conf
  [ "$status" -eq 10 ]
  [ -f "$HOME/.config/wallp/wallp.conf" ]
  [[ "$output" == *"fill it in"* ]]
}

@test "load_conf hard-errors on missing required key" {
  mkdir -p "$HOME/.config/wallp"
  printf '%s\n' 'OUTPUT_H=DP-1' 'OUTPUT_V=DP-3' 'DEFAULT_H=/a.png' \
    > "$HOME/.config/wallp/wallp.conf"   # DEFAULT_V missing
  run load_conf
  [ "$status" -eq 1 ]
  [[ "$output" == *"DEFAULT_V"* ]]
}

@test "load_conf ok with THEME fallback" {
  mkdir -p "$HOME/.config/wallp"
  printf '%s\n' 'OUTPUT_H=DP-1' 'OUTPUT_V=DP-3' \
    'DEFAULT_H=/a.png' 'DEFAULT_V=/b.png' \
    > "$HOME/.config/wallp/wallp.conf"   # no THEME
  load_conf
  [ "$?" -eq 0 ]
  [ "$CONF_THEME" = "sexy-splurge" ]
}
```

Note: the third test calls `load_conf` directly (not via `run`) because it must
assert on `CONF_THEME`, which `run` would not propagate (subshell).

- [ ] **Step 2: Run, verify fail**

Run: `bats tests/wallp.bats`
Expected: FAIL — `load_conf: command not found`.

- [ ] **Step 3: Implement**

Add to `wallp`:

```bash
conf_path() { printf '%s\n' "$HOME/.config/wallp/wallp.conf"; }

write_conf_template() {
  local path; path="$(conf_path)"
  mkdir -p "$(dirname "$path")"
  cat > "$path" <<'EOF'
# wallp config. Fill in real values, then re-run wallp.
# THEME is optional (defaults to sexy-splurge).
THEME=sexy-splurge
# Physical output names (see: swaymsg -t get_outputs / wlr-randr)
OUTPUT_H=DP-1
OUTPUT_V=DP-3
# Default wallpapers used by --restore when no saved state exists.
DEFAULT_H=~/Pictures/wallpapers/SFW/horizontal.png
DEFAULT_V=~/Pictures/wallpapers/SFW/vertical.png
EOF
}

# Hard-error if any required path/output key is empty. Returns 1 on first miss.
require_conf_keys() {
  local k var
  for k in OUTPUT_H OUTPUT_V DEFAULT_H DEFAULT_V; do
    var="CONF_$k"
    if [ -z "${!var}" ]; then
      echo "wallp: required config key '$k' is missing or empty in $(conf_path)" >&2
      return 1
    fi
  done
  return 0
}

# Returns: 0 ok, 1 invalid conf, 10 bootstrapped (caller should exit 0).
load_conf() {
  local path; path="$(conf_path)"
  if [ ! -f "$path" ]; then
    write_conf_template
    echo "wallp: generated config at $path — fill it in and re-run." >&2
    command -v notify-send >/dev/null 2>&1 && \
      notify-send -u normal "wallp" "Config generated at $path. Fill it in and re-run."
    return 10
  fi
  parse_conf "$path"
  [ -z "$CONF_THEME" ] && CONF_THEME="sexy-splurge"
  require_conf_keys || return 1
  return 0
}
```

Note: `load_conf`'s message and the `notify-send` text both contain "fill it in"
so the bootstrap test matches regardless of whether notify-send exists.

- [ ] **Step 4: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `0 failures`.

- [ ] **Step 5: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "feat: conf bootstrap + required-key validation"
```

---

## Task 5: Theme resolution + persistence

`resolve_theme <flag_theme>` precedence: flag > persisted (`~/.config/wallp/theme`) > `CONF_THEME` > `sexy-splurge`. `persist_theme <name>` writes the file.

**Files:**
- Modify: `wallp` (add `theme_file`, `persist_theme`, `resolve_theme`)
- Test: `tests/wallp.bats`

- [ ] **Step 1: Write failing tests**

```bash
@test "resolve_theme flag wins over all" {
  CONF_THEME="conf"; mkdir -p "$HOME/.config/wallp"
  echo persisted > "$HOME/.config/wallp/theme"
  run resolve_theme flagged
  [ "$output" = "flagged" ]
}

@test "resolve_theme persisted beats conf" {
  CONF_THEME="conf"; mkdir -p "$HOME/.config/wallp"
  echo persisted > "$HOME/.config/wallp/theme"
  run resolve_theme ''
  [ "$output" = "persisted" ]
}

@test "resolve_theme uses conf when no persisted" {
  CONF_THEME="conf"
  run resolve_theme ''
  [ "$output" = "conf" ]
}

@test "persist_theme writes the file" {
  persist_theme "abc"
  [ "$(cat "$HOME/.config/wallp/theme")" = "abc" ]
}
```

- [ ] **Step 2: Run, verify fail**

Run: `bats tests/wallp.bats`
Expected: FAIL — functions not defined.

- [ ] **Step 3: Implement**

```bash
theme_file() { printf '%s\n' "$HOME/.config/wallp/theme"; }

persist_theme() {
  local f; f="$(theme_file)"
  mkdir -p "$(dirname "$f")"
  printf '%s\n' "$1" > "$f"
}

# Precedence: flag arg > persisted file > CONF_THEME > sexy-splurge.
resolve_theme() {
  local flag="$1" f; f="$(theme_file)"
  if [ -n "$flag" ]; then printf '%s\n' "$flag"; return; fi
  if [ -s "$f" ]; then head -n1 "$f"; return; fi
  if [ -n "${CONF_THEME:-}" ]; then printf '%s\n' "$CONF_THEME"; return; fi
  printf '%s\n' "sexy-splurge"
}
```

- [ ] **Step 4: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `0 failures`.

- [ ] **Step 5: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "feat: theme resolution + persistence"
```

---

## Task 6: Logical→physical output mapping + state paths

Map `H`/`V` to `CONF_OUTPUT_H`/`CONF_OUTPUT_V`; provide wallpaper-persistence and pid path helpers.

**Files:**
- Modify: `wallp`
- Test: `tests/wallp.bats`

- [ ] **Step 1: Write failing tests**

```bash
@test "output_for maps logical to physical" {
  CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"
  run output_for H
  [ "$output" = "DP-1" ]
  run output_for V
  [ "$output" = "DP-3" ]
}

@test "wall_file_for returns config paths" {
  run wall_file_for H
  [ "$output" = "$HOME/.config/wallp/wall_h" ]
  run wall_file_for V
  [ "$output" = "$HOME/.config/wallp/wall_v" ]
}

@test "pid_file_for returns cache paths" {
  run pid_file_for H
  [ "$output" = "$HOME/.cache/wallp/H.pid" ]
  run pid_file_for V
  [ "$output" = "$HOME/.cache/wallp/V.pid" ]
}
```

- [ ] **Step 2: Run, verify fail**

Run: `bats tests/wallp.bats`
Expected: FAIL — functions not defined.

- [ ] **Step 3: Implement**

```bash
output_for() {
  case "$1" in
    H) printf '%s\n' "$CONF_OUTPUT_H" ;;
    V) printf '%s\n' "$CONF_OUTPUT_V" ;;
    *) echo "wallp: unknown logical output '$1'" >&2; return 1 ;;
  esac
}
wall_file_for() {
  case "$1" in
    H) printf '%s\n' "$HOME/.config/wallp/wall_h" ;;
    V) printf '%s\n' "$HOME/.config/wallp/wall_v" ;;
  esac
}
pid_file_for() {
  case "$1" in
    H) printf '%s\n' "$HOME/.cache/wallp/H.pid" ;;
    V) printf '%s\n' "$HOME/.cache/wallp/V.pid" ;;
  esac
}
```

- [ ] **Step 4: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `0 failures`.

- [ ] **Step 5: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "feat: output mapping + state path helpers"
```

---

## Task 7: `--set H=/V=` argument parsing

`parse_set_args` consumes `H=<file>` / `V=<file>` tokens into globals `SET_H`/`SET_V`; rejects unknown tokens.

**Files:**
- Modify: `wallp`
- Test: `tests/wallp.bats`

- [ ] **Step 1: Write failing tests**

```bash
@test "parse_set_args captures both" {
  SET_H="" SET_V=""
  parse_set_args "H=/a.png" "V=/b.png"
  [ "$SET_H" = "/a.png" ]
  [ "$SET_V" = "/b.png" ]
}

@test "parse_set_args partial leaves other empty" {
  SET_H="" SET_V=""
  parse_set_args "V=/only.png"
  [ "$SET_H" = "" ]
  [ "$SET_V" = "/only.png" ]
}

@test "parse_set_args expands tilde" {
  SET_H="" SET_V=""
  parse_set_args "H=~/x.png"
  [ "$SET_H" = "$HOME/x.png" ]
}

@test "parse_set_args rejects unknown token" {
  SET_H="" SET_V=""
  run parse_set_args "Z=/bad.png"
  [ "$status" -eq 1 ]
  [[ "$output" == *"Z="* ]]
}
```

Note: the first three call `parse_set_args` directly (not `run`) so the `SET_*`
globals survive into the assertions. The rejection test uses `run` (checks
status/output, not globals).

- [ ] **Step 2: Run, verify fail**

Run: `bats tests/wallp.bats`
Expected: FAIL — `parse_set_args: command not found`.

- [ ] **Step 3: Implement**

```bash
SET_H="" SET_V=""
parse_set_args() {
  local tok
  for tok in "$@"; do
    case "$tok" in
      H=*) SET_H="$(expand_tilde "${tok#H=}")" ;;
      V=*) SET_V="$(expand_tilde "${tok#V=}")" ;;
      *) echo "wallp: unknown --set argument '$tok' (expected H=<file> or V=<file>)" >&2; return 1 ;;
    esac
  done
  return 0
}
```

- [ ] **Step 4: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `0 failures`.

- [ ] **Step 5: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "feat: parse --set H=/V= arguments"
```

---

## Task 8: Apply a single wallpaper (selective kill + swaybg + persist)

`apply_output <H|V> <file>`: validate file exists; selective-kill prior PID; launch swaybg; save new PID; persist path. Returns 1 (skip) on missing file without blanking.

**Files:**
- Modify: `wallp`
- Test: `tests/wallp.bats`

- [ ] **Step 1: Write failing tests**

```bash
@test "apply_output skips missing file without writing pid" {
  CONF_OUTPUT_H="DP-1"
  run apply_output H "$HOME/nope.png"
  [ "$status" -eq 1 ]
  [ ! -e "$HOME/.cache/wallp/H.pid" ]
}

@test "apply_output sets swaybg and persists path" {
  CONF_OUTPUT_H="DP-1"
  : > "$HOME/real.png"
  apply_output H "$HOME/real.png"
  [ "$?" -eq 0 ]
  [ "$(cat "$HOME/.config/wallp/wall_h")" = "$HOME/real.png" ]
  [ -f "$HOME/.cache/wallp/H.pid" ]
  grep -q "swaybg -o DP-1 -i $HOME/real.png" "$HOME/calls.log"
}

@test "apply_output on V does not touch H pid" {
  CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"
  mkdir -p "$HOME/.cache/wallp"; echo 99999 > "$HOME/.cache/wallp/H.pid"
  : > "$HOME/v.png"
  apply_output V "$HOME/v.png"
  [ "$(cat "$HOME/.cache/wallp/H.pid")" = "99999" ]
}
```

Note: tests 2 and 3 call `apply_output` directly (need `$?` / side effects in the
same shell). The `swaybg` stub exits 0 immediately, so `$!` is a real
already-exited PID — fine for the file-write assertions. `kill` stub exits 0.

- [ ] **Step 2: Run, verify fail**

Run: `bats tests/wallp.bats`
Expected: FAIL — `apply_output: command not found`.

- [ ] **Step 3: Implement**

```bash
# Kill the swaybg recorded for a logical output, if its PID is still alive.
kill_output() {
  local logical="$1" pf pid; pf="$(pid_file_for "$logical")"
  [ -f "$pf" ] || return 0
  pid="$(cat "$pf")"
  if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
    kill "$pid" 2>/dev/null
  fi
  rm -f "$pf"
}

# apply_output <H|V> <file>: set wallpaper for one output. rc1 = skipped.
apply_output() {
  local logical="$1" file="$2" output pf wf
  if [ ! -f "$file" ]; then
    echo "wallp: file not found, skipping $logical: $file" >&2
    return 1
  fi
  output="$(output_for "$logical")" || return 1
  kill_output "$logical"
  swaybg -o "$output" -i "$file" -m fill &
  pf="$(pid_file_for "$logical")"; mkdir -p "$(dirname "$pf")"
  printf '%s\n' "$!" > "$pf"
  wf="$(wall_file_for "$logical")"; mkdir -p "$(dirname "$wf")"
  printf '%s\n' "$file" > "$wf"
  return 0
}
```

Note: in the bats stub environment `kill -0 99999` returns non-zero (no such
process), so `kill_output` won't invoke the `kill` stub — the V-update test's H
pid stays `99999` as asserted.

- [ ] **Step 4: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `0 failures`.

- [ ] **Step 5: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "feat: apply_output with selective kill + persistence"
```

---

## Task 9: wpaper symlink + theme application

`update_wpaper`: symlink `~/.cache/wal/wpaper` → current H image (from `wall_h`). `apply_theme <theme>`: persist theme, run wal, notify. `finalize <theme>`: wpaper + apply_theme.

**Files:**
- Modify: `wallp`
- Test: `tests/wallp.bats`

- [ ] **Step 1: Write failing tests**

```bash
@test "update_wpaper symlinks to H image" {
  : > "$HOME/h.png"; mkdir -p "$HOME/.config/wallp"
  echo "$HOME/h.png" > "$HOME/.config/wallp/wall_h"
  update_wpaper
  [ "$(readlink "$HOME/.cache/wal/wpaper")" = "$HOME/h.png" ]
}

@test "apply_theme runs wal and persists theme" {
  apply_theme "mytheme"
  [ "$(cat "$HOME/.config/wallp/theme")" = "mytheme" ]
  grep -q "wal --backend colorz -nq --theme mytheme -o $HOME/bin/wal.sh" "$HOME/calls.log"
  grep -q "notify-send" "$HOME/calls.log"
}
```

- [ ] **Step 2: Run, verify fail**

Run: `bats tests/wallp.bats`
Expected: FAIL — functions not defined.

- [ ] **Step 3: Implement**

```bash
update_wpaper() {
  local wf img link; wf="$(wall_file_for H)"
  [ -s "$wf" ] || return 0
  img="$(cat "$wf")"
  link="$HOME/.cache/wal/wpaper"
  mkdir -p "$(dirname "$link")"
  ln -sf "$img" "$link"
}

apply_theme() {
  local theme="$1"
  persist_theme "$theme"
  wal --backend colorz -nq --theme "$theme" -o "$HOME/bin/wal.sh"
  command -v notify-send >/dev/null 2>&1 && \
    notify-send -u normal -t 5000 "color theme update" "setting color theme to: $theme"
}

finalize() {
  update_wpaper
  apply_theme "$1"
}
```

- [ ] **Step 4: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `0 failures`.

- [ ] **Step 5: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "feat: wpaper symlink + theme application"
```

---

## Task 10: Restore action

`do_restore <flag_theme>`: for H and V, use persisted `wall_h`/`wall_v` if present else `CONF_DEFAULT_*` (persisting the default via apply_output); finalize with resolved theme.

**Files:**
- Modify: `wallp`
- Test: `tests/wallp.bats`

- [ ] **Step 1: Write failing tests**

```bash
@test "restore uses persisted wallpapers" {
  CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"; CONF_THEME="conf"
  CONF_DEFAULT_H="$HOME/dh.png"; CONF_DEFAULT_V="$HOME/dv.png"
  : > "$HOME/saved_h.png"; : > "$HOME/saved_v.png"
  mkdir -p "$HOME/.config/wallp"
  echo "$HOME/saved_h.png" > "$HOME/.config/wallp/wall_h"
  echo "$HOME/saved_v.png" > "$HOME/.config/wallp/wall_v"
  do_restore ""
  grep -q "swaybg -o DP-1 -i $HOME/saved_h.png" "$HOME/calls.log"
  grep -q "swaybg -o DP-3 -i $HOME/saved_v.png" "$HOME/calls.log"
}

@test "restore falls back to conf defaults" {
  CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"; CONF_THEME="conf"
  : > "$HOME/dh.png"; : > "$HOME/dv.png"
  CONF_DEFAULT_H="$HOME/dh.png"; CONF_DEFAULT_V="$HOME/dv.png"
  do_restore ""
  grep -q "swaybg -o DP-1 -i $HOME/dh.png" "$HOME/calls.log"
  [ "$(cat "$HOME/.config/wallp/wall_h")" = "$HOME/dh.png" ]
}
```

- [ ] **Step 2: Run, verify fail**

Run: `bats tests/wallp.bats`
Expected: FAIL — `do_restore: command not found`.

- [ ] **Step 3: Implement**

```bash
# Pick saved path for a logical output, else its conf default.
restore_path_for() {
  local logical="$1" wf; wf="$(wall_file_for "$logical")"
  if [ -s "$wf" ]; then cat "$wf"; return; fi
  case "$logical" in
    H) printf '%s\n' "$CONF_DEFAULT_H" ;;
    V) printf '%s\n' "$CONF_DEFAULT_V" ;;
  esac
}

do_restore() {
  local flag_theme="$1" logical file
  for logical in H V; do
    file="$(restore_path_for "$logical")"
    apply_output "$logical" "$file" || true
  done
  finalize "$(resolve_theme "$flag_theme")"
}
```

- [ ] **Step 4: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `0 failures`.

- [ ] **Step 5: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "feat: restore action (saved -> default fallback)"
```

---

## Task 11: Set action (CLI args path; qarma path isolated)

`do_set <flag_theme> [H=.. V=..]`: parse args; if neither given and display present → `qarma_select`; apply each provided; finalize. `qarma_select` populates `SET_H`/`SET_V` via qarma — isolated so tests drive only the CLI path.

**Files:**
- Modify: `wallp`
- Test: `tests/wallp.bats`

- [ ] **Step 1: Write failing tests**

```bash
@test "set CLI partial sets only V" {
  CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"; CONF_THEME="conf"
  : > "$HOME/v.png"
  do_set "" "V=$HOME/v.png"
  grep -q "swaybg -o DP-3 -i $HOME/v.png" "$HOME/calls.log"
  ! grep -q "swaybg -o DP-1" "$HOME/calls.log"
}

@test "set CLI persists flag theme" {
  CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"; CONF_THEME="conf"
  : > "$HOME/h.png"
  do_set "flagtheme" "H=$HOME/h.png"
  [ "$(cat "$HOME/.config/wallp/theme")" = "flagtheme" ]
}

@test "set with no args and no display errors" {
  CONF_OUTPUT_H="DP-1"; CONF_OUTPUT_V="DP-3"; CONF_THEME="conf"
  unset WAYLAND_DISPLAY
  run do_set ""
  [ "$status" -eq 1 ]
  [[ "$output" == *"no display"* ]]
}
```

- [ ] **Step 2: Run, verify fail**

Run: `bats tests/wallp.bats`
Expected: FAIL — `do_set: command not found`.

- [ ] **Step 3: Implement**

```bash
# Interactive selection via qarma. Populates SET_H/SET_V. Isolated for testing.
qarma_select() {
  local choice file
  choice="$(qarma --list --title="wallp" --text="Which screen?" \
    --column="Screen" H V Both 2>/dev/null)" || return 1
  case "$choice" in
    H|Both)
      file="$(qarma --file-selection --preview-images 500 --width 1300 --height 600 \
        --title='Choose Horizontal Wallpaper')" && SET_H="$file" ;;
  esac
  case "$choice" in
    V|Both)
      file="$(qarma --file-selection --preview-images 500 --width 1300 --height 600 \
        --title='Choose Vertical Wallpaper')" && SET_V="$file" ;;
  esac
  return 0
}

do_set() {
  local flag_theme="$1"; shift
  SET_H="" SET_V=""
  if [ "$#" -gt 0 ]; then
    parse_set_args "$@" || return 1
  fi
  if [ -z "$SET_H" ] && [ -z "$SET_V" ]; then
    if [ -n "${WAYLAND_DISPLAY:-}" ]; then
      qarma_select || return 1
    else
      echo "wallp: no display: pass H=<file> and/or V=<file>" >&2
      return 1
    fi
  fi
  [ -n "$SET_H" ] && { apply_output H "$SET_H" || true; }
  [ -n "$SET_V" ] && { apply_output V "$SET_V" || true; }
  finalize "$(resolve_theme "$flag_theme")"
}
```

- [ ] **Step 4: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `0 failures`.

- [ ] **Step 5: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "feat: set action (CLI + qarma selection)"
```

---

## Task 12: Help screen + argument dispatch (`main`)

`show_help` (qarma window if display, else stdout). `main` parses `--set --restore --theme <name> --help/-h` plus `H=`/`V=` tokens, calls `load_conf` (rc 10 → exit 0), dispatches.

**Files:**
- Modify: `wallp` (replace stub `main`, add `show_help`)
- Test: `tests/wallp.bats`

- [ ] **Step 1: Write failing tests**

```bash
@test "bare invocation shows help" {
  unset WAYLAND_DISPLAY
  run main
  [ "$status" -eq 0 ]
  [[ "$output" == *"Usage"* ]]
}

@test "unknown flag errors with help" {
  unset WAYLAND_DISPLAY
  run main --bogus
  [ "$status" -eq 1 ]
  [[ "$output" == *"Usage"* ]]
}

@test "bootstrap on missing conf exits zero without setting wallpaper" {
  unset WAYLAND_DISPLAY
  run main --restore
  [ "$status" -eq 0 ]
  [ ! -e "$HOME/.cache/wallp/H.pid" ]
}

@test "theme flag honored through main with restore" {
  unset WAYLAND_DISPLAY
  mkdir -p "$HOME/.config/wallp"
  printf '%s\n' 'OUTPUT_H=DP-1' 'OUTPUT_V=DP-3' \
    'DEFAULT_H=/dh.png' 'DEFAULT_V=/dv.png' > "$HOME/.config/wallp/wallp.conf"
  main --restore --theme tflag
  [ "$(cat "$HOME/.config/wallp/theme")" = "tflag" ]
}
```

Note: the 4th test's defaults `/dh.png` `/dv.png` don't exist, so `apply_output`
skips both (rc1, swallowed), but `finalize` still runs and persists `tflag` —
which is what the assertion checks. It calls `main` directly (not `run`) so the
persisted file is written in the test's shell.

- [ ] **Step 2: Run, verify fail**

Run: `bats tests/wallp.bats`
Expected: FAIL — stub `main` returns 0 with no "Usage" output; bootstrap/theme tests fail.

- [ ] **Step 3: Implement** (replace the stub `main`)

```bash
show_help() {
  local text="wallp — unified wallpaper manager

Usage:
  wallp                      Show this help
  wallp --help | -h          Show this help
  wallp --set                Interactive (qarma) wallpaper selection
  wallp --set H=<file>       Set horizontal screen only
  wallp --set V=<file>       Set vertical screen only
  wallp --set H=<f> V=<f>    Set both screens
  wallp --restore            Restore last session (defaults if none)
  wallp --theme <name>       Override theme (with --set or --restore)

Config: ~/.config/wallp/wallp.conf"
  if [ -n "${WAYLAND_DISPLAY:-}" ] && command -v qarma >/dev/null 2>&1; then
    qarma --info --title="wallp help" --text="$text" 2>/dev/null
  else
    printf '%s\n' "$text"
  fi
}

main() {
  local action="" flag_theme="" set_args=()
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --help|-h) show_help; return 0 ;;
      --set)     action="set" ;;
      --restore) action="restore" ;;
      --theme)   shift; flag_theme="${1:-}";
                 [ -z "$flag_theme" ] && { echo "wallp: --theme needs a name" >&2; return 1; } ;;
      H=*|V=*)   set_args+=("$1") ;;
      *)         echo "wallp: unknown argument '$1'" >&2; show_help; return 1 ;;
    esac
    shift
  done

  if [ -z "$action" ]; then show_help; return 0; fi

  load_conf; local rc=$?
  if [ "$rc" -eq 10 ]; then return 0; fi   # bootstrapped, no change
  if [ "$rc" -ne 0 ]; then return 1; fi

  case "$action" in
    set)     do_set "$flag_theme" "${set_args[@]}" ;;
    restore) do_restore "$flag_theme" ;;
  esac
}
```

Note: `show_help` writes to stdout when no display; under bats `WAYLAND_DISPLAY`
is unset in these tests, so `run main` captures "Usage" in `$output`. `set_args`
may be empty; `"${set_args[@]}"` under `set -u` is safe in bash ≥ 4.4 (and the
stub `wallp` already uses `set -u`).

- [ ] **Step 4: Run, verify pass**

Run: `bats tests/wallp.bats`
Expected: `0 failures`.

- [ ] **Step 5: Commit**

```bash
git add wallp tests/wallp.bats
git commit -m "feat: help screen + main dispatch"
```

---

## Task 13: Ship example config + make executable

**Files:**
- Create: `wallp.conf.example`
- Modify: file mode of `wallp`

- [ ] **Step 1: Create `wallp.conf.example`**

```
# wallp config. Copy to ~/.config/wallp/wallp.conf and edit.
# THEME is optional (defaults to sexy-splurge).
THEME=sexy-splurge
# Physical output names (swaymsg -t get_outputs / wlr-randr)
OUTPUT_H=DP-1
OUTPUT_V=DP-3
# Defaults used by --restore when no saved state exists.
DEFAULT_H=~/Pictures/wallpapers/SFW/horizontal.png
DEFAULT_V=~/Pictures/wallpapers/SFW/vertical.png
```

- [ ] **Step 2: Make wallp executable**

Run: `chmod +x wallp`

- [ ] **Step 3: Verify full suite green**

Run: `bats tests/wallp.bats`
Expected: all tests pass, `0 failures`.

- [ ] **Step 4: Commit**

```bash
git add wallp wallp.conf.example
git commit -m "chore: ship example config, make wallp executable"
```

---

## Task 14: Manual smoke test on real two-screen setup

No automated test — real hardware. If a bug is found, add a failing bats test first, then fix.

- [ ] **Step 1: Backup current state**

Run: `cp -a ~/.config/wallp ~/.config/wallp.bak 2>/dev/null || true`

- [ ] **Step 2: First run bootstraps conf**

Run: `./wallp --restore`
Expected: "generated config at …" message; conf created; **no wallpaper change**. Edit conf with real outputs + default wallpaper paths.

- [ ] **Step 3: Restore with defaults**

Run: `./wallp --restore`
Expected: both screens show configured defaults; theme applied; notification appears.

- [ ] **Step 4: Partial CLI set (V only) does not blank H**

Run: `./wallp --set V=/path/to/vertical.png`
Expected: vertical changes; **horizontal stays** (not black).

- [ ] **Step 5: qarma flow**

Run: `./wallp --set`
Expected: qarma menu H/V/Both → file picker(s) → chosen screen(s) update. Cancel skips.

- [ ] **Step 6: Theme persistence across restore**

Run:
```bash
./wallp --set H=/some/h.png --theme some-other-theme
./wallp --restore
```
Expected: after restore, `some-other-theme` reapplied (persisted wins), not conf default.

- [ ] **Step 7: Help**

Run: `./wallp` and `./wallp --help`
Expected: usage shown (qarma window under wayland, else stdout).

---

## Task 15: Cut over wal.sh and remove old scripts

Only after Task 14 passes. `wal.sh` is outside the repo (`/home/danix/bin/wal.sh`).

- [ ] **Step 1: Trim `wal.sh`**

Edit `/home/danix/bin/wal.sh` — remove the wallpaper-pointer + wpaper logic (old lines 15-27), keeping only dunst + kitty glue. Result:

```sh
#!/bin/sh

# uncomment for debug
#set -ex

# Symlink dunst config
ln -sf ~/.cache/wal/dunstrc ~/.config/dunst/dunstrc
# Restart dunst with the new color scheme
pkill dunst
dunst &

# symlink kitty colors
ln -sf ~/.cache/wal/colors-kitty.conf ~/.config/kitty/current-theme.conf
```

- [ ] **Step 2: Verify wpaper still maintained by wallp**

Run: `./wallp --restore && readlink ~/.cache/wal/wpaper`
Expected: symlink points at current H image. Confirm rofi menus still themed.

- [ ] **Step 3: Remove superseded scripts**

Run: `git rm multiwal.sh qar-lastwall.sh`

- [ ] **Step 4: Repoint autostart**

Manually check wayland/hyprland autostart (e.g. hyprland `exec-once`) that called `qar-lastwall.sh` — repoint to `wallp --restore`. Note the edited file in the commit body.

- [ ] **Step 5: Commit**

```bash
git add -A
git commit -m "chore: cut over to wallp, remove multiwal.sh + qar-lastwall.sh"
```

---

## Self-Review Notes

- Spec coverage: CLI forms (T7,T11,T12), conf bootstrap + hard-error (T4), logical H/V mapping (T6), theme persistence + precedence (T5, asserted T11/T12), selective per-output kill / no-blank partial (T8, asserted T11), wpaper kept for rofi (T9,T15), drop wal/wal2 (no writes anywhere in plan — verified), restore saved→default (T10), help (T12), error handling (T4,T7,T8,T11,T12). All covered.
- Naming consistency: `apply_output`, `output_for`, `wall_file_for`, `pid_file_for`, `resolve_theme`, `persist_theme`, `do_set`, `do_restore`, `finalize`, `update_wpaper`, `apply_theme`, `parse_conf`, `load_conf`, `parse_set_args`, `kill_output`, `restore_path_for`, `qarma_select`, `show_help` — consistent across tasks.
- Test framework: bats 1.5.0; tests that assert on globals or in-shell side effects call functions directly, tests that assert on status/output use `run`. No bats-support/-assert helpers required.
