v0.45.0 cb619f7

spinner.go RawHistoryFolder

1package modes
2
3import (
4	"time"
5
6	"github.com/bnema/zut/packages/tui"
7)
8
9// spinner drives the busy animation shown while an operation is active. The
10// operation label is owned by the caller's activity state; the spinner owns
11// only the visual frame and elapsed time.
12type spinner struct {
13	frames    []string
14	interval  time.Duration
15	startedAt time.Time
16}
17
18// newSpinner constructs a fresh spinner.
19func newSpinner(th tui.Theme) *spinner {
20	s := &spinner{}
21	s.Configure(th)
22	return s
23}
24
25func (s *spinner) Configure(th tui.Theme) {
26	s.frames = append([]string(nil), th.SpinnerFrames...)
27	if len(s.frames) == 0 {
28		s.frames = []string{"⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"}
29	}
30	interval := th.SpinnerIntervalMS
31	if interval <= 0 {
32		interval = 80
33	}
34	s.interval = time.Duration(interval) * time.Millisecond
35}
36
37// Start resets the animation clock for a new operation.
38func (s *spinner) Start() {
39	s.startedAt = time.Now()
40}
41
42// Frame returns the current spinner glyph for the running animation.
43func (s *spinner) Frame() string {
44	if len(s.frames) == 0 {
45		return ""
46	}
47	if s.startedAt.IsZero() {
48		return s.frames[0]
49	}
50	interval := s.interval
51	if interval <= 0 {
52		interval = 80 * time.Millisecond
53	}
54	elapsed := time.Since(s.startedAt)
55	idx := int(elapsed/interval) % len(s.frames)
56	return s.frames[idx]
57}
58
59// FrameAt returns a frame based on an absolute clock. Independent background
60// activity uses it so its animation does not need to reset or interfere with
61// the main turn's spinner lifecycle.
62func (s *spinner) FrameAt(now time.Time) string {
63	if len(s.frames) == 0 {
64		return ""
65	}
66	if now.IsZero() {
67		now = time.Now()
68	}
69	interval := s.interval
70	if interval <= 0 {
71		interval = 80 * time.Millisecond
72	}
73	ticks := now.UnixNano() / int64(interval)
74	idx := int(ticks % int64(len(s.frames)))
75	if idx < 0 {
76		idx += len(s.frames)
77	}
78	return s.frames[idx]
79}
80
81// Elapsed returns the wall-clock duration the spinner has been running.
82func (s *spinner) Elapsed() time.Duration {
83	if s.startedAt.IsZero() {
84		return 0
85	}
86	return time.Since(s.startedAt).Round(time.Second)
87}

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 1

Tags 476

↑↓ moveenter openesc close