skim/
lib.rs

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