返回 CodeWhale
settings.rs
1 //! Settings say what the value is, where it came from and when it applies.
2 //! Hosts own persistence, schema validation and reset actions.
3
4 use crate::{Paint, Role, Theme, glyphs, text};
5 use ratatui::{
6 buffer::Buffer,
7 layout::Rect,
8 style::Modifier,
9 text::{Line, Span},
10 widgets::{Paragraph, Widget},
11 };
12 use std::borrow::Cow;
13 use unicode_segmentation::UnicodeSegmentation;
14
15 // Prefer word boundaries without discarding whitespace or changing styled runs.
16 // Words wider than the row still break at whole graphemes.
17 fn wrap_spans(spans: &[Span<'static>], width: u16) -> Vec<Line<'static>> {
18 let width = usize::from(width);
19 if width == 0 {
20 return Vec::new();
21 }
22 let graphemes: Vec<_> = spans
23 .iter()
24 .flat_map(|span| {
25 span.content.graphemes(true).map(move |grapheme| {
26 let cells = text::width(grapheme);
27 if cells > width {
28 ("?", 1, span.style)
29 } else {
30 (grapheme, cells, span.style)
31 }
32 })
33 })
34 .collect();
35 let whitespace = |grapheme: &str| grapheme.chars().next().is_some_and(char::is_whitespace);
36 let mut lines = Vec::new();
37 let mut start = 0;
38 while start < graphemes.len() {
39 let mut end = start;
40 let mut cells = 0;
41 let mut last_boundary = None;
42 while end < graphemes.len() && cells + graphemes[end].1 <= width {
43 cells += graphemes[end].1;
44 if whitespace(graphemes[end].0) {
45 last_boundary = Some(end + 1);
46 }
47 end += 1;
48 }
49 if end < graphemes.len()
50 && !whitespace(graphemes[end].0)
51 && let Some(boundary) = last_boundary
52 {
53 end = boundary;
54 }
55 let mut current: Vec<Span<'static>> = Vec::new();
56 for &(grapheme, _, style) in &graphemes[start..end] {
57 if let Some(last) = current.last_mut()
58 && last.style == style
59 {
60 last.content.to_mut().push_str(grapheme);
61 } else {
62 current.push(Span::styled(grapheme.to_string(), style));
63 }
64 }
65 lines.push(Line::from(current));
66 start = end;
67 }
68 lines
69 }
70
71 #[derive(Clone, Debug, PartialEq, Eq)]
72 pub struct SettingWords {
73 pub applies_now: Cow<'static, str>,
74 pub changed: Cow<'static, str>,
75 pub reset: Cow<'static, str>,
76 pub default: Cow<'static, str>,
77 }
78 impl Default for SettingWords {
79 fn default() -> Self {
80 Self {
81 applies_now: "Applies now".into(),
82 changed: "changed".into(),
83 reset: "reset to default".into(),
84 default: "Default".into(),
85 }
86 }
87 }
88
89 #[derive(Clone, Debug)]
90 pub struct SettingRow<'a> {
91 pub label: Cow<'a, str>,
92 pub value: Cow<'a, str>,
93 pub source: Option<Cow<'a, str>>,
94 pub apply: Option<Cow<'a, str>>,
95 pub locked: Option<Cow<'a, str>>,
96 pub changed: bool,
97 pub modified: bool,
98 pub selected: bool,
99 pub words: SettingWords,
100 }
101 impl<'a> SettingRow<'a> {
102 #[must_use]
103 pub fn new(label: impl Into<Cow<'a, str>>, value: impl Into<Cow<'a, str>>) -> Self {
104 Self {
105 label: label.into(),
106 value: value.into(),
107 source: None,
108 apply: None,
109 locked: None,
110 changed: false,
111 modified: false,
112 selected: false,
113 words: SettingWords::default(),
114 }
115 }
116 #[must_use]
117 pub fn source(mut self, source: impl Into<Cow<'a, str>>) -> Self {
118 self.source = Some(source.into());
119 self
120 }
121 #[must_use]
122 pub fn apply(mut self, apply: impl Into<Cow<'a, str>>) -> Self {
123 self.apply = Some(apply.into());
124 self
125 }
126 #[must_use]
127 pub fn locked(mut self, reason: impl Into<Cow<'a, str>>) -> Self {
128 self.locked = Some(reason.into());
129 self
130 }
131 #[must_use]
132 pub fn changed(mut self, changed: bool) -> Self {
133 self.changed = changed;
134 self
135 }
136 #[must_use]
137 pub fn modified(mut self, modified: bool) -> Self {
138 self.modified = modified;
139 self
140 }
141 #[must_use]
142 pub fn selected(mut self, selected: bool) -> Self {
143 self.selected = selected;
144 self
145 }
146 #[must_use]
147 pub fn with_words(mut self, words: &SettingWords) -> Self {
148 self.words = words.clone();
149 self
150 }
151 #[must_use]
152 pub fn lines(&self, width: u16, theme: &Theme) -> Vec<Line<'static>> {
153 let label_style = if self.selected {
154 theme.fg(Role::Foreground).add_modifier(Modifier::BOLD)
155 } else {
156 theme.fg(Role::Foreground)
157 };
158 let marker = glyphs::pick(glyphs::selection_marker(self.selected), theme.ascii());
159 let mut spans = vec![
160 Span::styled(format!("{marker} "), theme.fg(Role::Primary)),
161 Span::styled(text::display_safe(&self.label).into_owned(), label_style),
162 Span::raw(" "),
163 Span::styled(
164 text::display_safe(&self.value).into_owned(),
165 theme.fg(if self.locked.is_some() {
166 Role::Muted
167 } else {
168 Role::Foreground
169 }),
170 ),
171 ];
172 let separator = if theme.ascii() { " " } else { " · " };
173 for fact in [
174 self.source.as_deref(),
175 Some(self.apply.as_deref().unwrap_or(&self.words.applies_now)),
176 self.locked.as_deref(),
177 self.changed.then_some(self.words.changed.as_ref()),
178 ]
179 .into_iter()
180 .flatten()
181 {
182 if !fact.is_empty() {
183 spans.push(Span::styled(
184 format!("{separator}{}", text::display_safe(fact)),
185 theme.fg(Role::Muted),
186 ));
187 }
188 }
189 // A locked setting cannot promise a reset action.
190 if self.modified && self.locked.is_none() {
191 spans.push(Span::styled(
192 format!("{separator}r {}", text::display_safe(&self.words.reset)),
193 theme.fg(Role::Muted),
194 ));
195 }
196 wrap_spans(&spans, width)
197 }
198 }
199 impl Paint for SettingRow<'_> {
200 fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) {
201 let area = area.intersection(buf.area);
202 if area.is_empty() {
203 return;
204 }
205 let lines = self.lines(area.width, theme);
206 if self.selected {
207 let shown = u16::try_from(lines.len())
208 .unwrap_or(u16::MAX)
209 .min(area.height);
210 buf.set_style(
211 Rect {
212 height: shown,
213 ..area
214 },
215 theme.bg(Role::Selected),
216 );
217 }
218 Paragraph::new(lines).render(area, buf);
219 }
220 fn height(&self, width: u16, theme: &Theme) -> u16 {
221 u16::try_from(self.lines(width, theme).len()).unwrap_or(u16::MAX)
222 }
223 }
224
225 #[derive(Clone, Debug)]
226 pub struct SettingDetail<'a> {
227 pub label: Cow<'a, str>,
228 pub description: Cow<'a, str>,
229 pub apply: Option<Cow<'a, str>>,
230 pub source: Option<Cow<'a, str>>,
231 pub default_value: Option<Cow<'a, str>>,
232 pub locked: Option<Cow<'a, str>>,
233 pub modified: bool,
234 pub words: SettingWords,
235 }
236 impl<'a> SettingDetail<'a> {
237 #[must_use]
238 pub fn new(label: impl Into<Cow<'a, str>>, description: impl Into<Cow<'a, str>>) -> Self {
239 Self {
240 label: label.into(),
241 description: description.into(),
242 apply: None,
243 source: None,
244 default_value: None,
245 locked: None,
246 modified: false,
247 words: SettingWords::default(),
248 }
249 }
250 #[must_use]
251 pub fn apply(mut self, apply: impl Into<Cow<'a, str>>) -> Self {
252 self.apply = Some(apply.into());
253 self
254 }
255 #[must_use]
256 pub fn source(mut self, source: impl Into<Cow<'a, str>>) -> Self {
257 self.source = Some(source.into());
258 self
259 }
260 #[must_use]
261 pub fn default_value(mut self, value: impl Into<Cow<'a, str>>) -> Self {
262 self.default_value = Some(value.into());
263 self
264 }
265 #[must_use]
266 pub fn locked(mut self, reason: impl Into<Cow<'a, str>>) -> Self {
267 self.locked = Some(reason.into());
268 self
269 }
270 #[must_use]
271 pub fn modified(mut self, modified: bool) -> Self {
272 self.modified = modified;
273 self
274 }
275 #[must_use]
276 pub fn with_words(mut self, words: &SettingWords) -> Self {
277 self.words = words.clone();
278 self
279 }
280 #[must_use]
281 pub fn lines(&self, width: u16, theme: &Theme) -> Vec<Line<'static>> {
282 if width == 0 {
283 return Vec::new();
284 }
285 let mut lines = wrap_spans(
286 &[Span::styled(
287 text::display_safe(&self.label).into_owned(),
288 theme.fg(Role::Foreground).add_modifier(Modifier::BOLD),
289 )],
290 width,
291 );
292 for paragraph in self.description.lines() {
293 lines.extend(wrap_spans(
294 &[Span::styled(
295 text::display_safe(paragraph).into_owned(),
296 theme.fg(Role::Foreground),
297 )],
298 width,
299 ));
300 }
301 let mut facts = vec![
302 self.apply
303 .as_deref()
304 .unwrap_or(&self.words.applies_now)
305 .to_string(),
306 ];
307 if let Some(source) = &self.source {
308 facts.push(source.to_string());
309 }
310 if let Some(value) = &self.default_value {
311 facts.push(format!("{}: {value}", self.words.default));
312 }
313 if let Some(reason) = &self.locked {
314 facts.push(reason.to_string());
315 }
316 if self.modified && self.locked.is_none() {
317 facts.push(format!("r {}", self.words.reset));
318 }
319 for fact in facts {
320 lines.extend(wrap_spans(
321 &[Span::styled(
322 text::display_safe(&fact).into_owned(),
323 theme.fg(Role::Muted),
324 )],
325 width,
326 ));
327 }
328 lines
329 }
330 }
331 impl Paint for SettingDetail<'_> {
332 fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) {
333 let area = area.intersection(buf.area);
334 if area.is_empty() {
335 return;
336 }
337 Paragraph::new(self.lines(area.width, theme)).render(area, buf);
338 }
339 fn height(&self, width: u16, theme: &Theme) -> u16 {
340 u16::try_from(self.lines(width, theme).len()).unwrap_or(u16::MAX)
341 }
342 }
343
343 lines RUST