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