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::thread;
9
10use crossbeam_channel::{Receiver, Sender, unbounded};
11use tuikit::prelude::{Event as TermEvent, *};
12
13pub use crate::ansi::AnsiString;
14pub use crate::engine::fuzzy::FuzzyAlgorithm;
15use crate::event::{EventReceiver, EventSender};
16use crate::model::Model;
17pub use crate::options::SkimOptions;
18pub use crate::output::SkimOutput;
19pub use crate::reader::CommandCollector;
20use crate::reader::Reader;
21
22#[cfg(feature = "malloc_trim")]
23#[cfg(target_os = "linux")]
24#[cfg(target_env = "gnu")]
25use libc as raw_libc;
26
27mod ansi;
28mod engine;
29mod event;
30pub mod field;
31mod global;
32mod header;
33mod helper;
34mod input;
35mod item;
36mod matcher;
37mod model;
38mod options;
39mod orderedvec;
40mod output;
41pub mod prelude;
42mod previewer;
43mod query;
44mod reader;
45mod selection;
46mod spinlock;
47mod theme;
48mod util;
49
50pub trait AsAny {
52 fn as_any(&self) -> &dyn Any;
53 fn as_any_mut(&mut self) -> &mut dyn Any;
54}
55
56impl<T: Any> AsAny for T {
57 fn as_any(&self) -> &dyn Any {
58 self
59 }
60
61 fn as_any_mut(&mut self) -> &mut dyn Any {
62 self
63 }
64}
65
66pub trait SkimItem: AsAny + Send + Sync + 'static {
107 fn text(&self) -> Cow<'_, str>;
109
110 fn display(&self, context: DisplayContext) -> AnsiString {
112 AnsiString::from(context)
113 }
114
115 fn preview(&self, _context: PreviewContext) -> ItemPreview {
118 ItemPreview::Global
119 }
120
121 fn output(&self) -> Cow<'_, str> {
126 self.text()
127 }
128
129 fn get_matching_ranges(&self) -> Option<&[(usize, usize)]> {
132 None
133 }
134}
135
136impl<T: AsRef<str> + Send + Sync + 'static> SkimItem for T {
140 fn text(&self) -> Cow<'_, str> {
141 Cow::Borrowed(self.as_ref())
142 }
143}
144
145pub enum Matches<'a> {
148 CharIndices(&'a [usize]),
149 CharRange(usize, usize),
150 ByteRange(usize, usize),
151}
152
153pub struct DisplayContext<'a> {
154 pub text: &'a str,
155 pub score: i32,
156 pub matches: Option<Matches<'a>>,
157 pub container_width: usize,
158 pub highlight_attr: Attr,
159}
160
161impl<'a> From<DisplayContext<'a>> for AnsiString {
162 fn from(context: DisplayContext<'a>) -> Self {
163 match context.matches {
164 Some(Matches::CharIndices(indices)) => AnsiString::from((context.text, indices, context.highlight_attr)),
165 Some(Matches::CharRange(start, end)) => {
166 AnsiString::new_str(context.text, vec![(context.highlight_attr, (start as u32, end as u32))])
167 }
168 Some(Matches::ByteRange(start, end)) => {
169 let ch_start = context.text[..start].len();
170 let ch_end = ch_start + context.text[start..end].len();
171 AnsiString::new_str(
172 context.text,
173 vec![(context.highlight_attr, (ch_start as u32, ch_end as u32))],
174 )
175 }
176 None => AnsiString::new_str(context.text, vec![]),
177 }
178 }
179}
180
181pub struct PreviewContext<'a> {
185 pub query: &'a str,
186 pub cmd_query: &'a str,
187 pub width: usize,
188 pub height: usize,
189 pub current_index: usize,
190 pub current_selection: &'a str,
191 pub selected_indices: &'a [usize],
193 pub selections: &'a [Box<str>],
195}
196
197#[derive(Default, Copy, Clone, Debug)]
200pub struct PreviewPosition {
201 pub h_scroll: Size,
202 pub h_offset: Size,
203 pub v_scroll: Size,
204 pub v_offset: Size,
205}
206
207pub enum ItemPreview {
208 Command(String),
210 Text(String),
212 AnsiText(String),
214 CommandWithPos(String, PreviewPosition),
215 TextWithPos(String, PreviewPosition),
216 AnsiWithPos(String, PreviewPosition),
217 Global,
219}
220
221#[derive(Eq, PartialEq, Debug, Copy, Clone, Default)]
225pub enum CaseMatching {
226 Respect,
227 Ignore,
228 #[default]
229 Smart,
230}
231
232#[derive(PartialEq, Eq, Clone, Debug)]
233#[allow(dead_code)]
234pub enum MatchRange {
235 ByteRange(usize, usize),
236 Chars(Box<[usize]>), }
239
240pub type Rank = [i32; 4];
241
242#[derive(Clone)]
243pub struct MatchResult {
244 pub rank: Rank,
245 pub matched_range: MatchRange,
246}
247
248impl MatchResult {
249 pub fn range_char_indices(&self, text: &str) -> Vec<usize> {
250 match &self.matched_range {
251 &MatchRange::ByteRange(start, end) => {
252 let first = text[..start].len();
253 let last = first + text[start..end].len();
254 (first..last).collect()
255 }
256 MatchRange::Chars(vec) => vec.clone().into(),
257 }
258 }
259}
260
261pub trait MatchEngine: Sync + Send + Display {
262 fn match_item(&self, item: &dyn SkimItem, item_idx: usize) -> Option<MatchResult>;
263}
264
265pub trait MatchEngineFactory {
266 fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine>;
267 fn create_engine(&self, query: &str) -> Box<dyn MatchEngine> {
268 self.create_engine_with_case(query, CaseMatching::default())
269 }
270}
271
272pub trait Selector: Send + Sync {
277 fn should_select(&self, index: usize, item: &dyn SkimItem) -> bool;
278}
279
280pub type SkimItemSender = Sender<Vec<Arc<dyn SkimItem>>>;
282pub type SkimItemReceiver = Receiver<Vec<Arc<dyn SkimItem>>>;
283
284pub struct Skim {}
285
286impl Skim {
287 pub fn run_with(options: &SkimOptions, source: Option<SkimItemReceiver>) -> Option<SkimOutput> {
296 let min_height = options
297 .min_height
298 .map(Skim::parse_height_string)
299 .expect("min_height should have default values");
300 let height = options
301 .height
302 .map(Skim::parse_height_string)
303 .expect("height should have default values");
304
305 let (tx, rx): (EventSender, EventReceiver) = unbounded();
306 let term = Arc::new(
307 Term::with_options(
308 TermOptions::default()
309 .min_height(min_height)
310 .height(height)
311 .clear_on_exit(!options.no_clear)
312 .disable_alternate_screen(options.no_clear_start)
313 .clear_on_start(!options.no_clear_start)
314 .hold(options.select1 || options.exit0 || options.sync),
315 )
316 .unwrap(),
317 );
318 if !options.no_mouse {
319 let _ = term.enable_mouse_support();
320 }
321
322 let mut input = input::Input::new();
325 input.parse_keymaps(&options.bind);
326 input.parse_expect_keys(options.expect.as_deref());
327
328 let tx_clone = tx.clone();
329 let term_clone = term.clone();
330 let input_thread = thread::spawn(move || {
331 loop {
332 if let Ok(key) = term_clone.poll_event() {
333 if key == TermEvent::User(()) {
334 break;
335 }
336
337 let (key, action_chain) = input.translate_event(key);
338 for event in action_chain {
339 let _ = tx_clone.send((key, event));
340 }
341 }
342 }
343
344 #[cfg(feature = "malloc_trim")]
345 #[cfg(target_os = "linux")]
346 #[cfg(target_env = "gnu")]
347 malloc_trim();
348 });
349
350 let reader = Reader::with_options(options).source(source);
354
355 let mut model = Model::new(rx, tx, reader, term.clone(), options);
358 let ret = model.start();
359 let _ = term.send_event(TermEvent::User(())); let _ = input_thread.join();
361
362 #[cfg(feature = "malloc_trim")]
363 #[cfg(target_os = "linux")]
364 #[cfg(target_env = "gnu")]
365 malloc_trim();
366
367 ret
368 }
369
370 fn parse_height_string(string: &str) -> TermHeight {
373 if string.ends_with('%') {
374 TermHeight::Percent(string[0..string.len() - 1].parse().unwrap_or(100))
375 } else {
376 TermHeight::Fixed(string.parse().unwrap_or(0))
377 }
378 }
379}
380
381#[cfg(feature = "malloc_trim")]
382#[cfg(target_os = "linux")]
383#[cfg(target_env = "gnu")]
384pub fn malloc_trim() {
385 unsafe {
386 let _ = raw_libc::malloc_trim(0usize);
387 }
388}