Skip to main content

qemu_command_builder/args/
display.rs

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/// QEMU `-display` backend selection.
11///
12/// Each variant models one documented `-display` form from the bundled QEMU
13/// option reference. `to_args()` emits raw `argv` values, while `FromStr`
14/// accepts the single argument that follows `-display`.
15#[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}