summaryrefslogtreecommitdiffstats
path: root/src/skim.rs
blob: ba84a13e83bc26d541ca6122d2493107dc14a124 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
use anyhow::{Context, Result};
use skim::prelude::*;
use tuikit::term::Term;

use crate::constant_strings_paths::{
    BAT_EXECUTABLE, CAT_EXECUTABLE, GREP_EXECUTABLE, RG_EXECUTABLE,
};
use crate::utils::is_program_in_path;

/// Used to call skim, a clone of fzf.
/// It's a simple wrapper around `Skim` which is used to simplify the interface.
pub struct Skimer {
    skim: Skim,
    previewer: &'static str,
    file_matcher: &'static str,
}

impl Skimer {
    /// Creates a new `Skimer` instance.
    /// `term` is an `Arc<term>` clone of the default term.
    /// It tries to preview with `bat`, but choose `cat` if it can't.
    pub fn new(term: Arc<Term>) -> Result<Self> {
        Ok(Self {
            skim: Skim::new_from_term(term),
            previewer: pick_first_installed(&[BAT_EXECUTABLE, CAT_EXECUTABLE])
                .context("Neither bat nor cat are installed")?,
            file_matcher: pick_first_installed(&[RG_EXECUTABLE, GREP_EXECUTABLE])
                .context("Neither ripgrep nor grep are installed")?,
        })
    }

    /// Call skim on its term.
    /// Once the user has selected a file, it will returns its results
    /// as a vec of skimitems.
    /// The preview is enabled by default and we assume the previewer won't be uninstalled during the lifetime
    /// of the application.
    pub fn search_filename(&self, path_str: &str) -> Vec<Arc<dyn SkimItem>> {
        let Some(output) =
            self.skim
                .run_internal(None, path_str.to_owned(), Some(self.previewer), None)
        else {
            return vec![];
        };
        if output.is_abort {
            vec![]
        } else {
            output.selected_items
        }
    }

    /// Call skim on its term.
    /// Returns the file whose line match a pattern from current folder using ripgrep or grep.
    pub fn search_line_in_file(&self, path_str: &str) -> Vec<Arc<dyn SkimItem>> {
        let Some(output) = self.skim.run_internal(
            None,
            path_str.to_owned(),
            None,
            Some(self.file_matcher.to_owned()),
        ) else {
            return vec![];
        };
        if output.is_abort {
            vec![]
        } else {
            output.selected_items
        }
    }

    /// Search in a text content, splitted by line.
    /// Returns the selected line.
    pub fn search_in_text(&self, text: &str) -> Vec<Arc<dyn SkimItem>> {
        let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded();
        for line in text.lines().rev() {
            let _ = tx_item.send(Arc::new(StringWrapper {
                inner: line.to_string(),
            }));
        }
        drop(tx_item); // so that skim could know when to stop waiting for more items.
        let Some(output) = self
            .skim
            .run_internal(Some(rx_item), "".to_owned(), None, None)
        else {
            return vec![];
        };
        if output.is_abort {
            vec![]
        } else {
            output.selected_items
        }
    }
}

struct StringWrapper {
    inner: String,
}

impl SkimItem for StringWrapper {
    fn text(&self) -> Cow<str> {
        Cow::Borrowed(&self.inner)
    }

    fn preview(&self, _context: PreviewContext) -> ItemPreview {
        ItemPreview::Text(self.inner.clone())
    }
}

fn pick_first_installed<'a>(commands: &'a [&'a str]) -> Option<&'a str> {
    for command in commands {
        let Some(program) = command.split_whitespace().next() else {
            continue;
        };
        if is_program_in_path(program) {
            return Some(command);
        }
    }
    None
}

/// Print an ANSI escaped with corresponding colors.
pub fn print_ansi_str(
    text: &str,
    term: &Arc<tuikit::term::Term>,
    col: Option<usize>,
    row: Option<usize>,
) -> anyhow::Result<()> {
    let mut col = col.unwrap_or(0);
    let row = row.unwrap_or(0);
    for (chr, attr) in skim::AnsiString::parse(text).iter() {
        col += term.print_with_attr(row, col, &chr.to_string(), attr)?;
    }
    Ok(())
}