Skip to main content

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