Skip to main content

zellij_utils/input/
options.rs

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