Commit log

03575a7 Add backup hooks

Click to expand commit body
Support backup-specific _pre_hooks and _post_hooks config keys with normal most-specific override semantics. Pre-hooks run before restic backup and can stop the backup; post-hooks run after restic is attempted and receive restic outcome environment variables.

Supervise restic as a child process so post-hooks can run while preserving restic exit codes, workdir handling, and interactive terminal behavior. Document hook usage and status mapping in the examples.

Amolith created

536ddad Validate required inputs in non-interactive mode

Click to expand commit body
When running non-interactively, keld now validates that required inputs
are present before executing restic. Two cases checked:

- Multiple presets with no --preset specified: returns an error listing
available presets instead of silently using global defaults.
- backup with no paths: returns an error directing user to pass paths
as arguments or set _arguments in the preset.

Task: td-H0EDND6

Amolith created

10e49c7 Guard root TUI entry against non-interactive env

Click to expand commit body
Bare keld with no subcommand now checks isInteractive() before entering
the TUI. In non-interactive environments, returns a clear error message
directing the user to specify a subcommand instead of crashing with a
bubbletea TTY error.

Task: td-MY949DM

Amolith created

4645715 Guard subcommand TUI entry against non-interactive

Click to expand commit body
When a wrapped subcommand is invoked with no args, the code now checks
isInteractive() before entering the TUI session. In non-interactive
environments (stdin not a tty or KELD_NONINTERACTIVE set), the command
runs directly with resolved config instead of crashing with "could not
open TTY".

This fixes the systemd timer regression where backup invocations would
fail because root's TraverseChildren consumes --preset, leaving the
subcommand with an empty args slice that incorrectly triggered the TUI.

Task: td-B2KD251

Amolith created

c6d482d Add isInteractive helper to detect tty and env var

Click to expand commit body
The helper detects whether keld is running in an interactive environment
by checking if stdin is a terminal and whether KELD_NONINTERACTIVE is
set. This mirrors the same check bubbletea performs internally and will
guard the TUI entry points against non-interactive invocations like
systemd timers.

The function is injectable for testing via the isStdinTerminal package
variable.

Task: td-47ZDQ11

Amolith created

55fc62a Add spec for non-interactive execution

Click to expand commit body
keld is often invoked from systemd timers, cron jobs, CI pipelines, and
shell scripts with no controlling terminal. Capture the intended
behaviour as a Gherkin feature ahead of implementation.

The spec covers four rules: without a terminal keld never launches the
TUI, non-interactive mode requires explicit inputs (with specific errors
when preset selection, backup paths, or the subcommand itself is
missing), interactive mode is preserved when a terminal is attached, and
KELD_NONINTERACTIVE overrides terminal detection as a fallback for users
who want scripted behaviour on a terminal.

Pure spec, no automation β€” matches the other files under features/.

Amolith created

dc8e2eb Show note when command preview may be incomplete

Click to expand commit body
When the confirm screen is shown for a command with no dedicated TUI
screens (e.g. backup), append a note explaining that the preview may be
incomplete and additional prompts may follow. Points the user to
--show-command for the final resolved command.

Amolith created

2a50370 Pass session preset explicitly to runCommand

Click to expand commit body
Replace the package-level presetResolved bool with an explicit
sessionPreset *string parameter on runCommand. This makes the dependency
visible in the function signature and eliminates shared mutable state
that required manual cleanup in tests.

When sessionPreset is non-nil, its value is used as the preset and the
standalone preset prompt is skipped. A nil value means no TUI session
ran.

Amolith created

fbcedf5 Enter TUI when subcommand has no args

Click to expand commit body
When a wrapped command is invoked with no flags or arguments (e.g. "keld
restore"), enter the interactive TUI session with the command
pre-selected, skipping the menu screen. Commands with arguments continue
through the non-interactive path as before.

runInteractive now accepts a preselectedCommand parameter. When
non-empty, the menu screen is omitted and the pre-selected command is
used throughout the session.

Amolith created

4840435 Add confirm screen for command preview

Click to expand commit body
Add a Confirm screen adapter that shows the fully resolved restic
command line and environment variables before execution. Enter confirms,
Esc goes back. When --show-command is active, the screen auto-completes
so the session exits and the existing dry-run path prints the command.

The confirm screen is always appended as the last screen in the flow by
the resolve screen's builder function.

Amolith created

374fb1e Treat empty environ values as absent in HasEnvSource

Amolith created

bb02f61 Export CommandSuffix from config and remove duplicate in restic

Amolith created

1151e5f Store window size during file picker loading phase

Amolith created

8e16c60 Preserve manual entry input on theme change in snapshot screen

Amolith created

34f656b Extract generic drain test helper for screen adapters

Amolith created

0cbccb7 Replace double-pointer output params with return struct

Amolith created

33c6189 Pass session overrides directly to runCommand

Amolith created

2694d2d Remove legacy standalone restore prompts

Amolith created

f859b25 Add HasEnvSource to ResolvedConfig

Click to expand commit body
The snapshot loader was silently falling back to manual ID entry for
presets that configure the repository via _COMMAND environ keys (e.g.
RESTIC_REPOSITORY_COMMAND). This happened because hasRepoSource checked
for RESTIC_REPOSITORY in the environ map but not for
RESTIC_REPOSITORY_COMMAND, which keld resolves to RESTIC_REPOSITORY at
execution time.

Add HasEnvSource(key) to ResolvedConfig in the config package, which
checks the environ map for both the direct key and the _COMMAND variant,
plus the process environment as a fallback. This centralises the
_COMMAND convention in one place so callers never need to remember it.

Simplify hasRepoSource in the restic package to delegate to HasEnvSource
for environ checks, replacing the hand-rolled map lookups and os.Getenv
calls.

Amolith created

1365389 Wire restore flow into unified session

Click to expand commit body
Add a Resolve screen adapter that bridges the menu/preset phase and the
command-specific phase within a single tea.Program. After the user
selects a command and preset, the resolve screen calls config.Resolve to
determine which screens are needed, builds them, and injects them via
ExtendMsg + DoneCmd (using tea.Sequence to guarantee ordering).

The restore flow now runs as one continuous session with full breadcrumb
and back-navigation through all screens: menu β†’ preset β†’ resolve β†’
snapshot β†’ file picker β†’ target β†’ overwrite

Skip logic matches the existing promptRestore semantics:
- Snapshot screen omitted when config already has arguments
- File picker omitted when include flags are already set
- File picker's loader returns nil for snapshotID:subfolder syntax
- Target screen omitted when config has --target
- Overwrite screen omitted when config has --overwrite

runInteractive now returns overrides from completed screens, which are
converted to raw CLI args and passed to runCommand. This means
runCommand's existing promptForCommand path handles the overrides via
its normal merge logic.

Tested interactively: bare keld β†’ menu β†’ preset β†’ snapshot
(manual) β†’ file picker (notice) β†’ target β†’ overwrite β†’ all
screens show in breadcrumb, Esc navigates back with state preserved,
Ctrl+C exits cleanly.

Amolith created

51a8e47 Add file picker screen adapter

Click to expand commit body
Add screens.FilePicker, a Screen adapter that wraps the vendored picker
component for selecting files to restore from a snapshot.

Internal state machine with three phases:
- Loading: spinner while an injected FileLoader fetches the
snapshot's file listing
- Picking: interactive file picker with theme styling, tri-state
checkboxes, and directory navigation
- Notice: displayed when loading fails or returns empty, with a
"press any key" prompt before auto-advancing with a full restore

The snapshot ID comes from an injected closure so it reads the value
from the preceding snapshot screen. Esc at the picker's root directory
returns BackCmd; deeper Esc navigates to the parent.

Includes() returns restic --include paths with leading "/" for absolute
syntax, or nil for a full restore.

Amolith created

e0c07c6 Add snapshot selection screen adapter

Click to expand commit body
Add screens.Snapshot, the most complex screen adapter, with an internal
state machine that handles three phases:

- Loading: spinner while an injected SnapshotLoader fetches
snapshots asynchronously
- Selecting: filterable huh Select with formatted snapshot lines
plus an "Enter ID manually…" option
- Manual: huh Input fallback for typing arbitrary snapshot IDs

Error handling follows the existing promptSnapshotID semantics:
ErrNoRepo triggers silent fallback to manual entry, other errors show a
notice above the input, empty repositories show a notice.

A generation counter protects against stale async results when the user
navigates back during a load and re-enters the screen. The loader is
injected as a function for testability β€” tests simulate load results
directly via snapshotsLoadedMsg without calling restic.

Amolith created

e46b8af Add overwrite selector screen adapter

Click to expand commit body
Add screens.Overwrite, a Screen adapter wrapping a huh Select for
choosing the restore --overwrite behaviour. Options are if-changed
(default), if-newer, never, and always β€” matching the existing
form.SelectOverwrite choices.

Follows the same huh-embedding pattern as Preset and Target.

Amolith created

704675a Add target directory screen adapter

Click to expand commit body
Add screens.Target, a Screen adapter wrapping a huh Input for entering
the restore target directory. Follows the same pattern as the Preset
adapter: builds the huh form on Init, intercepts Esc for back
navigation, returns DoneCmd on completion, and rebuilds on
BackgroundColorMsg for theme changes.

Value() returns the entered path for config overrides. Selection()
returns the same for the breadcrumb.

Amolith created

1af9ca1 Fix pre-existing review findings

Click to expand commit body
Address issues found by CodeRabbit in pre-existing code:

- Fix FormatSnapshotLine doc comment to note that both paths and
tags are omitted when empty, not just tags
- Fix misleading "resolveFixtureTOML-style" comment in snapshot
tests β€” the fixtures are JSON, not TOML
- Remove redundant tt := tt loop variable captures in exec_test.go
and list_snapshots_test.go (unnecessary since Go 1.22)
- Reject whitespace-only input in notEmpty validator by trimming
before checking; update the test expectation to match

Amolith created

909e8a6 Add ExtendMsg to session for dynamic screen lists

Click to expand commit body
The restore flow needs screens built dynamically after the command and
preset are known (snapshot selection depends on resolved config, file
picker depends on the chosen snapshot).

Add ExtendMsg which appends screens after the current cursor, replacing
any previously-queued future screens. This lets a single tea.Program
drive the entire interactive flow without splitting into multiple
sessions.

Key behaviours:
- Empty Screens slice is a no-op
- Screens beyond the cursor are truncated before appending
- Extended screens participate in normal advance/back navigation
- Window size is replayed to extended screens on activation

Amolith created

0e6e32f Wire unified TUI session into cmd

Click to expand commit body
Replace the chain of independent tea.Programs (standalone menu, then
standalone huh form) with a single session that manages both the command
menu and preset selector as screens.

The interactive entry point now builds screens conditionally:
- Menu screen: always present
- Preset screen: only when >1 preset is defined; auto-selects when
exactly one preset exists; skips entirely when none are defined

After the session completes, the selected command and preset are passed
to runCommand. A presetResolved flag prevents the legacy promptPreset
path from re-prompting when the user chose global defaults (preset =
"").

Remove internal/menu since the new screens.Menu adapter replaces it.
Drop the "quit" menu item from wrapspec.go since the session handles
cancellation via Esc and Ctrl+C.

Add Preset.Value() to distinguish the resolved config preset name (which
may be "") from the breadcrumb display label returned by Selection().

Amolith created

31613db Extract activateScreen helper in session

Click to expand commit body
The init-then-replay-size sequence was duplicated verbatim in both
navigateBack and advance. Extract it into a single activateScreen
method.

Amolith created

5c7e66b Add menu and preset screen adapters

Click to expand commit body
Implement the first two Screen adapters for the unified TUI session:

Menu screen (internal/ui/screens/menu.go):
- Fresh implementation with its own MenuItem type, no dependency on
the legacy internal/menu package
- Cursor navigation with clamping, hotkey instant-selection, enter to
confirm
- Rune-aware hotkey matching and label highlighting
- Uses theme.Cursor and accent colour from shared *theme.Styles
- Returns BackCmd on Esc, DoneCmd on selection
- No help text, quit handling, or background detection (session's job)

Preset selector screen (internal/ui/screens/preset.go):
- Wraps a *huh.Form with a filterable Select field
- Form construction deferred to Init() so it picks up the settled theme
- Esc handling checks selectFld.GetFiltering(): if filtering, Esc
closes the filter (delegated to huh); if not, returns BackCmd
- Selection() returns "(global defaults)" for the empty-preset choice,
avoiding ambiguity with the "" not-yet-selected state
- Rebuilds the form on BackgroundColorMsg to pick up updated styles.Huh
- Preserves the previously selected value across re-init for back
navigation

Amolith created

6190c5c Fix theme ownership and propagation

Click to expand commit body
The session previously allocated *theme.Styles internally and exposed it
via a Styles() accessor. This created a chicken-and-egg problem: screens
needed the pointer at construction time, but they were passed to New()
which hadn't been called yet. Tests worked around it by mutating
s.screens directly, but external callers in cmd/ could not.

Change New() to accept *theme.Styles from the caller, who allocates once
and passes the same pointer to both screen constructors and the session.
Remove the Styles() accessor since it is no longer needed.

Additionally, the session was swallowing BackgroundColorMsg after
updating the shared styles, preventing screens from reacting to theme
changes. Screens that cache themed state (like huh forms) had stale
themes if background detection arrived after construction. Forward
BackgroundColorMsg to the active screen after updating styles so screens
can rebuild cached state.

Amolith created

183afe2 Add session model for unified interactive TUI

Amolith created

9503d23 Add unified TUI theme package with bubbletint

Amolith created

1a0fd16 Add Gherkin feature files for interactive TUI session

Click to expand commit body
Specify the interactive session behaviour across four feature files:

- session_shell: terminal session lifecycle, theme detection, colour
  palette, help bar, breadcrumb, screen titles, and Ctrl+C handling
- navigation: Esc back-navigation, state retention, screen skipping
  when values are already resolved, and CLI-provided value handling
- restore_flow: restore-specific screens for snapshot selection, file
  picking, target directory, and overwrite behaviour
- command_preview: confirmation screen, command execution, and
  --show-command integration

Add bubbletint/v2 dependency for terminal colour palette support.

Amolith created

89bfe72 Add quit handler and help chrome to file picker

Amolith created

2f552b6 Simplify picker to multi-select only

Amolith created

4ee76b2 Add picker key-handling tests

Amolith created

53c8c61 Wire file picker into interactive restore flow

Amolith created

1efbdf4 Add vendored filepicker with fs.FS multi-select and tri-state

Amolith created

fea34df Add in-memory snapshot filesystem from ls node list

Amolith created

efaebc8 Add restic ls JSON parsing with test fixture repo

Amolith created

fc28c5a Update interactive restore flow with snapshot picker and overwrite prompt

Amolith created

14bb0c9 Add snapshot listing via restic subprocess

Amolith created

5288918 Add snapshot JSON parsing and formatting

Amolith created

d618909 Rework systemd unit examples

Click to expand commit body
Rename keld-verify to keld-integrity with --read-data for monthly full
data verification. The daily backup unit now chains backup && check in a
single ExecStart via /bin/sh -c.

Rename env files from %I.env and %I-verify.env to %i_backup.env and
%i_integrity.env, using %i (escaped instance) for consistent file
naming.

Amolith created

ad5c445 Update examples and docs for path-based repos and _COMMAND

Click to expand commit body
Rework examples/keld/config.toml to demonstrate:
- Path-based repository URLs for rclone and S3 backends
- _COMMAND environ for credential injection (B2 and rclone)
- BorgBase per-repo presets with randomized example creds

Rename example timer env files to _backup/_integrity suffixes.
Rename keld-verify to keld-integrity with --read-data for monthly
full data verification; daily backup unit now chains backup && check.

Add FOO_COMMAND to the special config keys table in AGENTS.md.

Amolith created

93489d8 Support _COMMAND suffix in environ sections

Click to expand commit body
Add resolveEnvironCommands to internal/restic/exec.go. When an
.environ key ends in _COMMAND, keld executes its value via sh -c,
captures stdout (stripping trailing newlines), and sets the base
key to the result.

RESTIC_PASSWORD_COMMAND and RESTIC_FROM_PASSWORD_COMMAND are passed
through to restic as-is since restic handles those natively.

DryRun does NOT resolve _COMMAND keys, avoiding secret leakage in
--show-command output. Environ keys are sorted for deterministic
dry-run output.

Amolith created

cc57dc2 Remove stale architecture diagram from AGENTS.md

Amolith created

0c6feff Restructure CLI to use cobra subcommands with generated restic metadata

Amolith created

ac70768 Add build-time contract tests and restic flag completions

Amolith created

c38fda5 Warn on unknown restic flags at runtime

Amolith created