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
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.
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.
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
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.
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.
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.
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.
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.
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.
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
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().
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
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
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