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 {
60 source.Thaw() // a recording 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}