v0.23.1 6c872d5

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	ctx    context.Context
23	cancel context.CancelFunc
24}
25
26// NewService creates a new snapshot service.
27func NewService(
28	snapshotUC *usecase.SnapshotSessionUseCase,
29	provider port.TabListProvider,
30	intervalMs int,
31) *Service {
32	if intervalMs <= 0 {
33		intervalMs = 5000 // Default 5 seconds
34	}
35	return &Service{
36		snapshotUC: snapshotUC,
37		provider:   provider,
38		interval:   time.Duration(intervalMs) * time.Millisecond,
39	}
40}
41
42// Start begins watching for dirty state.
43func (s *Service) Start(ctx context.Context) {
44	s.mu.Lock()
45	defer s.mu.Unlock()
46
47	s.ctx, s.cancel = context.WithCancel(ctx)
48	logging.FromContext(ctx).Debug().Dur("interval", s.interval).Msg("snapshot service started")
49}
50
51// Stop stops the service and saves final state.
52func (s *Service) Stop(ctx context.Context) error {
53	s.mu.Lock()
54	if s.cancel != nil {
55		s.cancel()
56	}
57	if s.timer != nil {
58		s.timer.Stop()
59		s.timer = nil
60	}
61	s.mu.Unlock()
62
63	// Final save on shutdown
64	return s.SaveNow(ctx)
65}
66
67// MarkDirty signals that state has changed.
68// Debounces saves to avoid excessive DB writes.
69func (s *Service) MarkDirty() {
70	s.mu.Lock()
71	defer s.mu.Unlock()
72
73	s.dirty = true
74
75	// Reset or create timer
76	if s.timer != nil {
77		s.timer.Stop()
78	}
79
80	s.timer = time.AfterFunc(s.interval, func() {
81		s.mu.Lock()
82		ctx := s.ctx
83		s.mu.Unlock()
84
85		if ctx == nil {
86			return
87		}
88
89		if err := s.saveSnapshot(ctx); err != nil {
90			logging.FromContext(ctx).Error().Err(err).Msg("failed to save session snapshot")
91		}
92	})
93}
94
95// SaveNow forces immediate save (for shutdown).
96func (s *Service) SaveNow(ctx context.Context) error {
97	s.mu.Lock()
98	if s.timer != nil {
99		s.timer.Stop()
100		s.timer = nil
101	}
102	dirty := s.dirty
103	s.mu.Unlock()
104
105	if !dirty {
106		return nil
107	}
108
109	return s.saveSnapshot(ctx)
110}
111
112func (s *Service) saveSnapshot(ctx context.Context) error {
113	s.mu.Lock()
114	s.dirty = false
115	s.mu.Unlock()
116
117	tabList := s.provider.GetTabList()
118	sessionID := s.provider.GetSessionID()
119
120	if sessionID == "" {
121		return nil
122	}
123
124	return s.snapshotUC.Execute(ctx, usecase.SnapshotInput{
125		SessionID: sessionID,
126		TabList:   tabList,
127	})
128}

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