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
39pub struct LinePrinter {
51 start: usize,
52 end: usize,
53 current_pos: i32,
54 screen_col: usize,
55
56 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 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 } else if current < self.start + 2 && self.start > 0 {
156 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 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 }
177 '\t' => {
178 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
199pub 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
214pub 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 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 (full_width - container_width, full_width)
256 } else if w1 <= w3 && w1 + w2 <= container_width {
257 (0, full_width)
259 } else {
260 (acc_width[match_end] - container_width + 2, full_width)
262 }
263}
264
265pub 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
276pub 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#[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
326pub fn depends_on_items(cmd: &str) -> bool {
329 RE_ITEMS.is_match(cmd)
330}
331
332pub fn inject_command<'a>(cmd: &'a str, context: InjectContext<'a>) -> Cow<'a, str> {
342 RE_FIELDS.replace_all(cmd, |caps: &Captures| {
343 if &caps[0][0..1] == "\\" {
345 return caps[0].to_string();
346 }
347
348 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 ¤t_selection
358 } else {
359 context.selections
360 };
361 let current_index = vec![context.current_index];
362 let indices = if context.indices.is_empty() {
363 ¤t_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 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}