Skip to main content

zellij_tile/ui_components/
text.rs

1use std::ops::Bound;
2use std::ops::RangeBounds;
3use zellij_utils::data::StyledText;
4
5#[derive(Debug, Default, Clone)]
6pub struct Text {
7    text: String,
8    selected: bool,
9    opaque: bool,
10    disabled: bool,
11    indices: Vec<Vec<usize>>,
12}
13
14impl From<StyledText> for Text {
15    fn from(styled_text: StyledText) -> Self {
16        Text {
17            text: styled_text.text,
18            selected: false,
19            opaque: false,
20            disabled: false,
21            indices: styled_text.indices,
22        }
23    }
24}
25
26impl Text {
27    pub fn new<S: AsRef<str>>(content: S) -> Self
28    where
29        S: ToString,
30    {
31        Text {
32            text: content.to_string(),
33            selected: false,
34            opaque: false,
35            disabled: false,
36            indices: vec![],
37        }
38    }
39    pub fn selected(mut self) -> Self {
40        self.selected = true;
41        self
42    }
43    pub fn opaque(mut self) -> Self {
44        self.opaque = true;
45        self
46    }
47    pub fn disabled(mut self) -> Self {
48        self.disabled = true;
49        self
50    }
51    pub fn dim_indices(mut self, mut indices: Vec<usize>) -> Self {
52        const DIM_LEVEL: usize = 4;
53        self.pad_indices(DIM_LEVEL);
54        self.indices
55            .get_mut(DIM_LEVEL)
56            .map(|i| i.append(&mut indices));
57        self
58    }
59    pub fn dim_range<R: RangeBounds<usize>>(mut self, indices: R) -> Self {
60        const DIM_LEVEL: usize = 4;
61        self.pad_indices(DIM_LEVEL);
62        let start = match indices.start_bound() {
63            Bound::Unbounded => 0,
64            Bound::Included(s) => *s,
65            Bound::Excluded(s) => *s,
66        };
67        let end = match indices.end_bound() {
68            Bound::Unbounded => self.text.chars().count(),
69            Bound::Included(s) => *s + 1,
70            Bound::Excluded(s) => *s,
71        };
72        let indices = (start..end).into_iter();
73        self.indices
74            .get_mut(DIM_LEVEL)
75            .map(|i| i.append(&mut indices.into_iter().collect()));
76        self
77    }
78    pub fn dim_substring<S: AsRef<str>>(mut self, substr: S) -> Self {
79        let substr = substr.as_ref();
80        let mut start = 0;
81
82        while let Some(pos) = self.text[start..].find(substr) {
83            let abs_pos = start + pos;
84            self = self.dim_range(abs_pos..abs_pos + substr.chars().count());
85            start = abs_pos + substr.len();
86        }
87
88        self
89    }
90    pub fn dim_all(self) -> Self {
91        const DIM_LEVEL: usize = 4;
92        self.color_range(DIM_LEVEL, ..)
93    }
94    pub fn unbold_indices(mut self, mut indices: Vec<usize>) -> Self {
95        const UNBOLD_LEVEL: usize = 5;
96        self.pad_indices(UNBOLD_LEVEL);
97        self.indices
98            .get_mut(UNBOLD_LEVEL)
99            .map(|i| i.append(&mut indices));
100        self
101    }
102    pub fn unbold_range<R: RangeBounds<usize>>(mut self, indices: R) -> Self {
103        const UNBOLD_LEVEL: usize = 5;
104        self.pad_indices(UNBOLD_LEVEL);
105        let start = match indices.start_bound() {
106            Bound::Unbounded => 0,
107            Bound::Included(s) => *s,
108            Bound::Excluded(s) => *s,
109        };
110        let end = match indices.end_bound() {
111            Bound::Unbounded => self.text.chars().count(),
112            Bound::Included(s) => *s + 1,
113            Bound::Excluded(s) => *s,
114        };
115        let indices = (start..end).into_iter();
116        self.indices
117            .get_mut(UNBOLD_LEVEL)
118            .map(|i| i.append(&mut indices.into_iter().collect()));
119        self
120    }
121    pub fn unbold_substring<S: AsRef<str>>(mut self, substr: S) -> Self {
122        let substr = substr.as_ref();
123        let mut start = 0;
124
125        while let Some(pos) = self.text[start..].find(substr) {
126            let abs_pos = start + pos;
127            self = self.unbold_range(abs_pos..abs_pos + substr.chars().count());
128            start = abs_pos + substr.len();
129        }
130
131        self
132    }
133    pub fn unbold_all(self) -> Self {
134        const UNBOLD_LEVEL: usize = 5;
135        self.color_range(UNBOLD_LEVEL, ..)
136    }
137    pub fn error_color_indices(mut self, mut indices: Vec<usize>) -> Self {
138        const ERROR_COLOR_LEVEL: usize = 6;
139        self.pad_indices(ERROR_COLOR_LEVEL);
140        self.indices
141            .get_mut(ERROR_COLOR_LEVEL)
142            .map(|i| i.append(&mut indices));
143        self
144    }
145    pub fn error_color_range<R: RangeBounds<usize>>(mut self, indices: R) -> Self {
146        const ERROR_COLOR_LEVEL: usize = 6;
147        self.pad_indices(ERROR_COLOR_LEVEL);
148        let start = match indices.start_bound() {
149            Bound::Unbounded => 0,
150            Bound::Included(s) => *s,
151            Bound::Excluded(s) => *s,
152        };
153        let end = match indices.end_bound() {
154            Bound::Unbounded => self.text.chars().count(),
155            Bound::Included(s) => *s + 1,
156            Bound::Excluded(s) => *s,
157        };
158        let indices = (start..end).into_iter();
159        self.indices
160            .get_mut(ERROR_COLOR_LEVEL)
161            .map(|i| i.append(&mut indices.into_iter().collect()));
162        self
163    }
164    pub fn error_color_substring<S: AsRef<str>>(mut self, substr: S) -> Self {
165        let substr = substr.as_ref();
166        let mut start = 0;
167
168        while let Some(pos) = self.text[start..].find(substr) {
169            let abs_pos = start + pos;
170            self = self.error_color_range(abs_pos..abs_pos + substr.chars().count());
171            start = abs_pos + substr.len();
172        }
173
174        self
175    }
176    pub fn error_color_nth_substring<S: AsRef<str>>(
177        self,
178        substr: S,
179        occurrence_index: usize,
180    ) -> Self {
181        const ERROR_COLOR_LEVEL: usize = 6;
182        let substr = substr.as_ref();
183        let mut start = 0;
184        let mut count = 0;
185
186        while let Some(pos) = self.text[start..].find(substr) {
187            if count == occurrence_index {
188                let abs_pos = start + pos;
189                return self.color_range(ERROR_COLOR_LEVEL, abs_pos..abs_pos + substr.len());
190            }
191            count += 1;
192            start = start + pos + substr.len();
193        }
194
195        self
196    }
197
198    pub fn error_color_last_substring<S: AsRef<str>>(self, substr: S) -> Self {
199        const ERROR_COLOR_LEVEL: usize = 6;
200        let substr = substr.as_ref();
201        let mut start = 0;
202        let mut last_pos = None;
203
204        while let Some(pos) = self.text[start..].find(substr) {
205            last_pos = Some(start + pos);
206            start = start + pos + substr.len();
207        }
208
209        if let Some(abs_pos) = last_pos {
210            return self.color_range(ERROR_COLOR_LEVEL, abs_pos..abs_pos + substr.len());
211        }
212        self
213    }
214
215    pub fn error_color_all(self) -> Self {
216        const ERROR_COLOR_LEVEL: usize = 6;
217        self.color_range(ERROR_COLOR_LEVEL, ..)
218    }
219    pub fn success_color_indices(mut self, mut indices: Vec<usize>) -> Self {
220        const SUCCESS_COLOR_LEVEL: usize = 7;
221        self.pad_indices(SUCCESS_COLOR_LEVEL);
222        self.indices
223            .get_mut(SUCCESS_COLOR_LEVEL)
224            .map(|i| i.append(&mut indices));
225        self
226    }
227    pub fn success_color_range<R: RangeBounds<usize>>(mut self, indices: R) -> Self {
228        const SUCCESS_COLOR_LEVEL: usize = 7;
229        self.pad_indices(SUCCESS_COLOR_LEVEL);
230        let start = match indices.start_bound() {
231            Bound::Unbounded => 0,
232            Bound::Included(s) => *s,
233            Bound::Excluded(s) => *s,
234        };
235        let end = match indices.end_bound() {
236            Bound::Unbounded => self.text.chars().count(),
237            Bound::Included(s) => *s + 1,
238            Bound::Excluded(s) => *s,
239        };
240        let indices = (start..end).into_iter();
241        self.indices
242            .get_mut(SUCCESS_COLOR_LEVEL)
243            .map(|i| i.append(&mut indices.into_iter().collect()));
244        self
245    }
246    pub fn success_color_substring<S: AsRef<str>>(mut self, substr: S) -> Self {
247        let substr = substr.as_ref();
248        let mut start = 0;
249
250        while let Some(pos) = self.text[start..].find(substr) {
251            let abs_pos = start + pos;
252            self = self.success_color_range(abs_pos..abs_pos + substr.chars().count());
253            start = abs_pos + substr.len();
254        }
255
256        self
257    }
258    pub fn success_color_nth_substring<S: AsRef<str>>(
259        self,
260        substr: S,
261        occurrence_index: usize,
262    ) -> Self {
263        const SUCCESS_COLOR_LEVEL: usize = 7;
264        let substr = substr.as_ref();
265        let mut start = 0;
266        let mut count = 0;
267
268        while let Some(pos) = self.text[start..].find(substr) {
269            if count == occurrence_index {
270                let abs_pos = start + pos;
271                return self.color_range(SUCCESS_COLOR_LEVEL, abs_pos..abs_pos + substr.len());
272            }
273            count += 1;
274            start = start + pos + substr.len();
275        }
276
277        self
278    }
279
280    pub fn success_color_last_substring<S: AsRef<str>>(self, substr: S) -> Self {
281        const SUCCESS_COLOR_LEVEL: usize = 7;
282        let substr = substr.as_ref();
283        let mut start = 0;
284        let mut last_pos = None;
285
286        while let Some(pos) = self.text[start..].find(substr) {
287            last_pos = Some(start + pos);
288            start = start + pos + substr.len();
289        }
290
291        if let Some(abs_pos) = last_pos {
292            return self.color_range(SUCCESS_COLOR_LEVEL, abs_pos..abs_pos + substr.len());
293        }
294        self
295    }
296
297    pub fn success_color_all(self) -> Self {
298        const SUCCESS_COLOR_LEVEL: usize = 7;
299        self.color_range(SUCCESS_COLOR_LEVEL, ..)
300    }
301    pub fn color_indices(mut self, index_level: usize, mut indices: Vec<usize>) -> Self {
302        self.pad_indices(index_level);
303        self.indices
304            .get_mut(index_level)
305            .map(|i| i.append(&mut indices));
306        self
307    }
308    pub fn color_range<R: RangeBounds<usize>>(mut self, index_level: usize, indices: R) -> Self {
309        self.pad_indices(index_level);
310        let start = match indices.start_bound() {
311            Bound::Unbounded => 0,
312            Bound::Included(s) => *s,
313            Bound::Excluded(s) => *s,
314        };
315        let end = match indices.end_bound() {
316            Bound::Unbounded => self.text.chars().count(),
317            Bound::Included(s) => *s + 1,
318            Bound::Excluded(s) => *s,
319        };
320        let indices = (start..end).into_iter();
321        self.indices
322            .get_mut(index_level)
323            .map(|i| i.append(&mut indices.into_iter().collect()));
324        self
325    }
326
327    pub fn color_substring<S: AsRef<str>>(mut self, index_level: usize, substr: S) -> Self {
328        let substr = substr.as_ref();
329        let mut start = 0;
330        while let Some(pos) = self.text[start..].find(substr) {
331            let abs_pos = start + pos;
332            let char_start = self.text[..abs_pos].chars().count();
333            let char_end = char_start + substr.chars().count();
334            self = self.color_range(index_level, char_start..char_end);
335            start = abs_pos + substr.len();
336        }
337        self
338    }
339
340    pub fn color_all(self, index_level: usize) -> Self {
341        self.color_range(index_level, ..)
342    }
343
344    pub fn color_nth_substring<S: AsRef<str>>(
345        self,
346        index_level: usize,
347        substr: S,
348        occurrence_index: usize,
349    ) -> Self {
350        let substr = substr.as_ref();
351        let mut start = 0;
352        let mut count = 0;
353
354        while let Some(pos) = self.text[start..].find(substr) {
355            if count == occurrence_index {
356                let abs_pos = start + pos;
357                return self.color_range(index_level, abs_pos..abs_pos + substr.len());
358            }
359            count += 1;
360            start = start + pos + substr.len();
361        }
362
363        self
364    }
365
366    pub fn color_last_substring<S: AsRef<str>>(self, index_level: usize, substr: S) -> Self {
367        let substr = substr.as_ref();
368        let mut start = 0;
369        let mut last_pos = None;
370
371        while let Some(pos) = self.text[start..].find(substr) {
372            last_pos = Some(start + pos);
373            start = start + pos + substr.len();
374        }
375
376        if let Some(abs_pos) = last_pos {
377            return self.color_range(index_level, abs_pos..abs_pos + substr.len());
378        }
379        self
380    }
381
382    pub fn content(&self) -> &str {
383        &self.text
384    }
385    fn pad_indices(&mut self, index_level: usize) {
386        if self.indices.get(index_level).is_none() {
387            for _ in self.indices.len()..=index_level {
388                self.indices.push(vec![]);
389            }
390        }
391    }
392    pub fn serialize(&self) -> String {
393        let text = self
394            .text
395            .to_string()
396            .as_bytes()
397            .iter()
398            .map(|b| b.to_string())
399            .collect::<Vec<_>>()
400            .join(",");
401        let mut indices = String::new();
402        for index_variants in &self.indices {
403            indices.push_str(&format!(
404                "{}$",
405                index_variants
406                    .iter()
407                    .map(|i| i.to_string())
408                    .collect::<Vec<_>>()
409                    .join(",")
410            ));
411        }
412
413        let mut prefix = "".to_owned();
414
415        if self.selected {
416            prefix.push('x');
417        }
418
419        if self.opaque {
420            prefix.push('z');
421        }
422
423        if self.disabled {
424            prefix.push('d');
425        }
426
427        format!("{}{}{}", prefix, indices, text)
428    }
429    pub fn len(&self) -> usize {
430        self.text.chars().count()
431    }
432}
433
434pub fn print_text(text: Text) {
435    print!("\u{1b}Pztext;{}\u{1b}\\", text.serialize())
436}
437
438pub fn print_text_with_coordinates(
439    text: Text,
440    x: usize,
441    y: usize,
442    width: Option<usize>,
443    height: Option<usize>,
444) {
445    let width = width.map(|w| w.to_string()).unwrap_or_default();
446    let height = height.map(|h| h.to_string()).unwrap_or_default();
447    print!(
448        "\u{1b}Pztext;{}/{}/{}/{};{}\u{1b}\\",
449        x,
450        y,
451        width,
452        height,
453        text.serialize()
454    )
455}
456
457pub fn serialize_text(text: &Text) -> String {
458    format!("\u{1b}Pztext;{}\u{1b}\\", text.serialize())
459}
460
461pub fn serialize_text_with_coordinates(
462    text: &Text,
463    x: usize,
464    y: usize,
465    width: Option<usize>,
466    height: Option<usize>,
467) -> String {
468    let width = width.map(|w| w.to_string()).unwrap_or_default();
469    let height = height.map(|h| h.to_string()).unwrap_or_default();
470    format!(
471        "\u{1b}Pztext;{}/{}/{}/{};{}\u{1b}\\",
472        x,
473        y,
474        width,
475        height,
476        text.serialize()
477    )
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    fn text_body(text: &str) -> String {
485        text.as_bytes()
486            .iter()
487            .map(|byte| byte.to_string())
488            .collect::<Vec<_>>()
489            .join(",")
490    }
491
492    #[test]
493    fn disabled_flag_serializes_with_a_d_prefix() {
494        let serialized = Text::new("x").disabled().serialize();
495        assert_eq!(serialized, format!("d{}", text_body("x")));
496    }
497
498    #[test]
499    fn opaque_and_disabled_serialize_in_push_order() {
500        let serialized = Text::new("x").disabled().opaque().serialize();
501        assert_eq!(serialized, format!("zd{}", text_body("x")));
502    }
503
504    #[test]
505    fn all_flags_serialize_in_selected_opaque_disabled_order() {
506        let serialized = Text::new("x").disabled().opaque().selected().serialize();
507        assert_eq!(serialized, format!("xzd{}", text_body("x")));
508    }
509
510    #[test]
511    fn flags_do_not_disturb_indices_or_text() {
512        let serialized = Text::new("Foo bar baz")
513            .disabled()
514            .color_indices(0, vec![0, 1, 2])
515            .serialize();
516        assert!(serialized.starts_with('d'));
517        assert!(serialized.contains("0,1,2$"));
518        assert!(serialized.ends_with(&text_body("Foo bar baz")));
519    }
520}