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>>>;