返回 CodeWhale
status.rs
1 //! Status marks: a glyph and a word, never color alone.
2 //!
3 //! Replaces the engine's `menu_style::StatusMark` (`crates/tui/src/tui/
4 //! menu_style.rs`, `Hmbown/CodeWhale` `58b1dd3dd`). The constructor takes a
5 //! [`State`], and the word comes with it, so there is no way to build a
6 //! colored dot that means nothing without its color.
7
8 use std::borrow::Cow;
9
10 use ratatui::{buffer::Buffer, layout::Rect, text::Span, widgets::Widget};
11
12 use crate::{Paint, Role, Theme, glyphs, text};
13
14 /// Product state. One hue, one mark and one word each, on every surface.
15 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16 pub enum State {
17 Working,
18 Done,
19 NeedsYou,
20 Failed,
21 Stopped,
22 Ready,
23 /// Nobody reported a state. Shown as unknown, never as a failure.
24 Unknown,
25 }
26
27 impl State {
28 pub const ALL: [State; 7] = [
29 State::Working,
30 State::Done,
31 State::NeedsYou,
32 State::Failed,
33 State::Stopped,
34 State::Ready,
35 State::Unknown,
36 ];
37
38 #[must_use]
39 pub const fn role(self) -> Role {
40 match self {
41 State::Working | State::Done => Role::Live,
42 State::NeedsYou => Role::Attention,
43 State::Failed => Role::Danger,
44 State::Stopped | State::Ready | State::Unknown => Role::Muted,
45 }
46 }
47
48 #[must_use]
49 pub const fn glyph(self) -> &'static str {
50 match self {
51 State::Working => glyphs::CURRENT,
52 State::Done => glyphs::DONE,
53 State::NeedsYou => glyphs::ATTENTION,
54 State::Failed => glyphs::FAILED,
55 State::Stopped => glyphs::STOPPED,
56 State::Ready => glyphs::READY,
57 State::Unknown => glyphs::UNKNOWN,
58 }
59 }
60
61 /// The English default word, from [`StateWords::english`]. Hosts pass
62 /// their own words with [`StatusMark::with_words`] or
63 /// [`StatusMark::word`].
64 #[must_use]
65 pub const fn word(self) -> &'static str {
66 StateWords::english(self)
67 }
68 }
69
70 /// The word shown beside each [`State`]'s mark. The kit owns no copy beyond
71 /// this default: a host fills one per locale and passes it in. `Default` is
72 /// English.
73 #[derive(Clone, Debug, PartialEq, Eq)]
74 pub struct StateWords {
75 pub working: Cow<'static, str>,
76 pub done: Cow<'static, str>,
77 pub needs_you: Cow<'static, str>,
78 pub failed: Cow<'static, str>,
79 pub stopped: Cow<'static, str>,
80 pub ready: Cow<'static, str>,
81 pub unknown: Cow<'static, str>,
82 }
83
84 impl StateWords {
85 /// The English word for `state`: the only copy in the kit's status
86 /// marks, and what [`StateWords::default`] holds.
87 #[must_use]
88 pub const fn english(state: State) -> &'static str {
89 match state {
90 State::Working => "Working",
91 State::Done => "Done",
92 State::NeedsYou => "Needs you",
93 State::Failed => "Failed",
94 State::Stopped => "Stopped",
95 State::Ready => "Ready",
96 State::Unknown => "Unknown",
97 }
98 }
99
100 /// The word for `state`.
101 #[must_use]
102 pub fn get(&self, state: State) -> &str {
103 match state {
104 State::Working => &self.working,
105 State::Done => &self.done,
106 State::NeedsYou => &self.needs_you,
107 State::Failed => &self.failed,
108 State::Stopped => &self.stopped,
109 State::Ready => &self.ready,
110 State::Unknown => &self.unknown,
111 }
112 }
113 }
114
115 impl Default for StateWords {
116 fn default() -> Self {
117 let english = |state| Cow::Borrowed(Self::english(state));
118 Self {
119 working: english(State::Working),
120 done: english(State::Done),
121 needs_you: english(State::NeedsYou),
122 failed: english(State::Failed),
123 stopped: english(State::Stopped),
124 ready: english(State::Ready),
125 unknown: english(State::Unknown),
126 }
127 }
128 }
129
130 /// `● Working`, `✓ Done`, `◆ Needs you`: the mark takes the state's hue; the
131 /// word stays `Foreground` so it reads at full contrast.
132 #[derive(Clone, Debug, PartialEq, Eq)]
133 pub struct StatusMark {
134 pub state: State,
135 pub word: Cow<'static, str>,
136 /// Paint the word in the state's hue too, for a mark that stands alone.
137 pub tinted: bool,
138 }
139
140 impl StatusMark {
141 #[must_use]
142 pub fn new(state: State) -> Self {
143 Self {
144 state,
145 word: Cow::Borrowed(state.word()),
146 tinted: false,
147 }
148 }
149
150 /// A mark whose word comes from the host's [`StateWords`].
151 #[must_use]
152 pub fn with_words(state: State, words: &StateWords) -> Self {
153 Self {
154 state,
155 word: Cow::Owned(words.get(state).to_owned()),
156 tinted: false,
157 }
158 }
159
160 #[must_use]
161 pub fn word(mut self, word: impl Into<Cow<'static, str>>) -> Self {
162 self.word = word.into();
163 self
164 }
165
166 #[must_use]
167 pub fn tinted(mut self) -> Self {
168 self.tinted = true;
169 self
170 }
171
172 /// The mark's glyph for this terminal.
173 #[must_use]
174 pub fn glyph(&self, theme: &Theme) -> &'static str {
175 glyphs::pick(self.state.glyph(), theme.ascii())
176 }
177
178 #[must_use]
179 pub fn spans(&self, theme: &Theme) -> Vec<Span<'static>> {
180 let hue = theme.fg(self.state.role());
181 let word_style = if self.tinted {
182 hue
183 } else {
184 theme.fg(Role::Foreground)
185 };
186 vec![
187 Span::styled(self.glyph(theme), hue),
188 Span::raw(" "),
189 Span::styled(text::display_safe(&self.word).into_owned(), word_style),
190 ]
191 }
192 }
193
194 impl Paint for StatusMark {
195 fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) {
196 ratatui::text::Line::from(self.spans(theme)).render(area, buf);
197 }
198 }
199
200 #[cfg(test)]
201 mod tests {
202 use super::*;
203 use crate::testing::Profile;
204
205 #[test]
206 fn every_state_has_a_distinct_mark_and_a_word() {
207 for profile in [Profile::DarkTrue, Profile::Ascii] {
208 let theme = profile.theme();
209 let marks: Vec<_> = State::ALL
210 .iter()
211 .map(|s| StatusMark::new(*s).glyph(&theme))
212 .collect();
213 for (i, a) in marks.iter().enumerate() {
214 assert!(!a.trim().is_empty());
215 for b in &marks[i + 1..] {
216 assert_ne!(a, b, "{profile:?}: two states share {a}");
217 }
218 }
219 }
220 for state in State::ALL {
221 assert!(!state.word().is_empty());
222 }
223 }
224
225 /// The default words are the English ones, and a host's words reach the
226 /// painted mark.
227 #[test]
228 fn words_are_parameters_with_english_defaults() {
229 let english = StateWords::default();
230 for state in State::ALL {
231 assert_eq!(english.get(state), state.word());
232 assert_eq!(
233 StatusMark::new(state),
234 StatusMark::with_words(state, &english)
235 );
236 }
237 let host = StateWords {
238 needs_you: "Braucht dich".into(),
239 ..StateWords::default()
240 };
241 let theme = Profile::DarkTrue.theme();
242 let buf = crate::testing::render(20, 1, |area, buf| {
243 StatusMark::with_words(State::NeedsYou, &host).paint(area, buf, &theme);
244 });
245 assert_eq!(crate::testing::text(&buf), "\u{25c6} Braucht dich");
246 assert_eq!(host.get(State::Done), "Done");
247 }
248 }
249
249 lines RUST