use std::process::{Command, Stdio};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::item::{ItemPool, MatchedItem};
use crate::matcher::{Matcher, MatcherControl};
use crate::prelude::ExactOrFuzzyEngineFactory;
use crate::tui::SkimRender;
use crate::tui::input::StatusInfo;
use crate::tui::layout::{AppLayout, LayoutTemplate};
use crate::tui::options::TuiLayout;
use crate::tui::statusline::InfoDisplay;
use crate::tui::widget::SkimWidget;
use crate::{ItemPreview, PreviewContext, SkimItem, SkimOptions};
use crate::{Rank, util};
use super::Event;
use super::Tui;
use super::event::Action;
use super::header::Header;
use super::item_list::ItemList;
use super::{input, preview};
use color_eyre::eyre::{Result, bail};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
use input::Input;
use preview::Preview;
use ratatui::buffer::Buffer;
use ratatui::crossterm::event::KeyCode::Char;
use ratatui::layout::Rect;
use ratatui::prelude::Backend;
use ratatui::widgets::Widget;
use rayon::ThreadPool;
use std::sync::LazyLock;
static NUM_THREADS: LazyLock<usize> = LazyLock::new(|| {
std::thread::available_parallelism()
.ok()
.map(|inner| inner.get())
.unwrap_or_else(|| 0)
});
const FRAME_TIME_MS: u128 = 1000 / 30;
const MATCHER_DEBOUNCE_MS: u128 = 200;
const HIDE_GRACE_MS: u128 = 500;
pub struct App {
pub item_pool: Arc<ItemPool>,
pub thread_pool: Arc<ThreadPool>,
pub should_quit: bool,
pub cursor_pos: (u16, u16),
pub matcher_control: MatcherControl,
pub matcher: Matcher,
pub yank_register: String,
pub last_matcher_restart: std::time::Instant,
pub pending_matcher_restart: bool,
pub needs_render: Arc<AtomicBool>,
pub last_render_timer: std::time::Instant,
pub input: Input,
pub preview: Preview,
pub header: Header,
pub item_list: ItemList,
pub theme: Arc<crate::theme::ColorTheme>,
pub matcher_timer: std::time::Instant,
pub spinner_last_change: std::time::Instant,
pub show_spinner: bool,
pub spinner_start: std::time::Instant,
pub query_history: Vec<String>,
pub history_index: Option<usize>,
pub saved_input: String,
pub cmd_history: Vec<String>,
pub cmd_history_index: Option<usize>,
pub saved_cmd_input: String,
pub options: SkimOptions,
pub cmd: String,
pub layout_template: LayoutTemplate,
pub layout: AppLayout,
pub last_preview_spawn: std::time::Instant,
pub pending_preview_run: bool,
reader_timer: std::time::Instant,
items_just_updated: bool,
}
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut res = SkimRender::default();
let has_border = self.options.border.is_some();
self.header.set_header_lines(self.item_pool.reserved());
self.layout = self.layout_template.apply(area);
if let Some(header_area) = self.layout.header_area {
res |= self.header.render(header_area, buf);
}
if let Some(preview_area) = self.layout.preview_area {
res |= self.preview.render(preview_area, buf);
}
res |= self.item_list.render(self.layout.list_area, buf);
self.input.status_info = if self.options.info != InfoDisplay::Hidden {
Some(StatusInfo {
total: self.item_pool.len(),
matched: self.item_list.count(),
processed: self.matcher_control.get_num_processed(),
show_spinner: self.show_spinner,
matcher_mode: if self.options.regex {
"RE".to_string()
} else {
String::new()
},
multi_selection: self.options.multi,
selected: self.item_list.selection.len(),
current_item_idx: self.item_list.current,
hscroll_offset: self.item_list.manual_hscroll as i64,
start: Some(self.spinner_start),
})
} else {
None
};
res |= self.input.render(self.layout.input_area, buf);
self.cursor_pos = (
self.layout.input_area.x + self.input.cursor_pos() + if has_border { 1 } else { 0 },
self.layout.input_area.y
+ if self.options.layout == TuiLayout::Reverse && self.options.border.is_none() {
0
} else {
1
},
);
if res.run_preview {
self.pending_preview_run = true;
}
}
}
impl Default for App {
fn default() -> Self {
let theme = Arc::new(crate::theme::ColorTheme::default());
let opts = SkimOptions::default();
let header = Header::from_options(&opts, theme.clone());
let layout_template = LayoutTemplate::from_options(&opts, header.height());
let layout = layout_template.apply(Rect::default());
Self {
input: Input::from_options(&opts, theme.clone()),
preview: Preview::from_options(&opts, theme.clone()),
header,
item_list: ItemList::from_options(&opts, theme.clone()),
thread_pool: Arc::new(
rayon::ThreadPoolBuilder::new()
.num_threads(*NUM_THREADS)
.build()
.unwrap(),
),
item_pool: Arc::default(),
theme,
should_quit: false,
cursor_pos: (0, 0),
matcher: Matcher::builder(Rc::new(ExactOrFuzzyEngineFactory::builder().build()))
.case(crate::CaseMatching::default())
.build(),
yank_register: String::new(),
matcher_control: MatcherControl::default(),
matcher_timer: std::time::Instant::now(),
last_matcher_restart: std::time::Instant::now(),
pending_matcher_restart: false,
needs_render: Arc::new(AtomicBool::new(true)),
last_render_timer: std::time::Instant::now() - std::time::Duration::from_secs(1),
spinner_last_change: std::time::Instant::now(),
show_spinner: false,
spinner_start: std::time::Instant::now(),
query_history: Vec::new(),
history_index: None,
saved_input: String::new(),
cmd_history: Vec::new(),
cmd_history_index: None,
saved_cmd_input: String::new(),
options: opts,
cmd: String::new(),
layout_template,
layout,
last_preview_spawn: std::time::Instant::now(),
pending_preview_run: false,
reader_timer: std::time::Instant::now(),
items_just_updated: false,
}
}
}
impl App {
pub fn from_options(options: SkimOptions, theme: Arc<crate::theme::ColorTheme>, cmd: String) -> Self {
let header = Header::from_options(&options, theme.clone());
let layout_template = LayoutTemplate::from_options(&options, header.height());
let layout = layout_template.apply(Rect::default());
Self {
input: Input::from_options(&options, theme.clone()),
preview: Preview::from_options(&options, theme.clone()),
header,
thread_pool: Arc::new(
rayon::ThreadPoolBuilder::new()
.num_threads(*NUM_THREADS)
.build()
.unwrap(),
),
item_pool: Arc::new(ItemPool::from_options(&options)),
item_list: ItemList::from_options(&options, theme.clone()),
theme,
should_quit: false,
cursor_pos: (0, 0),
matcher: Matcher::from_options(&options),
yank_register: String::new(),
matcher_control: MatcherControl::default(),
reader_timer: std::time::Instant::now(),
matcher_timer: std::time::Instant::now(),
last_matcher_restart: std::time::Instant::now(),
pending_matcher_restart: false,
needs_render: Arc::new(AtomicBool::new(true)),
last_render_timer: std::time::Instant::now() - std::time::Duration::from_secs(1),
spinner_last_change: std::time::Instant::now(),
show_spinner: false,
spinner_start: std::time::Instant::now(),
items_just_updated: false,
query_history: options.query_history.clone(),
history_index: None,
saved_input: String::new(),
cmd_history: options.cmd_history.clone(),
cmd_history_index: None,
saved_cmd_input: String::new(),
options,
cmd,
layout_template,
layout,
last_preview_spawn: std::time::Instant::now() - std::time::Duration::from_secs(1),
pending_preview_run: false,
}
}
pub fn resize(&mut self, cols: u16, rows: u16) {
self.layout_template = LayoutTemplate::from_options(&self.options, self.header.height());
self.layout = self.layout_template.apply(Rect::new(0, 0, cols, rows));
}
}
impl App {
fn calculate_preview_offset(&self, offset_expr: &str) -> u16 {
let expr = offset_expr.trim_start_matches('+');
let substituted = self.expand_cmd(expr, true);
if let Some((left, right)) = substituted.split_once('-') {
let left_val = left.trim_matches(|x: char| !x.is_numeric()).parse::<u16>().unwrap_or(0);
let right_val = right
.trim_matches(|x: char| !x.is_numeric())
.parse::<u16>()
.unwrap_or(0);
left_val.saturating_sub(right_val)
} else if let Some((left, right)) = substituted.split_once('+') {
let left_val = left.trim_matches(|x: char| !x.is_numeric()).parse::<u16>().unwrap_or(0);
let right_val = right
.trim_matches(|x: char| !x.is_numeric())
.parse::<u16>()
.unwrap_or(0);
left_val.saturating_add(right_val)
} else {
substituted
.trim_matches(|x: char| !x.is_numeric())
.parse::<u16>()
.unwrap_or(0)
}
}
fn needs_render(&mut self) {
self.needs_render.store(true, Ordering::Relaxed)
}
fn on_items_updated(&mut self) {
self.pending_matcher_restart = true;
trace!("Got new items, len {}", self.item_pool.len());
self.reader_timer = std::time::Instant::now();
self.items_just_updated = true;
}
fn on_selection_changed(&mut self) -> Result<Vec<Event>> {
Ok(vec![Event::RunPreview])
}
fn on_query_changed(&mut self) -> Result<Vec<Event>> {
if self.options.interactive && self.options.cmd.is_some() {
let expanded_cmd = self.expand_cmd(&self.cmd, true);
return Ok(vec![Event::Reload(expanded_cmd)]);
}
self.restart_matcher_debounced();
Ok(vec![
Event::Key(KeyEvent::new(KeyCode::F(255), KeyModifiers::NONE)), Event::RunPreview,
])
}
fn update_spinner(&mut self) {
let matcher_running = !self.matcher_control.stopped();
let time_since_match = self.matcher_timer.elapsed();
let reading = self.item_pool.num_not_taken() != 0;
let should_show_spinner = reading || (matcher_running && time_since_match.as_millis() > MATCHER_DEBOUNCE_MS);
if should_show_spinner && !self.show_spinner {
self.toggle_spinner();
} else if !should_show_spinner && self.show_spinner {
if self.spinner_last_change.elapsed().as_millis() >= HIDE_GRACE_MS {
self.toggle_spinner();
}
}
if self.show_spinner {
self.needs_render.store(true, Ordering::Relaxed);
}
}
fn run_preview<B: Backend>(&mut self, tui: &mut Tui<B>) -> Result<()>
where
B::Error: Send + Sync + 'static,
{
const DEBOUNCE_MS: u64 = 50;
let now = std::time::Instant::now();
let elapsed = now.duration_since(self.last_preview_spawn);
if elapsed.as_millis() < DEBOUNCE_MS as u128 {
self.pending_preview_run = true;
return Ok(());
}
self.pending_preview_run = false;
self.last_preview_spawn = now;
if let Some(preview_opt) = &self.options.preview
&& let Some(item) = self.item_list.selected()
{
let selection: Vec<_> = self.item_list.selection.iter().map(|i| i.text().into_owned()).collect();
let selection_str: Vec<_> = selection.iter().map(|s| s.as_str()).collect();
let selected = self.item_list.selected();
let ctx = PreviewContext {
query: &self.input.value,
cmd_query: if self.options.interactive {
&self.input.value
} else {
self.options.cmd_query.as_deref().unwrap_or(&self.input.value)
},
width: self.preview.cols as usize,
height: self.preview.rows as usize,
current_index: selected.as_ref().map(|i| i.rank.index as usize).unwrap_or_default(),
current_selection: &selected.map(|i| i.text().into_owned()).unwrap_or_default(),
selected_indices: &self
.item_list
.selection
.iter()
.map(|v| v.rank.index as usize)
.collect::<Vec<_>>(),
selections: &selection_str,
};
let preview = item.preview(ctx);
let preview_ready = !matches!(
preview,
ItemPreview::Global | ItemPreview::Command(_) | ItemPreview::CommandWithPos(_, _)
);
match preview {
ItemPreview::Command(cmd) => self.preview.spawn(tui, &self.expand_cmd(&cmd, true))?,
ItemPreview::Text(t) | ItemPreview::AnsiText(t) => self.preview.content(t.bytes().collect())?,
ItemPreview::CommandWithPos(cmd, preview_position) => {
self.preview.spawn(tui, &self.expand_cmd(&cmd, true))?;
let v_scroll = match preview_position.v_scroll {
crate::tui::Size::Fixed(n) => n,
crate::tui::Size::Percent(p) => (self.preview.rows as u32 * p as u32 / 100) as u16,
};
let v_offset = match preview_position.v_offset {
crate::tui::Size::Fixed(n) => n,
crate::tui::Size::Percent(p) => (self.preview.rows as u32 * p as u32 / 100) as u16,
};
self.preview.scroll_y = v_scroll;
self.preview.scroll_down(v_offset);
let h_scroll = match preview_position.h_scroll {
crate::tui::Size::Fixed(n) => n,
crate::tui::Size::Percent(p) => (self.preview.cols as u32 * p as u32 / 100) as u16,
};
let h_offset = match preview_position.h_offset {
crate::tui::Size::Fixed(n) => n,
crate::tui::Size::Percent(p) => (self.preview.cols as u32 * p as u32 / 100) as u16,
};
self.preview.scroll_x = h_scroll.saturating_add(h_offset);
}
ItemPreview::TextWithPos(t, preview_position) => self
.preview
.content_with_position(t.bytes().collect(), preview_position)?,
ItemPreview::AnsiWithPos(t, preview_position) => self
.preview
.content_with_position(t.bytes().collect(), preview_position)?,
ItemPreview::Global => self.preview.spawn(tui, &self.expand_cmd(preview_opt, true))?,
}
if preview_ready {
let _ = tui.event_tx.try_send(Event::PreviewReady);
}
} else if let Some(cb) = &self.options.preview_fn {
let selection: Vec<Arc<dyn SkimItem>>;
if self.options.multi {
selection = self.item_list.selection.iter().map(|i| i.item.clone()).collect();
} else if let Some(sel) = self.item_list.selected() {
selection = vec![sel.item];
} else {
selection = Vec::new();
}
self.preview.content(cb(selection).join("\n").into_bytes())?;
}
Ok(())
}
pub fn handle_event<B: Backend>(&mut self, tui: &mut Tui<B>, event: &Event) -> Result<()>
where
B::Error: Send + Sync + 'static,
{
match event {
Event::Render => {
tui.get_frame();
tui.draw(|f| {
f.render_widget(&mut *self, f.area());
f.set_cursor_position(self.cursor_pos);
})?;
}
Event::Heartbeat | Event::Tick => {
self.update_spinner();
if self.pending_matcher_restart {
self.restart_matcher(true);
}
if self.needs_render.load(Ordering::Relaxed)
&& self.last_render_timer.elapsed().as_millis() > FRAME_TIME_MS
{
debug!("Triggering render");
self.needs_render.store(false, Ordering::Relaxed);
self.last_render_timer = std::time::Instant::now();
tui.event_tx.try_send(Event::Render)?;
}
if self.pending_preview_run
&& let Err(e) = self.run_preview(tui)
{
warn!("Heartbeat RunPreview: error {e:?}");
}
}
Event::RunPreview => {
if let Err(e) = self.run_preview(tui) {
warn!("RunPreview: error {e:?}");
}
}
Event::Clear => {
tui.clear()?;
}
Event::Quit => {
tui.exit()?;
self.should_quit = true;
}
Event::Close => {
tui.exit()?;
self.should_quit = true;
}
Event::PreviewReady => {
if let Some(offset_expr) = &self.options.preview_window.offset {
let offset = self.calculate_preview_offset(offset_expr);
self.preview.set_offset(offset);
}
self.needs_render();
}
Event::Error(msg) => {
tui.exit()?;
bail!(msg.to_owned());
}
Event::Action(act) => {
let events = self.handle_action(act)?;
for evt in events {
tui.event_tx.try_send(evt)?;
}
tui.event_tx.try_send(Event::Render)?;
}
Event::Key(key) => {
let events = self.handle_key(key);
for evt in events {
tui.event_tx.try_send(evt)?;
}
}
Event::Paste(text) => {
let cleaned: String = text.chars().filter(|c| *c != '\n' && *c != '\r').collect();
if !cleaned.is_empty() {
self.input.insert_str(&cleaned);
let events = self.on_query_changed()?;
for evt in events {
tui.event_tx.try_send(evt)?;
}
}
}
Event::Redraw => {
tui.clear()?;
}
Event::Resize(cols, rows) => {
self.resize(*cols, *rows);
if let Err(e) = self.run_preview(tui) {
warn!("error while rerunnig preview after resize: {e}");
}
}
Event::Mouse(mouse_event) => {
self.handle_mouse(mouse_event, tui)?;
}
Event::InvalidInput => {
warn!("Received invalid input");
}
Event::ClearItems => {
self.item_pool.clear();
self.restart_matcher(true);
}
Event::AppendItems(items) => {
self.item_pool.append(items.to_owned());
self.restart_matcher(false);
}
Event::Reload(_) => {
unreachable!("Reload is handled by the TUI event loop in lib.rs")
}
};
Ok(())
}
pub fn handle_items(&mut self, items: Vec<Arc<dyn SkimItem>>) {
self.item_pool.append(items);
trace!("Got new items, len {}", self.item_pool.len());
self.on_items_updated();
}
fn handle_key(&mut self, key: &KeyEvent) -> Vec<Event> {
debug!("key event: {:?}", key);
if let Some(act) = &self.options.keymap.get(key) {
debug!("{act:?}");
return act.iter().map(|a| Event::Action(a.clone())).collect();
}
match key.modifiers {
KeyModifiers::CONTROL => {
if let Char('c') = key.code {
return vec![Event::Quit];
}
}
KeyModifiers::NONE => {
if let Char(c) = key.code {
return vec![Event::Action(Action::AddChar(c))];
}
}
KeyModifiers::SHIFT => {
if let Char(c) = key.code {
return vec![Event::Action(Action::AddChar(c.to_uppercase().next().unwrap()))];
}
}
_ => (),
};
vec![]
}
fn handle_action(&mut self, act: &Action) -> Result<Vec<Event>> {
use Action::*;
match act {
Abort => {
self.should_quit = true;
}
Accept(_) => {
self.should_quit = true;
}
AddChar(c) => {
self.input.insert(*c);
return self.on_query_changed();
}
AppendAndSelect => {
let value = self.input.value.clone();
let item: Arc<dyn SkimItem> = Arc::new(value);
let rank = Rank {
index: self.item_pool.len() as i32,
..Default::default()
};
self.item_pool.append(vec![item.clone()]);
self.item_list.append(&mut vec![MatchedItem {
item,
rank,
rank_builder: self.matcher.rank_builder.clone(),
matched_range: None,
}]);
self.item_list.select_row(self.item_list.items.len() - 1);
self.restart_matcher_debounced();
return self.on_selection_changed();
}
BackwardChar => {
self.input.move_cursor(-1);
}
BackwardDeleteChar => {
if self.input.delete(-1).is_some() {
return self.on_query_changed();
}
}
BackwardDeleteCharEof => {
if self.input.is_empty() {
self.should_quit = true;
return Ok(vec![]);
} else {
self.input.delete(-1);
return self.on_query_changed();
}
}
BackwardKillWord => {
let deleted = self.input.delete_backward_word();
if !deleted.is_empty() {
self.yank(deleted);
return self.on_query_changed();
}
}
BackwardWord => {
self.input.move_cursor_backward_word();
}
BeginningOfLine => {
self.input.move_cursor_to(0);
}
Cancel => {
self.matcher_control.kill();
self.preview.kill();
}
ClearScreen => {
return Ok(vec![Event::Clear]);
}
DeleteChar => {
if self.input.delete(0).is_some() {
return self.on_query_changed();
}
}
DeleteCharEof => {
if self.input.is_empty() {
self.should_quit = true;
return Ok(vec![]);
} else if self.input.delete(0).is_some() {
return self.on_query_changed();
}
}
DeselectAll => {
if !self.item_list.selection.is_empty() {
self.item_list.selection = Default::default();
return self.on_selection_changed();
}
}
Down(n) => {
use ratatui::widgets::ListDirection::*;
match self.item_list.direction {
TopToBottom => self.item_list.scroll_by(*n as i32),
BottomToTop => self.item_list.scroll_by(-(*n as i32)),
}
return self.on_selection_changed();
}
EndOfLine => {
self.input.move_to_end();
}
Execute(cmd) => {
let mut command = Command::new("sh");
let expanded_cmd = self.expand_cmd(cmd, true);
debug!("execute: {}", expanded_cmd);
command.args(["-c", &expanded_cmd]);
let in_raw_mode = crossterm::terminal::is_raw_mode_enabled()?;
if in_raw_mode {
crossterm::terminal::disable_raw_mode()?;
}
crossterm::execute!(
std::io::stderr(),
crossterm::terminal::LeaveAlternateScreen,
crossterm::event::DisableMouseCapture
)?;
let _ = command.spawn().and_then(|mut c| c.wait());
if in_raw_mode {
crossterm::terminal::enable_raw_mode()?;
}
crossterm::execute!(
std::io::stderr(),
crossterm::terminal::EnterAlternateScreen,
crossterm::event::EnableMouseCapture
)?;
return Ok(vec![Event::Redraw]);
}
ExecuteSilent(cmd) => {
let mut command = Command::new("sh");
let expanded_cmd = self.expand_cmd(cmd, true);
command.args(["-c", &expanded_cmd]);
command.stdout(Stdio::null());
command.stderr(Stdio::null());
let _ = command.spawn();
}
First | Top => {
self.item_list.jump_to_first();
return self.on_selection_changed();
}
ForwardChar => {
self.input.move_cursor(1);
}
ForwardWord => {
self.input.move_cursor_forward_word();
}
IfQueryEmpty(then, otherwise) => {
let inner = crate::binds::parse_action_chain(then)?;
if self.input.is_empty() {
return Ok(inner.iter().map(|e| Event::Action(e.to_owned())).collect());
} else if let Some(o) = otherwise {
return Ok(crate::binds::parse_action_chain(o)?
.iter()
.map(|e| Event::Action(e.to_owned()))
.collect());
}
}
IfQueryNotEmpty(then, otherwise) => {
let inner = crate::binds::parse_action_chain(then)?;
if !self.input.is_empty() {
return Ok(inner.iter().map(|e| Event::Action(e.to_owned())).collect());
} else if let Some(o) = otherwise {
return Ok(crate::binds::parse_action_chain(o)?
.iter()
.map(|e| Event::Action(e.to_owned()))
.collect());
}
}
IfNonMatched(then, otherwise) => {
let inner = crate::binds::parse_action_chain(then)?;
if self.item_list.items.is_empty() {
return Ok(inner.iter().map(|e| Event::Action(e.to_owned())).collect());
} else if let Some(o) = otherwise {
return Ok(crate::binds::parse_action_chain(o)?
.iter()
.map(|e| Event::Action(e.to_owned()))
.collect());
}
}
Ignore => (),
KillLine => {
let cursor = self.input.cursor_pos as usize;
let deleted = self.input.split_off(cursor);
self.yank(deleted);
return self.on_query_changed();
}
KillWord => {
let deleted = self.input.delete_forward_word();
self.yank(deleted);
return self.on_query_changed();
}
Last => {
self.item_list.jump_to_last();
return self.on_selection_changed();
}
NextHistory => {
let (history, history_index, saved_input) = if self.options.interactive {
(
&self.cmd_history,
&mut self.cmd_history_index,
&mut self.saved_cmd_input,
)
} else {
(&self.query_history, &mut self.history_index, &mut self.saved_input)
};
if history.is_empty() {
return Ok(vec![]);
}
match *history_index {
None => {
}
Some(idx) => {
if idx + 1 >= history.len() {
self.input.value = saved_input.clone();
self.input.move_to_end();
*history_index = None;
} else {
let new_idx = idx + 1;
self.input.value = history[new_idx].clone();
self.input.move_to_end();
*history_index = Some(new_idx);
}
}
}
return self.on_query_changed();
}
HalfPageDown(n) => {
let offset = self.item_list.height as i32 / 2;
if self.options.layout == TuiLayout::Default {
self.item_list.scroll_by(-offset * n);
} else {
self.item_list.scroll_by(offset * n);
}
return self.on_selection_changed();
}
HalfPageUp(n) => {
let offset = self.item_list.height as i32 / 2;
if self.options.layout == TuiLayout::Default {
self.item_list.scroll_by(offset * n);
} else {
self.item_list.scroll_by(-offset * n);
}
return self.on_selection_changed();
}
PageDown(n) => {
let offset = self.item_list.height as i32;
if self.options.layout == TuiLayout::Default {
self.item_list.scroll_by(-offset * n);
} else {
self.item_list.scroll_by(offset * n);
}
return self.on_selection_changed();
}
PageUp(n) => {
let offset = self.item_list.height as i32;
if self.options.layout == TuiLayout::Default {
self.item_list.scroll_by(offset * n);
} else {
self.item_list.scroll_by(-offset * n);
}
return self.on_selection_changed();
}
PreviewUp(n) => {
self.preview.scroll_up(*n as u16);
self.needs_render();
}
PreviewDown(n) => {
self.preview.scroll_down(*n as u16);
self.needs_render();
}
PreviewLeft(n) => {
self.preview.scroll_left(*n as u16);
self.needs_render();
}
PreviewRight(n) => {
self.preview.scroll_right(*n as u16);
self.needs_render();
}
PreviewPageUp(_n) => {
self.preview.page_up();
self.needs_render();
}
PreviewPageDown(_n) => {
self.preview.page_down();
self.needs_render();
}
PreviousHistory => {
let (history, history_index, saved_input) = if self.options.interactive {
(
&self.cmd_history,
&mut self.cmd_history_index,
&mut self.saved_cmd_input,
)
} else {
(&self.query_history, &mut self.history_index, &mut self.saved_input)
};
if history.is_empty() {
return Ok(vec![]);
}
match *history_index {
None => {
*saved_input = self.input.value.clone();
let new_idx = history.len() - 1;
self.input.value = history[new_idx].clone();
self.input.move_to_end();
*history_index = Some(new_idx);
}
Some(idx) => {
if idx > 0 {
let new_idx = idx - 1;
self.input.value = history[new_idx].clone();
self.input.move_to_end();
*history_index = Some(new_idx);
}
}
}
return self.on_query_changed();
}
Redraw => return Ok(vec![Event::Clear]),
Reload(Some(s)) => {
self.item_list.clear_selection();
return Ok(vec![Event::Reload(self.expand_cmd(s, true))]);
}
Reload(None) => {
self.item_list.clear_selection();
return Ok(vec![Event::Reload(self.cmd.clone())]);
}
RefreshCmd => {
if self.options.interactive {
let expanded_cmd = self.expand_cmd(&self.cmd, true);
return Ok(vec![Event::Reload(expanded_cmd)]);
}
}
RefreshPreview => {
return Ok(vec![Event::RunPreview]);
}
RestartMatcher => {
self.restart_matcher(true);
}
RotateMode => {
if self.options.regex {
self.options.regex = false;
self.options.exact = false;
} else if self.options.exact {
self.options.exact = false;
self.options.regex = true;
} else {
self.options.exact = true;
}
self.matcher = Matcher::from_options(&self.options);
self.restart_matcher(true);
}
ScrollLeft(n) => {
self.item_list.manual_hscroll = self.item_list.manual_hscroll.saturating_sub(*n);
}
ScrollRight(n) => {
self.item_list.manual_hscroll = self.item_list.manual_hscroll.saturating_add(*n);
}
SelectAll => {
self.item_list.select_all();
return self.on_selection_changed();
}
SelectRow(row) => {
self.item_list.select_row(*row);
return self.on_selection_changed();
}
Select => {
self.item_list.select();
return self.on_selection_changed();
}
SetHeader(opt_header) => {
self.options.header = opt_header.to_owned();
self.header = Header::from_options(&self.options, self.theme.clone());
self.layout_template = LayoutTemplate::from_options(&self.options, self.header.height());
}
SetPreviewCmd(cmd) => {
self.options.preview = Some(cmd.to_owned());
return Ok(vec![Event::RunPreview]);
}
SetQuery(value) => {
self.input.value = self.expand_cmd(value, false);
self.input.move_to_end();
return self.on_query_changed();
}
Toggle => {
self.item_list.toggle();
return self.on_selection_changed();
}
ToggleAll => {
self.item_list.toggle_all();
return self.on_selection_changed();
}
ToggleIn => {
self.item_list.toggle();
use ratatui::widgets::ListDirection::*;
match self.item_list.direction {
TopToBottom => self.item_list.select_next(),
BottomToTop => self.item_list.select_previous(),
}
return self.on_selection_changed();
}
ToggleInteractive => {
self.options.interactive = !self.options.interactive;
self.input.switch_mode();
self.restart_matcher(true);
}
ToggleOut => {
self.item_list.toggle();
use ratatui::widgets::ListDirection::*;
match self.item_list.direction {
TopToBottom => self.item_list.select_previous(),
BottomToTop => self.item_list.select_next(),
}
return self.on_selection_changed();
}
TogglePreview => {
self.options.preview_window.hidden = !self.options.preview_window.hidden;
self.layout_template = LayoutTemplate::from_options(&self.options, self.header.height());
self.needs_render();
}
TogglePreviewWrap => {
self.preview.wrap = !self.preview.wrap;
self.needs_render();
}
ToggleSort => {
self.options.no_sort = !self.options.no_sort;
self.restart_matcher(true);
}
UnixLineDiscard => {
if !self.input.delete_to_beginning().is_empty() {
return self.on_query_changed();
}
}
UnixWordRubout => {
if !self.input.delete_backward_to_whitespace().is_empty() {
return self.on_query_changed();
}
}
Up(n) => {
use ratatui::widgets::ListDirection::*;
match self.item_list.direction {
TopToBottom => self.item_list.scroll_by(-(*n as i32)),
BottomToTop => self.item_list.scroll_by(*n as i32),
}
return self.on_selection_changed();
}
Yank => {
self.input.insert_str(&self.yank_register);
return self.on_query_changed();
}
Custom(cb) => {
return cb.call(self).map_err(|e| color_eyre::eyre::eyre!("{}", e));
}
}
Ok(Vec::default())
}
pub fn results(&mut self) -> Vec<MatchedItem> {
if self.options.filter.is_some() {
self.item_list.items.drain(..).collect()
} else if self.options.multi && !self.item_list.selection.is_empty() {
self.item_list.selection.clone().into_iter().collect()
} else if let Some(sel) = self.item_list.selected() {
vec![sel]
} else {
vec![]
}
}
pub fn restart_matcher(&mut self, force: bool) {
if let Some(min_length) = self.options.min_query_length
&& !self.options.disabled
{
let query_to_check = &self.input.value;
if query_to_check.chars().count() < min_length {
self.matcher_control.kill();
self.item_list.items.clear();
self.item_list.current = 0;
self.item_list.offset = 0;
return;
}
}
let matcher_stopped = self.matcher_control.stopped();
if force || (matcher_stopped && self.item_pool.num_not_taken() > 0) {
trace!("restarting matcher, force={force}");
self.last_matcher_restart = std::time::Instant::now();
self.pending_matcher_restart = false;
self.matcher_control.kill();
self.matcher_timer = std::time::Instant::now();
let query = if self.options.disabled {
""
} else if self.options.interactive {
&input::Input::default()
} else {
&self.input
};
let item_pool = self.item_pool.clone();
let thread_pool = &self.thread_pool;
let processed_items = self.item_list.processed_items.clone();
let no_sort = self.options.no_sort;
if force {
self.item_pool.reset();
}
let needs_render = self.needs_render.clone();
self.matcher_control = self.matcher.run(query, item_pool, thread_pool, move |mut matches| {
debug!("Got {} results from matcher, sending to item list...", matches.len());
if !no_sort {
matches.sort();
}
use crate::tui::item_list::{MergeStrategy, ProcessedItems};
if force {
*processed_items.lock() = Some(ProcessedItems {
items: matches,
merge: MergeStrategy::Replace,
});
} else {
let merge_strategy = if no_sort {
MergeStrategy::Append
} else {
MergeStrategy::SortedMerge
};
let mut guard = processed_items.lock();
if let Some(ref mut existing) = *guard {
if no_sort {
existing.items.extend(matches);
} else {
MatchedItem::merge_into_sorted(&mut existing.items, matches);
}
} else {
*guard = Some(ProcessedItems {
items: matches,
merge: merge_strategy,
});
}
}
needs_render.store(true, Ordering::Relaxed);
});
}
}
fn yank(&mut self, contents: String) {
self.yank_register = contents;
}
pub fn expand_cmd(&self, cmd: &str, quote_args: bool) -> String {
util::printf(
cmd,
&self.options.delimiter,
&self.options.replstr,
self.item_list.selection.iter(),
self.item_list.selected(),
&self.input.value,
&self.input.value,
quote_args,
)
}
fn restart_matcher_debounced(&mut self) {
if self.options.disabled {
return;
}
const DEBOUNCE_MS: u64 = 50;
if self.last_matcher_restart.elapsed().as_millis() > DEBOUNCE_MS as u128 {
debug!("restart_matcher_debounced: true");
self.restart_matcher(true);
} else {
debug!("restart_matcher_debounced: false");
self.pending_matcher_restart = true;
}
}
fn handle_mouse<B: Backend>(&mut self, mouse_event: &MouseEvent, tui: &mut Tui<B>) -> Result<()>
where
B::Error: Send + Sync + 'static,
{
let mouse_pos = ratatui::layout::Position {
x: mouse_event.column,
y: mouse_event.row,
};
match mouse_event.kind {
MouseEventKind::ScrollUp => {
if let Some(preview_area) = self.layout.preview_area
&& preview_area.contains(mouse_pos)
{
for evt in self.handle_action(&Action::PreviewUp(3))? {
tui.event_tx.try_send(evt)?;
}
return Ok(());
}
for evt in self.handle_action(&Action::Up(1))? {
tui.event_tx.try_send(evt)?;
}
}
MouseEventKind::ScrollDown => {
if let Some(preview_area) = self.layout.preview_area
&& preview_area.contains(mouse_pos)
{
for evt in self.handle_action(&Action::PreviewDown(3))? {
tui.event_tx.try_send(evt)?;
}
return Ok(());
}
for evt in self.handle_action(&Action::Down(1))? {
tui.event_tx.try_send(evt)?;
}
}
_ => {
}
}
tui.event_tx.try_send(Event::Render)?;
Ok(())
}
fn toggle_spinner(&mut self) {
self.show_spinner = !self.show_spinner;
self.spinner_last_change = std::time::Instant::now();
self.needs_render.store(true, Ordering::Relaxed);
}
}