Skip to main content

zellij_utils/input/
options.rs

1//! Handles cli and configuration options
2use crate::cli::Command;
3use crate::data::{InputMode, WebSharing};
4use clap::{Args, ValueEnum};
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7use std::str::FromStr;
8
9use std::net::IpAddr;
10
11pub const DEFAULT_WORD_SEPARATORS: &str = "[]{}<>()";
12
13#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize, ValueEnum)]
14pub enum OnForceClose {
15    #[serde(alias = "quit")]
16    Quit,
17    #[serde(alias = "detach")]
18    Detach,
19}
20
21#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ValueEnum)]
22pub enum NestedSessionHandling {
23    #[serde(alias = "ask")]
24    Ask,
25    #[serde(alias = "fullscreen")]
26    Fullscreen,
27    #[serde(alias = "descend")]
28    Descend,
29    #[serde(alias = "never")]
30    Never,
31}
32
33impl Default for NestedSessionHandling {
34    fn default() -> Self {
35        Self::Ask
36    }
37}
38
39impl FromStr for NestedSessionHandling {
40    type Err = String;
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        match s {
43            "Ask" | "ask" => Ok(Self::Ask),
44            "Fullscreen" | "fullscreen" => Ok(Self::Fullscreen),
45            "Descend" | "descend" => Ok(Self::Descend),
46            "Never" | "never" => Ok(Self::Never),
47            _ => Err(format!("No such nested_session_handling: {}", s)),
48        }
49    }
50}
51
52impl Default for OnForceClose {
53    fn default() -> Self {
54        Self::Detach
55    }
56}
57
58impl FromStr for OnForceClose {
59    type Err = Box<dyn std::error::Error>;
60
61    fn from_str(s: &str) -> Result<Self, Self::Err> {
62        match s {
63            "quit" => Ok(Self::Quit),
64            "detach" => Ok(Self::Detach),
65            e => Err(e.to_string().into()),
66        }
67    }
68}
69
70#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
71#[serde(rename_all = "lowercase")]
72pub enum PaneFrameStyle {
73    Full,
74    Titles,
75    None,
76}
77
78impl Default for PaneFrameStyle {
79    fn default() -> Self {
80        PaneFrameStyle::Titles
81    }
82}
83
84impl PaneFrameStyle {
85    pub fn draws_full_frames(&self) -> bool {
86        matches!(self, PaneFrameStyle::Full)
87    }
88
89    pub fn draws_titles(&self) -> bool {
90        matches!(self, PaneFrameStyle::Titles)
91    }
92
93    pub fn from_options(options: &Options) -> Self {
94        if options.pane_frames == Some(false) {
95            return PaneFrameStyle::None;
96        }
97        match options.pane_frame_style {
98            Some(PaneFrameStyle::Full) => PaneFrameStyle::Full,
99            _ => PaneFrameStyle::Titles,
100        }
101    }
102}
103
104impl FromStr for PaneFrameStyle {
105    type Err = Box<dyn std::error::Error>;
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        match s.trim().to_lowercase().as_str() {
108            "full" => Ok(PaneFrameStyle::Full),
109            "titles" => Ok(PaneFrameStyle::Titles),
110            "none" => Ok(PaneFrameStyle::None),
111            e => Err(format!(
112                "Unknown pane frame style: '{}' (expected 'full', 'titles' or 'none')",
113                e
114            )
115            .into()),
116        }
117    }
118}
119
120#[derive(Clone, Default, Debug, PartialEq, Deserialize, Serialize, Args)]
121/// Options that can be set either through the config file,
122/// or cli flags - cli flags should take precedence over the config file
123/// TODO: In order to correctly parse boolean flags, this is currently split
124/// into Options and CliOptions, this could be a good canditate for a macro
125pub struct Options {
126    /// Allow plugins to use a more simplified layout
127    /// that is compatible with more fonts (true or false)
128    #[clap(long, value_parser)]
129    #[serde(default)]
130    pub simplified_ui: Option<bool>,
131    /// Set the default theme
132    #[clap(long, value_parser)]
133    pub theme: Option<String>,
134    /// Theme name to apply when the host terminal reports a dark color palette
135    /// (CSI 2031 / DSR 997). Requires `theme_light` to also be set; if either
136    /// is missing the static `theme` remains authoritative.
137    #[clap(long, value_parser)]
138    pub theme_dark: Option<String>,
139    /// Theme name to apply when the host terminal reports a light color palette
140    /// (CSI 2031 / DSR 997). Requires `theme_dark` to also be set; if either
141    /// is missing the static `theme` remains authoritative.
142    #[clap(long, value_parser)]
143    pub theme_light: Option<String>,
144    /// Set the default mode
145    #[clap(long, value_enum, hide_possible_values = true, value_parser)]
146    pub default_mode: Option<InputMode>,
147    /// Set the default shell
148    #[clap(long, value_parser)]
149    pub default_shell: Option<PathBuf>,
150    /// Set the default cwd
151    #[clap(long, value_parser)]
152    pub default_cwd: Option<PathBuf>,
153    /// Set the default layout
154    #[clap(long, value_parser)]
155    pub default_layout: Option<PathBuf>,
156    /// Set the layout_dir, defaults to
157    /// subdirectory of config dir
158    #[clap(long, value_parser)]
159    pub layout_dir: Option<PathBuf>,
160    /// Set the theme_dir, defaults to
161    /// subdirectory of config dir
162    #[clap(long, value_parser)]
163    pub theme_dir: Option<PathBuf>,
164    #[clap(long, value_parser)]
165    #[serde(default)]
166    /// Set the handling of mouse events (true or false)
167    /// Can be temporarily bypassed by the [SHIFT] key
168    pub mouse_mode: Option<bool>,
169    #[clap(long, value_parser)]
170    #[serde(default)]
171    /// Set display of the pane frames (true or false)
172    pub pane_frames: Option<bool>,
173    #[clap(long, value_enum, hide_possible_values = true, value_parser)]
174    #[serde(default)]
175    pub pane_frame_style: Option<PaneFrameStyle>,
176    #[clap(long, value_parser)]
177    #[serde(default)]
178    /// Mirror session when multiple users are connected (true or false)
179    pub mirror_session: Option<bool>,
180    /// Set behaviour on force close (quit or detach)
181    #[clap(long, value_enum, hide_possible_values = true, value_parser)]
182    pub on_force_close: Option<OnForceClose>,
183    #[clap(long, value_parser)]
184    pub scroll_buffer_size: Option<usize>,
185
186    /// Switch to using a user supplied command for clipboard instead of OSC52
187    #[clap(long, value_parser)]
188    #[serde(default)]
189    pub copy_command: Option<String>,
190
191    /// OSC52 destination clipboard
192    #[clap(
193        long,
194        value_enum,
195        ignore_case = true,
196        conflicts_with = "copy_command",
197        value_parser
198    )]
199    #[serde(default)]
200    pub copy_clipboard: Option<Clipboard>,
201
202    /// Automatically copy when selecting text (true or false)
203    #[clap(long, value_parser)]
204    #[serde(default)]
205    pub copy_on_select: Option<bool>,
206
207    /// Enable OSC8 hyperlink output (true or false)
208    #[clap(long, value_parser)]
209    #[serde(default)]
210    pub osc8_hyperlinks: Option<bool>,
211
212    /// Explicit full path to open the scrollback editor (default is $EDITOR or $VISUAL)
213    #[clap(long, value_parser)]
214    pub scrollback_editor: Option<PathBuf>,
215
216    /// The name of the session to create when starting Zellij
217    #[clap(long, value_parser)]
218    #[serde(default)]
219    pub session_name: Option<String>,
220
221    /// Whether to attach to a session specified in "session-name" if it exists
222    #[clap(long, value_parser)]
223    #[serde(default)]
224    pub attach_to_session: Option<bool>,
225
226    /// Whether to lay out panes in a predefined set of layouts whenever possible
227    #[clap(long, value_parser)]
228    #[serde(default)]
229    pub auto_layout: Option<bool>,
230
231    /// Whether sessions should be serialized to the HD so that they can be later resurrected,
232    /// default is true
233    #[clap(long, value_parser)]
234    #[serde(default)]
235    pub session_serialization: Option<bool>,
236
237    /// Whether pane viewports are serialized along with the session, default is false
238    #[clap(long, value_parser)]
239    #[serde(default)]
240    pub serialize_pane_viewport: Option<bool>,
241
242    /// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0
243    /// defaults to the scrollback size. If this number is higher than the scrollback size, it will
244    /// also default to the scrollback size
245    #[clap(long, value_parser)]
246    #[serde(default)]
247    pub scrollback_lines_to_serialize: Option<usize>,
248
249    /// Whether to use ANSI styled underlines
250    #[clap(long, value_parser)]
251    #[serde(default)]
252    pub styled_underlines: Option<bool>,
253
254    /// The interval at which to serialize sessions for resurrection (in seconds)
255    #[clap(long, value_parser)]
256    pub serialization_interval: Option<u64>,
257
258    /// If true, will disable writing session metadata to disk
259    #[clap(long, value_parser)]
260    pub disable_session_metadata: Option<bool>,
261
262    /// Whether to enable support for the Kitty keyboard protocol (must also be supported by the
263    /// host terminal), defaults to true if the terminal supports it
264    #[clap(long, value_parser)]
265    #[serde(default)]
266    pub support_kitty_keyboard_protocol: Option<bool>,
267
268    /// Whether to enable support for the Kitty graphics (image) protocol (must also be supported
269    /// by the host terminal), defaults to true if the terminal supports it
270    #[clap(long, value_parser)]
271    #[serde(default)]
272    pub support_kitty_graphics_protocol: Option<bool>,
273
274    /// Whether to make sure a local web server is running when a new Zellij session starts.
275    /// This web server will allow creating new sessions and attaching to existing ones that have
276    /// opted in to being shared in the browser.
277    ///
278    /// Note: a local web server can still be manually started from within a Zellij session or from the CLI.
279    /// If this is not desired, one can use a version of Zellij compiled without
280    /// web_server_capability
281    ///
282    /// Possible values:
283    /// - true
284    /// - false
285    /// Default: false
286    #[clap(long, value_parser)]
287    #[serde(default)]
288    pub web_server: Option<bool>,
289
290    /// Whether to allow new sessions to be shared through a local web server, assuming one is
291    /// running (see the `web_server` option for more details).
292    ///
293    /// Note: if Zellij was compiled without web_server_capability, this option will be locked to
294    /// "disabled"
295    ///
296    /// Possible values:
297    /// - "on" (new sessions will allow web sharing through the local web server if it
298    /// is online)
299    /// - "off" (new sessions will not allow web sharing unless they explicitly opt-in to it)
300    /// - "disabled" (new sessions will not allow web sharing and will not be able to opt-in to it)
301    /// Default: "off"
302    #[clap(long, value_parser)]
303    #[serde(default)]
304    pub web_sharing: Option<WebSharing>,
305
306    /// Whether to stack panes when resizing beyond a certain size
307    /// default is true
308    #[clap(long, value_parser)]
309    #[serde(default)]
310    pub stacked_resize: Option<bool>,
311
312    #[clap(long, value_parser)]
313    #[serde(default)]
314    pub stacked_pane_list: Option<bool>,
315
316    /// Whether to show startup tips when starting a new session
317    /// default is true
318    #[clap(long, value_parser)]
319    #[serde(default)]
320    pub show_startup_tips: Option<bool>,
321
322    /// Whether to show release notes on first run of a new version
323    /// default is true
324    #[clap(long, value_parser)]
325    #[serde(default)]
326    pub show_release_notes: Option<bool>,
327
328    /// Whether to enable mouse hover effects and pane grouping functionality
329    /// default is true
330    #[clap(long, value_parser)]
331    #[serde(default)]
332    pub advanced_mouse_actions: Option<bool>,
333
334    /// Whether Ctrl+ScrollWheel resizes panes
335    /// default is true
336    #[clap(long, value_parser)]
337    #[serde(default)]
338    pub mouse_scroll_resize: Option<bool>,
339
340    /// Whether to enable mouse hover visual effects (frame highlight and help text)
341    /// default is true
342    #[clap(long, value_parser)]
343    #[serde(default)]
344    pub mouse_hover_effects: Option<bool>,
345
346    /// Whether to show mouse hover help-text tips (resize help and group shortcuts)
347    /// default is true
348    #[clap(long, value_parser)]
349    #[serde(default)]
350    pub mouse_hover_tips: Option<bool>,
351
352    /// Whether to show visual bell indicators (pane/tab frame flash and [!] suffix)
353    /// default is true
354    #[clap(long, value_parser)]
355    #[serde(default)]
356    pub visual_bell: Option<bool>,
357
358    /// Whether to focus panes on mouse hover (true or false)
359    /// default is false
360    #[clap(long, value_parser)]
361    #[serde(default)]
362    pub focus_follows_mouse: Option<bool>,
363
364    /// Whether clicking a pane to focus it also sends the click into the pane (true or false)
365    /// default is false
366    #[clap(long, value_parser)]
367    #[serde(default)]
368    pub mouse_click_through: Option<bool>,
369
370    /// Whether triple-clicking inside shell-marked (OSC 133) command output selects the command
371    /// and its output rather than the logical line
372    /// default is true
373    #[clap(long, value_parser)]
374    #[serde(default)]
375    pub osc133_command_selection: Option<bool>,
376
377    /// Characters that terminate a word when double-clicking to select it, in addition to
378    /// whitespace (which is always a separator)
379    /// default is "[]{}<>()"
380    #[clap(long, value_parser)]
381    #[serde(default)]
382    pub word_separators: Option<String>,
383
384    #[clap(long, value_parser)]
385    #[serde(default)]
386    pub host_notification_protocol: Option<HostNotificationProtocol>,
387
388    // these are intentionally excluded from the CLI options as they must be specified in the
389    // configuration file
390    pub web_server_ip: Option<IpAddr>,
391    pub web_server_port: Option<u16>,
392    pub web_server_cert: Option<PathBuf>,
393    pub web_server_key: Option<PathBuf>,
394    pub enforce_https_for_localhost: Option<bool>,
395    /// A command to run after the discovery of running commands when serializing, for the purpose
396    /// of manipulating the command (eg. with a regex) before it gets serialized
397    #[clap(long, value_parser)]
398    pub post_command_discovery_hook: Option<String>,
399
400    /// Number of async worker tasks to spawn per active client.
401    ///
402    /// Allocating few tasks may result in resource contention and lags. Small values (around 4)
403    /// should typically work best. Set to 0 to use the number of (physical) CPU cores.
404    /// NOTE: This only applies to web clients at the moment.
405    #[clap(long)]
406    pub client_async_worker_tasks: Option<usize>,
407
408    /// How to handle a nested Zellij session detected inside a pane
409    /// (ask, fullscreen, descend, never)
410    #[clap(long, value_enum, hide_possible_values = true, value_parser)]
411    #[serde(default)]
412    pub nested_session_handling: Option<NestedSessionHandling>,
413
414    #[clap(long, value_parser)]
415    #[serde(default)]
416    pub dangerously_enable_paste_buffer_read: Option<bool>,
417}
418
419#[derive(ValueEnum, Deserialize, Serialize, Debug, Clone, Copy, PartialEq)]
420pub enum Clipboard {
421    #[serde(alias = "system")]
422    System,
423    #[serde(alias = "primary")]
424    Primary,
425}
426
427impl Default for Clipboard {
428    fn default() -> Self {
429        Self::System
430    }
431}
432
433#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ValueEnum)]
434pub enum HostNotificationProtocol {
435    #[serde(alias = "auto")]
436    Auto,
437    #[serde(alias = "osc9")]
438    Osc9,
439    #[serde(alias = "osc99")]
440    Osc99,
441    #[serde(alias = "bell")]
442    Bell,
443    #[serde(alias = "off")]
444    Off,
445}
446
447impl Default for HostNotificationProtocol {
448    fn default() -> Self {
449        Self::Auto
450    }
451}
452
453impl HostNotificationProtocol {
454    pub fn as_str(&self) -> &'static str {
455        match self {
456            Self::Auto => "auto",
457            Self::Osc9 => "osc9",
458            Self::Osc99 => "osc99",
459            Self::Bell => "bell",
460            Self::Off => "off",
461        }
462    }
463}
464
465impl FromStr for HostNotificationProtocol {
466    type Err = String;
467    fn from_str(s: &str) -> Result<Self, Self::Err> {
468        match s {
469            "Auto" | "auto" => Ok(Self::Auto),
470            "Osc9" | "osc9" => Ok(Self::Osc9),
471            "Osc99" | "osc99" => Ok(Self::Osc99),
472            "Bell" | "bell" => Ok(Self::Bell),
473            "Off" | "off" => Ok(Self::Off),
474            _ => Err(format!("No such host_notification_protocol: {}", s)),
475        }
476    }
477}
478
479impl FromStr for Clipboard {
480    type Err = String;
481    fn from_str(s: &str) -> Result<Self, Self::Err> {
482        match s {
483            "System" | "system" => Ok(Self::System),
484            "Primary" | "primary" => Ok(Self::Primary),
485            _ => Err(format!("No such clipboard: {}", s)),
486        }
487    }
488}
489
490impl Options {
491    pub fn from_yaml(from_yaml: Option<Options>) -> Options {
492        if let Some(opts) = from_yaml {
493            opts
494        } else {
495            Options::default()
496        }
497    }
498    /// Merges two [`Options`] structs, a `Some` in `other`
499    /// will supersede a `Some` in `self`
500    // TODO: Maybe a good candidate for a macro?
501    pub fn merge(&self, other: Options) -> Options {
502        let mouse_mode = other.mouse_mode.or(self.mouse_mode);
503        let pane_frames = other.pane_frames.or(self.pane_frames);
504        let pane_frame_style = other.pane_frame_style.or(self.pane_frame_style);
505        let auto_layout = other.auto_layout.or(self.auto_layout);
506        let mirror_session = other.mirror_session.or(self.mirror_session);
507        let simplified_ui = other.simplified_ui.or(self.simplified_ui);
508        let default_mode = other.default_mode.or(self.default_mode);
509        let default_shell = other.default_shell.or_else(|| self.default_shell.clone());
510        let default_cwd = other.default_cwd.or_else(|| self.default_cwd.clone());
511        let default_layout = other.default_layout.or_else(|| self.default_layout.clone());
512        let layout_dir = other.layout_dir.or_else(|| self.layout_dir.clone());
513        let theme_dir = other.theme_dir.or_else(|| self.theme_dir.clone());
514        let theme = other.theme.or_else(|| self.theme.clone());
515        let theme_dark = other.theme_dark.or_else(|| self.theme_dark.clone());
516        let theme_light = other.theme_light.or_else(|| self.theme_light.clone());
517        let on_force_close = other.on_force_close.or(self.on_force_close);
518        let scroll_buffer_size = other.scroll_buffer_size.or(self.scroll_buffer_size);
519        let copy_command = other.copy_command.or_else(|| self.copy_command.clone());
520        let copy_clipboard = other.copy_clipboard.or(self.copy_clipboard);
521        let copy_on_select = other.copy_on_select.or(self.copy_on_select);
522        let osc8_hyperlinks = other.osc8_hyperlinks.or(self.osc8_hyperlinks);
523        let scrollback_editor = other
524            .scrollback_editor
525            .or_else(|| self.scrollback_editor.clone());
526        let session_name = other.session_name.or_else(|| self.session_name.clone());
527        let attach_to_session = other
528            .attach_to_session
529            .or_else(|| self.attach_to_session.clone());
530        let session_serialization = other.session_serialization.or(self.session_serialization);
531        let serialize_pane_viewport = other
532            .serialize_pane_viewport
533            .or(self.serialize_pane_viewport);
534        let scrollback_lines_to_serialize = other
535            .scrollback_lines_to_serialize
536            .or(self.scrollback_lines_to_serialize);
537        let styled_underlines = other.styled_underlines.or(self.styled_underlines);
538        let serialization_interval = other.serialization_interval.or(self.serialization_interval);
539        let disable_session_metadata = other
540            .disable_session_metadata
541            .or(self.disable_session_metadata);
542        let support_kitty_keyboard_protocol = other
543            .support_kitty_keyboard_protocol
544            .or(self.support_kitty_keyboard_protocol);
545        let support_kitty_graphics_protocol = other
546            .support_kitty_graphics_protocol
547            .or(self.support_kitty_graphics_protocol);
548        let web_server = other.web_server.or(self.web_server);
549        let web_sharing = other.web_sharing.or(self.web_sharing);
550        let stacked_resize = other.stacked_resize.or(self.stacked_resize);
551        let stacked_pane_list = other.stacked_pane_list.or(self.stacked_pane_list);
552        let show_startup_tips = other.show_startup_tips.or(self.show_startup_tips);
553        let show_release_notes = other.show_release_notes.or(self.show_release_notes);
554        let advanced_mouse_actions = other.advanced_mouse_actions.or(self.advanced_mouse_actions);
555        let mouse_scroll_resize = other.mouse_scroll_resize.or(self.mouse_scroll_resize);
556        let mouse_hover_effects = other.mouse_hover_effects.or(self.mouse_hover_effects);
557        let mouse_hover_tips = other.mouse_hover_tips.or(self.mouse_hover_tips);
558        let visual_bell = other.visual_bell.or(self.visual_bell);
559        let focus_follows_mouse = other.focus_follows_mouse.or(self.focus_follows_mouse);
560        let mouse_click_through = other.mouse_click_through.or(self.mouse_click_through);
561        let osc133_command_selection = other
562            .osc133_command_selection
563            .or(self.osc133_command_selection);
564        let word_separators = other
565            .word_separators
566            .or_else(|| self.word_separators.clone());
567        let host_notification_protocol = other
568            .host_notification_protocol
569            .or(self.host_notification_protocol);
570        let web_server_ip = other.web_server_ip.or(self.web_server_ip);
571        let web_server_port = other.web_server_port.or(self.web_server_port);
572        let web_server_cert = other
573            .web_server_cert
574            .or_else(|| self.web_server_cert.clone());
575        let web_server_key = other.web_server_key.or_else(|| self.web_server_key.clone());
576        let enforce_https_for_localhost = other
577            .enforce_https_for_localhost
578            .or(self.enforce_https_for_localhost);
579        let post_command_discovery_hook = other
580            .post_command_discovery_hook
581            .or(self.post_command_discovery_hook.clone());
582        let client_async_worker_tasks = other
583            .client_async_worker_tasks
584            .or(self.client_async_worker_tasks);
585        let nested_session_handling = other
586            .nested_session_handling
587            .or(self.nested_session_handling);
588        let dangerously_enable_paste_buffer_read = other
589            .dangerously_enable_paste_buffer_read
590            .or(self.dangerously_enable_paste_buffer_read);
591
592        Options {
593            simplified_ui,
594            theme,
595            theme_dark,
596            theme_light,
597            default_mode,
598            default_shell,
599            default_cwd,
600            default_layout,
601            layout_dir,
602            theme_dir,
603            mouse_mode,
604            pane_frames,
605            pane_frame_style,
606            mirror_session,
607            on_force_close,
608            scroll_buffer_size,
609            copy_command,
610            copy_clipboard,
611            copy_on_select,
612            osc8_hyperlinks,
613            scrollback_editor,
614            session_name,
615            attach_to_session,
616            auto_layout,
617            session_serialization,
618            serialize_pane_viewport,
619            scrollback_lines_to_serialize,
620            styled_underlines,
621            serialization_interval,
622            disable_session_metadata,
623            support_kitty_keyboard_protocol,
624            support_kitty_graphics_protocol,
625            web_server,
626            web_sharing,
627            stacked_resize,
628            stacked_pane_list,
629            show_startup_tips,
630            show_release_notes,
631            advanced_mouse_actions,
632            mouse_scroll_resize,
633            mouse_hover_effects,
634            mouse_hover_tips,
635            visual_bell,
636            focus_follows_mouse,
637            mouse_click_through,
638            osc133_command_selection,
639            word_separators,
640            host_notification_protocol,
641            web_server_ip,
642            web_server_port,
643            web_server_cert,
644            web_server_key,
645            enforce_https_for_localhost,
646            post_command_discovery_hook,
647            client_async_worker_tasks,
648            nested_session_handling,
649            dangerously_enable_paste_buffer_read,
650        }
651    }
652
653    /// Merges two [`Options`] structs,
654    /// - `Some` in `other` will supersede a `Some` in `self`
655    /// - `Some(bool)` in `other` will toggle a `Some(bool)` in `self`
656    // TODO: Maybe a good candidate for a macro?
657    pub fn merge_from_cli(&self, other: Options) -> Options {
658        let merge_bool = |opt_other: Option<bool>, opt_self: Option<bool>| {
659            if opt_other.is_some() ^ opt_self.is_some() {
660                opt_other.or(opt_self)
661            } else if opt_other.is_some() && opt_self.is_some() {
662                Some(opt_other.unwrap() ^ opt_self.unwrap())
663            } else {
664                None
665            }
666        };
667
668        let simplified_ui = merge_bool(other.simplified_ui, self.simplified_ui);
669        let mouse_mode = merge_bool(other.mouse_mode, self.mouse_mode);
670        let pane_frames = merge_bool(other.pane_frames, self.pane_frames);
671        let pane_frame_style = other.pane_frame_style.or(self.pane_frame_style);
672        let auto_layout = merge_bool(other.auto_layout, self.auto_layout);
673        let mirror_session = merge_bool(other.mirror_session, self.mirror_session);
674        let session_serialization =
675            merge_bool(other.session_serialization, self.session_serialization);
676        let serialize_pane_viewport =
677            merge_bool(other.serialize_pane_viewport, self.serialize_pane_viewport);
678
679        let default_mode = other.default_mode.or(self.default_mode);
680        let default_shell = other.default_shell.or_else(|| self.default_shell.clone());
681        let default_cwd = other.default_cwd.or_else(|| self.default_cwd.clone());
682        let default_layout = other.default_layout.or_else(|| self.default_layout.clone());
683        let layout_dir = other.layout_dir.or_else(|| self.layout_dir.clone());
684        let theme_dir = other.theme_dir.or_else(|| self.theme_dir.clone());
685        let theme = other.theme.or_else(|| self.theme.clone());
686        let theme_dark = other.theme_dark.or_else(|| self.theme_dark.clone());
687        let theme_light = other.theme_light.or_else(|| self.theme_light.clone());
688        let on_force_close = other.on_force_close.or(self.on_force_close);
689        let scroll_buffer_size = other.scroll_buffer_size.or(self.scroll_buffer_size);
690        let copy_command = other.copy_command.or_else(|| self.copy_command.clone());
691        let copy_clipboard = other.copy_clipboard.or(self.copy_clipboard);
692        let copy_on_select = other.copy_on_select.or(self.copy_on_select);
693        let osc8_hyperlinks = other.osc8_hyperlinks.or(self.osc8_hyperlinks);
694        let scrollback_editor = other
695            .scrollback_editor
696            .or_else(|| self.scrollback_editor.clone());
697        let session_name = other.session_name.or_else(|| self.session_name.clone());
698        let attach_to_session = other
699            .attach_to_session
700            .or_else(|| self.attach_to_session.clone());
701        let scrollback_lines_to_serialize = other
702            .scrollback_lines_to_serialize
703            .or_else(|| self.scrollback_lines_to_serialize.clone());
704        let styled_underlines = other.styled_underlines.or(self.styled_underlines);
705        let serialization_interval = other.serialization_interval.or(self.serialization_interval);
706        let disable_session_metadata = other
707            .disable_session_metadata
708            .or(self.disable_session_metadata);
709        let support_kitty_keyboard_protocol = other
710            .support_kitty_keyboard_protocol
711            .or(self.support_kitty_keyboard_protocol);
712        let support_kitty_graphics_protocol = other
713            .support_kitty_graphics_protocol
714            .or(self.support_kitty_graphics_protocol);
715        let web_server = other.web_server.or(self.web_server);
716        let web_sharing = other.web_sharing.or(self.web_sharing);
717        let stacked_resize = other.stacked_resize.or(self.stacked_resize);
718        let stacked_pane_list = other.stacked_pane_list.or(self.stacked_pane_list);
719        let show_startup_tips = other.show_startup_tips.or(self.show_startup_tips);
720        let show_release_notes = other.show_release_notes.or(self.show_release_notes);
721        let advanced_mouse_actions = other.advanced_mouse_actions.or(self.advanced_mouse_actions);
722        let mouse_scroll_resize = other.mouse_scroll_resize.or(self.mouse_scroll_resize);
723        let mouse_hover_effects = other.mouse_hover_effects.or(self.mouse_hover_effects);
724        let mouse_hover_tips = other.mouse_hover_tips.or(self.mouse_hover_tips);
725        let visual_bell = other.visual_bell.or(self.visual_bell);
726        let focus_follows_mouse = merge_bool(other.focus_follows_mouse, self.focus_follows_mouse);
727        let mouse_click_through = merge_bool(other.mouse_click_through, self.mouse_click_through);
728        let osc133_command_selection = other
729            .osc133_command_selection
730            .or(self.osc133_command_selection);
731        let word_separators = other
732            .word_separators
733            .or_else(|| self.word_separators.clone());
734        let host_notification_protocol = other
735            .host_notification_protocol
736            .or(self.host_notification_protocol);
737        let web_server_ip = other.web_server_ip.or(self.web_server_ip);
738        let web_server_port = other.web_server_port.or(self.web_server_port);
739        let web_server_cert = other
740            .web_server_cert
741            .or_else(|| self.web_server_cert.clone());
742        let web_server_key = other.web_server_key.or_else(|| self.web_server_key.clone());
743        let enforce_https_for_localhost = other
744            .enforce_https_for_localhost
745            .or(self.enforce_https_for_localhost);
746        let post_command_discovery_hook = other
747            .post_command_discovery_hook
748            .or_else(|| self.post_command_discovery_hook.clone());
749        let client_async_worker_tasks = other
750            .client_async_worker_tasks
751            .or(self.client_async_worker_tasks);
752        let nested_session_handling = other
753            .nested_session_handling
754            .or(self.nested_session_handling);
755        let dangerously_enable_paste_buffer_read = other
756            .dangerously_enable_paste_buffer_read
757            .or(self.dangerously_enable_paste_buffer_read);
758
759        Options {
760            simplified_ui,
761            theme,
762            theme_dark,
763            theme_light,
764            default_mode,
765            default_shell,
766            default_cwd,
767            default_layout,
768            layout_dir,
769            theme_dir,
770            mouse_mode,
771            pane_frames,
772            pane_frame_style,
773            mirror_session,
774            on_force_close,
775            scroll_buffer_size,
776            copy_command,
777            copy_clipboard,
778            copy_on_select,
779            osc8_hyperlinks,
780            scrollback_editor,
781            session_name,
782            attach_to_session,
783            auto_layout,
784            session_serialization,
785            serialize_pane_viewport,
786            scrollback_lines_to_serialize,
787            styled_underlines,
788            serialization_interval,
789            disable_session_metadata,
790            support_kitty_keyboard_protocol,
791            support_kitty_graphics_protocol,
792            web_server,
793            web_sharing,
794            stacked_resize,
795            stacked_pane_list,
796            show_startup_tips,
797            show_release_notes,
798            advanced_mouse_actions,
799            mouse_scroll_resize,
800            mouse_hover_effects,
801            mouse_hover_tips,
802            visual_bell,
803            focus_follows_mouse,
804            mouse_click_through,
805            osc133_command_selection,
806            word_separators,
807            host_notification_protocol,
808            web_server_ip,
809            web_server_port,
810            web_server_cert,
811            web_server_key,
812            enforce_https_for_localhost,
813            post_command_discovery_hook,
814            client_async_worker_tasks,
815            nested_session_handling,
816            dangerously_enable_paste_buffer_read,
817        }
818    }
819
820    pub fn from_cli(&self, other: Option<Command>) -> Options {
821        if let Some(Command::Options(options)) = other {
822            Options::merge_from_cli(self, options.into())
823        } else {
824            self.to_owned()
825        }
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832
833    #[test]
834    fn pane_frame_style_from_str_accepts_all_variants() {
835        assert_eq!(
836            "full".parse::<PaneFrameStyle>().unwrap(),
837            PaneFrameStyle::Full
838        );
839        assert_eq!(
840            "titles".parse::<PaneFrameStyle>().unwrap(),
841            PaneFrameStyle::Titles
842        );
843        assert_eq!(
844            "none".parse::<PaneFrameStyle>().unwrap(),
845            PaneFrameStyle::None
846        );
847        assert_eq!(
848            "NONE".parse::<PaneFrameStyle>().unwrap(),
849            PaneFrameStyle::None
850        );
851        assert!("bogus".parse::<PaneFrameStyle>().is_err());
852    }
853
854    #[test]
855    fn host_notification_protocol_from_str_accepts_all_variants() {
856        assert_eq!(
857            "auto".parse::<HostNotificationProtocol>().unwrap(),
858            HostNotificationProtocol::Auto
859        );
860        assert_eq!(
861            "osc9".parse::<HostNotificationProtocol>().unwrap(),
862            HostNotificationProtocol::Osc9
863        );
864        assert_eq!(
865            "osc99".parse::<HostNotificationProtocol>().unwrap(),
866            HostNotificationProtocol::Osc99
867        );
868        assert_eq!(
869            "bell".parse::<HostNotificationProtocol>().unwrap(),
870            HostNotificationProtocol::Bell
871        );
872        assert_eq!(
873            "off".parse::<HostNotificationProtocol>().unwrap(),
874            HostNotificationProtocol::Off
875        );
876        assert!("bogus".parse::<HostNotificationProtocol>().is_err());
877    }
878
879    #[test]
880    fn every_host_notification_protocol_variant_stringifies_back_to_itself() {
881        for variant in [
882            HostNotificationProtocol::Auto,
883            HostNotificationProtocol::Osc9,
884            HostNotificationProtocol::Osc99,
885            HostNotificationProtocol::Bell,
886            HostNotificationProtocol::Off,
887        ] {
888            assert_eq!(
889                variant
890                    .as_str()
891                    .parse::<HostNotificationProtocol>()
892                    .unwrap(),
893                variant
894            );
895        }
896    }
897
898    #[test]
899    fn the_host_notification_protocol_defaults_to_auto() {
900        assert_eq!(
901            HostNotificationProtocol::default(),
902            HostNotificationProtocol::Auto
903        );
904    }
905
906    #[test]
907    fn a_configured_host_notification_protocol_is_overridden_by_the_merged_in_one() {
908        let config = Options {
909            host_notification_protocol: Some(HostNotificationProtocol::Osc9),
910            ..Default::default()
911        };
912        let layout = Options {
913            host_notification_protocol: Some(HostNotificationProtocol::Bell),
914            ..Default::default()
915        };
916        assert_eq!(
917            config.merge(layout).host_notification_protocol,
918            Some(HostNotificationProtocol::Bell)
919        );
920    }
921
922    #[test]
923    fn an_unset_host_notification_protocol_does_not_clobber_the_configured_one() {
924        let config = Options {
925            host_notification_protocol: Some(HostNotificationProtocol::Osc9),
926            ..Default::default()
927        };
928        assert_eq!(
929            config.merge(Options::default()).host_notification_protocol,
930            Some(HostNotificationProtocol::Osc9)
931        );
932    }
933
934    #[test]
935    fn a_host_notification_protocol_unset_everywhere_stays_unset() {
936        assert_eq!(
937            Options::default()
938                .merge(Options::default())
939                .host_notification_protocol,
940            None
941        );
942        assert_eq!(
943            Options::default()
944                .merge_from_cli(Options::default())
945                .host_notification_protocol,
946            None
947        );
948    }
949
950    #[test]
951    fn a_host_notification_protocol_given_on_the_command_line_wins() {
952        let config = Options {
953            host_notification_protocol: Some(HostNotificationProtocol::Osc9),
954            ..Default::default()
955        };
956        let cli = Options {
957            host_notification_protocol: Some(HostNotificationProtocol::Off),
958            ..Default::default()
959        };
960        assert_eq!(
961            config.merge_from_cli(cli).host_notification_protocol,
962            Some(HostNotificationProtocol::Off)
963        );
964    }
965
966    #[test]
967    fn a_host_notification_protocol_absent_from_the_command_line_keeps_the_configured_one() {
968        let config = Options {
969            host_notification_protocol: Some(HostNotificationProtocol::Osc99),
970            ..Default::default()
971        };
972        assert_eq!(
973            config
974                .merge_from_cli(Options::default())
975                .host_notification_protocol,
976            Some(HostNotificationProtocol::Osc99),
977            "the option is carried over verbatim, not toggled like the boolean options are"
978        );
979    }
980}