From d028163a5e1427f4dd67b94574a79b82aa3d97a0 Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier Date: Sun, 22 Sep 2024 18:54:06 +0200 Subject: [PATCH] work in progress --- .config/config.json5 | 4 +- Cargo.lock | 13 +- Cargo.toml | 1 + src/action.rs | 24 ++- src/app.rs | 57 +++-- src/cli.rs | 6 +- src/components/finders.rs | 2 +- src/components/finders/env.rs | 81 ++++---- src/components/input/backend.rs | 2 + src/components/pickers.rs | 16 +- src/components/pickers/env.rs | 18 +- src/components/television.rs | 358 +++++++++++++++++++++----------- src/config.rs | 6 +- src/render.rs | 37 ++-- 14 files changed, 390 insertions(+), 235 deletions(-) diff --git a/.config/config.json5 b/.config/config.json5 index a1aec77..a682d54 100644 --- a/.config/config.json5 +++ b/.config/config.json5 @@ -7,11 +7,13 @@ "": "Quit", // Yet another way to quit "": "Suspend", // Suspend the application "": "GoToNextPane", // Move to the next pane - "": "GoToPreviousPane", // Move to the previous pane + "": "GoToPrevPane", // Move to the previous pane "": "GoToPaneUp", // Move to the pane above "": "GoToPaneDown", // Move to the pane below "": "GoToPaneLeft", // Move to the pane to the left "": "GoToPaneRight", // Move to the pane to the right + "": "SelectNextEntry", // Move to the next entry + "": "SelectPrevEntry", // Move to the previous entry }, } } diff --git a/Cargo.lock b/Cargo.lock index 15e5e97..471b88b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,7 +278,7 @@ dependencies = [ "strsim", "terminal_size", "unicase", - "unicode-width", + "unicode-width 0.1.13", ] [[package]] @@ -1860,7 +1860,7 @@ dependencies = [ "strum_macros", "unicode-segmentation", "unicode-truncate", - "unicode-width", + "unicode-width 0.1.13", ] [[package]] @@ -2239,6 +2239,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "unicode-width 0.2.0", "vergen-gix", ] @@ -2573,7 +2574,7 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.13", ] [[package]] @@ -2582,6 +2583,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "url" version = "2.5.2" diff --git a/Cargo.toml b/Cargo.toml index c8b1df6..d354092 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ toml = "0.8.19" tracing = "0.1.40" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] } +unicode-width = "0.2.0" [build-dependencies] anyhow = "1.0.86" diff --git a/src/action.rs b/src/action.rs index fe791fc..d73f7dd 100644 --- a/src/action.rs +++ b/src/action.rs @@ -3,28 +3,40 @@ use strum::Display; #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] pub enum Action { - KeyPress(char), - Tick, + // input actions + AddInputChar(char), + DeletePrevChar, + DeleteNextChar, + GoToPrevChar, + GoToNextChar, + GoToInputStart, + GoToInputEnd, + // rendering actions Render, Resize(u16, u16), + ClearScreen, + // results actions SelectNextEntry, - SelectPreviousEntry, + SelectPrevEntry, + // navigation actions GoToPaneUp, GoToPaneDown, GoToPaneLeft, GoToPaneRight, GoToNextPane, - GoToPreviousPane, + GoToPrevPane, + // preview actions ScrollPreviewUp, ScrollPreviewDown, ScrollPreviewHalfPageUp, ScrollPreviewHalfPageDown, OpenEntry, + // application actions + Tick, Suspend, Resume, Quit, - ClearScreen, - Error(String), Help, + Error(String), NoOp, } diff --git a/src/app.rs b/src/app.rs index 7f7d3dd..d34f3d0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -56,7 +56,7 @@ use tracing::debug; use crate::{ action::Action, - cli::TvChannel, + cli::UnitTvChannel, components::{television::Television, Component}, config::Config, event::{Event, EventLoop, Key}, @@ -70,7 +70,9 @@ pub struct App { // via the cli? tick_rate: f64, frame_rate: f64, - components: Arc>>>, + //components: Arc>>>, + television: Arc>, + active_component: usize, should_quit: bool, should_suspend: bool, mode: Mode, @@ -91,16 +93,17 @@ pub enum Mode { } impl App { - pub fn new(channel: TvChannel, tick_rate: f64, frame_rate: f64) -> Result { + pub fn new(channel: UnitTvChannel, tick_rate: f64, frame_rate: f64) -> Result { let (action_tx, action_rx) = mpsc::unbounded_channel(); let (render_tx, _) = mpsc::unbounded_channel(); let event_loop = EventLoop::new(tick_rate, true); - let television = Television::new(channel); + let television = Arc::new(Mutex::new(Television::new(channel))); Ok(Self { tick_rate, frame_rate, - components: Arc::new(Mutex::new(vec![Box::new(television)])), + television, + active_component: 0, should_quit: false, should_suspend: false, config: Config::new()?, @@ -121,7 +124,7 @@ impl App { self.render_tx = render_tx.clone(); let action_tx_r = self.action_tx.clone(); let config_r = self.config.clone(); - let components_r = self.components.clone(); + let television_r = self.television.clone(); let frame_rate = self.frame_rate; tokio::spawn(async move { render( @@ -130,7 +133,7 @@ impl App { //render_tx, action_tx_r, config_r, - components_r, + television_r, frame_rate, ) .await @@ -157,12 +160,32 @@ impl App { fn convert_event_to_action(&self, event: Event) -> Action { match event { - Event::Input(keycode) => self - .config - .keybindings - .get(&self.mode) - .and_then(|keymap| keymap.get(&keycode).cloned()) - .unwrap_or(Action::NoOp), + Event::Input(keycode) => { + // if the current component is the television + // and the mode is input, we want to handle + if self.television.lock().unwrap().is_input_focused() { + match keycode { + Key::Backspace => return Action::DeletePrevChar, + Key::Delete => return Action::DeleteNextChar, + Key::Left => return Action::GoToPrevChar, + Key::Right => return Action::GoToNextChar, + Key::Home | Key::Ctrl('a') => return Action::GoToInputStart, + Key::End | Key::Ctrl('e') => return Action::GoToInputEnd, + Key::Char(c) => return Action::AddInputChar(c), + _ => {} + } + } + return self + .config + .keybindings + .get(&self.mode) + .and_then(|keymap| keymap.get(&keycode).cloned()) + .unwrap_or(if let Key::Char(c) = keycode { + Action::AddInputChar(c) + } else { + Action::NoOp + }); + } Event::Tick => Action::Tick, Event::Resize(x, y) => Action::Resize(x, y), Event::FocusGained => Action::Resume, @@ -195,11 +218,9 @@ impl App { Action::Render => self.render_tx.send(RenderingTask::Render)?, _ => {} } - for component in self.components.lock().unwrap().iter_mut() { - if let Some(action) = component.update(action.clone())? { - self.action_tx.send(action)? - }; - } + if let Some(action) = self.television.lock().unwrap().update(action.clone())? { + self.action_tx.send(action)? + }; } Ok(()) } diff --git a/src/cli.rs b/src/cli.rs index a9b4fd3..9a8d325 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,7 +3,7 @@ use clap::{Parser, ValueEnum}; use crate::config::{get_config_dir, get_data_dir}; #[derive(Clone, ValueEnum, Debug, Default)] -pub enum TvChannel { +pub enum UnitTvChannel { #[default] ENV, OTHER, @@ -14,10 +14,10 @@ pub enum TvChannel { pub struct Cli { /// Which channel shall we watch? #[arg(value_enum)] - pub channel: TvChannel, + pub channel: UnitTvChannel, /// Tick rate, i.e. number of ticks per second - #[arg(short, long, value_name = "FLOAT", default_value_t = 20.0)] + #[arg(short, long, value_name = "FLOAT", default_value_t = 50.0)] pub tick_rate: f64, /// Frame rate, i.e. number of frames per second diff --git a/src/components/finders.rs b/src/components/finders.rs index 817c6c6..2635150 100644 --- a/src/components/finders.rs +++ b/src/components/finders.rs @@ -21,5 +21,5 @@ pub struct Entry { /// # Methods /// - `find`: Find entries based on a pattern. pub trait Finder { - fn find(&self, pattern: &str) -> impl Iterator; + fn find(&mut self, pattern: &str) -> impl Iterator; } diff --git a/src/components/finders/env.rs b/src/components/finders/env.rs index 462f1c7..5335cc5 100644 --- a/src/components/finders/env.rs +++ b/src/components/finders/env.rs @@ -1,4 +1,4 @@ -use std::{env::vars, path::Path}; +use std::{cmp::max, collections::HashMap, env::vars, path::Path}; use fuzzy_matcher::skim::SkimMatcherV2; use rust_devicons::{icon_for_file, File, FileIcon}; @@ -14,6 +14,7 @@ pub struct EnvVarFinder { env_vars: Vec, matcher: SkimMatcherV2, file_icon: FileIcon, + cache: HashMap>, } impl EnvVarFinder { @@ -27,54 +28,62 @@ impl EnvVarFinder { env_vars, matcher: SkimMatcherV2::default(), file_icon, + cache: HashMap::new(), } } } impl Finder for EnvVarFinder { - fn find(&self, pattern: &str) -> impl Iterator { - let mut results = Vec::new(); - for env_var in &self.env_vars { - if pattern.is_empty() { - results.push(Entry { - name: env_var.name.clone(), - preview: Some(env_var.value.clone()), - score: 0, - name_match_ranges: None, - preview_match_ranges: None, - icon: Some(self.file_icon.clone()), - line_number: None, - }); - } else { - let mut total_score = 0; - let preview_match_ranges = if let Some((score, indices)) = - self.matcher.fuzzy(&env_var.value, pattern, true) - { - total_score += score; - Some(indices.iter().map(|i| (*i, *i + 1)).collect()) - } else { - None - }; - let name_match_ranges = if let Some((score, indices)) = - self.matcher.fuzzy(&env_var.name, pattern, true) - { - total_score += score; - Some(indices.iter().map(|i| (*i, *i + 1)).collect()) - } else { - None - }; - if preview_match_ranges.is_some() || name_match_ranges.is_some() { + fn find(&mut self, pattern: &str) -> impl Iterator { + let mut results: Vec = Vec::new(); + // try to get from cache + if let Some(entries) = self.cache.get(pattern) { + results.extend(entries.iter().cloned()); + } else { + for env_var in &self.env_vars { + if pattern.is_empty() { results.push(Entry { name: env_var.name.clone(), preview: Some(env_var.value.clone()), - score: total_score, - name_match_ranges, - preview_match_ranges, + score: 0, + name_match_ranges: None, + preview_match_ranges: None, icon: Some(self.file_icon.clone()), line_number: None, }); + } else { + let mut final_score = 0; + let preview_match_ranges = if let Some((score, indices)) = + self.matcher.fuzzy(&env_var.value, pattern, true) + { + final_score = max(final_score, score); + Some(indices.iter().map(|i| (*i, *i + 1)).collect()) + } else { + None + }; + let name_match_ranges = if let Some((score, indices)) = + self.matcher.fuzzy(&env_var.name, pattern, true) + { + final_score = max(final_score, score); + Some(indices.iter().map(|i| (*i, *i + 1)).collect()) + } else { + None + }; + if preview_match_ranges.is_some() || name_match_ranges.is_some() { + results.push(Entry { + name: env_var.name.clone(), + preview: Some(env_var.value.clone()), + score: final_score, + name_match_ranges, + preview_match_ranges, + icon: Some(self.file_icon.clone()), + line_number: None, + }); + } } } + // cache the results + self.cache.insert(pattern.to_string(), results.clone()); } results.into_iter() } diff --git a/src/components/input/backend.rs b/src/components/input/backend.rs index 7b2e73b..3c1810a 100644 --- a/src/components/input/backend.rs +++ b/src/components/input/backend.rs @@ -1,3 +1,5 @@ +use crate::event::Key; + use super::{Input, InputRequest, StateChanged}; use ratatui::crossterm::event::{ Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, diff --git a/src/components/pickers.rs b/src/components/pickers.rs index ab21da0..86e616e 100644 --- a/src/components/pickers.rs +++ b/src/components/pickers.rs @@ -1,4 +1,4 @@ -use crate::cli::TvChannel; +use crate::cli::UnitTvChannel; use super::finders; use super::finders::Entry; @@ -25,28 +25,24 @@ pub trait Picker { type F: finders::Finder; type P: previewers::Previewer; + /// NOTE: this method could eventually be async /// Load entries based on a pattern. - /// TODO: implement some caching mechanism to avoid loading entries every time. fn load_entries(&mut self, pattern: &str) -> Result<()>; - /// Select an entry based on an index. - fn select_entry(&mut self, index: usize); - /// Get the selected entry. - fn selected_entry(&self) -> Option; /// Get all entries. fn entries(&self) -> &Vec; /// Clear all entries. fn clear(&mut self); /// Get a preview of the currently selected entry. - fn get_preview(&self) -> Option; + fn get_preview(&self, entry: Entry) -> Option; } -pub enum PickerType { +pub enum TvChannel { Env(env::EnvVarPicker), } -pub fn get_picker_type(channel: TvChannel) -> PickerType { +pub fn get_tv_channel(channel: UnitTvChannel) -> TvChannel { match channel { - TvChannel::ENV => PickerType::Env(env::EnvVarPicker::new()), + UnitTvChannel::ENV => TvChannel::Env(env::EnvVarPicker::new()), _ => unimplemented!(), } } diff --git a/src/components/pickers/env.rs b/src/components/pickers/env.rs index 2a749dc..9b31f06 100644 --- a/src/components/pickers/env.rs +++ b/src/components/pickers/env.rs @@ -6,8 +6,6 @@ use crate::components::previewers::{self, Previewer}; pub struct EnvVarPicker { finder: finders::EnvVarFinder, entries: Vec, - // maybe this should be an index instead of a clone of the entry - selected_entry: Option, previewer: previewers::EnvVarPreviewer, } @@ -16,7 +14,6 @@ impl EnvVarPicker { EnvVarPicker { finder: finders::EnvVarFinder::new(), entries: Vec::new(), - selected_entry: None, previewer: previewers::EnvVarPreviewer::new(), } } @@ -32,26 +29,15 @@ impl super::Picker for EnvVarPicker { Ok(()) } - fn select_entry(&mut self, index: usize) { - self.selected_entry = self.entries.get(index).cloned(); - } - - fn selected_entry(&self) -> Option { - self.selected_entry.clone() - } - fn entries(&self) -> &Vec { &self.entries } fn clear(&mut self) { self.entries.clear(); - self.selected_entry = None; } - fn get_preview(&self) -> Option { - self.selected_entry - .as_ref() - .map(|e| self.previewer.preview(e.clone())) + fn get_preview(&self, entry: finders::Entry) -> Option { + Some(self.previewer.preview(entry)) } } diff --git a/src/components/television.rs b/src/components/television.rs index f97ce00..d095921 100644 --- a/src/components/television.rs +++ b/src/components/television.rs @@ -21,13 +21,13 @@ use crate::components::utils::centered_rect; use crate::components::Component; use crate::{action::Action, config::Config}; use crate::{ - cli::TvChannel, + cli::UnitTvChannel, components::finders::{Entry, Finder}, }; use super::{ - input::{backend::EventHandler, Input}, - pickers::{get_picker_type, PickerType}, + input::{backend::EventHandler, Input, InputRequest, StateChanged}, + pickers::{get_tv_channel, TvChannel}, }; #[derive(PartialEq, Copy, Clone)] @@ -39,23 +39,28 @@ enum Pane { static PANES: [Pane; 3] = [Pane::Input, Pane::Results, Pane::Preview]; +struct ResultsList { + results: Vec, + state: ListState, +} + pub struct Television { action_tx: Option>, config: Config, - picker: PickerType, + channel: TvChannel, current_pane: Pane, - pattern: String, input: Input, + results_list: ResultsList, } const EMPTY_STRING: &str = ""; impl Television { - pub fn new(channel: TvChannel) -> Self { - let mut picker = get_picker_type(channel); - match picker { + pub fn new(channel: UnitTvChannel) -> Self { + let mut tv_channel = get_tv_channel(channel); + match tv_channel { // preload entries for the env picker - PickerType::Env(ref mut picker) => { + TvChannel::Env(ref mut picker) => { picker.load_entries(EMPTY_STRING).unwrap(); debug!("Loaded env picker"); } @@ -63,46 +68,39 @@ impl Television { Self { action_tx: None, config: Config::default(), - picker, + channel: tv_channel, current_pane: Pane::Input, - pattern: String::new(), input: Input::new(EMPTY_STRING.to_string()), + results_list: ResultsList { + results: Vec::new(), + state: ListState::default(), + }, } } fn load_entries(&mut self, pattern: &str) -> Result<()> { - match &mut self.picker { - PickerType::Env(picker) => picker.load_entries(pattern), - } - } - - fn select_entry(&mut self, index: usize) { - match &mut self.picker { - PickerType::Env(picker) => picker.select_entry(index), - } - } - - fn selected_entry(&self) -> Option { - match &self.picker { - PickerType::Env(picker) => picker.selected_entry(), + match &mut self.channel { + TvChannel::Env(picker) => picker.load_entries(pattern)?, } + self.results_list.results = self.entries().clone(); + Ok(()) } fn entries(&self) -> &Vec { - match &self.picker { - PickerType::Env(picker) => picker.entries(), + match &self.channel { + TvChannel::Env(picker) => picker.entries(), } } fn clear(&mut self) { - match &mut self.picker { - PickerType::Env(picker) => picker.clear(), + match &mut self.channel { + TvChannel::Env(picker) => picker.clear(), } } - fn get_preview(&self) -> Option { - match &self.picker { - PickerType::Env(picker) => picker.get_preview(), + fn get_preview(&self, entry: Entry) -> Option { + match &self.channel { + TvChannel::Env(picker) => picker.get_preview(entry), } } @@ -200,15 +198,43 @@ impl Television { _ => {} } } + + pub fn is_input_focused(&self) -> bool { + Pane::Input == self.current_pane + } + + pub fn select_next_entry(&mut self) { + let selected = self.results_list.state.selected().unwrap_or(0); + let total_search_results = self.results_list.results.len(); + self.results_list + .state + .select(Some((selected + 1) % total_search_results)); + } + + pub fn select_prev_entry(&mut self) { + let selected = self.results_list.state.selected().unwrap_or(0); + if selected > 0 { + self.results_list.state.select(Some(selected - 1)); + } else { + self.results_list + .state + .select(Some(self.results_list.results.len() - 1)); + } + } } // UI size const UI_WIDTH_PERCENT: u16 = 70; const UI_HEIGHT_PERCENT: u16 = 70; +// Styles +// results const DEFAULT_RESULT_NAME_FG: Color = Color::Blue; const DEFAULT_RESULT_PREVIEW_FG: Color = Color::Rgb(150, 150, 150); const DEFAULT_RESULT_LINE_NUMBER_FG: Color = Color::Yellow; +// input +const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200); +const DEFAULT_RESULTS_COUNT_FG: Color = Color::Rgb(150, 150, 150); impl Component for Television { fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { @@ -223,12 +249,6 @@ impl Component for Television { fn update(&mut self, action: Action) -> Result> { match action { - Action::Tick => { - // add any logic here that should run on every tick - } - Action::Render => { - // add any logic here that should run on every render - } Action::GoToPaneUp => { self.move_to_pane_on_top(); } @@ -244,16 +264,33 @@ impl Component for Television { Action::GoToNextPane => { self.next_pane(); } - Action::GoToPreviousPane => { + Action::GoToPrevPane => { self.previous_pane(); } - Action::KeyPress(key) => { - // oh shit, we need to transform that back into a crossterm event - // ooorrr - // add logic to handle KeyCode events directly to the `input` module - //self.input.handle_event(...) + // handle input actions + Action::AddInputChar(_) + | Action::DeletePrevChar + | Action::DeleteNextChar + | Action::GoToInputEnd + | Action::GoToInputStart + | Action::GoToNextChar + | Action::GoToPrevChar + if self.is_input_focused() => + { + self.input.handle_action(&action); + match action { + Action::AddInputChar(_) | Action::DeletePrevChar | Action::DeleteNextChar => { + let pat = self.input.value().to_string(); + self.load_entries(&pat)?; + } + _ => {} + } + } + Action::SelectNextEntry => self.select_next_entry(), + Action::SelectPrevEntry => self.select_prev_entry(), + _ => { + info!("Unhandled action: {:?}", action); } - _ => {} } Ok(None) } @@ -274,82 +311,15 @@ impl Component for Television { .style(Style::default()) .padding(Padding::right(1)); - let results_list = List::new(self.entries().iter().map(|entry| { - let mut spans = Vec::new(); - // optional icon - if let Some(icon) = &entry.icon { - spans.push(Span::styled( - icon.to_string(), - Style::default().fg(Color::from_str(icon.color).unwrap()), - )); - spans.push(Span::raw(" ")); - } - // entry name - if let Some(name_match_ranges) = &entry.name_match_ranges { - let mut last_match_end = 0; - for (start, end) in name_match_ranges { - spans.push(Span::styled( - &entry.name[last_match_end..*start], - Style::default().fg(DEFAULT_RESULT_NAME_FG), - )); - spans.push(Span::styled( - &entry.name[*start..*end], - Style::default().fg(Color::Red), - )); - last_match_end = *end; - } - spans.push(Span::styled( - &entry.name[name_match_ranges.last().unwrap().1..], - Style::default().fg(DEFAULT_RESULT_NAME_FG), - )); - } else { - spans.push(Span::styled( - &entry.name, - Style::default().fg(DEFAULT_RESULT_NAME_FG), - )); - } - // optional line number - if let Some(line_number) = entry.line_number { - spans.push(Span::styled( - format!(":{}", line_number), - Style::default().fg(DEFAULT_RESULT_LINE_NUMBER_FG), - )); - } - // optional preview - if let Some(preview) = &entry.preview { - spans.push(Span::raw(": ")); + let results_list = self.build_results_list(results_block); - if let Some(preview_match_ranges) = &entry.preview_match_ranges { - let mut last_match_end = 0; - for (start, end) in preview_match_ranges { - spans.push(Span::styled( - &preview[last_match_end..*start], - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), - )); - spans.push(Span::styled( - &preview[*start..*end], - Style::default().fg(Color::Red), - )); - last_match_end = *end; - } - spans.push(Span::styled( - &preview[preview_match_ranges.last().unwrap().1..], - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), - )); - } else { - spans.push(Span::styled( - preview, - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), - )); - } - } - Line::from(spans) - })) - .direction(ListDirection::BottomToTop) - .block(results_block); - - frame.render_stateful_widget(results_list, results_area, &mut ListState::default()); + frame.render_stateful_widget( + results_list, + results_area, + &mut self.results_list.state.clone(), + ); + // bottom left block: input let input_block = Block::default() .title( Title::from(" Pattern ") @@ -361,8 +331,58 @@ impl Component for Television { .border_style(self.get_border_style(Pane::Input == self.current_pane)) .style(Style::default()); + let input_block_inner = input_block.inner(input_area); + frame.render_widget(input_block, input_area); + let total_search_results = self.entries().len(); + let inner_input_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length(2), + Constraint::Fill(1), + Constraint::Length( + 2 * ((total_search_results as f32).log10().ceil() as u16 + 1) + 3, + ), + ]) + .split(input_block_inner); + + let arrow_block = Block::default(); + let arrow = Paragraph::new(Span::styled("> ", Style::default())).block(arrow_block); + frame.render_widget(arrow, inner_input_chunks[0]); + + let interactive_input_block = Block::default(); + let width = inner_input_chunks[1].width.max(3) - 3; // keep 2 for borders and 1 for cursor + let scroll = self.input.visual_scroll(width as usize); + let input = Paragraph::new(self.input.value()) + .scroll((0, scroll as u16)) + .block(interactive_input_block) + .style(Style::default().fg(DEFAULT_INPUT_FG)) + .alignment(Alignment::Left); + frame.render_widget(input, inner_input_chunks[1]); + + if let Some(selected) = self.results_list.state.selected() { + let result_count_block = Block::default(); + let result_count = Paragraph::new(Span::styled( + format!(" {} / {} ", selected + 1, total_search_results,), + Style::default().fg(DEFAULT_RESULTS_COUNT_FG), + )) + .block(result_count_block) + .alignment(Alignment::Right); + frame.render_widget(result_count, inner_input_chunks[2]); + } + + if let Pane::Input = self.current_pane { + // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering + frame.set_cursor_position(( + // Put cursor past the end of the input text + inner_input_chunks[1].x + + ((self.input.visual_cursor()).max(scroll) - scroll) as u16, + // Move one line down, from the border to the input line + inner_input_chunks[1].y, + )) + } + // top right block: preview title let preview_file_path = Paragraph::new(" Preview Title ") .block( @@ -432,4 +452,104 @@ impl Television { right_chunks[1], ) } + + fn build_results_list<'a>(&'a self, results_block: Block<'a>) -> List<'a> { + List::new(self.entries().iter().map(|entry| { + let mut spans = Vec::new(); + // optional icon + if let Some(icon) = &entry.icon { + spans.push(Span::styled( + icon.to_string(), + Style::default().fg(Color::from_str(icon.color).unwrap()), + )); + spans.push(Span::raw(" ")); + } + // entry name + if let Some(name_match_ranges) = &entry.name_match_ranges { + let mut last_match_end = 0; + for (start, end) in name_match_ranges { + spans.push(Span::styled( + &entry.name[last_match_end..*start], + Style::default().fg(DEFAULT_RESULT_NAME_FG), + )); + spans.push(Span::styled( + &entry.name[*start..*end], + Style::default().fg(Color::Red), + )); + last_match_end = *end; + } + spans.push(Span::styled( + &entry.name[name_match_ranges.last().unwrap().1..], + Style::default().fg(DEFAULT_RESULT_NAME_FG), + )); + } else { + spans.push(Span::styled( + &entry.name, + Style::default().fg(DEFAULT_RESULT_NAME_FG), + )); + } + // optional line number + if let Some(line_number) = entry.line_number { + spans.push(Span::styled( + format!(":{}", line_number), + Style::default().fg(DEFAULT_RESULT_LINE_NUMBER_FG), + )); + } + // optional preview + if let Some(preview) = &entry.preview { + spans.push(Span::raw(": ")); + + if let Some(preview_match_ranges) = &entry.preview_match_ranges { + let mut last_match_end = 0; + for (start, end) in preview_match_ranges { + spans.push(Span::styled( + &preview[last_match_end..*start], + Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + )); + spans.push(Span::styled( + &preview[*start..*end], + Style::default().fg(Color::Red), + )); + last_match_end = *end; + } + spans.push(Span::styled( + &preview[preview_match_ranges.last().unwrap().1..], + Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + )); + } else { + spans.push(Span::styled( + preview, + Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + )); + } + } + Line::from(spans) + })) + .direction(ListDirection::BottomToTop) + .highlight_style(Style::default().bg(Color::Rgb(50, 50, 50))) + .highlight_symbol("> ") + .block(results_block) + } +} + +/// This makes the `Action` type compatible with the `Input` logic. +pub trait InputActionHandler { + // Handle Key event. + fn handle_action(&mut self, action: &Action) -> Option; +} + +impl InputActionHandler for Input { + /// Handle Key event. + fn handle_action(&mut self, action: &Action) -> Option { + match action { + Action::AddInputChar(c) => self.handle(InputRequest::InsertChar(*c)), + Action::DeletePrevChar => self.handle(InputRequest::DeletePrevChar), + Action::DeleteNextChar => self.handle(InputRequest::DeleteNextChar), + Action::GoToPrevChar => self.handle(InputRequest::GoToPrevChar), + Action::GoToNextChar => self.handle(InputRequest::GoToNextChar), + Action::GoToInputStart => self.handle(InputRequest::GoToStart), + Action::GoToInputEnd => self.handle(InputRequest::GoToEnd), + _ => None, + } + } } diff --git a/src/config.rs b/src/config.rs index 3ee0fec..b04e08e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] // Remove this once you start using the code - use std::{collections::HashMap, env, path::PathBuf}; use color_eyre::Result; @@ -84,9 +82,7 @@ impl Config { for (mode, default_bindings) in default_config.keybindings.iter() { let user_bindings = cfg.keybindings.entry(*mode).or_default(); for (key, cmd) in default_bindings.iter() { - user_bindings - .entry(*key) - .or_insert_with(|| cmd.clone()); + user_bindings.entry(*key).or_insert_with(|| cmd.clone()); } } for (mode, default_styles) in default_config.styles.iter() { diff --git a/src/render.rs b/src/render.rs index 67d0d05..001cc72 100644 --- a/src/render.rs +++ b/src/render.rs @@ -4,7 +4,12 @@ use std::sync::{Arc, Mutex}; use tokio::{select, sync::mpsc}; -use crate::{action::Action, components::Component, config::Config, tui::Tui}; +use crate::{ + action::Action, + components::{television::Television, Component}, + config::Config, + tui::Tui, +}; pub enum RenderingTask { ClearScreen, @@ -20,20 +25,20 @@ pub async fn render( mut render_rx: mpsc::UnboundedReceiver, action_tx: mpsc::UnboundedSender, config: Config, - components: Arc>>>, + television: Arc>, frame_rate: f64, ) -> Result<()> { tui.enter()?; - for component in components.lock().unwrap().iter_mut() { - component.register_action_handler(action_tx.clone())?; - } - for component in components.lock().unwrap().iter_mut() { - component.register_config_handler(config.clone())?; - } - for component in components.lock().unwrap().iter_mut() { - component.init(tui.size().unwrap())?; - } + television + .lock() + .unwrap() + .register_action_handler(action_tx.clone())?; + television + .lock() + .unwrap() + .register_config_handler(config.clone())?; + television.lock().unwrap().init(tui.size().unwrap())?; // Rendering loop loop { @@ -48,13 +53,11 @@ pub async fn render( tui.terminal.clear()?; } RenderingTask::Render => { - let mut c = components.lock().unwrap(); + let mut television = television.lock().unwrap(); tui.terminal.draw(|frame| { - for component in c.iter_mut() { - if let Err(err) = component.draw(frame, frame.area()) { - let _ = action_tx - .send(Action::Error(format!("Failed to draw: {:?}", err))); - } + if let Err(err) = television.draw(frame, frame.area()) { + let _ = action_tx + .send(Action::Error(format!("Failed to draw: {:?}", err))); } })?; }