Skip to main content

skim/
lib.rs

1//! Skim is a fuzzy finder library for Rust.
2//!
3//! It provides a fast and customizable way to filter and select items interactively,
4//! similar to fzf. Skim can be used as a library or as a command-line tool.
5//!
6//! # Examples
7//!
8//! ```no_run
9//! use skim::prelude::*;
10//! use std::io::Cursor;
11//!
12//! let options = SkimOptionsBuilder::default()
13//!     .height("50%")
14//!     .multi(true)
15//!     .build()
16//!     .unwrap();
17//!
18//! let input = "awk\nbash\ncsh\ndash\nfish\nksh\nzsh";
19//! let item_reader = SkimItemReader::default();
20//! let items = item_reader.of_bufread(Cursor::new(input));
21//!
22//! let output = Skim::run_with(options, Some(items)).unwrap();
23//! ```
24#![warn(missing_docs)]
25
26#[macro_use]
27extern crate log;
28
29#[global_allocator]
30static GLOBAL_ALLOCATOR: mimalloc::MiMalloc = mimalloc::MiMalloc;
31
32use std::any::Any;
33use std::borrow::Cow;
34use std::fmt::Display;
35use std::sync::Arc;
36
37use crate::fuzzy_matcher::MatchIndices;
38use ratatui::{
39    style::Style,
40    text::{Line, Span},
41};
42
43pub use crate::engine::fuzzy::FuzzyAlgorithm;
44pub use crate::item::RankCriteria;
45pub use crate::options::SkimOptions;
46pub use crate::output::SkimOutput;
47pub use crate::skim::*;
48pub use crate::skim_item::SkimItem;
49use crate::tui::Size;
50pub use util::printf;
51
52pub mod binds;
53mod engine;
54pub mod field;
55pub mod fuzzy_matcher;
56pub mod helper;
57pub mod item;
58pub mod matcher;
59pub mod options;
60mod output;
61pub mod prelude;
62pub mod reader;
63mod skim;
64mod skim_item;
65pub mod spinlock;
66pub mod theme;
67pub mod tmux;
68pub mod tui;
69mod util;
70
71#[cfg(feature = "cli")]
72pub mod manpage;
73#[cfg(feature = "cli")]
74pub mod shell;
75
76//------------------------------------------------------------------------------
77/// Trait for downcasting to concrete types from trait objects
78pub trait AsAny {
79    /// Returns a reference to the value as `Any`
80    fn as_any(&self) -> &dyn Any;
81    /// Returns a mutable reference to the value as `Any`
82    fn as_any_mut(&mut self) -> &mut dyn Any;
83}
84
85impl<T: Any> AsAny for T {
86    fn as_any(&self) -> &dyn Any {
87        self
88    }
89
90    fn as_any_mut(&mut self) -> &mut dyn Any {
91        self
92    }
93}
94
95//------------------------------------------------------------------------------
96// Display Context
97#[derive(Default, Debug, Clone)]
98/// Represents how a query matches an item
99pub enum Matches {
100    /// No matches
101    #[default]
102    None,
103    /// Matches at specific character indices
104    CharIndices(MatchIndices),
105    /// Matches in a character range (start, end)
106    CharRange(usize, usize),
107    /// Matches in a byte range (start, end)
108    ByteRange(usize, usize),
109}
110
111#[derive(Default, Clone)]
112/// Context information for displaying an item
113pub struct DisplayContext {
114    /// The match score for this item
115    pub score: i32,
116    /// Where the query matched in the item
117    pub matches: Matches,
118    /// The width of the container to display in
119    pub container_width: usize,
120    /// The base style to apply to non-matched portions
121    pub base_style: Style,
122    /// The style to apply to matched portions
123    pub matched_syle: Style,
124}
125
126impl DisplayContext {
127    /// Converts the context and text into a styled `Line` with highlighted matches
128    pub fn to_line(self, cow: Cow<str>) -> Line {
129        let text: String = cow.into_owned();
130
131        // Combine base_style with match style for highlighted text
132        // Match style takes precedence for fg, but inherits bg from base if not set
133        match &self.matches {
134            Matches::CharIndices(indices) => {
135                let mut res = Line::default();
136                let mut chars = text.chars();
137                let mut prev_index = 0;
138                for &index in indices {
139                    let span_content = chars.by_ref().take(index - prev_index);
140                    res.push_span(Span::styled(span_content.collect::<String>(), self.base_style));
141                    let highlighted_char = chars.next().unwrap_or_default().to_string();
142
143                    res.push_span(Span::styled(highlighted_char, self.base_style.patch(self.matched_syle)));
144                    prev_index = index + 1;
145                }
146                res.push_span(Span::styled(chars.collect::<String>(), self.base_style));
147                res
148            }
149            // AnsiString::from((context.text, indices, context.highlight_attr)),
150            #[allow(clippy::cast_possible_truncation)]
151            Matches::CharRange(start, end) => {
152                let mut chars = text.chars();
153                let mut res = Line::default();
154                res.push_span(Span::styled(
155                    chars.by_ref().take(*start).collect::<String>(),
156                    self.base_style,
157                ));
158                let highlighted_text = chars.by_ref().take(*end - *start).collect::<String>();
159
160                res.push_span(Span::styled(highlighted_text, self.base_style.patch(self.matched_syle)));
161                res.push_span(Span::styled(chars.collect::<String>(), self.base_style));
162                res
163            }
164            Matches::ByteRange(start, end) => {
165                let mut bytes = text.bytes();
166                let mut res = Line::default();
167                res.push_span(Span::styled(
168                    String::from_utf8(bytes.by_ref().take(*start).collect()).unwrap(),
169                    self.base_style,
170                ));
171                let highlighted_bytes = bytes.by_ref().take(*end - *start).collect();
172                let highlighted_text = String::from_utf8(highlighted_bytes).unwrap();
173
174                res.push_span(Span::styled(highlighted_text, self.base_style.patch(self.matched_syle)));
175                res.push_span(Span::styled(
176                    String::from_utf8(bytes.collect()).unwrap(),
177                    self.base_style,
178                ));
179                res
180            }
181            Matches::None => Line::from(vec![Span::styled(text, self.base_style)]),
182        }
183    }
184}
185
186//------------------------------------------------------------------------------
187// Preview Context
188
189/// Context information for generating item previews
190pub struct PreviewContext<'a> {
191    /// The current search query
192    pub query: &'a str,
193    /// The current command query (for interactive mode)
194    pub cmd_query: &'a str,
195    /// Width of the preview window
196    pub width: usize,
197    /// Height of the preview window
198    pub height: usize,
199    /// Index of the current item
200    pub current_index: usize,
201    /// Text of the current selection
202    pub current_selection: &'a str,
203    /// selected item indices (may or may not include current item)
204    pub selected_indices: &'a [usize],
205    /// selected item texts (may or may not include current item)
206    pub selections: &'a [&'a str],
207}
208
209//------------------------------------------------------------------------------
210// Preview
211
212/// Position and scroll information for preview display
213#[derive(Default, Copy, Clone, Debug)]
214pub struct PreviewPosition {
215    /// Horizontal scroll position
216    pub h_scroll: Size,
217    /// Horizontal offset
218    pub h_offset: Size,
219    /// Vertical scroll position
220    pub v_scroll: Size,
221    /// Vertical offset
222    pub v_offset: Size,
223}
224
225/// Defines how an item should be previewed
226pub enum ItemPreview {
227    /// execute the command and print the command's output
228    Command(String),
229    /// Display the prepared text(lines)
230    Text(String),
231    /// Display the colored text(lines)
232    AnsiText(String),
233    /// Execute a command and display output with position
234    CommandWithPos(String, PreviewPosition),
235    /// Display text with position
236    TextWithPos(String, PreviewPosition),
237    /// Display ANSI-colored text with position
238    AnsiWithPos(String, PreviewPosition),
239    /// Use global command settings to preview the item
240    Global,
241}
242
243//==============================================================================
244// A match engine will execute the matching algorithm
245
246/// Case sensitivity mode for matching
247#[derive(Eq, PartialEq, Debug, Copy, Clone, Default)]
248#[cfg_attr(feature = "cli", derive(clap::ValueEnum), clap(rename_all = "snake_case"))]
249pub enum CaseMatching {
250    /// Case-sensitive matching
251    Respect,
252    /// Case-insensitive matching
253    Ignore,
254    /// Smart case: case-insensitive unless query contains uppercase
255    #[default]
256    Smart,
257}
258
259/// Typo tolerance configuration for fuzzy matching
260///
261/// Controls how many character mismatches (typos) are allowed when matching.
262#[derive(Eq, PartialEq, Debug, Copy, Clone, Default)]
263pub enum Typos {
264    /// No typo tolerance — query must match exactly
265    #[default]
266    Disabled,
267    /// Adaptive typo tolerance — allows `pattern_length / 4` typos
268    Smart,
269    /// Fixed typo tolerance — allows exactly `n` typos
270    Fixed(usize),
271}
272
273impl From<usize> for Typos {
274    fn from(n: usize) -> Self {
275        match n {
276            0 => Typos::Disabled,
277            n => Typos::Fixed(n),
278        }
279    }
280}
281
282/// Represents the range of a match in an item
283#[derive(PartialEq, Eq, Clone, Debug)]
284pub enum MatchRange {
285    /// Range of bytes (start, end)
286    ByteRange(usize, usize),
287    /// Individual character indices that matched
288    Chars(MatchIndices),
289}
290
291/// Rank stores the raw match measurements used for sorting results.
292///
293/// Named fields preserve the semantic meaning of each value. The actual
294/// sort key (taking into account the user-configured tiebreak criteria and
295/// their direction) is computed lazily via [`Rank::sort_key`] rather than
296/// being baked in at construction time.
297#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
298pub struct Rank {
299    /// Raw fuzzy/exact match score (higher is a better match)
300    pub score: i32,
301    /// Index of the first matched character (0-based)
302    pub begin: i32,
303    /// Index of the last matched character (0-based)
304    pub end: i32,
305    /// Length of the item text in bytes
306    pub length: i32,
307    /// Ordinal position of the item in the input stream
308    pub index: i32,
309    /// Byte offset of the first character after the last path separator (`/` or `\`).
310    /// Equal to `0` when the item text contains no path separator.
311    pub path_name_offset: i32,
312}
313
314/// Result of matching a query against an item
315#[derive(Clone, Debug)]
316pub struct MatchResult {
317    /// The rank/score of this match
318    pub rank: Rank,
319    /// The range where the match occurred
320    pub matched_range: MatchRange,
321}
322
323impl MatchResult {
324    #[must_use]
325    /// Converts the match range to character indices
326    pub fn range_char_indices(&self, text: &str) -> MatchIndices {
327        match &self.matched_range {
328            &MatchRange::ByteRange(start, end) => {
329                let first = text[..start].chars().count();
330                let last = first + text[start..end].chars().count();
331                (first..last).collect()
332            }
333            MatchRange::Chars(vec) => vec.clone(),
334        }
335    }
336}
337
338/// A matching engine that can match queries against items
339pub trait MatchEngine: Sync + Send + Display {
340    /// Matches an item against the query, returning a result if matched
341    fn match_item(&self, item: &dyn SkimItem) -> Option<MatchResult>;
342}
343
344/// Factory for creating match engines
345pub trait MatchEngineFactory {
346    /// Creates a match engine with explicit case sensitivity
347    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine>;
348    /// Creates a match engine with default case sensitivity
349    fn create_engine(&self, query: &str) -> Box<dyn MatchEngine> {
350        self.create_engine_with_case(query, CaseMatching::default())
351    }
352}
353
354//------------------------------------------------------------------------------
355// Preselection
356
357/// A selector that determines whether an item should be "pre-selected" in multi-selection mode
358pub trait Selector {
359    /// Returns true if the item at the given index should be pre-selected
360    fn should_select(&self, index: usize, item: &dyn SkimItem) -> bool;
361}
362
363//------------------------------------------------------------------------------
364/// Sender for streaming items to skim
365pub type SkimItemSender = kanal::Sender<Vec<Arc<dyn SkimItem>>>;
366/// Receiver for streaming items to skim
367pub type SkimItemReceiver = kanal::Receiver<Vec<Arc<dyn SkimItem>>>;