zellij_tile/ui_components/
text.rs1use 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}