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 sel, accepted, err := selector.Select(ctx, outputs)
47 if err != nil || !accepted {
48 return ports.Selection{}, false, err
49 }
50 sel.Duration = options.Duration
51 if err := assignDestination(&sel, options, settings); err != nil {
52 return ports.Selection{}, false, err
53 }
54 return sel, true, nil
55}
56
57// checkDirs creates and checks the default directories a confirmed capture
58// can use, so a bad screenshot.dir or video.dir fails before the selector
59// opens. All-in-one can reach both modes.
60func checkDirs(options cli.Options, settings config.Settings) error {
61 if options.Path != "" {
62 return nil
63 }
64 all := options.Command == cli.AllInOne
65 if all || options.Command == cli.Rec {
66 if _, err := destination.VideoIn(settings.VideoDir); err != nil {
67 return fmt.Errorf("video.dir: %w", err)
68 }
69 }
70 if (all || options.Command == cli.Shot) && !options.Clipboard && settings.Output != config.OutputClipboard {
71 if _, err := destination.ScreenshotIn(settings.ScreenshotDir); err != nil {
72 return fmt.Errorf("screenshot.dir: %w", err)
73 }
74 }
75 return nil
76}
77
78// assignDestination resolves where an interactive capture goes. Flags win over
79// the config: -file alone is a file only, -clipboard alone is clipboard only,
80// both save and copy. Without flags a screenshot follows screenshot.output.
81// Recordings always write a file; only screenshots are copied.
82func assignDestination(sel *ports.Selection, options cli.Options, settings config.Settings) (err error) {
83 if sel.Mode == ports.Record {
84 if sel.Path = options.Path; sel.Path == "" {
85 sel.Path, err = destination.VideoIn(settings.VideoDir)
86 }
87 return err
88 }
89 if options.Path != "" || options.Clipboard {
90 sel.Path, sel.Clipboard = options.Path, options.Clipboard
91 return nil
92 }
93 sel.Clipboard = settings.Output != config.OutputFile
94 if settings.Output != config.OutputClipboard {
95 sel.Path, err = destination.ScreenshotIn(settings.ScreenshotDir)
96 }
97 return err
98}
99
100func rejectCompetingSelector(ctx context.Context) error {
101 active, err := control.Query(ctx)
102 if err != nil {
103 return err
104 }
105 if active {
106 // A selector on another capture connection is not part of the
107 // recording's excluded set: it would show up in the video.
108 return errors.New("recording is active; stop it before opening a capture selector")
109 }
110 return nil
111}