main 3d41bc5

flash.go RawHistoryFolder

1// Package flash confirms a saved screenshot: a green frame covers the captured
2// area and fades out in about 300 ms. It is drawn only after the capture, so it
3// never appears in the image. The surface takes no keyboard focus and lets
4// every click through.
5package flash
6
7import (
8	"context"
9	_ "embed"
10	"errors"
11	"fmt"
12	"sync"
13	"time"
14
15	"github.com/bnema/neferclient"
16	"github.com/bnema/nefergui"
17
18	"github.com/bnema/nefercap/internal/adapters/layerui"
19	"github.com/bnema/nefercap/internal/adapters/uierrors"
20	"github.com/bnema/nefercap/internal/adapters/uistyle"
21	"github.com/bnema/nefercap/internal/ports"
22)
23
24//go:embed style.css
25var styleSheet string
26
27const (
28	namespace = "nefercap-flash"
29	step      = 30 * time.Millisecond
30	// Limit bounds the whole effect, setup included, so a slow compositor or
31	// GPU never keeps the process alive.
32	Limit = 2 * time.Second
33)
34
35// stepClasses are the opacity steps of style.css, from opaque to faint.
36var stepClasses = [...]string{"s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9"}
37
38var errClosed = errors.New("flash closed by the compositor")
39
40// Show flashes area on output and returns when the fade ends. The zero area is
41// the whole output. It returns nil when the fade completed or ctx ended.
42func Show(ctx context.Context, output ports.Output, area ports.Region) error {
43	if output.Name == "" {
44		return fmt.Errorf("flash: output %d has no name: %w", output.ID, ports.ErrOutputNotFound)
45	}
46	if err := show(ctx, output, area); err != nil {
47		return fmt.Errorf("flash: output %s: %w", output.Name, err)
48	}
49	return nil
50}
51
52func show(ctx context.Context, output ports.Output, area ports.Region) error {
53	sheet, remove, err := uistyle.Write(styleSheet, "nefercap-flash-*.css")
54	if err != nil {
55		return err
56	}
57	defer remove()
58
59	child, cancel := context.WithTimeout(ctx, Limit)
60	wake := make(chan struct{}, 1)
61	var wg sync.WaitGroup
62	wg.Go(func() { ticker(child, wake) })
63	defer func() { cancel(); wg.Wait() }()
64
65	m := &fade{done: cancel}
66	runErr := layerui.Run(child, layerui.Config{
67		Layer:  layerConfig(output.Name, area),
68		Styles: sheet,
69		Wake:   wake,
70	}, m, view)
71	// The fade, the time limit or the parent ended the run on purpose; only
72	// an end with neither is the compositor closing the surface.
73	explained := m.ended || child.Err() != nil
74	return uierrors.Ended(runErr, explained, ctx.Err(), errClosed)
75}
76
77// layerConfig covers area, or the whole output for the zero area, above every
78// window, without focus or pointer input.
79func layerConfig(output string, area ports.Region) neferclient.LayerConfig {
80	c := neferclient.LayerConfig{
81		Output:        output,
82		Namespace:     namespace,
83		Level:         neferclient.LayerOverlay,
84		Anchors:       neferclient.AnchorTop | neferclient.AnchorBottom | neferclient.AnchorLeft | neferclient.AnchorRight,
85		Keyboard:      neferclient.KeyboardNone,
86		ExclusiveZone: -1,
87		InputRects:    []neferclient.Rect{},
88	}
89	if area.Width > 0 && area.Height > 0 {
90		c.Anchors = neferclient.AnchorTop | neferclient.AnchorLeft
91		c.Margin = [4]int32{int32(area.Y), 0, 0, int32(area.X)}
92		c.Width, c.Height = int32(area.Width), int32(area.Height)
93	}
94	return c
95}
96
97// ticker sends a non-blocking wake token every step until ctx ends.
98func ticker(ctx context.Context, wake chan<- struct{}) {
99	t := time.NewTicker(step)
100	defer t.Stop()
101	for {
102		select {
103		case <-ctx.Done():
104			return
105		case <-t.C:
106			select {
107			case wake <- struct{}{}:
108			default:
109			}
110		}
111	}
112}
113
114// fade is the view model. It is used only on the layerui owner goroutine.
115type fade struct {
116	start time.Time // first frame; zero until then
117	ended bool
118	done  func()
119}
120
121// class returns the opacity class for now. The fade starts with the first
122// frame, so a slow setup does not eat into it. Past the last step it ends the
123// run and keeps the faintest class.
124func (f *fade) class(now time.Time) string {
125	if f.start.IsZero() {
126		f.start = now
127	}
128	i := int(now.Sub(f.start) / step)
129	if i >= len(stepClasses) {
130		if !f.ended {
131			f.ended = true
132			f.done()
133		}
134		i = len(stepClasses) - 1
135	}
136	return stepClasses[max(i, 0)]
137}
138
139func view(f *nefergui.Frame, m *fade) {
140	f.Root().Box(nefergui.Key("flash"), nefergui.Class("flash"), nefergui.Class(m.class(time.Now())))
141}

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 2

Tags 9

↑↓ moveenter openesc close