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}