Skip to main content

lattice_config/
decorations.rs

1//! Value type for the `ui.window.decorations` typed option.
2//!
3//! Controls OS window chrome on the GPUI peer: `full` (default) keeps the
4//! system titlebar + controls; `none` requests a borderless window (as in
5//! alacritty `decorations = none` / kitty / emacs `undecorated`); `transparent`
6//! is frameless-looking but stays resizable. Pure
7//! presentation policy read only by the GPUI renderer — like [`crate::SignColumn`]
8//! the value type lives here and impls [`OptionType`] locally. The TUI never
9//! reads it. See `docs/dev/architecture/gpui-window-chrome.md`.
10
11use crate::option_type::{EnumeratedValue, OptionType};
12
13/// `ui.window.decorations` — OS window chrome.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Decorations {
16    /// System titlebar + controls (the default; today's behavior).
17    #[default]
18    Full,
19    /// Borderless: no titlebar / controls. `None_` avoids the `Option::None`
20    /// name clash; the on-disk / `:set` label is `none`. On macOS this window is
21    /// non-resizable (see the design fragment); use `transparent` if you need
22    /// the window to stay resizable / controllable by Raycast/yabai.
23    None_,
24    /// Frameless-looking but still resizable: a transparent, full-size-content
25    /// titlebar with the traffic-light buttons hidden (on macOS, via `setHidden:`
26    /// after the window opens — NOT moved off-screen, which would break the
27    /// window's Accessibility geometry). Unlike `none` the window keeps
28    /// `NSResizableWindowMask` on macOS, so edge-resize and external window
29    /// managers (Raycast/yabai) work. Keeps rounded corners + shadow. The
30    /// on-disk / `:set` label is `transparent`.
31    Transparent,
32}
33
34impl Decorations {
35    /// The on-disk / `:set` spelling: `full`, `none` or `transparent`.
36    /// The inverse of [`Self::parse_label`] and what
37    /// [`OptionType::format`] emits.
38    pub fn label(&self) -> &'static str {
39        match self {
40            Decorations::Full => "full",
41            Decorations::None_ => "none",
42            Decorations::Transparent => "transparent",
43        }
44    }
45
46    /// One-line description of this value, shown beside it in the
47    /// `:set ui.window.decorations=<Tab>` completion marginalia.
48    pub fn doc(&self) -> &'static str {
49        match self {
50            Decorations::Full => "System titlebar and window controls (default)",
51            Decorations::None_ => {
52                "Borderless window: no titlebar or controls (non-resizable on macOS)"
53            }
54            Decorations::Transparent => {
55                "Frameless-looking but resizable: transparent titlebar, hidden controls"
56            }
57        }
58    }
59
60    /// True when the window is drawn without OS chrome AND is non-resizable
61    /// (the macOS `titlebar: None` case). `transparent` looks frameless but
62    /// stays resizable, so it is NOT borderless by this predicate — the
63    /// maximize path relies on this to decide whether zoom works.
64    pub fn is_borderless(&self) -> bool {
65        matches!(self, Decorations::None_)
66    }
67
68    /// Every value, in completion order. Drives
69    /// [`OptionType::enumerate`], so the value set is closed.
70    pub fn all() -> [Decorations; 3] {
71        [
72            Decorations::Full,
73            Decorations::None_,
74            Decorations::Transparent,
75        ]
76    }
77
78    /// Parse a [`Self::label`] back into a value. Exact match — no
79    /// trimming, no case folding; anything else is an `Err` naming the
80    /// option and the accepted forms.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use lattice_config::Decorations;
86    ///
87    /// assert_eq!(Decorations::parse_label("none"), Ok(Decorations::None_));
88    /// assert_eq!(Decorations::None_.label(), "none");
89    /// // `transparent` looks frameless but is not "borderless": it stays resizable.
90    /// assert!(!Decorations::Transparent.is_borderless());
91    /// assert!(Decorations::parse_label("None").is_err());
92    /// ```
93    pub fn parse_label(s: &str) -> Result<Self, String> {
94        match s {
95            "full" => Ok(Decorations::Full),
96            "none" => Ok(Decorations::None_),
97            "transparent" => Ok(Decorations::Transparent),
98            other => Err(format!(
99                "ui.window.decorations: expected `full`, `none`, or `transparent`, got `{other}`"
100            )),
101        }
102    }
103}
104
105impl OptionType for Decorations {
106    fn parse(s: &str) -> Result<Self, String> {
107        Decorations::parse_label(s)
108    }
109    fn format(&self) -> String {
110        self.label().to_string()
111    }
112    fn type_label() -> &'static str {
113        "decorations"
114    }
115    fn enumerate() -> Option<Vec<&'static str>> {
116        Some(Decorations::all().iter().map(|v| v.label()).collect())
117    }
118
119    /// TC.1: closed — `parse` accepts these forms and nothing else, so
120    /// the schema is an `enum` and `:customize` can offer a picker.
121    fn enumerate_is_exhaustive() -> bool {
122        true
123    }
124    fn enumerate_with_docs() -> Option<Vec<EnumeratedValue>> {
125        Some(
126            Decorations::all()
127                .iter()
128                .map(|v| EnumeratedValue {
129                    form: v.label(),
130                    doc: v.doc(),
131                })
132                .collect(),
133        )
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn default_is_full_not_borderless() {
143        assert_eq!(Decorations::default(), Decorations::Full);
144        assert!(!Decorations::default().is_borderless());
145    }
146
147    #[test]
148    fn parse_round_trips_every_value() {
149        for v in Decorations::all() {
150            assert_eq!(Decorations::parse_label(v.label()).unwrap(), v);
151            assert_eq!(Decorations::parse(v.label()).unwrap(), v);
152            assert_eq!(v.format(), v.label());
153        }
154    }
155
156    #[test]
157    fn parse_rejects_unknown() {
158        assert!(Decorations::parse_label("buttonless").is_err());
159        assert!(Decorations::parse_label("true").is_err());
160    }
161
162    #[test]
163    fn none_is_borderless() {
164        assert!(Decorations::None_.is_borderless());
165    }
166
167    #[test]
168    fn enumerate_lists_all_forms() {
169        assert_eq!(
170            Decorations::enumerate().unwrap(),
171            vec!["full", "none", "transparent"]
172        );
173    }
174
175    #[test]
176    fn transparent_is_not_borderless() {
177        // `transparent` looks frameless but stays resizable, so the maximize
178        // path must NOT treat it as the non-resizable borderless case.
179        assert!(!Decorations::Transparent.is_borderless());
180    }
181}