v0.6.0 7d47882

interactive.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"errors"
6	"fmt"
7
8	"github.com/bnema/nefercap/internal/adapters/cli"
9	"github.com/bnema/nefercap/internal/adapters/config"
10	"github.com/bnema/nefercap/internal/adapters/control"
11	"github.com/bnema/nefercap/internal/adapters/destination"
12	"github.com/bnema/nefercap/internal/adapters/selection"
13	"github.com/bnema/nefercap/internal/adapters/uierrors"
14	"github.com/bnema/nefercap/internal/adapters/wayland"
15	"github.com/bnema/nefercap/internal/ports"
16)
17
18// chooseInteractive offers workspaces only when the compositor lists them.
19// Monitor and region captures work with the standard protocols.
20func chooseInteractive(ctx context.Context, source *wayland.Source, outputs []ports.Output, options cli.Options, settings config.Settings) (ports.Selection, bool, error) {
21	mode := ports.Screenshot
22	if options.Command == cli.Rec {
23		mode = ports.Record
24	}
25	if options.Output != "" {
26		chosen, err := resolveOutput(outputs, options.Output)
27		if err != nil {
28			return ports.Selection{}, false, err
29		}
30		outputs = []ports.Output{chosen}
31	}
32	workspaces, err := source.Workspaces(ctx)
33	if err != nil {
34		if ctx.Err() != nil && uierrors.IsCancellation(err) {
35			return ports.Selection{}, false, ctx.Err()
36		}
37		return ports.Selection{}, false, fmt.Errorf("list workspace capture targets: %w", err)
38	}
39	if err := checkDirs(options, settings); err != nil {
40		return ports.Selection{}, false, err
41	}
42	selector := selection.New(mode, options.Video, workspaces, settings.Grid).WithCapabilities(source.Capabilities())
43	if options.Command == cli.AllInOne {
44		selector.AllowToggle()
45	}
46	if freezes(options, settings) {
47		frames, err := source.Freeze(ctx, frozenOutputs(outputs))
48		if err != nil {
49			return ports.Selection{}, false, err
50		}
51		if _, err := selector.WithBackdrops(frames); err != nil {
52			return ports.Selection{}, false, err
53		}
54	}
55	sel, accepted, err := selector.Select(ctx, outputs)
56	if err != nil || !accepted {
57		return ports.Selection{}, false, err
58	}
59	if sel.Mode == ports.Record || !selector.Frozen() {
60		source.Thaw() // a recording, or F in the selector, captures the live screen
61	} else {
62		source.Thaw(sel.Target.OutputID) // only the chosen output is captured
63	}
64	sel.Duration = options.Duration
65	if err := assignDestination(&sel, options, settings); err != nil {
66		return ports.Selection{}, false, err
67	}
68	return sel, true, nil
69}
70
71// freezes reports whether the selector shows, and a screenshot captures, a
72// still copy of the screen. rec always records the live screen; all-in-one
73// shows the copy only in screenshot mode and thaws for a recording.
74func freezes(options cli.Options, settings config.Settings) bool {
75	return settings.Freeze && (options.Command == cli.Shot || options.Command == cli.AllInOne)
76}
77
78// frozenOutputs lists the outputs that can show a frozen screen; larger ones
79// stay live, in the selector and in the screenshot.
80func frozenOutputs(outputs []ports.Output) []uint32 {
81	var ids []uint32
82	for _, o := range outputs {
83		if selection.CanBackdrop(o) {
84			ids = append(ids, o.ID)
85		}
86	}
87	return ids
88}
89
90// checkDirs creates and checks the default directories a confirmed capture
91// can use, so a bad screenshot.dir or video.dir fails before the selector
92// opens. All-in-one can reach both modes.
93func checkDirs(options cli.Options, settings config.Settings) error {
94	if options.Path != "" {
95		return nil
96	}
97	all := options.Command == cli.AllInOne
98	if all || options.Command == cli.Rec {
99		if _, err := destination.VideoIn(settings.VideoDir); err != nil {
100			return fmt.Errorf("video.dir: %w", err)
101		}
102	}
103	if (all || options.Command == cli.Shot) && !options.Clipboard && settings.Output != config.OutputClipboard {
104		if _, err := destination.ScreenshotIn(settings.ScreenshotDir); err != nil {
105			return fmt.Errorf("screenshot.dir: %w", err)
106		}
107	}
108	return nil
109}
110
111// assignDestination resolves where an interactive capture goes. Flags win over
112// the config: -file alone is a file only, -clipboard alone is clipboard only,
113// both save and copy. Without flags a screenshot follows screenshot.output.
114// Recordings always write a file; only screenshots are copied.
115func assignDestination(sel *ports.Selection, options cli.Options, settings config.Settings) (err error) {
116	if sel.Mode == ports.Record {
117		if sel.Path = options.Path; sel.Path == "" {
118			sel.Path, err = destination.VideoIn(settings.VideoDir)
119		}
120		return err
121	}
122	if options.Path != "" || options.Clipboard {
123		sel.Path, sel.Clipboard = options.Path, options.Clipboard
124		return nil
125	}
126	sel.Clipboard = settings.Output != config.OutputFile
127	if settings.Output != config.OutputClipboard {
128		sel.Path, err = destination.ScreenshotIn(settings.ScreenshotDir)
129	}
130	return err
131}
132
133func rejectCompetingSelector(ctx context.Context) error {
134	active, err := control.Query(ctx)
135	if err != nil {
136		return err
137	}
138	if active {
139		// A selector on another capture connection is not part of the
140		// recording's excluded set: it would show up in the video.
141		return errors.New("recording is active; stop it before opening a capture selector")
142	}
143	return nil
144}

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