skimmer/
lib.rs

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