v0.2.0 a558f49

app.go RawHistoryFolder

1// Package app wires the capture adapters to the core workflow.
2package app
3
4import (
5	"context"
6	"errors"
7	"fmt"
8	"io"
9	"time"
10
11	"github.com/bnema/nefercap/internal/adapters/cli"
12	"github.com/bnema/nefercap/internal/adapters/clipboard"
13	"github.com/bnema/nefercap/internal/adapters/config"
14	"github.com/bnema/nefercap/internal/adapters/png"
15	"github.com/bnema/nefercap/internal/adapters/wayland"
16	"github.com/bnema/nefercap/internal/core"
17	"github.com/bnema/nefercap/internal/logging"
18	"github.com/bnema/nefercap/internal/ports"
19)
20
21const discoveryTimeout = 5 * time.Second
22
23// Run owns the source connection and all capture work for one invocation.
24func Run(ctx context.Context, options cli.Options, output io.Writer) (err error) {
25	if handled, controlErr := controlCommand(ctx, options.Command, output); handled {
26		return controlErr
27	}
28	discovery, cancel := context.WithTimeout(ctx, discoveryTimeout)
29	source, err := wayland.New(discovery, "")
30	if err != nil {
31		cancel()
32		return discoveryError(ctx, "connect Wayland", err)
33	}
34	defer func() {
35		if closeErr := source.Close(); closeErr != nil {
36			err = errors.Join(err, closeErr)
37		}
38	}()
39	outputs, err := source.Outputs(discovery)
40	cancel()
41	if err != nil {
42		return discoveryError(ctx, "list outputs", err)
43	}
44	if options.Command == cli.Outputs {
45		for _, item := range outputs {
46			if _, err := fmt.Fprintf(output, "%s\t%dx%d\tscale=%d\n", item.Name, item.Width, item.Height, item.Scale); err != nil {
47				return err
48			}
49		}
50		return nil
51	}
52	var selection ports.Selection
53	if options.Command == cli.Shot || options.Command == cli.Rec || options.Command == cli.AllInOne {
54		// Fail on a bad config before anything is shown.
55		settings, loadErr := config.Load()
56		if loadErr != nil {
57			return fmt.Errorf("load config: %w", loadErr)
58		}
59		if err := rejectCompetingSelector(ctx); err != nil {
60			return err
61		}
62		var accepted bool
63		selection, accepted, err = chooseInteractive(ctx, source, outputs, options, settings)
64		if err != nil || !accepted {
65			return err
66		}
67	} else {
68		target, err := resolveOutput(outputs, options.Output)
69		if err != nil {
70			return err
71		}
72		selection = ports.Selection{Target: ports.Target{OutputID: target.ID, Region: options.Region}, Path: options.Path, Clipboard: options.Clipboard, Video: options.Video, Duration: options.Duration}
73		switch options.Command {
74		case cli.Screenshot:
75			selection.Mode = ports.Screenshot
76		case cli.Record:
77			selection.Mode = ports.Record
78		default:
79			return fmt.Errorf("unsupported command %q", options.Command)
80		}
81	}
82	log := logging.For(ctx, "app")
83	log.Info().Str("mode", string(selection.Mode)).Msg("capture started")
84	var saved bool
85	if selection.Mode == ports.Record {
86		var outcome recordingOutcome
87		outcome, err = recordInteractive(ctx, output, source, outputs, selection)
88		saved = outcome.Saved
89	} else {
90		var writer ports.ScreenshotWriter = png.New()
91		var copied *clipboard.Writer
92		if selection.Clipboard {
93			copied = clipboard.NewWriter(writer, clipboard.New())
94			writer = copied
95		}
96		err = core.New(source, writer, nil).Run(ctx, selection)
97		saved = err == nil && selection.Path != ""
98		if copied != nil {
99			saved = copied.Saved
100		}
101	}
102	if saved && err == nil {
103		log.Info().Msg("capture complete")
104	}
105	return finishCapture(output, selection.Path, saved, err)
106}
107
108// finishCapture prints the path of a file that exists, whatever error the
109// capture ended with, so a reported failure never hides where the file is.
110// Nothing is printed when no file was saved. The original error and a failed
111// write are both returned.
112func finishCapture(output io.Writer, path string, saved bool, err error) error {
113	if !saved {
114		return err
115	}
116	if _, printErr := fmt.Fprintln(output, path); printErr != nil {
117		return errors.Join(err, printErr)
118	}
119	return err
120}
121
122// discoveryError preserves a user interrupt, but reports local timeouts and
123// actual protocol failures with the failing phase.
124func discoveryError(ctx context.Context, phase string, err error) error {
125	if ctx.Err() != nil && err == ctx.Err() {
126		return err
127	}
128	return fmt.Errorf("%s: %w", phase, err)
129}
130
131func resolveOutput(outputs []ports.Output, name string) (ports.Output, error) {
132	if name == "" {
133		if len(outputs) == 1 {
134			return outputs[0], nil
135		}
136		return ports.Output{}, fmt.Errorf("select an output with -output (use outputs to list names)")
137	}
138	for _, output := range outputs {
139		if output.Name == name {
140			return output, nil
141		}
142	}
143	return ports.Output{}, fmt.Errorf("%w: %q", ports.ErrOutputNotFound, name)
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