返回 CodeWhale
grammar.rs
根目录 / crates / palette / src / grammar.rs
1 //! Closed color vocabulary for status-bar chrome.
2 //!
3 //! Five visual families: surface, neutral text, action, live/outcome, and
4 //! attention. Semantic roles stay explicit even when they share a hue;
5 //! modes and outcomes are named by their labels, not extra rainbow lanes.
6 //! Attention preserves each theme's permission, warning and danger shades.
7 //!
8 //! Contract: `docs/design/STATUS_BAR_COLOR_GRAMMAR.md`.
9
10 use ratatui::style::{Color, Style};
11
12 use super::themes::UiTheme;
13
14 /// The five visual families. Surface is the canvas, not a foreground ink.
15 #[cfg_attr(not(test), allow(dead_code))]
16 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
17 pub enum SemanticFamily {
18 Surface,
19 Neutral,
20 Action,
21 Live,
22 Attention,
23 }
24
25 impl SemanticFamily {
26 #[cfg(test)]
27 pub const ALL: [Self; 5] = [
28 Self::Surface,
29 Self::Neutral,
30 Self::Action,
31 Self::Live,
32 Self::Attention,
33 ];
34 }
35
36 /// Named status-bar inks. Each variant is an existing `UiTheme` slot, not a
37 /// new theme. Adding a variant requires assigning one of the five families.
38 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
39 pub enum ChromeInk {
40 Outcome,
41 PermissionAsk,
42 PermissionAutoReview,
43 PermissionFullAccess,
44 Waiting,
45 Attention,
46 Active,
47 PolicyAct,
48 PolicyPlan,
49 PolicyOperate,
50 Identity,
51 Info,
52 MetadataValue,
53 Metadata,
54 MetadataHint,
55 MetadataDim,
56 Failure,
57 }
58
59 impl ChromeInk {
60 #[cfg(test)]
61 pub const ALL: [Self; 17] = [
62 Self::Outcome,
63 Self::PermissionAsk,
64 Self::PermissionAutoReview,
65 Self::PermissionFullAccess,
66 Self::Waiting,
67 Self::Attention,
68 Self::Active,
69 Self::PolicyAct,
70 Self::PolicyPlan,
71 Self::PolicyOperate,
72 Self::Identity,
73 Self::Info,
74 Self::MetadataValue,
75 Self::Metadata,
76 Self::MetadataHint,
77 Self::MetadataDim,
78 Self::Failure,
79 ];
80
81 #[must_use]
82 #[cfg_attr(not(test), allow(dead_code))]
83 pub const fn family(self) -> SemanticFamily {
84 match self {
85 Self::Outcome | Self::Active => SemanticFamily::Live,
86 Self::PermissionAsk
87 | Self::PermissionAutoReview
88 | Self::PermissionFullAccess
89 | Self::Waiting
90 | Self::Attention
91 | Self::Failure => SemanticFamily::Attention,
92 Self::PolicyAct
93 | Self::PolicyPlan
94 | Self::PolicyOperate
95 | Self::Identity
96 | Self::Info => SemanticFamily::Action,
97 Self::MetadataValue | Self::Metadata | Self::MetadataHint | Self::MetadataDim => {
98 SemanticFamily::Neutral
99 }
100 }
101 }
102
103 /// Resolve through the live theme. Ordinary navigation and mode share
104 /// its action hue; active and completed work share its live hue. Safety
105 /// retains the existing permission and warning/error distinctions.
106 #[must_use]
107 pub fn color(self, theme: &UiTheme) -> Color {
108 match self {
109 Self::Outcome => theme.status_working,
110 Self::PermissionAsk => theme.permission_ask,
111 Self::PermissionAutoReview => theme.permission_auto_review,
112 Self::PermissionFullAccess => theme.permission_full_access,
113 Self::Waiting => theme.accent_action,
114 Self::Attention => theme.warning,
115 Self::Active => theme.status_working,
116 Self::PolicyAct => theme.accent_primary,
117 Self::PolicyPlan => theme.accent_primary,
118 Self::PolicyOperate => theme.accent_primary,
119 Self::Identity => theme.accent_primary,
120 Self::Info => theme.accent_primary,
121 Self::MetadataValue => theme.text_soft,
122 Self::Metadata => theme.text_muted,
123 Self::MetadataHint => theme.text_hint,
124 Self::MetadataDim => theme.text_dim,
125 Self::Failure => theme.error_fg,
126 }
127 }
128 }
129
130 #[must_use]
131 pub fn chrome_style(theme: &UiTheme, ink: ChromeInk) -> Style {
132 Style::default().fg(ink.color(theme))
133 }
134
135 #[cfg(test)]
136 mod tests {
137 use super::*;
138 use crate::themes::SELECTABLE_THEMES;
139
140 #[test]
141 fn chrome_has_five_visual_families_with_explicit_safety_roles() {
142 assert_eq!(SemanticFamily::ALL.len(), 5);
143 for ink in ChromeInk::ALL {
144 assert!(SemanticFamily::ALL.contains(&ink.family()));
145 }
146 assert_eq!(ChromeInk::Failure.family(), SemanticFamily::Attention);
147 assert_eq!(
148 ChromeInk::PermissionFullAccess.family(),
149 SemanticFamily::Attention
150 );
151 assert_eq!(ChromeInk::PolicyOperate.family(), SemanticFamily::Action);
152 assert_eq!(ChromeInk::Outcome.family(), SemanticFamily::Live);
153 }
154
155 #[test]
156 fn every_selectable_theme_limits_ordinary_chrome_to_action_and_live() {
157 for id in SELECTABLE_THEMES {
158 let theme = id.ui_theme();
159 // Changing a mode or finishing work must not add a third hue.
160 for ink in [
161 ChromeInk::Identity,
162 ChromeInk::Info,
163 ChromeInk::PolicyAct,
164 ChromeInk::PolicyPlan,
165 ChromeInk::PolicyOperate,
166 ] {
167 assert_eq!(
168 ink.color(&theme),
169 theme.accent_primary,
170 "{} {ink:?}",
171 id.name()
172 );
173 }
174 for ink in [ChromeInk::Active, ChromeInk::Outcome] {
175 assert_eq!(
176 ink.color(&theme),
177 theme.status_working,
178 "{} {ink:?}",
179 id.name()
180 );
181 }
182 assert_eq!(ChromeInk::Metadata.color(&theme), theme.text_muted);
183 }
184 }
185
186 #[test]
187 fn every_selectable_theme_preserves_permission_and_failure_meaning() {
188 for id in SELECTABLE_THEMES {
189 let theme = id.ui_theme();
190 let permissions = [
191 ChromeInk::PermissionAsk.color(&theme),
192 ChromeInk::PermissionAutoReview.color(&theme),
193 ChromeInk::PermissionFullAccess.color(&theme),
194 ];
195 assert_eq!(
196 permissions,
197 [
198 theme.permission_ask,
199 theme.permission_auto_review,
200 theme.permission_full_access
201 ],
202 "{} permission authority",
203 id.name()
204 );
205 assert_ne!(
206 permissions[0],
207 permissions[1],
208 "{} Ask/Auto-Review",
209 id.name()
210 );
211 assert_ne!(
212 permissions[1],
213 permissions[2],
214 "{} Auto-Review/Full Access",
215 id.name()
216 );
217 assert_ne!(
218 permissions[0],
219 permissions[2],
220 "{} Ask/Full Access",
221 id.name()
222 );
223 assert_eq!(ChromeInk::Attention.color(&theme), theme.warning);
224 assert_eq!(ChromeInk::Failure.color(&theme), theme.error_fg);
225 assert_eq!(
226 chrome_style(&theme, ChromeInk::Failure).fg,
227 Some(theme.error_fg)
228 );
229 }
230 }
231 }
232
232 lines RUST