@@ -0,0 +1,341 @@
+package screens
+
+import (
+ "errors"
+ "fmt"
+ "sync/atomic"
+
+ "charm.land/bubbles/v2/key"
+ "charm.land/bubbles/v2/spinner"
+ tea "charm.land/bubbletea/v2"
+
+ "charm.land/huh/v2"
+
+ "git.secluded.site/keld/internal/restic"
+ "git.secluded.site/keld/internal/theme"
+ "git.secluded.site/keld/internal/ui"
+)
+
+// manualEntryValue is the sentinel option value returned by the
+// snapshot select when the user chooses "Enter ID manually…".
+const manualEntryValue = "__manual__"
+
+// snapshotPhase tracks where the snapshot screen is in its
+// internal flow.
+type snapshotPhase int
+
+const (
+ phaseSnapshotLoading snapshotPhase = iota // spinner while fetching
+ phaseSnapshotSelecting // huh Select with results
+ phaseSnapshotManual // huh Input fallback
+)
+
+// SnapshotLoader fetches snapshots from the repository. Injected at
+// construction so tests can provide a fake.
+type SnapshotLoader func() ([]restic.Snapshot, error)
+
+// snapshotsLoadedMsg carries the result of an async snapshot load.
+type snapshotsLoadedMsg struct {
+ gen int64
+ snapshots []restic.Snapshot
+ err error
+}
+
+// Snapshot is a Screen adapter for selecting a restic snapshot ID.
+// It loads snapshots asynchronously, presents a filterable list, and
+// falls back to manual text entry on failure or user choice.
+type Snapshot struct {
+ loader SnapshotLoader
+ styles *theme.Styles
+ spinner spinner.Model
+
+ phase snapshotPhase
+ gen int64 // generation counter for stale-result protection
+
+ // Selecting phase.
+ selectForm *huh.Form
+ selected string
+ snapshots []restic.Snapshot // cached for rebuild after back-nav
+
+ // Manual entry phase.
+ manualForm *huh.Form
+ entered string
+ notice string // displayed above the manual input
+
+ // Result.
+ value string
+ selection string
+}
+
+// NewSnapshot creates a snapshot selection screen. If loader is nil,
+// the screen goes directly to manual entry (for when no repository
+// is configured and the caller knows it upfront).
+func NewSnapshot(loader SnapshotLoader, styles *theme.Styles) *Snapshot {
+ sp := spinner.New(spinner.WithSpinner(spinner.Dot))
+ sp.Style = sp.Style.Foreground(styles.Accent)
+
+ return &Snapshot{
+ loader: loader,
+ styles: styles,
+ spinner: sp,
+ }
+}
+
+// Init starts the async snapshot load (or goes straight to manual
+// entry if no loader is configured). Each call bumps the generation
+// counter so stale results from prior loads are ignored.
+func (s *Snapshot) Init() tea.Cmd {
+ s.selection = ""
+ s.value = ""
+
+ if s.loader == nil {
+ s.phase = phaseSnapshotManual
+ s.buildManualForm()
+ return s.manualForm.Init()
+ }
+
+ s.phase = phaseSnapshotLoading
+ gen := atomic.AddInt64(&s.gen, 1)
+
+ return tea.Batch(s.spinner.Tick, func() tea.Msg {
+ snaps, err := s.loader()
+ return snapshotsLoadedMsg{gen: gen, snapshots: snaps, err: err}
+ })
+}
+
+// Update handles messages across all phases.
+func (s *Snapshot) Update(msg tea.Msg) (ui.Screen, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.BackgroundColorMsg:
+ s.spinner.Style = s.spinner.Style.Foreground(s.styles.Accent)
+ // Rebuild whichever form is active.
+ switch s.phase {
+ case phaseSnapshotSelecting:
+ // Rebuild would lose scroll position; skip for now.
+ case phaseSnapshotManual:
+ s.buildManualForm()
+ return s, s.manualForm.Init()
+ }
+ return s, nil
+
+ case tea.KeyPressMsg:
+ if msg.Code == tea.KeyEscape {
+ return s.handleEsc()
+ }
+
+ case snapshotsLoadedMsg:
+ return s.handleLoaded(msg)
+ }
+
+ // Delegate to the active phase.
+ switch s.phase {
+ case phaseSnapshotLoading:
+ return s.updateLoading(msg)
+ case phaseSnapshotSelecting:
+ return s.updateSelecting(msg)
+ case phaseSnapshotManual:
+ return s.updateManual(msg)
+ }
+
+ return s, nil
+}
+
+// handleEsc processes Esc across all phases.
+func (s *Snapshot) handleEsc() (ui.Screen, tea.Cmd) {
+ switch s.phase {
+ case phaseSnapshotLoading:
+ return s, ui.BackCmd
+ case phaseSnapshotSelecting:
+ return s, ui.BackCmd
+ case phaseSnapshotManual:
+ // If we came from the select phase (user chose manual entry),
+ // Esc should go back to the select. Otherwise, Esc backs out
+ // of the whole screen.
+ if s.selectForm != nil {
+ s.phase = phaseSnapshotSelecting
+ s.rebuildSelectForm()
+ return s, s.selectForm.Init()
+ }
+ return s, ui.BackCmd
+ }
+ return s, ui.BackCmd
+}
+
+// handleLoaded processes the async snapshot load result.
+func (s *Snapshot) handleLoaded(msg snapshotsLoadedMsg) (ui.Screen, tea.Cmd) {
+ // Ignore stale results from a prior generation.
+ if msg.gen != atomic.LoadInt64(&s.gen) {
+ return s, nil
+ }
+
+ if msg.err != nil {
+ if errors.Is(msg.err, restic.ErrNoRepo) {
+ // Silent fallback — no notice.
+ s.switchToManual("")
+ } else {
+ s.switchToManual(fmt.Sprintf("Could not list snapshots: %v", msg.err))
+ }
+ return s, s.manualForm.Init()
+ }
+
+ if len(msg.snapshots) == 0 {
+ s.switchToManual("No snapshots found in repository.")
+ return s, s.manualForm.Init()
+ }
+
+ s.buildSelectForm(msg.snapshots)
+ s.phase = phaseSnapshotSelecting
+ return s, s.selectForm.Init()
+}
+
+// updateLoading forwards messages to the spinner during the loading phase.
+func (s *Snapshot) updateLoading(msg tea.Msg) (ui.Screen, tea.Cmd) {
+ var cmd tea.Cmd
+ s.spinner, cmd = s.spinner.Update(msg)
+ return s, cmd
+}
+
+// updateSelecting forwards messages to the huh select form.
+func (s *Snapshot) updateSelecting(msg tea.Msg) (ui.Screen, tea.Cmd) {
+ if s.selectForm == nil {
+ return s, nil
+ }
+
+ model, cmd := s.selectForm.Update(msg)
+ if f, ok := model.(*huh.Form); ok {
+ s.selectForm = f
+ }
+
+ if s.selectForm.State == huh.StateCompleted {
+ if s.selected == manualEntryValue {
+ s.switchToManual("")
+ return s, s.manualForm.Init()
+ }
+ s.value = s.selected
+ s.selection = s.selected
+ return s, ui.DoneCmd
+ }
+
+ return s, cmd
+}
+
+// updateManual forwards messages to the huh manual input form.
+func (s *Snapshot) updateManual(msg tea.Msg) (ui.Screen, tea.Cmd) {
+ if s.manualForm == nil {
+ return s, nil
+ }
+
+ model, cmd := s.manualForm.Update(msg)
+ if f, ok := model.(*huh.Form); ok {
+ s.manualForm = f
+ }
+
+ if s.manualForm.State == huh.StateCompleted {
+ s.value = s.entered
+ s.selection = s.entered
+ return s, ui.DoneCmd
+ }
+
+ return s, cmd
+}
+
+// buildSelectForm constructs the huh Select from loaded snapshots.
+func (s *Snapshot) buildSelectForm(snapshots []restic.Snapshot) {
+ s.snapshots = snapshots
+ opts := make([]huh.Option[string], 0, len(snapshots)+1)
+ for _, snap := range snapshots {
+ opts = append(opts, huh.NewOption(restic.FormatSnapshotLine(snap), snap.ShortID))
+ }
+ opts = append(opts, huh.NewOption("Enter ID manually…", manualEntryValue))
+
+ s.selected = ""
+ sel := huh.NewSelect[string]().
+ Options(opts...).
+ Filtering(true).
+ Value(&s.selected)
+
+ s.selectForm = huh.NewForm(
+ huh.NewGroup(sel),
+ ).WithTheme(s.styles.Huh).WithShowHelp(false)
+}
+
+// rebuildSelectForm reconstructs the select form from the cached
+// snapshots. Called when navigating back from manual entry to
+// ensure the form has fresh internal state (filter, cursor, focus).
+func (s *Snapshot) rebuildSelectForm() {
+ if len(s.snapshots) > 0 {
+ s.buildSelectForm(s.snapshots)
+ }
+}
+
+// buildManualForm constructs the huh Input for manual snapshot ID entry.
+func (s *Snapshot) buildManualForm() {
+ s.entered = ""
+
+ input := huh.NewInput().
+ Title("Snapshot ID").
+ Description("Supports snapshotID:subfolder syntax, or \"latest\".").
+ Placeholder("e.g. latest or a1b2c3d4").
+ Value(&s.entered).
+ Validate(huh.ValidateNotEmpty())
+
+ s.manualForm = huh.NewForm(
+ huh.NewGroup(input),
+ ).WithTheme(s.styles.Huh).WithShowHelp(false)
+}
+
+// switchToManual transitions to the manual entry phase with an
+// optional notice message.
+func (s *Snapshot) switchToManual(notice string) {
+ s.phase = phaseSnapshotManual
+ s.notice = notice
+ s.buildManualForm()
+}
+
+// View renders the current phase.
+func (s *Snapshot) View() string {
+ switch s.phase {
+ case phaseSnapshotLoading:
+ return s.spinner.View() + " Loading snapshots…"
+ case phaseSnapshotSelecting:
+ if s.selectForm == nil {
+ return ""
+ }
+ return s.selectForm.View()
+ case phaseSnapshotManual:
+ var view string
+ if s.notice != "" {
+ view = s.notice + "\n\n"
+ }
+ if s.manualForm != nil {
+ view += s.manualForm.View()
+ }
+ return view
+ }
+ return ""
+}
+
+// Title returns the screen's display title.
+func (s *Snapshot) Title() string { return "Select a snapshot" }
+
+// KeyBindings returns bindings for the help bar.
+func (s *Snapshot) KeyBindings() []key.Binding {
+ switch s.phase {
+ case phaseSnapshotSelecting:
+ return []key.Binding{
+ key.NewBinding(key.WithKeys("↑/↓"), key.WithHelp("↑/↓", "navigate")),
+ key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")),
+ }
+ default:
+ return []key.Binding{
+ key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "confirm")),
+ }
+ }
+}
+
+// Selection returns the chosen snapshot ID for breadcrumb display,
+// or "" if nothing has been selected yet.
+func (s *Snapshot) Selection() string { return s.selection }
+
+// Value returns the resolved snapshot ID.
+func (s *Snapshot) Value() string { return s.value }
@@ -0,0 +1,372 @@
+package screens
+
+import (
+ "errors"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ tea "charm.land/bubbletea/v2"
+
+ "git.secluded.site/keld/internal/restic"
+ "git.secluded.site/keld/internal/ui"
+)
+
+func testSnapshots() []restic.Snapshot {
+ return []restic.Snapshot{
+ {
+ ID: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
+ ShortID: "abcdef01",
+ Time: time.Date(2026, 3, 15, 10, 30, 0, 0, time.UTC),
+ Hostname: "host1",
+ Paths: []string{"/home"},
+ Tags: []string{"daily"},
+ },
+ {
+ ID: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
+ ShortID: "12345678",
+ Time: time.Date(2026, 3, 14, 9, 0, 0, 0, time.UTC),
+ Hostname: "host2",
+ Paths: []string{"/etc"},
+ },
+ }
+}
+
+// drainSnapshot feeds commands back into the snapshot screen until a
+// DoneMsg or BackMsg is produced, or the command chain is exhausted.
+func drainSnapshot(s *Snapshot, initialCmd tea.Cmd) (*Snapshot, tea.Cmd) {
+ cmd := initialCmd
+ for cmd != nil {
+ msg := cmd()
+ if msg == nil {
+ return s, nil
+ }
+ switch msg.(type) {
+ case ui.DoneMsg:
+ return s, cmd
+ case ui.BackMsg:
+ return s, cmd
+ }
+ var screen ui.Screen
+ screen, cmd = s.Update(msg)
+ s = screen.(*Snapshot)
+ }
+ return s, nil
+}
+
+// simulateLoad sends a snapshotsLoadedMsg directly to the screen,
+// bypassing tea.Batch. This is how tests deliver async results —
+// in the real runtime, the Batch command produces this message.
+func simulateLoad(s *Snapshot, snaps []restic.Snapshot, err error) *Snapshot {
+ gen := atomic.LoadInt64(&s.gen)
+ msg := snapshotsLoadedMsg{gen: gen, snapshots: snaps, err: err}
+ screen, cmd := s.Update(msg)
+ s = screen.(*Snapshot)
+ // Drain any form init commands.
+ s, _ = drainSnapshot(s, cmd)
+ return s
+}
+
+// simulateStaleLoad sends a snapshotsLoadedMsg with an old generation,
+// simulating a result from a prior Init that should be ignored.
+func simulateStaleLoad(s *Snapshot, snaps []restic.Snapshot, err error, staleGen int64) *Snapshot {
+ msg := snapshotsLoadedMsg{gen: staleGen, snapshots: snaps, err: err}
+ screen, _ := s.Update(msg)
+ return screen.(*Snapshot)
+}
+
+func initSnapshot(s *Snapshot) *Snapshot {
+ s.Init()
+ screen, _ := s.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
+ return screen.(*Snapshot)
+}
+
+func TestSnapshotTitle(t *testing.T) {
+ t.Parallel()
+
+ s := NewSnapshot(nil, testStyles())
+ if got := s.Title(); got != "Select a snapshot" {
+ t.Errorf("Title() = %q, want %q", got, "Select a snapshot")
+ }
+}
+
+func TestSnapshotSelectionEmpty(t *testing.T) {
+ t.Parallel()
+
+ s := NewSnapshot(nil, testStyles())
+ if got := s.Selection(); got != "" {
+ t.Errorf("Selection() before interaction = %q, want empty", got)
+ }
+}
+
+func TestSnapshotKeyBindings(t *testing.T) {
+ t.Parallel()
+
+ s := NewSnapshot(nil, testStyles())
+ if len(s.KeyBindings()) == 0 {
+ t.Fatal("KeyBindings() returned no bindings")
+ }
+}
+
+func TestSnapshotLoadsAndPresentsSelect(t *testing.T) {
+ t.Parallel()
+
+ snaps := testSnapshots()
+ loader := func() ([]restic.Snapshot, error) { return snaps, nil }
+
+ s := NewSnapshot(loader, testStyles())
+ s = initSnapshot(s)
+ s = simulateLoad(s, snaps, nil)
+
+ // Should now be in the selecting phase. Press enter to pick first.
+ screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+ s, cmd = drainSnapshot(screen.(*Snapshot), cmd)
+
+ if cmd == nil {
+ t.Fatal("expected DoneCmd after selecting a snapshot")
+ }
+ if _, ok := cmd().(ui.DoneMsg); !ok {
+ t.Errorf("cmd produced %T, want ui.DoneMsg", cmd())
+ }
+
+ if got := s.Value(); got != "abcdef01" {
+ t.Errorf("Value() = %q, want %q", got, "abcdef01")
+ }
+}
+
+func TestSnapshotSelectionShowsShortID(t *testing.T) {
+ t.Parallel()
+
+ snaps := testSnapshots()
+ loader := func() ([]restic.Snapshot, error) { return snaps, nil }
+
+ s := NewSnapshot(loader, testStyles())
+ s = initSnapshot(s)
+ s = simulateLoad(s, snaps, nil)
+
+ // Select first snapshot.
+ screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+ s, _ = drainSnapshot(screen.(*Snapshot), cmd)
+
+ if got := s.Selection(); got != "abcdef01" {
+ t.Errorf("Selection() = %q, want %q", got, "abcdef01")
+ }
+}
+
+func TestSnapshotErrorFallsBackToManual(t *testing.T) {
+ t.Parallel()
+
+ netErr := errors.New("network error")
+ loader := func() ([]restic.Snapshot, error) { return nil, netErr }
+
+ s := NewSnapshot(loader, testStyles())
+ s = initSnapshot(s)
+ s = simulateLoad(s, nil, netErr)
+
+ // Should be in manual entry phase with error notice.
+ if s.notice == "" {
+ t.Error("notice should be set on error")
+ }
+
+ // Type a snapshot ID and submit.
+ for _, ch := range "latest" {
+ screen, _ := s.Update(tea.KeyPressMsg{Code: ch, Text: string(ch)})
+ s = screen.(*Snapshot)
+ }
+
+ screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+ s, cmd = drainSnapshot(screen.(*Snapshot), cmd)
+
+ if cmd == nil {
+ t.Fatal("expected DoneCmd after manual entry")
+ }
+ if _, ok := cmd().(ui.DoneMsg); !ok {
+ t.Errorf("cmd produced %T, want ui.DoneMsg", cmd())
+ }
+
+ if got := s.Value(); got != "latest" {
+ t.Errorf("Value() = %q, want %q", got, "latest")
+ }
+}
+
+func TestSnapshotNoRepoFallsBackSilently(t *testing.T) {
+ t.Parallel()
+
+ loader := func() ([]restic.Snapshot, error) { return nil, restic.ErrNoRepo }
+
+ s := NewSnapshot(loader, testStyles())
+ s = initSnapshot(s)
+ s = simulateLoad(s, nil, restic.ErrNoRepo)
+
+ // Should be in manual entry phase with no error notice.
+ if s.notice != "" {
+ t.Errorf("notice = %q, want empty for ErrNoRepo (silent fallback)", s.notice)
+ }
+
+ // Type and submit.
+ for _, ch := range "abc123" {
+ screen, _ := s.Update(tea.KeyPressMsg{Code: ch, Text: string(ch)})
+ s = screen.(*Snapshot)
+ }
+ screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+ s, cmd = drainSnapshot(screen.(*Snapshot), cmd)
+
+ if cmd == nil {
+ t.Fatal("expected DoneCmd after manual entry")
+ }
+ if got := s.Value(); got != "abc123" {
+ t.Errorf("Value() = %q, want %q", got, "abc123")
+ }
+}
+
+func TestSnapshotEmptyRepoFallsBackWithNotice(t *testing.T) {
+ t.Parallel()
+
+ loader := func() ([]restic.Snapshot, error) { return []restic.Snapshot{}, nil }
+
+ s := NewSnapshot(loader, testStyles())
+ s = initSnapshot(s)
+ s = simulateLoad(s, []restic.Snapshot{}, nil)
+
+ // Should show a notice about empty repo.
+ if s.notice == "" {
+ t.Error("notice should be set when repository has no snapshots")
+ }
+}
+
+func TestSnapshotNilLoaderGoesDirectToManual(t *testing.T) {
+ t.Parallel()
+
+ s := NewSnapshot(nil, testStyles())
+ cmd := s.Init()
+ screen, _ := s.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
+ s = screen.(*Snapshot)
+
+ // Drain any init commands from the manual form.
+ s, _ = drainSnapshot(s, cmd)
+
+ // With no loader, should start in manual entry immediately.
+ for _, ch := range "latest" {
+ screen, _ := s.Update(tea.KeyPressMsg{Code: ch, Text: string(ch)})
+ s = screen.(*Snapshot)
+ }
+ screen, cmd = s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+ s, cmd = drainSnapshot(screen.(*Snapshot), cmd)
+
+ if cmd == nil {
+ t.Fatal("expected DoneCmd after manual entry")
+ }
+ if got := s.Value(); got != "latest" {
+ t.Errorf("Value() = %q, want %q", got, "latest")
+ }
+}
+
+func TestSnapshotManualEntryFromSelect(t *testing.T) {
+ t.Parallel()
+
+ snaps := testSnapshots()
+ loader := func() ([]restic.Snapshot, error) { return snaps, nil }
+
+ s := NewSnapshot(loader, testStyles())
+ s = initSnapshot(s)
+ s = simulateLoad(s, snaps, nil)
+
+ // Navigate down to the "Enter ID manually…" option (last item,
+ // 2 snapshots + 1 manual = index 2, so press down twice).
+ screen, _ := s.Update(tea.KeyPressMsg{Code: tea.KeyDown})
+ s = screen.(*Snapshot)
+ screen, _ = s.Update(tea.KeyPressMsg{Code: tea.KeyDown})
+ s = screen.(*Snapshot)
+
+ // Select the manual entry option.
+ screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+ s, _ = drainSnapshot(screen.(*Snapshot), cmd)
+
+ // Should now be in manual entry phase. Type an ID.
+ for _, ch := range "abc123:subfolder" {
+ screen, _ := s.Update(tea.KeyPressMsg{Code: ch, Text: string(ch)})
+ s = screen.(*Snapshot)
+ }
+ screen, cmd = s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+ s, cmd = drainSnapshot(screen.(*Snapshot), cmd)
+
+ if cmd == nil {
+ t.Fatal("expected DoneCmd after manual entry from select")
+ }
+ if got := s.Value(); got != "abc123:subfolder" {
+ t.Errorf("Value() = %q, want %q", got, "abc123:subfolder")
+ }
+}
+
+func TestSnapshotEscDuringLoadingReturnsBack(t *testing.T) {
+ t.Parallel()
+
+ loader := func() ([]restic.Snapshot, error) { return nil, nil }
+
+ s := NewSnapshot(loader, testStyles())
+ s = initSnapshot(s)
+
+ // Press Esc during loading phase.
+ _, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEscape})
+
+ if cmd == nil {
+ t.Fatal("expected BackCmd on Esc during loading")
+ }
+ if _, ok := cmd().(ui.BackMsg); !ok {
+ t.Errorf("cmd produced %T, want ui.BackMsg", cmd())
+ }
+}
+
+func TestSnapshotEscDuringSelectReturnsBack(t *testing.T) {
+ t.Parallel()
+
+ snaps := testSnapshots()
+ loader := func() ([]restic.Snapshot, error) { return snaps, nil }
+
+ s := NewSnapshot(loader, testStyles())
+ s = initSnapshot(s)
+ s = simulateLoad(s, snaps, nil)
+
+ // Esc during selection should back out.
+ _, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEscape})
+
+ if cmd == nil {
+ t.Fatal("expected BackCmd on Esc during selection")
+ }
+ if _, ok := cmd().(ui.BackMsg); !ok {
+ t.Errorf("cmd produced %T, want ui.BackMsg", cmd())
+ }
+}
+
+func TestSnapshotStaleLoadIgnored(t *testing.T) {
+ t.Parallel()
+
+ snaps := testSnapshots()
+ loader := func() ([]restic.Snapshot, error) { return snaps, nil }
+
+ s := NewSnapshot(loader, testStyles())
+ s = initSnapshot(s)
+
+ // Record the current generation, then re-init to bump it.
+ staleGen := atomic.LoadInt64(&s.gen)
+ s.Init()
+
+ // Deliver a result with the stale generation. Should be ignored.
+ s = simulateStaleLoad(s, snaps, nil, staleGen)
+
+ // Should still be loading (stale result ignored).
+ if s.phase != phaseSnapshotLoading {
+ t.Fatalf("phase = %d, want %d (phaseSnapshotLoading) after stale result", s.phase, phaseSnapshotLoading)
+ }
+
+ // Deliver with the current generation.
+ s = simulateLoad(s, snaps, nil)
+
+ // Now should be in selecting phase.
+ screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+ _, cmd = drainSnapshot(screen.(*Snapshot), cmd)
+
+ if cmd == nil {
+ t.Fatal("expected DoneCmd after selecting from fresh load")
+ }
+}