1use std::fmt;
9
10use crate::input::{Colour, GridAttr, COLOUR_DEFAULT};
11
12#[path = "style/colour.rs"]
13mod colour;
14#[path = "style/grammar.rs"]
15mod grammar;
16#[path = "style/types.rs"]
17mod types;
18
19pub use colour::{colour_to_string, parse_colour, ColourParseError};
20pub use types::{
21 Style, StyleAlign, StyleCell, StyleDefaultType, StyleList, StyleRange, StyleWidth,
22};
23
24use grammar::{attributes_to_string, parse_attributes, parse_range, parse_width, strip_prefix_ci};
25
26const STYLE_TOKEN_DELIMITERS: &[char] = &[' ', ',', '\n'];
27
28impl Style {
29 pub fn parse(input: &str) -> Result<Self, StyleParseError> {
31 let mut style = Self::default();
32 style.parse_in_place(&StyleCell::default(), input)?;
33 Ok(style)
34 }
35
36 #[must_use]
38 pub fn with_cell(cell: StyleCell) -> Self {
39 Self {
40 cell,
41 ..Self::default()
42 }
43 }
44
45 pub fn parse_in_place(&mut self, base: &StyleCell, input: &str) -> Result<(), StyleParseError> {
48 if input.is_empty() {
49 return Ok(());
50 }
51
52 let saved = self.clone();
53 for token in input
54 .split(|character| STYLE_TOKEN_DELIMITERS.contains(&character))
55 .filter(|token| !token.is_empty())
56 {
57 if let Err(error) = self.apply_token(base, token) {
58 *self = saved;
59 return Err(error);
60 }
61 }
62 Ok(())
63 }
64
65 pub fn applied(&self, base: &StyleCell, input: &str) -> Result<Self, StyleParseError> {
67 let mut next = self.clone();
68 next.parse_in_place(base, input)?;
69 Ok(next)
70 }
71
72 pub fn overlaid(&self, input: &str) -> Result<Self, StyleParseError> {
75 self.applied(&self.cell, input)
76 }
77
78 fn apply_token(&mut self, base: &StyleCell, token: &str) -> Result<(), StyleParseError> {
79 let lowered = token.to_ascii_lowercase();
80
81 match lowered.as_str() {
83 "default" => {
84 self.cell = *base;
85 return Ok(());
86 }
87 "ignore" => {
88 self.ignore = true;
89 return Ok(());
90 }
91 "noignore" => {
92 self.ignore = false;
93 return Ok(());
94 }
95 "push-default" => {
96 self.default_type = StyleDefaultType::Push;
97 return Ok(());
98 }
99 "pop-default" => {
100 self.default_type = StyleDefaultType::Pop;
101 return Ok(());
102 }
103 "set-default" => {
104 self.default_type = StyleDefaultType::Set;
105 return Ok(());
106 }
107 "nolist" => {
108 self.list = StyleList::Off;
109 return Ok(());
110 }
111 "norange" => {
112 self.range = StyleRange::None;
113 return Ok(());
114 }
115 "noalign" => {
116 self.align = StyleAlign::Default;
117 return Ok(());
118 }
119 "none" => {
120 self.cell.attr = 0;
121 return Ok(());
122 }
123 _ => {}
124 }
125
126 if let Some(value) = strip_prefix_ci(token, "list=") {
129 self.list = match value.to_ascii_lowercase().as_str() {
130 "on" => StyleList::On,
131 "focus" => StyleList::Focus,
132 "left-marker" => StyleList::LeftMarker,
133 "right-marker" => StyleList::RightMarker,
134 _ => return Err(StyleParseError::invalid(token)),
135 };
136 return Ok(());
137 }
138
139 if let Some(value) = strip_prefix_ci(token, "range=") {
140 self.range = parse_range(value).ok_or_else(|| StyleParseError::invalid(token))?;
141 return Ok(());
142 }
143
144 if let Some(value) = strip_prefix_ci(token, "align=") {
145 self.align = match value.to_ascii_lowercase().as_str() {
146 "left" => StyleAlign::Left,
147 "centre" => StyleAlign::Centre,
148 "right" => StyleAlign::Right,
149 "absolute-centre" => StyleAlign::AbsoluteCentre,
150 _ => return Err(StyleParseError::invalid(token)),
151 };
152 return Ok(());
153 }
154
155 if let Some(value) = strip_prefix_ci(token, "fill=") {
156 self.fill = parse_colour(value).map_err(|_| StyleParseError::invalid(token))?;
157 return Ok(());
158 }
159
160 if let Some(value) = strip_prefix_ci(token, "fg=") {
161 self.cell.fg = resolve_style_colour(base.fg, value, token)?;
162 return Ok(());
163 }
164
165 if let Some(value) = strip_prefix_ci(token, "bg=") {
166 self.cell.bg = resolve_style_colour(base.bg, value, token)?;
167 return Ok(());
168 }
169
170 if let Some(value) = strip_prefix_ci(token, "us=") {
171 self.cell.us = resolve_style_colour(base.us, value, token)?;
172 return Ok(());
173 }
174
175 if let Some(value) = strip_prefix_ci(token, "width=") {
176 self.width = Some(parse_width(value).ok_or_else(|| StyleParseError::invalid(token))?);
177 return Ok(());
178 }
179
180 if let Some(value) = strip_prefix_ci(token, "pad=") {
181 self.pad = Some(
182 value
183 .parse::<u32>()
184 .map_err(|_| StyleParseError::invalid(token))?,
185 );
186 return Ok(());
187 }
188
189 if let Some(attr_name) = lowered.strip_prefix("no") {
192 if attr_name == "attr" {
193 self.cell.attr |= GridAttr::NOATTR;
194 return Ok(());
195 }
196 let bits =
197 parse_attributes(attr_name).ok_or_else(|| StyleParseError::invalid(token))?;
198 self.cell.attr &= !bits;
199 return Ok(());
200 }
201
202 if let Ok(colour) = parse_colour(token) {
204 self.cell.fg = if colour == COLOUR_DEFAULT {
205 base.fg
206 } else {
207 colour
208 };
209 return Ok(());
210 }
211
212 let bits = parse_attributes(token).ok_or_else(|| StyleParseError::invalid(token))?;
214 self.cell.attr |= bits;
215 Ok(())
216 }
217}
218
219pub fn style_parse(
221 style: &mut Style,
222 base: &StyleCell,
223 input: &str,
224) -> Result<(), StyleParseError> {
225 style.parse_in_place(base, input)
226}
227
228#[must_use]
230pub fn style_tostring(style: &Style) -> String {
231 let mut tokens = Vec::new();
232
233 if let Some(list) = style.list.as_tmux_str() {
234 tokens.push(format!("list={list}"));
235 }
236 if let Some(range) = style.range.as_tmux_value() {
237 tokens.push(format!("range={range}"));
238 }
239 if let Some(align) = style.align.as_tmux_str() {
240 tokens.push(format!("align={align}"));
241 }
242 if let Some(default_type) = style.default_type.as_tmux_str() {
243 tokens.push(default_type.to_owned());
244 }
245 if style.fill != COLOUR_DEFAULT {
246 tokens.push(format!("fill={}", colour_to_string(style.fill)));
247 }
248 if style.cell.fg != COLOUR_DEFAULT {
249 tokens.push(format!("fg={}", colour_to_string(style.cell.fg)));
250 }
251 if style.cell.bg != COLOUR_DEFAULT {
252 tokens.push(format!("bg={}", colour_to_string(style.cell.bg)));
253 }
254 if style.cell.us != COLOUR_DEFAULT {
255 tokens.push(format!("us={}", colour_to_string(style.cell.us)));
256 }
257 if style.cell.attr != 0 {
258 tokens.push(attributes_to_string(style.cell.attr));
259 }
260 if let Some(width) = style.width {
261 tokens.push(format!("width={}", width.as_tmux_value()));
262 }
263 if let Some(pad) = style.pad {
264 tokens.push(format!("pad={pad}"));
265 }
266
267 if tokens.is_empty() {
268 "default".to_owned()
269 } else {
270 tokens.join(",")
271 }
272}
273
274fn resolve_style_colour(base: Colour, value: &str, token: &str) -> Result<Colour, StyleParseError> {
275 let colour = parse_colour(value).map_err(|_| StyleParseError::invalid(token))?;
276 Ok(if colour == COLOUR_DEFAULT {
277 base
278 } else {
279 colour
280 })
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct StyleParseError {
286 token: String,
287}
288
289impl StyleParseError {
290 fn invalid(token: &str) -> Self {
291 Self {
292 token: token.to_owned(),
293 }
294 }
295}
296
297impl fmt::Display for StyleParseError {
298 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
299 write!(formatter, "invalid style token: {}", self.token)
300 }
301}
302
303impl std::error::Error for StyleParseError {}
304
305#[cfg(test)]
306mod tests {
307 use super::{
308 colour_to_string, parse_colour, style_parse, style_tostring, ColourParseError, Style,
309 StyleAlign, StyleCell, StyleDefaultType, StyleList, StyleRange, StyleWidth,
310 };
311 use crate::input::{
312 colour_join_rgb, GridAttr, COLOUR_DEFAULT, COLOUR_FLAG_256, COLOUR_NONE, COLOUR_TERMINAL,
313 };
314
315 fn default_base() -> StyleCell {
316 StyleCell::default()
317 }
318
319 fn parse_style(input: &str) -> Style {
320 Style::parse(input).expect("style parses")
321 }
322
323 #[test]
324 fn colour_parser_accepts_all_supported_forms() {
325 for (input, expected) in [
326 ("black", 0),
327 ("red", 1),
328 ("green", 2),
329 ("yellow", 3),
330 ("blue", 4),
331 (concat!("mag", "enta"), 5),
332 ("cyan", 6),
333 ("white", 7),
334 ("brightblack", 90),
335 ("brightred", 91),
336 ("brightgreen", 92),
337 ("brightyellow", 93),
338 ("brightblue", 94),
339 (concat!("bright", "mag", "enta"), 95),
340 ("brightcyan", 96),
341 ("brightwhite", 97),
342 ("colour214", COLOUR_FLAG_256 | 214),
343 ("color33", COLOUR_FLAG_256 | 33),
344 ("214", COLOUR_FLAG_256 | 214),
345 ("default", COLOUR_DEFAULT),
346 ("terminal", COLOUR_TERMINAL),
347 ("none", COLOUR_NONE),
348 ] {
349 assert_eq!(parse_colour(input), Ok(expected), "{input}");
350 }
351 assert_eq!(
352 parse_colour("#123456"),
353 Ok(colour_join_rgb(0x12, 0x34, 0x56))
354 );
355 assert_eq!(
356 parse_colour("colour256"),
357 Err(ColourParseError::Invalid("colour256".to_owned()))
358 );
359 }
360
361 #[test]
362 fn colour_tostring_canonicalizes_supported_forms() {
363 assert_eq!(colour_to_string(COLOUR_NONE), "none");
364 assert_eq!(colour_to_string(COLOUR_DEFAULT), "default");
365 assert_eq!(colour_to_string(COLOUR_TERMINAL), "terminal");
366 assert_eq!(colour_to_string(COLOUR_FLAG_256 | 214), "colour214");
367 assert_eq!(
368 colour_to_string(colour_join_rgb(0x12, 0x34, 0x56)),
369 "#123456"
370 );
371 }
372
373 #[test]
374 fn style_parser_accepts_attributes_none_noattr_and_negation() {
375 let style = parse_style(
376 "acs,bold,bright,dim,underscore,blink,reverse,hidden,italics,\
377 strikethrough,double-underscore,curly-underscore,dotted-underscore,\
378 dashed-underscore,overline,noattr",
379 );
380 assert_eq!(
381 style.cell.attr,
382 GridAttr::CHARSET
383 | GridAttr::BRIGHT
384 | GridAttr::DIM
385 | GridAttr::UNDERSCORE
386 | GridAttr::BLINK
387 | GridAttr::REVERSE
388 | GridAttr::HIDDEN
389 | GridAttr::ITALICS
390 | GridAttr::STRIKETHROUGH
391 | GridAttr::UNDERSCORE_2
392 | GridAttr::UNDERSCORE_3
393 | GridAttr::UNDERSCORE_4
394 | GridAttr::UNDERSCORE_5
395 | GridAttr::OVERLINE
396 | GridAttr::NOATTR
397 );
398
399 let style = parse_style("bold,reverse,nobold,noreverse");
400 assert_eq!(style.cell.attr, 0);
401
402 let style = parse_style("none");
403 assert_eq!(style.cell.attr, 0);
404 }
405
406 #[test]
407 fn style_parser_accepts_prefixed_and_bare_colours() {
408 let style = parse_style("fg=red,bg=colour214,us=#123456,fill=214,blue");
409 assert_eq!(style.cell.fg, 4);
410 assert_eq!(style.cell.bg, COLOUR_FLAG_256 | 214);
411 assert_eq!(style.cell.us, colour_join_rgb(0x12, 0x34, 0x56));
412 assert_eq!(style.fill, COLOUR_FLAG_256 | 214);
413 }
414
415 #[test]
416 fn style_parser_accepts_list_align_range_width_pad_and_defaults() {
417 let style = parse_style(
418 "list=focus,align=absolute-centre,range=window|42,width=50%,pad=3,push-default,ignore",
419 );
420 assert_eq!(style.list, StyleList::Focus);
421 assert_eq!(style.align, StyleAlign::AbsoluteCentre);
422 assert_eq!(style.range, StyleRange::Window(42));
423 assert_eq!(style.width, Some(StyleWidth::Percentage(50)));
424 assert_eq!(style.pad, Some(3));
425 assert_eq!(style.default_type, StyleDefaultType::Push);
426 assert!(style.ignore);
427 }
428
429 #[test]
430 fn style_parser_is_case_insensitive_and_accepts_space_and_newline_delimiters() {
431 let style = parse_style(
432 "FG=RED BG=blue\nUs=#123456 Fill=214 BOLD REVERSE LIST=FOCUS RANGE=PANE|%7",
433 );
434 assert_eq!(style.cell.fg, 1);
435 assert_eq!(style.cell.bg, 4);
436 assert_eq!(style.cell.us, colour_join_rgb(0x12, 0x34, 0x56));
437 assert_eq!(style.fill, COLOUR_FLAG_256 | 214);
438 assert_eq!(style.cell.attr, GridAttr::BRIGHT | GridAttr::REVERSE);
439 assert_eq!(style.list, StyleList::Focus);
440 assert_eq!(style.range, StyleRange::Pane(7));
441 }
442
443 #[test]
444 fn style_parser_validates_all_range_variants() {
445 for input in [
446 "range=left",
447 "range=right",
448 "range=pane|%7",
449 "range=window|9",
450 "range=session|$11",
451 "range=user|custom-tag",
452 "range=control|9",
453 ] {
454 parse_style(input);
455 }
456
457 for input in [
458 "range=left|1",
459 "range=pane|7",
460 "range=session|11",
461 "range=control|10",
462 "range=user|",
463 "range=user|0123456789abcdef",
464 "range=window|x",
465 ] {
466 let mut style = Style::default();
467 assert!(
468 style_parse(&mut style, &default_base(), input).is_err(),
469 "{input}"
470 );
471 assert_eq!(style, Style::default(), "{input}");
472 }
473 }
474
475 #[test]
476 fn style_parser_supports_norange_nolist_noalign_and_noignore() {
477 let mut style =
478 parse_style("list=left-marker,align=right,range=right,ignore,width=3,pad=2");
479 style_parse(
480 &mut style,
481 &default_base(),
482 "nolist,noalign,norange,noignore",
483 )
484 .expect("second parse succeeds");
485 assert_eq!(style.list, StyleList::Off);
486 assert_eq!(style.align, StyleAlign::Default);
487 assert_eq!(style.range, StyleRange::None);
488 assert!(!style.ignore);
489 assert_eq!(style.width, Some(StyleWidth::Cells(3)));
490 assert_eq!(style.pad, Some(2));
491 }
492
493 #[test]
494 fn style_parser_is_additive_and_default_resets_to_base_cell() {
495 let base = parse_style("fg=red,bg=blue,bold");
496 let style = base.overlaid("bg=green,reverse").expect("overlay parses");
497 assert_eq!(style.cell.fg, 1);
498 assert_eq!(style.cell.bg, 2);
499 assert_eq!(style.cell.attr, GridAttr::BRIGHT | GridAttr::REVERSE);
500
501 let reset = style
502 .applied(&base.cell, "default")
503 .expect("default parses");
504 assert_eq!(reset.cell, base.cell);
505 assert_eq!(reset.fill, style.fill);
506 }
507
508 #[test]
509 fn empty_style_string_is_a_no_op() {
510 let style = parse_style("fg=red");
511 let next = style.overlaid("").expect("empty parse succeeds");
512 assert_eq!(next, style);
513 }
514
515 #[test]
516 fn invalid_style_keeps_the_original_value() {
517 let original = parse_style("fg=red,bold");
518 let mut style = original.clone();
519 assert!(style_parse(&mut style, &default_base(), "fg=invalid").is_err());
520 assert_eq!(style, original);
521 }
522
523 #[test]
524 fn style_tostring_round_trips_supported_states() {
525 let cases = [
526 "default",
527 "fg=red,bg=blue",
528 "fill=#123456,fg=214,bg=none,us=colour33,bold,reverse,width=42,pad=0",
529 "list=left-marker,range=left,align=centre,push-default,ignore",
530 "range=pane|%9,list=focus,fg=none,noattr",
531 "range=session|$5,list=on,align=absolute-centre,width=50%",
532 "range=user|custom,list=right-marker,pop-default",
533 "range=control|7,set-default",
534 ];
535
536 for input in cases {
537 let style = parse_style(input);
538 let rendered = style_tostring(&style);
539 let mut round_tripped = Style::default();
540 style_parse(&mut round_tripped, &default_base(), &rendered)
541 .expect("rendered style parses");
542 if style.ignore {
543 let mut expected = style.clone();
544 expected.ignore = false;
545 assert_eq!(round_tripped, expected, "{input} => {rendered}");
546 } else {
547 assert_eq!(round_tripped, style, "{input} => {rendered}");
548 }
549 }
550 }
551
552 #[test]
553 fn style_tostring_emits_default_for_empty_style() {
554 assert_eq!(style_tostring(&Style::default()), "default");
555 }
556
557 #[test]
558 fn style_tostring_omits_ignore_like_tmux() {
559 let style = parse_style("fg=red,ignore");
560 assert_eq!(style_tostring(&style), "fg=red");
561 }
562}