1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
use std::{env, path::PathBuf};
use crate::cli::Cli;
use crate::config::{self, Config, Geometry};
use crate::screenshot::CaptureMode;
use crate::utils::{self, EncodingFormat};
// ─── Command ──────────────────────────────────────────────────────────────────
/// The top-level operation to perform, fully resolved from CLI + config.
pub(crate) enum Command {
/// Print the names of all connected outputs and exit.
ListOutputs,
/// Print detailed info about all connected outputs and exit.
ListOutputsInfo,
/// Print the id+title strings of all active toplevels and exit.
ListToplevels,
/// Pick a pixel color interactively and exit.
#[cfg(feature = "color_picker")]
ColorPicker(crate::cli::ColorFormat),
/// Capture a screenshot using the given mode.
Screenshot(CaptureMode),
}
// ─── Resolved settings ────────────────────────────────────────────────────────
/// Runtime settings derived by merging CLI flags with the config file.
///
/// Priority for every field: explicit CLI flag > config file value > built-in default.
/// After `resolve()` returns, nothing in the call chain needs `cli` or `config`.
pub(crate) struct AppSettings {
/// Top-level operation to execute.
pub(crate) command: Command,
/// Whether to render the cursor in the captured image.
pub(crate) cursor: bool,
/// When true, freeze the screen before region/point selection; when false, select on live display.
pub(crate) freeze: bool,
/// Milliseconds to wait before capturing; `None` means no delay.
pub(crate) delay: Option<u32>,
/// Final encoding format, after resolving extension / flag / config precedence.
pub(crate) encoding: EncodingFormat,
/// Destination file path, or `None` to skip file output.
pub(crate) file: Option<PathBuf>,
/// Write image bytes to stdout.
pub(crate) stdout_print: bool,
/// JPEG-XL encoder settings (always present; used only when encoding is Jxl).
pub(crate) jxl: config::Jxl,
/// PNG encoder settings.
pub(crate) png: config::Png,
#[cfg(feature = "clipboard")]
pub(crate) clipboard: bool,
#[cfg(feature = "notifications")]
pub(crate) notifications: bool,
#[cfg(feature = "notifications")]
pub(crate) notification_action: Option<String>,
}
impl AppSettings {
pub(crate) fn resolve(cli: &Cli, config: &Config) -> Self {
let base = config.base.clone().unwrap_or_default();
let file_config = config.file.clone().unwrap_or_default();
let geometry_config = config.geometry.clone().unwrap_or_default();
let encoding_config = config.encoding.clone().unwrap_or_default();
// ── Cursor ────────────────────────────────────────────────────────────
// Either the --cursor flag or config `cursor = true` enables cursor capture.
let cursor = cli.cursor || base.cursor.unwrap_or_default();
// ── Freeze ─────────────────────────────────────────────────────────────
// Freeze screen before selection; false when CLI --no-freeze or config freeze = false.
let freeze = !cli.no_freeze && base.freeze.unwrap_or(true);
// ── Delay ─────────────────────────────────────────────────────────────
// Wait N ms before capture; CLI overrides config; None = no delay.
let delay = cli.delay.or(base.delay);
// ── Encoding ──────────────────────────────────────────────────────────
// Resolution order:
// 1. --encoding flag
// 2. format inferred from the FILE extension
// 3. config `[file] encoding`
// 4. built-in default (PNG)
let input_encoding: Option<EncodingFormat> =
cli.file.as_ref().and_then(|p| p.try_into().ok());
let encoding = cli
.encoding
.or(input_encoding)
.unwrap_or_else(|| file_config.encoding.unwrap_or_default());
if let Some(ie) = input_encoding
&& ie != encoding
{
tracing::warn!(
"Requested encoding '{encoding}' does not match \
the file extension '{ie}'. Using the requested encoding."
);
}
// ── File name format ──────────────────────────────────────────────────
// CLI --file-name-format overrides config; falls back to a timestamp pattern.
let file_name_format = cli.file_name_format.clone().unwrap_or_else(|| {
file_config
.name_format
.clone()
.unwrap_or_else(|| "wayshot-%Y_%m_%d-%H_%M_%S".to_string())
});
// ── Stdout / file path ────────────────────────────────────────────────
// stdout_print starts from config `stdout = true`.
// resolve_output_file may also flip it to true when FILE is `-`.
let mut stdout_print = base.stdout.unwrap_or_default();
let file = Self::resolve_output_file(
cli.file.clone(),
&base,
&file_config,
&file_name_format,
encoding,
&mut stdout_print,
);
// ── Command ───────────────────────────────────────────────────────────
// Query commands are checked first; screenshot mode is the default.
let command = 'cmd: {
if cli.list_outputs {
break 'cmd Command::ListOutputs;
}
if cli.list_outputs_info {
break 'cmd Command::ListOutputsInfo;
}
if cli.list_toplevels {
break 'cmd Command::ListToplevels;
}
#[cfg(feature = "color_picker")]
if let Some(fmt) = cli.color.clone() {
break 'cmd Command::ColorPicker(fmt);
}
let output = cli.output.clone().or_else(|| base.output.clone());
Command::Screenshot(Self::resolve_capture_mode(cli, output, geometry_config))
};
AppSettings {
command,
cursor,
freeze,
delay,
encoding,
file,
stdout_print,
jxl: encoding_config.jxl.unwrap_or_default(),
png: encoding_config.png.unwrap_or_default(),
#[cfg(feature = "clipboard")]
clipboard: cli.clipboard || base.clipboard.unwrap_or_default(),
#[cfg(feature = "notifications")]
notifications: !cli.silent && base.notifications.unwrap_or(true),
#[cfg(feature = "notifications")]
notification_action: config.notification.as_ref().and_then(|n| n.action.clone()),
}
}
fn resolve_capture_mode(
cli: &Cli,
output: Option<String>,
geometry_config: Geometry,
) -> CaptureMode {
if let Some(geometry) = &cli.geometry {
match geometry {
Some(s) if !s.trim().is_empty() => match utils::parse_slurp_geometry(s) {
Ok(region) => return CaptureMode::GeometryRegion(region),
Err(e) => {
tracing::error!("invalid geometry: {e}");
std::process::exit(1);
}
},
Some(_) => {
tracing::error!("geometry string is empty or incorrect");
std::process::exit(1);
}
None => {
#[cfg(feature = "selector")]
return CaptureMode::Geometry {
foreground_color: cli
.geometry_foreground_color
.clone()
.or(geometry_config.foreground_color),
background_color: cli
.geometry_background_color
.clone()
.or(geometry_config.background_color),
};
#[cfg(not(feature = "selector"))]
{
let _ = geometry_config; // suppress clippy unused warning
tracing::error!(
"interactive geometry selection requires the selector feature; \
provide a geometry string instead, e.g. wayshot -g \"$(slurp)\""
);
std::process::exit(1);
}
}
}
}
if let Some(ref name) = cli.toplevel {
CaptureMode::Toplevel(name.clone())
} else if cli.choose_toplevel {
CaptureMode::ChooseToplevel
} else if let Some(name) = output {
CaptureMode::Output(name)
} else if cli.choose_output {
CaptureMode::ChooseOutput
} else {
CaptureMode::All
}
}
fn resolve_output_file(
cli_file: Option<PathBuf>,
base: &config::Base,
file_config: &config::File,
file_name_format: &str,
encoding: EncodingFormat,
stdout_print: &mut bool,
) -> Option<PathBuf> {
if let Some(path) = cli_file {
if path.to_string_lossy() == "-" {
*stdout_print = true;
return None;
}
return Some(utils::get_full_file_name(&path, file_name_format, encoding));
}
if base.file.unwrap_or_default() {
let dir = file_config
.path
.clone()
.unwrap_or_else(|| env::current_dir().unwrap_or_default());
return Some(utils::get_full_file_name(&dir, file_name_format, encoding));
}
None
}
}