v0.26.0 ae3a77a

service.go RawHistoryFolder

1package snapshot
2
3import (
4	"context"
5	"sync"
6	"time"
7
8	"github.com/bnema/dumber/internal/application/port"
9	"github.com/bnema/dumber/internal/application/usecase"
10	"github.com/bnema/dumber/internal/logging"
11)
12
13// Service handles debounced session state snapshots.
14type Service struct {
15	snapshotUC *usecase.SnapshotSessionUseCase
16	provider   port.TabListProvider
17	interval   time.Duration
18
19	mu     sync.Mutex
20	timer  *time.Timer
21	dirty  bool
22	ready  bool // true when session is persisted to DB and snapshots can be saved
23	ctx    context.Context
24	cancel context.CancelFunc
25}
26
27// NewService creates a new snapshot service.
28func NewService(
29	snapshotUC *usecase.SnapshotSessionUseCase,
30	provider port.TabListProvider,
31	intervalMs int,
32) *Service {
33	if intervalMs <= 0 {
34		intervalMs = 5000 // Default 5 seconds
35	}
36	return &Service{
37		snapshotUC: snapshotUC,
38		provider:   provider,
39		interval:   time.Duration(intervalMs) * time.Millisecond,
40	}
41}
42
43// Start begins watching for dirty state.
44func (s *Service) Start(ctx context.Context) {
45	s.mu.Lock()
46	defer s.mu.Unlock()
47
48	s.ctx, s.cancel = context.WithCancel(ctx)
49	logging.FromContext(ctx).Debug().Dur("interval", s.interval).Msg("snapshot service started")
50}
51
52// SetReady marks the service as ready to save snapshots.
53// Call this after the session has been persisted to the database
54// to avoid FK constraint violations.
55func (s *Service) SetReady() {
56	s.mu.Lock()
57	defer s.mu.Unlock()
58	s.ready = true
59}
60
61// Stop stops the service and saves final state.
62func (s *Service) Stop(ctx context.Context) error {
63	s.mu.Lock()
64	if s.cancel != nil {
65		s.cancel()
66	}
67	if s.timer != nil {
68		s.timer.Stop()
69		s.timer = nil
70	}
71	s.mu.Unlock()
72
73	// Final save on shutdown
74	return s.SaveNow(ctx)
75}
76
77// MarkDirty signals that state has changed.
78// Debounces saves to avoid excessive DB writes.
79func (s *Service) MarkDirty() {
80	s.mu.Lock()
81	defer s.mu.Unlock()
82
83	s.dirty = true
84
85	// Reset or create timer
86	if s.timer != nil {
87		s.timer.Stop()
88	}
89
90	s.timer = time.AfterFunc(s.interval, func() {
91		s.mu.Lock()
92		ctx := s.ctx
93		s.mu.Unlock()
94
95		if ctx == nil {
96			return
97		}
98
99		if err := s.saveSnapshot(ctx); err != nil {
100			logging.FromContext(ctx).Error().Err(err).Msg("failed to save session snapshot")
101		}
102	})
103}
104
105// SaveNow forces immediate save (for shutdown).
106func (s *Service) SaveNow(ctx context.Context) error {
107	s.mu.Lock()
108	if s.timer != nil {
109		s.timer.Stop()
110		s.timer = nil
111	}
112	dirty := s.dirty
113	s.mu.Unlock()
114
115	if !dirty {
116		return nil
117	}
118
119	return s.saveSnapshot(ctx)
120}
121
122func (s *Service) saveSnapshot(ctx context.Context) error {
123	s.mu.Lock()
124	ready := s.ready
125	if !ready {
126		// Don't clear dirty if not ready - keep pending snapshot for later
127		s.mu.Unlock()
128		return nil
129	}
130	// Only clear dirty when we're actually going to save
131	s.dirty = false
132	s.mu.Unlock()
133
134	tabList := s.provider.GetTabList()
135	sessionID := s.provider.GetSessionID()
136
137	if sessionID == "" {
138		return nil
139	}
140
141	return s.snapshotUC.Execute(ctx, usecase.SnapshotInput{
142		SessionID: sessionID,
143		TabList:   tabList,
144	})
145}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close