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::sync::mpsc::channel;
9use std::thread;
10
11use clap::ValueEnum;
12use crossbeam::channel::{Receiver, Sender};
13use skim_tuikit::prelude::{Event as TermEvent, *};
14
15pub use crate::ansi::AnsiString;
16pub use crate::engine::fuzzy::FuzzyAlgorithm;
17use crate::event::{EventReceiver, EventSender};
18pub use crate::item::RankCriteria;
19use crate::model::Model;
20pub use crate::options::SkimOptions;
21pub use crate::output::SkimOutput;
22use crate::reader::Reader;
23pub use skim_common::spinlock;
24pub use skim_tuikit as tuikit;
25
26mod ansi;
27mod engine;
28mod event;
29pub mod field;
30mod global;
31mod header;
32mod helper;
33mod input;
34pub mod item;
35mod matcher;
36mod model;
37pub mod options;
38mod orderedvec;
39mod output;
40pub mod prelude;
41mod previewer;
42mod query;
43pub mod reader;
44mod selection;
45mod theme;
46pub mod tmux;
47mod util;
48
49pub trait AsAny {
51 fn as_any(&self) -> &dyn Any;
52 fn as_any_mut(&mut self) -> &mut dyn Any;
53}
54
55impl<T: Any> AsAny for T {
56 fn as_any(&self) -> &dyn Any {
57 self
58 }
59
60 fn as_any_mut(&mut self) -> &mut dyn Any {
61 self
62 }
63}
64
65pub trait SkimItem: AsAny + Send + Sync + 'static {
106 fn text(&self) -> Cow<str>;
108
109 fn display<'a>(&'a self, context: DisplayContext<'a>) -> AnsiString<'a> {
111 AnsiString::from(context)
112 }
113
114 fn preview(&self, _context: PreviewContext) -> ItemPreview {
117 ItemPreview::Global
118 }
119
120 fn output(&self) -> Cow<str> {
125 self.text()
126 }
127
128 fn get_matching_ranges(&self) -> Option<&[(usize, usize)]> {
131 None
132 }
133
134 fn get_index(&self) -> usize {
138 0
139 }
140 fn set_index(&mut self, _index: usize) {}
144}
145
146impl<T: AsRef<str> + Send + Sync + 'static> SkimItem for T {
150 fn text(&self) -> Cow<str> {
151 Cow::Borrowed(self.as_ref())
152 }
153}
154
155pub enum Matches<'a> {
158 None,
159 CharIndices(&'a [usize]),
160 CharRange(usize, usize),
161 ByteRange(usize, usize),
162}
163
164pub struct DisplayContext<'a> {
165 pub text: &'a str,
166 pub score: i32,
167 pub matches: Matches<'a>,
168 pub container_width: usize,
169 pub highlight_attr: Attr,
170}
171
172impl<'a> From<DisplayContext<'a>> for AnsiString<'a> {
173 fn from(context: DisplayContext<'a>) -> Self {
174 match context.matches {
175 Matches::CharIndices(indices) => AnsiString::from((context.text, indices, context.highlight_attr)),
176 #[allow(clippy::cast_possible_truncation)]
177 Matches::CharRange(start, end) => {
178 AnsiString::new_str(context.text, vec![(context.highlight_attr, (start as u32, end as u32))])
179 }
180 Matches::ByteRange(start, end) => {
181 let ch_start = context.text[..start].chars().count();
182 let ch_end = ch_start + context.text[start..end].chars().count();
183 #[allow(clippy::cast_possible_truncation)]
184 AnsiString::new_str(
185 context.text,
186 vec![(context.highlight_attr, (ch_start as u32, ch_end as u32))],
187 )
188 }
189 Matches::None => AnsiString::new_str(context.text, vec![]),
190 }
191 }
192}
193
194pub struct PreviewContext<'a> {
198 pub query: &'a str,
199 pub cmd_query: &'a str,
200 pub width: usize,
201 pub height: usize,
202 pub current_index: usize,
203 pub current_selection: &'a str,
204 pub selected_indices: &'a [usize],
206 pub selections: &'a [&'a str],
208}
209
210#[derive(Default, Copy, Clone, Debug)]
213pub struct PreviewPosition {
214 pub h_scroll: Size,
215 pub h_offset: Size,
216 pub v_scroll: Size,
217 pub v_offset: Size,
218}
219
220pub enum ItemPreview {
221 Command(String),
223 Text(String),
225 AnsiText(String),
227 CommandWithPos(String, PreviewPosition),
228 TextWithPos(String, PreviewPosition),
229 AnsiWithPos(String, PreviewPosition),
230 Global,
232}
233
234#[derive(ValueEnum, Eq, PartialEq, Debug, Copy, Clone, Default)]
238#[clap(rename_all = "snake_case")]
239pub enum CaseMatching {
240 Respect,
241 Ignore,
242 #[default]
243 Smart,
244}
245
246#[derive(PartialEq, Eq, Clone, Debug)]
247#[allow(dead_code)]
248pub enum MatchRange {
249 ByteRange(usize, usize),
250 Chars(Vec<usize>), }
253
254pub type Rank = [i32; 5];
255
256#[derive(Clone)]
257pub struct MatchResult {
258 pub rank: Rank,
259 pub matched_range: MatchRange,
260}
261
262impl MatchResult {
263 #[must_use]
264 pub fn range_char_indices(&self, text: &str) -> Vec<usize> {
265 match &self.matched_range {
266 &MatchRange::ByteRange(start, end) => {
267 let first = text[..start].chars().count();
268 let last = first + text[start..end].chars().count();
269 (first..last).collect()
270 }
271 MatchRange::Chars(vec) => vec.clone(),
272 }
273 }
274}
275
276pub trait MatchEngine: Sync + Send + Display {
277 fn match_item(&self, item: Arc<dyn SkimItem>) -> Option<MatchResult>;
278}
279
280pub trait MatchEngineFactory {
281 fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine>;
282 fn create_engine(&self, query: &str) -> Box<dyn MatchEngine> {
283 self.create_engine_with_case(query, CaseMatching::default())
284 }
285}
286
287pub trait Selector {
292 fn should_select(&self, index: usize, item: &dyn SkimItem) -> bool;
293}
294
295pub type SkimItemSender = Sender<Arc<dyn SkimItem>>;
297pub type SkimItemReceiver = Receiver<Arc<dyn SkimItem>>;
298
299pub struct Skim {}
300
301impl Skim {
302 #[must_use]
317 pub fn run_with(options: &SkimOptions, source: Option<SkimItemReceiver>) -> Option<SkimOutput> {
318 let min_height = Skim::parse_height_string(&options.min_height);
319 let height = Skim::parse_height_string(&options.height);
320
321 let (tx, rx): (EventSender, EventReceiver) = channel();
322 let term = Arc::new(
323 Term::with_options(
324 TermOptions::default()
325 .min_height(min_height)
326 .height(height)
327 .clear_on_exit(!options.no_clear)
328 .disable_alternate_screen(options.no_clear_start)
329 .clear_on_start(!options.no_clear_start)
330 .hold(options.select_1 || options.exit_0 || options.sync),
331 )
332 .unwrap(),
333 );
334 if !options.no_mouse {
335 let _ = term.enable_mouse_support();
336 }
337
338 let mut input = input::Input::new();
341 input.parse_keymaps(options.bind.iter().map(String::as_str));
342 input.parse_expect_keys(options.expect.iter().map(String::as_str));
343
344 let tx_clone = tx.clone();
345 let term_clone = term.clone();
346 let input_thread = thread::spawn(move || {
347 loop {
348 if let Ok(key) = term_clone.poll_event() {
349 if key == TermEvent::User(()) {
350 break;
351 }
352
353 let (key, action_chain) = input.translate_event(key);
354 for event in action_chain {
355 let _ = tx_clone.send((key, event));
356 }
357 }
358 }
359 });
360
361 let reader = Reader::with_options(options).source(source);
365
366 let mut model = Model::new(rx, tx, reader, term.clone(), options);
369 let ret = model.start();
370 let _ = term.send_event(TermEvent::User(())); let _ = input_thread.join();
372 ret
373 }
374
375 fn parse_height_string(string: &str) -> TermHeight {
381 if string.ends_with('%') {
382 let inner = string[0..string.len() - 1].parse().unwrap_or(100);
383 TermHeight::Percent(inner.clamp(0, 100))
384 } else {
385 let inner = string.parse().unwrap_or(0);
386 TermHeight::Fixed(inner)
387 }
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 #[test]
395 fn parse_height_string_fixed() {
396 let TermHeight::Fixed(h) = Skim::parse_height_string("10") else {
397 panic!("Expected fixed, found percent");
398 };
399 assert_eq!(h, 10)
400 }
401 #[test]
402 fn parse_height_string_percent() {
403 let TermHeight::Percent(h) = Skim::parse_height_string("10%") else {
404 panic!("Expected percent, found fixed");
405 };
406 assert_eq!(h, 10)
407 }
408 #[test]
409 fn parse_height_string_percent_neg() {
410 let TermHeight::Percent(h) = Skim::parse_height_string("-20%") else {
411 panic!("Expected fixed, found percent");
412 };
413 assert_eq!(h, 100)
414 }
415 #[test]
416 fn parse_height_string_percent_too_large() {
417 let TermHeight::Percent(h) = Skim::parse_height_string("120%") else {
418 panic!("Expected percent, found fixed");
419 };
420 assert_eq!(h, 100)
421 }
422 #[test]
423 fn parse_height_string_fixed_neg() {
424 let TermHeight::Fixed(h) = Skim::parse_height_string("-20") else {
425 panic!("Expected fixed, found percent");
426 };
427 assert_eq!(h, 0)
428 }
429}