skim/
util.rs

1use std::borrow::Cow;
2use std::cmp::{max, min};
3use std::fs::File;
4use std::io::{BufRead, BufReader};
5use std::prelude::v1::*;
6use std::str::FromStr;
7use std::sync::LazyLock;
8
9use regex::{Captures, Regex};
10use skim_tuikit::prelude::*;
11use unicode_width::UnicodeWidthChar;
12
13use crate::AnsiString;
14use crate::field::get_string_by_range;
15
16static RE_ESCAPE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"['\U{00}]").unwrap());
17static RE_NUMBER: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[+|-]?\d+").unwrap());
18
19pub fn clear_canvas(canvas: &mut dyn Canvas) -> DrawResult<()> {
20    let (screen_width, screen_height) = canvas.size()?;
21    for y in 0..screen_height {
22        for x in 0..screen_width {
23            canvas.print(y, x, " ")?;
24        }
25    }
26    Ok(())
27}
28
29pub fn escape_single_quote(text: &str) -> String {
30    RE_ESCAPE
31        .replace_all(text, |x: &Captures| match x.get(0).unwrap().as_str() {
32            "'" => "'\\''".to_string(),
33            "\0" => "\\0".to_string(),
34            _ => "".to_string(),
35        })
36        .to_string()
37}
38
39/// use to print a single line, properly handle the tabstop and shift of a string
40/// e.g. a long line will be printed as `..some content` or `some content..` or `..some content..`
41/// depends on the container's width and the size of the content.
42///
43/// ```text
44/// let's say we have a very long line with lots of useless information
45///                                |.. with lots of use..|             // only to show this
46///                                |<- container width ->|
47///             |<-    shift    -> |
48/// |< hscroll >|
49/// ```
50pub struct LinePrinter {
51    start: usize,
52    end: usize,
53    current_pos: i32,
54    screen_col: usize,
55
56    // start position
57    row: usize,
58    col: usize,
59
60    tabstop: usize,
61    shift: usize,
62    text_width: usize,
63    container_width: usize,
64    hscroll_offset: i64,
65}
66
67impl LinePrinter {
68    pub fn builder() -> Self {
69        LinePrinter {
70            start: 0,
71            end: 0,
72            current_pos: -1,
73            screen_col: 0,
74
75            row: 0,
76            col: 0,
77
78            tabstop: 8,
79            shift: 0,
80            text_width: 0,
81            container_width: 0,
82            hscroll_offset: 0,
83        }
84    }
85
86    pub fn row(mut self, row: usize) -> Self {
87        self.row = row;
88        self
89    }
90
91    pub fn col(mut self, col: usize) -> Self {
92        self.col = col;
93        self
94    }
95
96    pub fn tabstop(mut self, tabstop: usize) -> Self {
97        self.tabstop = tabstop;
98        self
99    }
100
101    pub fn hscroll_offset(mut self, offset: i64) -> Self {
102        self.hscroll_offset = offset;
103        self
104    }
105
106    pub fn text_width(mut self, width: usize) -> Self {
107        self.text_width = width;
108        self
109    }
110
111    pub fn container_width(mut self, width: usize) -> Self {
112        self.container_width = width;
113        self
114    }
115
116    pub fn shift(mut self, shift: usize) -> Self {
117        self.shift = shift;
118        self
119    }
120
121    pub fn build(mut self) -> Self {
122        self.reset();
123        self
124    }
125
126    pub fn reset(&mut self) {
127        self.current_pos = 0;
128        self.screen_col = self.col;
129
130        self.start = max(self.shift as i64 + self.hscroll_offset, 0) as usize;
131        self.end = self.start + self.container_width;
132    }
133
134    fn print_ch_to_canvas(&mut self, canvas: &mut dyn Canvas, ch: char, attr: Attr, skip: bool) {
135        let w = ch.width().unwrap_or(2);
136
137        if !skip {
138            let _ = canvas.put_cell(self.row, self.screen_col, Cell::default().ch(ch).attribute(attr));
139        }
140
141        self.screen_col += w;
142    }
143
144    fn print_char_raw(&mut self, canvas: &mut dyn Canvas, ch: char, attr: Attr, skip: bool) {
145        // hide the content that outside the screen, and show the hint(i.e. `..`) for overflow
146        // the hidden character
147
148        let w = ch.width().unwrap_or(2);
149
150        assert!(self.current_pos >= 0);
151        let current = self.current_pos as usize;
152
153        if current < self.start || current >= self.end {
154            // pass if it is hidden
155        } else if current < self.start + 2 && self.start > 0 {
156            // print left ".."
157            for _ in 0..min(w, current - self.start + 1) {
158                self.print_ch_to_canvas(canvas, '.', attr, skip);
159            }
160        } else if self.end - current <= 2 && (self.text_width > self.end) {
161            // print right ".."
162            for _ in 0..min(w, self.end - current) {
163                self.print_ch_to_canvas(canvas, '.', attr, skip);
164            }
165        } else {
166            self.print_ch_to_canvas(canvas, ch, attr, skip);
167        }
168
169        self.current_pos += w as i32;
170    }
171
172    pub fn print_char(&mut self, canvas: &mut dyn Canvas, ch: char, attr: Attr, skip: bool) {
173        match ch {
174            '\u{08}' => {
175                // ignore \b character
176            }
177            '\t' => {
178                // handle tabstop
179                let rest = if self.current_pos < 0 {
180                    self.tabstop
181                } else {
182                    self.tabstop - (self.current_pos as usize) % self.tabstop
183                };
184                for _ in 0..rest {
185                    self.print_char_raw(canvas, ' ', attr, skip);
186                }
187            }
188            ch => self.print_char_raw(canvas, ch, attr, skip),
189        }
190    }
191}
192
193pub fn print_item(canvas: &mut dyn Canvas, printer: &mut LinePrinter, content: AnsiString, default_attr: Attr) {
194    for (ch, attr) in content.iter() {
195        printer.print_char(canvas, ch, default_attr.extend(attr), false);
196    }
197}
198
199/// return an array, arr[i] store the display width till char[i]
200pub fn accumulate_text_width(text: &str, tabstop: usize) -> Vec<usize> {
201    let mut ret = Vec::new();
202    let mut w = 0;
203    for ch in text.chars() {
204        w += if ch == '\t' {
205            tabstop - (w % tabstop)
206        } else {
207            ch.width().unwrap_or(2)
208        };
209        ret.push(w);
210    }
211    ret
212}
213
214/// "smartly" calculate the "start" position of the string in order to show the matched contents
215/// for example, if the match appear in the end of a long string, we need to show the right part.
216/// ```text
217/// xxxxxxxxxxxxxxxxxxxxxxxxxxMMxxxxxMxxxxx
218///               shift ->|               |
219/// ```
220///
221/// return (left_shift, full_print_width)
222pub fn reshape_string(
223    text: &str,
224    container_width: usize,
225    match_start: usize,
226    match_end: usize,
227    tabstop: usize,
228) -> (usize, usize) {
229    if text.is_empty() {
230        return (0, 0);
231    }
232
233    let acc_width = accumulate_text_width(text, tabstop);
234    let full_width = acc_width[acc_width.len() - 1];
235    if full_width <= container_width {
236        return (0, full_width);
237    }
238
239    // w1, w2, w3 = len_before_matched, len_matched, len_after_matched
240    let w1 = if match_start == 0 {
241        0
242    } else {
243        acc_width[match_start - 1]
244    };
245    let w2 = if match_end >= acc_width.len() {
246        full_width - w1
247    } else {
248        acc_width[match_end] - w1
249    };
250    let w3 = acc_width[acc_width.len() - 1] - w1 - w2;
251
252    if (w1 > w3 && w2 + w3 <= container_width) || (w3 <= 2) {
253        // right-fixed
254        //(right_fixed(&acc_width, container_width), full_width)
255        (full_width - container_width, full_width)
256    } else if w1 <= w3 && w1 + w2 <= container_width {
257        // left-fixed
258        (0, full_width)
259    } else {
260        // left-right
261        (acc_width[match_end] - container_width + 2, full_width)
262    }
263}
264
265/// margin option string -> Size
266/// 10 -> Size::Fixed(10)
267/// 10% -> Size::Percent(10)
268pub fn margin_string_to_size(margin: &str) -> Size {
269    if margin.ends_with('%') {
270        Size::Percent(min(100, margin[0..margin.len() - 1].parse::<usize>().unwrap_or(100)))
271    } else {
272        Size::Fixed(margin.parse::<usize>().unwrap_or(0))
273    }
274}
275
276/// Parse margin configuration, e.g.
277/// - `TRBL`     Same  margin  for  top,  right, bottom, and left
278/// - `TB,RL`    Vertical, horizontal margin
279/// - `T,RL,B`   Top, horizontal, bottom margin
280/// - `T,R,B,L`  Top, right, bottom, left margin
281pub fn parse_margin(margin_option: &str) -> (Size, Size, Size, Size) {
282    let margins = margin_option.split(',').collect::<Vec<&str>>();
283
284    match margins.len() {
285        1 => {
286            let margin = margin_string_to_size(margins[0]);
287            (margin, margin, margin, margin)
288        }
289        2 => {
290            let margin_tb = margin_string_to_size(margins[0]);
291            let margin_rl = margin_string_to_size(margins[1]);
292            (margin_tb, margin_rl, margin_tb, margin_rl)
293        }
294        3 => {
295            let margin_top = margin_string_to_size(margins[0]);
296            let margin_rl = margin_string_to_size(margins[1]);
297            let margin_bottom = margin_string_to_size(margins[2]);
298            (margin_top, margin_rl, margin_bottom, margin_rl)
299        }
300        4 => {
301            let margin_top = margin_string_to_size(margins[0]);
302            let margin_right = margin_string_to_size(margins[1]);
303            let margin_bottom = margin_string_to_size(margins[2]);
304            let margin_left = margin_string_to_size(margins[3]);
305            (margin_top, margin_right, margin_bottom, margin_left)
306        }
307        _ => (Size::Fixed(0), Size::Fixed(0), Size::Fixed(0), Size::Fixed(0)),
308    }
309}
310
311/// The context for injecting command.
312#[derive(Copy, Clone)]
313pub struct InjectContext<'a> {
314    pub delimiter: &'a Regex,
315    pub current_index: usize,
316    pub current_selection: &'a str,
317    pub indices: &'a [usize],
318    pub selections: &'a [&'a str],
319    pub query: &'a str,
320    pub cmd_query: &'a str,
321}
322
323static RE_ITEMS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\\?(\{ *-?[0-9.+]*? *})").unwrap());
324static RE_FIELDS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\\?(\{ *-?[0-9.,cq+n]*? *})").unwrap());
325
326/// Check if a command depends on item
327/// e.g. contains `{}`, `{1..}`, `{+}`
328pub fn depends_on_items(cmd: &str) -> bool {
329    RE_ITEMS.is_match(cmd)
330}
331
332/// inject the fields into commands
333/// cmd: `echo {1..}`, text: `a,b,c`, delimiter: `,`
334/// => `echo b,c`
335///
336/// * `{}` for current selection
337/// * `{1..}`, etc. for fields
338/// * `{+}` for all selections
339/// * `{q}` for query
340/// * `{cq}` for command query
341pub fn inject_command<'a>(cmd: &'a str, context: InjectContext<'a>) -> Cow<'a, str> {
342    RE_FIELDS.replace_all(cmd, |caps: &Captures| {
343        // \{...
344        if &caps[0][0..1] == "\\" {
345            return caps[0].to_string();
346        }
347
348        // {1..} and other variant
349        let range = &caps[1];
350        assert!(range.len() >= 2);
351        let range = &range[1..range.len() - 1];
352        let range = range.trim();
353
354        if range.starts_with('+') {
355            let current_selection = vec![context.current_selection];
356            let selections = if context.selections.is_empty() {
357                &current_selection
358            } else {
359                context.selections
360            };
361            let current_index = vec![context.current_index];
362            let indices = if context.indices.is_empty() {
363                &current_index
364            } else {
365                context.indices
366            };
367
368            return selections
369                .iter()
370                .zip(indices.iter())
371                .map(|(&s, &i)| {
372                    let rest = &range[1..];
373                    let index_str = format!("{i}");
374                    let replacement = match rest {
375                        "" => s,
376                        "n" => &index_str,
377                        _ => get_string_by_range(context.delimiter, s, rest).unwrap_or(""),
378                    };
379                    format!("'{}'", escape_single_quote(replacement))
380                })
381                .collect::<Vec<_>>()
382                .join(" ");
383        }
384
385        let index_str = format!("{}", context.current_index);
386        let replacement = match range {
387            "" => context.current_selection,
388            x if x.starts_with('+') => unreachable!(),
389            "n" => &index_str,
390            "q" => context.query,
391            "cq" => context.cmd_query,
392            _ => get_string_by_range(context.delimiter, context.current_selection, range).unwrap_or(""),
393        };
394
395        format!("'{}'", escape_single_quote(replacement))
396    })
397}
398
399pub fn str_lines(string: &str) -> Vec<&str> {
400    string.trim_end().split('\n').collect()
401}
402
403pub fn atoi<T: FromStr>(string: &str) -> Option<T> {
404    RE_NUMBER.find(string).and_then(|mat| mat.as_str().parse::<T>().ok())
405}
406
407pub fn read_file_lines(filename: &str) -> std::result::Result<Vec<String>, std::io::Error> {
408    let file = File::open(filename)?;
409    let ret = BufReader::new(file).lines().collect();
410    debug!("file content: {ret:?}");
411    ret
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn test_accumulate_text_width() {
420        assert_eq!(accumulate_text_width("abcdefg", 8), vec![1, 2, 3, 4, 5, 6, 7]);
421        assert_eq!(accumulate_text_width("ab中de国g", 8), vec![1, 2, 4, 5, 6, 8, 9]);
422        assert_eq!(accumulate_text_width("ab\tdefg", 8), vec![1, 2, 8, 9, 10, 11, 12]);
423        assert_eq!(accumulate_text_width("ab中\te国g", 8), vec![1, 2, 4, 8, 9, 11, 12]);
424    }
425
426    #[test]
427    fn test_reshape_string() {
428        // no match, left fixed to 0
429        assert_eq!(reshape_string("abc", 10, 0, 0, 8), (0, 3));
430        assert_eq!(reshape_string("a\tbc", 8, 0, 0, 8), (0, 10));
431        assert_eq!(reshape_string("a\tb\tc", 10, 0, 0, 8), (0, 17));
432        assert_eq!(reshape_string("a\t中b\tc", 8, 0, 0, 8), (0, 17));
433        assert_eq!(reshape_string("a\t中b\tc012345", 8, 0, 0, 8), (0, 23));
434    }
435
436    #[test]
437    fn test_inject_command() {
438        let delimiter = Regex::new(r",").unwrap();
439        let current_selection = "a,b,c";
440        let selections = vec!["a,b,c", "x,y,z"];
441        let query = "query";
442        let cmd_query = "cmd_query";
443
444        let default_context = InjectContext {
445            current_index: 0,
446            delimiter: &delimiter,
447            current_selection,
448            selections: &selections,
449            indices: &[0, 1],
450            query,
451            cmd_query,
452        };
453
454        assert_eq!("'a,b,c'", inject_command("{}", default_context));
455        assert_eq!("'a,b,c'", inject_command("{ }", default_context));
456
457        assert_eq!("'a'", inject_command("{1}", default_context));
458        assert_eq!("'b'", inject_command("{2}", default_context));
459        assert_eq!("'c'", inject_command("{3}", default_context));
460        assert_eq!("''", inject_command("{4}", default_context));
461        assert_eq!("'c'", inject_command("{-1}", default_context));
462        assert_eq!("'b'", inject_command("{-2}", default_context));
463        assert_eq!("'a'", inject_command("{-3}", default_context));
464        assert_eq!("''", inject_command("{-4}", default_context));
465        assert_eq!("'a,b'", inject_command("{1..2}", default_context));
466        assert_eq!("'b,c'", inject_command("{2..}", default_context));
467
468        assert_eq!("'query'", inject_command("{q}", default_context));
469        assert_eq!("'cmd_query'", inject_command("{cq}", default_context));
470        assert_eq!("'a,b,c' 'x,y,z'", inject_command("{+}", default_context));
471        assert_eq!("'0'", inject_command("{n}", default_context));
472        assert_eq!("'a' 'x'", inject_command("{+1}", default_context));
473        assert_eq!("'b' 'y'", inject_command("{+2}", default_context));
474        assert_eq!("'0' '1'", inject_command("{+n}", default_context));
475    }
476
477    #[test]
478    fn test_escape_single_quote() {
479        assert_eq!("'\\''a'\\''\\0", escape_single_quote("'a'\0"));
480    }
481
482    #[test]
483    fn test_atoi() {
484        assert_eq!(None, atoi::<usize>(""));
485        assert_eq!(Some(1), atoi::<usize>("1"));
486        assert_eq!(Some(usize::MAX), atoi::<usize>(&format!("{}", usize::MAX)));
487        assert_eq!(Some(1), atoi::<usize>("a1"));
488        assert_eq!(Some(1), atoi::<usize>("1b"));
489        assert_eq!(Some(1), atoi::<usize>("a1b"));
490        assert_eq!(None, atoi::<usize>("-1"));
491        assert_eq!(Some(-1), atoi::<i32>("a-1b"));
492        assert_eq!(None, atoi::<i32>("8589934592"));
493        assert_eq!(Some(123), atoi::<i32>("+'123'"));
494    }
495}