1use crate::parsers::ARG_DISPLAY;
2use std::path::PathBuf;
3use std::str::FromStr;
4
5use proptest_derive::Arbitrary;
6
7use crate::common::{OnOff, YesNo};
8use crate::to_command::{ToArg, ToCommand};
9
10#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
16pub enum OnCoreEsOff {
17 On,
18 Core,
19 Es,
20 Off,
21}
22
23impl ToArg for OnCoreEsOff {
24 fn to_arg(&self) -> &str {
25 match self {
26 OnCoreEsOff::On => "on",
27 OnCoreEsOff::Core => "core",
28 OnCoreEsOff::Es => "es",
29 OnCoreEsOff::Off => "off",
30 }
31 }
32}
33
34#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
35pub enum QemuDisplay {
36 Spice {
37 gl: Option<OnOff>,
38 },
39 Sdl {
40 gl: Option<OnCoreEsOff>,
41 grab_mod: Option<String>,
42 show_cursor: Option<OnOff>,
43 window_close: Option<OnOff>,
44 },
45 Gtk {
46 clipboard: Option<OnOff>,
47 fullscreen: Option<OnOff>,
48 gl: Option<OnOff>,
49 grab_on_hover: Option<OnOff>,
50 show_tabs: Option<OnOff>,
51 show_cursor: Option<OnOff>,
52 window_close: Option<OnOff>,
53 show_menubar: Option<OnOff>,
54 zoom_to_fit: Option<OnOff>,
55 },
56 Vnc {
57 vnc: String,
58 optargs: Option<String>,
59 },
60 Curses {
61 charset: Option<String>,
62 },
63 Cocoa {
64 full_grab: Option<OnOff>,
65 swap_opt_cmd: Option<OnOff>,
66 show_cursor: Option<OnOff>,
67 left_command_key: Option<OnOff>,
68 full_screen: Option<OnOff>,
69 zoom_to_fit: Option<OnOff>,
70 },
71 EglHeadless {
72 rendernode: Option<PathBuf>,
73 },
74 Dbus {
75 addr: Option<String>,
76 p2p: Option<YesNo>,
77 gl: Option<OnCoreEsOff>,
78 rendernode: Option<PathBuf>,
79 audiodev: Option<String>,
80 },
81 None,
82}
83
84impl ToCommand for QemuDisplay {
85 fn command(&self) -> String {
86 ARG_DISPLAY.to_string()
87 }
88 fn to_args(&self) -> Vec<String> {
89 let mut args = vec![];
90 match self {
91 QemuDisplay::Spice { gl } => {
92 args.push("spice-app".to_string());
93 if let Some(gl) = gl {
94 args.push(format!("gl={}", gl.to_arg()));
95 }
96 }
97 QemuDisplay::Sdl {
98 gl,
99 grab_mod,
100 show_cursor,
101 window_close,
102 } => {
103 args.push("sdl".to_string());
104 if let Some(gl) = gl {
105 args.push(format!("gl={}", gl.to_arg()));
106 }
107 if let Some(grab_mod) = grab_mod {
108 args.push(format!("grab-mod={}", grab_mod));
109 }
110 if let Some(show_cursor) = show_cursor {
111 args.push(format!("show-cursor={}", show_cursor.to_arg()));
112 }
113 if let Some(window_close) = window_close {
114 args.push(format!("window-close={}", window_close.to_arg()));
115 }
116 }
117 QemuDisplay::Gtk {
118 clipboard,
119 fullscreen,
120 gl,
121 grab_on_hover,
122 show_tabs,
123 show_cursor,
124 window_close,
125 show_menubar,
126 zoom_to_fit,
127 } => {
128 args.push("gtk".to_string());
129 if let Some(clipboard) = clipboard {
130 args.push(format!("clipboard={}", clipboard.to_arg()));
131 }
132 if let Some(fullscreen) = fullscreen {
133 args.push(format!("full-screen={}", fullscreen.to_arg()));
134 }
135 if let Some(gl) = gl {
136 args.push(format!("gl={}", gl.to_arg()));
137 }
138 if let Some(grab_on_hover) = grab_on_hover {
139 args.push(format!("grab-on-hover={}", grab_on_hover.to_arg()));
140 }
141 if let Some(show_tabs) = show_tabs {
142 args.push(format!("show-tabs={}", show_tabs.to_arg()));
143 }
144 if let Some(show_cursor) = show_cursor {
145 args.push(format!("show-cursor={}", show_cursor.to_arg()));
146 }
147 if let Some(window_close) = window_close {
148 args.push(format!("window-close={}", window_close.to_arg()));
149 }
150 if let Some(show_menubar) = show_menubar {
151 args.push(format!("show-menubar={}", show_menubar.to_arg()));
152 }
153 if let Some(zoom_to_fit) = zoom_to_fit {
154 args.push(format!("zoom-to-fit={}", zoom_to_fit.to_arg()));
155 }
156 }
157 QemuDisplay::Vnc { vnc, optargs } => {
158 args.push(format!("vnc={}", vnc.clone()));
159 if let Some(optargs) = optargs {
160 args.push(optargs.clone());
161 }
162 }
163 QemuDisplay::Curses { charset } => {
164 args.push("curses".to_string());
165 if let Some(charset) = charset {
166 args.push(format!("charset={}", charset));
167 }
168 }
169 QemuDisplay::Cocoa {
170 full_grab,
171 swap_opt_cmd,
172 show_cursor,
173 left_command_key,
174 full_screen,
175 zoom_to_fit,
176 } => {
177 args.push("cocoa".to_string());
178 if let Some(full_grab) = full_grab {
179 args.push(format!("full-grab={}", full_grab.to_arg()));
180 }
181 if let Some(swap_opt_cmd) = swap_opt_cmd {
182 args.push(format!("swap-opt-cmd={}", swap_opt_cmd.to_arg()));
183 }
184 if let Some(show_cursor) = show_cursor {
185 args.push(format!("show-cursor={}", show_cursor.to_arg()));
186 }
187 if let Some(left_command_key) = left_command_key {
188 args.push(format!("left-command-key={}", left_command_key.to_arg()));
189 }
190 if let Some(full_screen) = full_screen {
191 args.push(format!("full-screen={}", full_screen.to_arg()));
192 }
193 if let Some(zoom_to_fit) = zoom_to_fit {
194 args.push(format!("zoom-to-fit={}", zoom_to_fit.to_arg()));
195 }
196 }
197 QemuDisplay::EglHeadless { rendernode } => {
198 args.push("egl-headless".to_string());
199 if let Some(rendernode) = rendernode {
200 args.push(format!("rendernode={}", rendernode.display()));
201 }
202 }
203 QemuDisplay::Dbus { addr, p2p, gl, rendernode, audiodev } => {
204 args.push("dbus".to_string());
205 if let Some(addr) = addr {
206 args.push(format!("addr={}", addr));
207 }
208 if let Some(p2p) = p2p {
209 args.push(format!("p2p={}", p2p.to_arg()));
210 }
211 if let Some(gl) = gl {
212 args.push(format!("gl={}", gl.to_arg()));
213 }
214 if let Some(rendernode) = rendernode {
215 args.push(format!("rendernode={}", rendernode.display()));
216 }
217 if let Some(audiodev) = audiodev {
218 args.push(format!("audiodev={audiodev}"));
219 }
220 }
221 QemuDisplay::None => {
222 args.push("none".to_string());
223 }
224 }
225
226 vec![args.join(",")]
227 }
228}
229
230impl FromStr for QemuDisplay {
231 type Err = String;
232
233 fn from_str(s: &str) -> Result<Self, Self::Err> {
234 if s == "none" {
235 return Ok(Self::None);
236 }
237 if s == "spice-app" {
238 return Ok(Self::Spice { gl: None });
239 }
240 if let Some(rest) = s.strip_prefix("spice-app,") {
241 let mut gl = None;
242 for (key, value) in display_options(rest)? {
243 match key {
244 "gl" => gl = Some(parse_on_off(key, value)?),
245 other => return Err(format!("unsupported spice-app display option: {other}")),
246 }
247 }
248 return Ok(Self::Spice { gl });
249 }
250 if s == "sdl" {
251 return Ok(Self::Sdl {
252 gl: None,
253 grab_mod: None,
254 show_cursor: None,
255 window_close: None,
256 });
257 }
258 if let Some(rest) = s.strip_prefix("sdl,") {
259 let (mut gl, mut grab_mod, mut show_cursor, mut window_close) = (None, None, None, None);
260 for (key, value) in display_options(rest)? {
261 match key {
262 "gl" => gl = Some(parse_gl(value)?),
263 "grab-mod" => grab_mod = Some(value.to_string()),
264 "show-cursor" => show_cursor = Some(parse_on_off(key, value)?),
265 "window-close" => window_close = Some(parse_on_off(key, value)?),
266 other => return Err(format!("unsupported SDL display option: {other}")),
267 }
268 }
269 return Ok(Self::Sdl {
270 gl,
271 grab_mod,
272 show_cursor,
273 window_close,
274 });
275 }
276 if s == "gtk" {
277 return Ok(Self::Gtk {
278 clipboard: None,
279 fullscreen: None,
280 gl: None,
281 grab_on_hover: None,
282 show_tabs: None,
283 show_cursor: None,
284 window_close: None,
285 show_menubar: None,
286 zoom_to_fit: None,
287 });
288 }
289 if let Some(rest) = s.strip_prefix("gtk,") {
290 let mut display = Self::Gtk {
291 clipboard: None,
292 fullscreen: None,
293 gl: None,
294 grab_on_hover: None,
295 show_tabs: None,
296 show_cursor: None,
297 window_close: None,
298 show_menubar: None,
299 zoom_to_fit: None,
300 };
301 let Self::Gtk {
302 clipboard,
303 fullscreen,
304 gl,
305 grab_on_hover,
306 show_tabs,
307 show_cursor,
308 window_close,
309 show_menubar,
310 zoom_to_fit,
311 } = &mut display
312 else {
313 unreachable!()
314 };
315 for option in rest.split(',') {
316 let (key, value) = option.split_once('=').ok_or_else(|| format!("invalid GTK display option: {option}"))?;
317 let value = value.parse::<OnOff>().map_err(|_| format!("invalid {key} value: {value}"))?;
318 match key {
319 "clipboard" => *clipboard = Some(value),
320 "full-screen" => *fullscreen = Some(value),
321 "gl" => *gl = Some(value),
322 "grab-on-hover" => *grab_on_hover = Some(value),
323 "show-tabs" => *show_tabs = Some(value),
324 "show-cursor" => *show_cursor = Some(value),
325 "window-close" => *window_close = Some(value),
326 "show-menubar" => *show_menubar = Some(value),
327 "zoom-to-fit" => *zoom_to_fit = Some(value),
328 other => return Err(format!("unsupported GTK display option: {other}")),
329 }
330 }
331 return Ok(display);
332 }
333 if s == "curses" {
334 return Ok(Self::Curses { charset: None });
335 }
336 if let Some(rest) = s.strip_prefix("curses,") {
337 let options = display_options(rest)?;
338 if options.len() != 1 || options[0].0 != "charset" {
339 return Err(format!("unsupported curses display options: {rest}"));
340 }
341 return Ok(Self::Curses {
342 charset: Some(options[0].1.to_string()),
343 });
344 }
345 if s == "cocoa" {
346 return Ok(Self::Cocoa {
347 full_grab: None,
348 swap_opt_cmd: None,
349 show_cursor: None,
350 left_command_key: None,
351 full_screen: None,
352 zoom_to_fit: None,
353 });
354 }
355 if let Some(rest) = s.strip_prefix("cocoa,") {
356 let (mut full_grab, mut swap_opt_cmd, mut show_cursor, mut left_command_key, mut full_screen, mut zoom_to_fit) = (None, None, None, None, None, None);
357 for (key, value) in display_options(rest)? {
358 let value = parse_on_off(key, value)?;
359 match key {
360 "full-grab" => full_grab = Some(value),
361 "swap-opt-cmd" => swap_opt_cmd = Some(value),
362 "show-cursor" => show_cursor = Some(value),
363 "left-command-key" => left_command_key = Some(value),
364 "full-screen" => full_screen = Some(value),
365 "zoom-to-fit" => zoom_to_fit = Some(value),
366 other => return Err(format!("unsupported Cocoa display option: {other}")),
367 }
368 }
369 return Ok(Self::Cocoa {
370 full_grab,
371 swap_opt_cmd,
372 show_cursor,
373 left_command_key,
374 full_screen,
375 zoom_to_fit,
376 });
377 }
378 if s == "egl-headless" {
379 return Ok(Self::EglHeadless { rendernode: None });
380 }
381 if let Some(rest) = s.strip_prefix("egl-headless,") {
382 let options = display_options(rest)?;
383 if options.len() != 1 || options[0].0 != "rendernode" {
384 return Err(format!("unsupported egl-headless display options: {rest}"));
385 }
386 return Ok(Self::EglHeadless {
387 rendernode: Some(PathBuf::from(options[0].1)),
388 });
389 }
390 if s == "dbus" {
391 return Ok(Self::Dbus {
392 addr: None,
393 p2p: None,
394 gl: None,
395 rendernode: None,
396 audiodev: None,
397 });
398 }
399 if let Some(rest) = s.strip_prefix("dbus,") {
400 let (mut addr, mut p2p, mut gl, mut rendernode, mut audiodev) = (None, None, None, None, None);
401 for (key, value) in display_options(rest)? {
402 match key {
403 "addr" => addr = Some(value.to_string()),
404 "p2p" => p2p = Some(value.parse::<YesNo>().map_err(|_| format!("invalid p2p value: {value}"))?),
405 "gl" => gl = Some(parse_gl(value)?),
406 "rendernode" => rendernode = Some(PathBuf::from(value)),
407 "audiodev" => audiodev = Some(value.to_string()),
408 other => return Err(format!("unsupported D-Bus display option: {other}")),
409 }
410 }
411 return Ok(Self::Dbus { addr, p2p, gl, rendernode, audiodev });
412 }
413 if let Some(rest) = s.strip_prefix("vnc=") {
414 let (vnc, optargs) = match rest.split_once(',') {
415 Some((vnc, optargs)) => (vnc.to_string(), Some(optargs.to_string())),
416 None => (rest.to_string(), None),
417 };
418 return Ok(Self::Vnc { vnc, optargs });
419 }
420
421 Err(format!("unsupported -display value: {s}"))
422 }
423}
424
425fn display_options(value: &str) -> Result<Vec<(&str, &str)>, String> {
426 value.split(',').map(|part| part.split_once('=').ok_or_else(|| format!("invalid display option: {part}"))).collect()
427}
428
429fn parse_on_off(key: &str, value: &str) -> Result<OnOff, String> {
430 value.parse::<OnOff>().map_err(|_| format!("invalid {key} value: {value}"))
431}
432
433fn parse_gl(value: &str) -> Result<OnCoreEsOff, String> {
434 match value {
435 "on" => Ok(OnCoreEsOff::On),
436 "core" => Ok(OnCoreEsOff::Core),
437 "es" => Ok(OnCoreEsOff::Es),
438 "off" => Ok(OnCoreEsOff::Off),
439 _ => Err(format!("invalid gl value: {value}")),
440 }
441}