skim/
lib.rs

1#[macro_use]
2extern crate lazy_static;
3#[macro_use]
4extern crate log;
5
6use std::any::Any;
7use std::borrow::Cow;
8use std::fmt::Display;
9use std::sync::mpsc::channel;
10use std::sync::Arc;
11use std::thread;
12
13use crossbeam::channel::{Receiver, Sender};
14use tuikit::prelude::{Event as TermEvent, *};
15
16pub use crate::ansi::AnsiString;
17pub use crate::engine::fuzzy::FuzzyAlgorithm;
18use crate::event::{EventReceiver, EventSender};
19use crate::model::Model;
20pub use crate::options::{SkimOptions, SkimOptionsBuilder};
21pub use crate::output::SkimOutput;
22use crate::reader::Reader;
23
24mod ansi;
25mod engine;
26mod event;
27pub mod field;
28mod global;
29mod header;
30mod helper;
31mod input;
32mod item;
33mod matcher;
34mod model;
35mod options;
36mod orderedvec;
37mod output;
38pub mod prelude;
39mod previewer;
40mod query;
41mod reader;
42mod selection;
43mod spinlock;
44mod theme;
45mod util;
46
47//------------------------------------------------------------------------------
48pub trait AsAny {
49    fn as_any(&self) -> &dyn Any;
50    fn as_any_mut(&mut self) -> &mut dyn Any;
51}
52
53impl<T: Any> AsAny for T {
54    fn as_any(&self) -> &dyn Any {
55        self
56    }
57
58    fn as_any_mut(&mut self) -> &mut dyn Any {
59        self
60    }
61}
62
63/// A `SkimItem` defines what's been processed(fetched, matched, previewed and returned) by skim
64///
65/// # Downcast Example
66/// Skim will return the item back, but in `Arc<dyn SkimItem>` form. We might want a reference
67/// to the concrete type instead of trait object. Skim provide a somehow "complicated" way to
68/// `downcast` it back to the reference of the original concrete type.
69///
70/// ```rust
71/// use skim::prelude::*;
72///
73/// struct MyItem {}
74/// impl SkimItem for MyItem {
75///     fn text(&self) -> Cow<str> {
76///         unimplemented!()
77///     }
78/// }
79///
80/// impl MyItem {
81///     pub fn mutable(&mut self) -> i32 {
82///         1
83///     }
84///
85///     pub fn immutable(&self) -> i32 {
86///         0
87///     }
88/// }
89///
90/// let mut ret: Arc<dyn SkimItem> = Arc::new(MyItem{});
91/// let mutable: &mut MyItem = Arc::get_mut(&mut ret)
92///     .expect("item is referenced by others")
93///     .as_any_mut() // cast to Any
94///     .downcast_mut::<MyItem>() // downcast to (mut) concrete type
95///     .expect("something wrong with downcast");
96/// assert_eq!(mutable.mutable(), 1);
97///
98/// let immutable: &MyItem = (*ret).as_any() // cast to Any
99///     .downcast_ref::<MyItem>() // downcast to concrete type
100///     .expect("something wrong with downcast");
101/// assert_eq!(immutable.immutable(), 0)
102/// ```
103pub trait SkimItem: AsAny + Send + Sync + 'static {
104    /// The string to be used for matching (without color)
105    fn text(&self) -> Cow<str>;
106
107    /// The content to be displayed on the item list, could contain ANSI properties
108    fn display<'a>(&'a self, context: DisplayContext<'a>) -> AnsiString<'a> {
109        AnsiString::from(context)
110    }
111
112    /// Custom preview content, default to `ItemPreview::Global` which will use global preview
113    /// setting(i.e. the command set by `preview` option)
114    fn preview(&self, _context: PreviewContext) -> ItemPreview {
115        ItemPreview::Global
116    }
117
118    /// Get output text(after accept), default to `text()`
119    /// Note that this function is intended to be used by the caller of skim and will not be used by
120    /// skim. And since skim will return the item back in `SkimOutput`, if string is not what you
121    /// want, you could still use `downcast` to retain the pointer to the original struct.
122    fn output(&self) -> Cow<str> {
123        self.text()
124    }
125
126    /// we could limit the matching ranges of the `get_text` of the item.
127    /// providing (start_byte, end_byte) of the range
128    fn get_matching_ranges(&self) -> Option<&[(usize, usize)]> {
129        None
130    }
131}
132
133//------------------------------------------------------------------------------
134// Implement SkimItem for raw strings
135
136impl<T: AsRef<str> + Send + Sync + 'static> SkimItem for T {
137    fn text(&self) -> Cow<str> {
138        Cow::Borrowed(self.as_ref())
139    }
140}
141
142//------------------------------------------------------------------------------
143// Display Context
144pub enum Matches<'a> {
145    None,
146    CharIndices(&'a [usize]),
147    CharRange(usize, usize),
148    ByteRange(usize, usize),
149}
150
151pub struct DisplayContext<'a> {
152    pub text: &'a str,
153    pub score: i32,
154    pub matches: Matches<'a>,
155    pub container_width: usize,
156    pub highlight_attr: Attr,
157}
158
159impl<'a> From<DisplayContext<'a>> for AnsiString<'a> {
160    fn from(context: DisplayContext<'a>) -> Self {
161        match context.matches {
162            Matches::CharIndices(indices) => AnsiString::from((context.text, indices, context.highlight_attr)),
163            Matches::CharRange(start, end) => {
164                AnsiString::new_str(context.text, vec![(context.highlight_attr, (start as u32, end as u32))])
165            }
166            Matches::ByteRange(start, end) => {
167                let ch_start = context.text[..start].chars().count();
168                let ch_end = ch_start + context.text[start..end].chars().count();
169                AnsiString::new_str(
170                    context.text,
171                    vec![(context.highlight_attr, (ch_start as u32, ch_end as u32))],
172                )
173            }
174            Matches::None => AnsiString::new_str(context.text, vec![]),
175        }
176    }
177}
178
179//------------------------------------------------------------------------------
180// Preview Context
181
182pub struct PreviewContext<'a> {
183    pub query: &'a str,
184    pub cmd_query: &'a str,
185    pub width: usize,
186    pub height: usize,
187    pub current_index: usize,
188    pub current_selection: &'a str,
189    /// selected item indices (may or may not include current item)
190    pub selected_indices: &'a [usize],
191    /// selected item texts (may or may not include current item)
192    pub selections: &'a [&'a str],
193}
194
195//------------------------------------------------------------------------------
196// Preview
197#[derive(Default, Copy, Clone, Debug)]
198pub struct PreviewPosition {
199    pub h_scroll: Size,
200    pub h_offset: Size,
201    pub v_scroll: Size,
202    pub v_offset: Size,
203}
204
205pub enum ItemPreview {
206    /// execute the command and print the command's output
207    Command(String),
208    /// Display the prepared text(lines)
209    Text(String),
210    /// Display the colored text(lines)
211    AnsiText(String),
212    CommandWithPos(String, PreviewPosition),
213    TextWithPos(String, PreviewPosition),
214    AnsiWithPos(String, PreviewPosition),
215    /// Use global command settings to preview the item
216    Global,
217}
218
219//==============================================================================
220// A match engine will execute the matching algorithm
221
222#[derive(Eq, PartialEq, Debug, Copy, Clone)]
223pub enum CaseMatching {
224    Respect,
225    Ignore,
226    Smart,
227}
228
229impl Default for CaseMatching {
230    fn default() -> Self {
231        CaseMatching::Smart
232    }
233}
234
235#[derive(PartialEq, Eq, Clone, Debug)]
236#[allow(dead_code)]
237pub enum MatchRange {
238    ByteRange(usize, usize),
239    // range of bytes
240    Chars(Vec<usize>), // individual character indices matched
241}
242
243pub type Rank = [i32; 4];
244
245#[derive(Clone)]
246pub struct MatchResult {
247    pub rank: Rank,
248    pub matched_range: MatchRange,
249}
250
251impl MatchResult {
252    pub fn range_char_indices(&self, text: &str) -> Vec<usize> {
253        match &self.matched_range {
254            &MatchRange::ByteRange(start, end) => {
255                let first = text[..start].chars().count();
256                let last = first + text[start..end].chars().count();
257                (first..last).collect()
258            }
259            MatchRange::Chars(vec) => vec.clone(),
260        }
261    }
262}
263
264pub trait MatchEngine: Sync + Send + Display {
265    fn match_item(&self, item: Arc<dyn SkimItem>) -> Option<MatchResult>;
266}
267
268pub trait MatchEngineFactory {
269    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine>;
270    fn create_engine(&self, query: &str) -> Box<dyn MatchEngine> {
271        self.create_engine_with_case(query, CaseMatching::default())
272    }
273}
274
275//------------------------------------------------------------------------------
276// Preselection
277
278/// A selector that determines whether an item should be "pre-selected" in multi-selection mode
279pub trait Selector {
280    fn should_select(&self, index: usize, item: &dyn SkimItem) -> bool;
281}
282
283//------------------------------------------------------------------------------
284pub type SkimItemSender = Sender<Arc<dyn SkimItem>>;
285pub type SkimItemReceiver = Receiver<Arc<dyn SkimItem>>;
286
287pub struct Skim {
288    // pub options: &'a SkimOptions<'a>,
289    pub term: Arc<Term>,
290}
291
292impl Skim {
293    pub fn new_from_term(term: Arc<Term>) -> Self {
294        Self { term }
295    }
296
297    fn default_cmd(path_str: String) -> String {
298        format!("find {}", path_str)
299    }
300
301    /// params:
302    /// - source: a stream of items to be passed to skim for filtering.
303    ///   If None is given, skim will invoke the command given to fetch the items.
304    /// - path_str: a string to a valid path in the filesystem.
305    ///
306    /// return:
307    /// - None: on internal errors.
308    /// - SkimOutput: the collected key, event, query, selected items, etc.
309    pub fn run_internal(
310        self: &Self,
311        source: Option<SkimItemReceiver>,
312        path_str: String,
313        preview: Option<&str>,
314        cmd: Option<String>,
315    ) -> Option<SkimOutput> {
316        let mut options = SkimOptions::default();
317        let cmd = match cmd {
318            None => Self::default_cmd(path_str),
319            Some(cmd) => cmd,
320        };
321        options.cmd = Some(&cmd);
322        options.preview = preview;
323        options.multi = true;
324        let (tx, rx): (EventSender, EventReceiver) = channel();
325        if !options.no_mouse {
326            let _ = self.term.enable_mouse_support();
327        }
328
329        //------------------------------------------------------------------------------
330        // input
331        let mut input = input::Input::new();
332        input.parse_keymaps(&options.bind);
333        input.parse_expect_keys(options.expect.as_ref().map(String::as_str));
334
335        let tx_clone = tx.clone();
336        let term_clone = self.term.clone();
337        let input_thread = thread::spawn(move || loop {
338            if let Ok(key) = term_clone.poll_event() {
339                if key == TermEvent::User(()) {
340                    break;
341                }
342
343                let (key, action_chain) = input.translate_event(key);
344                for event in action_chain.into_iter() {
345                    let _ = tx_clone.send((key, event));
346                }
347            }
348        });
349
350        //------------------------------------------------------------------------------
351        // reader
352
353        let reader = Reader::with_options(&options).source(source);
354
355        //------------------------------------------------------------------------------
356        // model + previewer
357        let mut model = Model::new(rx, tx, reader, self.term.clone(), &options);
358        let ret = model.start();
359        let _ = self.term.send_event(TermEvent::User(())); // interrupt the input thread
360        let _ = input_thread.join();
361        ret
362    }
363
364    /// params:
365    /// - options: the "complex" options that control how skim behaves
366    /// - source: a stream of items to be passed to skim for filtering.
367    ///   If None is given, skim will invoke the command given to fetch the items.
368    ///
369    /// return:
370    /// - None: on internal errors.
371    /// - SkimOutput: the collected key, event, query, selected items, etc.
372    pub fn run_with(options: &SkimOptions, source: Option<SkimItemReceiver>) -> Option<SkimOutput> {
373        let min_height = options
374            .min_height
375            .map(Skim::parse_height_string)
376            .expect("min_height should have default values");
377        let height = options
378            .height
379            .map(Skim::parse_height_string)
380            .expect("height should have default values");
381
382        let (tx, rx): (EventSender, EventReceiver) = channel();
383        let term = Arc::new(
384            Term::with_options(
385                TermOptions::default()
386                    .min_height(min_height)
387                    .height(height)
388                    .clear_on_exit(!options.no_clear)
389                    .hold(options.select1 || options.exit0 || options.sync),
390            )
391            .unwrap(),
392        );
393        if !options.no_mouse {
394            let _ = term.enable_mouse_support();
395        }
396
397        //------------------------------------------------------------------------------
398        // input
399        let mut input = input::Input::new();
400        input.parse_keymaps(&options.bind);
401        input.parse_expect_keys(options.expect.as_ref().map(String::as_str));
402
403        let tx_clone = tx.clone();
404        let term_clone = term.clone();
405        let input_thread = thread::spawn(move || loop {
406            if let Ok(key) = term_clone.poll_event() {
407                if key == TermEvent::User(()) {
408                    break;
409                }
410
411                let (key, action_chain) = input.translate_event(key);
412                for event in action_chain.into_iter() {
413                    let _ = tx_clone.send((key, event));
414                }
415            }
416        });
417
418        //------------------------------------------------------------------------------
419        // reader
420
421        let reader = Reader::with_options(&options).source(source);
422
423        //------------------------------------------------------------------------------
424        // model + previewer
425        let mut model = Model::new(rx, tx, reader, term.clone(), &options);
426        let ret = model.start();
427        let _ = term.send_event(TermEvent::User(())); // interrupt the input thread
428        let _ = input_thread.join();
429        ret
430    }
431
432    // 10 -> TermHeight::Fixed(10)
433    // 10% -> TermHeight::Percent(10)
434    fn parse_height_string(string: &str) -> TermHeight {
435        if string.ends_with('%') {
436            TermHeight::Percent(string[0..string.len() - 1].parse().unwrap_or(100))
437        } else {
438            TermHeight::Fixed(string.parse().unwrap_or(0))
439        }
440    }
441}