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