diff --git a/.config/config.json5 b/.config/config.json5 index a682d54..7a75a7c 100644 --- a/.config/config.json5 +++ b/.config/config.json5 @@ -12,8 +12,8 @@ "": "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 + "": "SelectNextEntry", // Move to the next entry + "": "SelectPrevEntry", // Move to the previous entry }, } } diff --git a/Cargo.lock b/Cargo.lock index 471b88b..dcc536f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1941,9 +1941,9 @@ dependencies = [ [[package]] name = "rust-devicons" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443b373e066f073ec41ca3e382ddde7c10e5a3e9b94ea63d4166622251a51996" +checksum = "a024af3791e856d1021f79d9867d938bd6b2a39222b0ca8990d2bfcd4e910a03" dependencies = [ "lazy_static", ] diff --git a/src/app.rs b/src/app.rs index d34f3d0..fababe1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -52,7 +52,7 @@ use std::sync::{Arc, Mutex}; use color_eyre::Result; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; -use tracing::debug; +use tracing::{debug, info}; use crate::{ action::Action, @@ -161,6 +161,7 @@ impl App { fn convert_event_to_action(&self, event: Event) -> Action { match event { Event::Input(keycode) => { + info!("{:?}", 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() { diff --git a/src/components.rs b/src/components.rs index 5a6f512..d54b080 100644 --- a/src/components.rs +++ b/src/components.rs @@ -7,7 +7,6 @@ use tokio::sync::mpsc::UnboundedSender; use crate::{action::Action, config::Config}; -mod display; mod finders; pub mod fps; pub mod home; diff --git a/src/components/display.rs b/src/components/display.rs deleted file mode 100644 index a2b0596..0000000 --- a/src/components/display.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// This module contains abstract display logic for the application. - -/// A group of styles that can be applied to a string. -pub enum StyleGroup { - Default, - Entry, - EntryLineNumber, - EntryContent, -} - -/// A styled string with a text and a style group for that string. -/// This is used to produce styled output in a way that can then be translated to the actual -/// frontend implementation. -pub struct StyledString { - pub text: String, - pub style: StyleGroup, -} diff --git a/src/components/finders.rs b/src/components/finders.rs index 2635150..f2d613f 100644 --- a/src/components/finders.rs +++ b/src/components/finders.rs @@ -4,7 +4,7 @@ mod env; pub use env::EnvVarFinder; use rust_devicons::FileIcon; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct Entry { pub name: String, pub preview: Option, diff --git a/src/components/finders/env.rs b/src/components/finders/env.rs index 5335cc5..a8ceff7 100644 --- a/src/components/finders/env.rs +++ b/src/components/finders/env.rs @@ -2,6 +2,7 @@ use std::{cmp::max, collections::HashMap, env::vars, path::Path}; use fuzzy_matcher::skim::SkimMatcherV2; use rust_devicons::{icon_for_file, File, FileIcon}; +use tracing::info; use crate::components::finders::{Entry, Finder}; diff --git a/src/components/pickers.rs b/src/components/pickers.rs index 86e616e..7822962 100644 --- a/src/components/pickers.rs +++ b/src/components/pickers.rs @@ -33,13 +33,39 @@ pub trait Picker { /// Clear all entries. fn clear(&mut self); /// Get a preview of the currently selected entry. - fn get_preview(&self, entry: Entry) -> Option; + fn get_preview(&mut self, entry: &Entry) -> previewers::Preview; } pub enum TvChannel { Env(env::EnvVarPicker), } +impl TvChannel { + pub fn load_entries(&mut self, pattern: &str) -> Result<()> { + match self { + TvChannel::Env(picker) => picker.load_entries(pattern), + } + } + + pub fn entries(&self) -> &Vec { + match self { + TvChannel::Env(picker) => picker.entries(), + } + } + + pub fn clear(&mut self) { + match self { + TvChannel::Env(picker) => picker.clear(), + } + } + + pub fn get_preview(&mut self, entry: &Entry) -> previewers::Preview { + match self { + TvChannel::Env(picker) => picker.get_preview(entry), + } + } +} + pub fn get_tv_channel(channel: UnitTvChannel) -> TvChannel { match channel { UnitTvChannel::ENV => TvChannel::Env(env::EnvVarPicker::new()), diff --git a/src/components/pickers/env.rs b/src/components/pickers/env.rs index 9b31f06..896ecac 100644 --- a/src/components/pickers/env.rs +++ b/src/components/pickers/env.rs @@ -37,7 +37,7 @@ impl super::Picker for EnvVarPicker { self.entries.clear(); } - fn get_preview(&self, entry: finders::Entry) -> Option { - Some(self.previewer.preview(entry)) + fn get_preview(&mut self, entry: &finders::Entry) -> previewers::Preview { + self.previewer.preview(entry) } } diff --git a/src/components/previewers.rs b/src/components/previewers.rs index 30d75bf..6263075 100644 --- a/src/components/previewers.rs +++ b/src/components/previewers.rs @@ -1,18 +1,27 @@ -use super::{display::StyledString, finders::Entry}; +use super::finders::Entry; mod env; // previewer types pub use env::EnvVarPreviewer; +#[derive(Debug, Clone)] +pub enum PreviewContent { + PlainText(String), + HighlightedText(String), + Empty, +} + /// A preview of an entry. /// /// # Fields /// - `title`: The title of the preview. /// - `content`: The content of the preview. +#[derive(Debug, Clone)] pub struct Preview { pub title: String, - pub content: Vec, + // maybe images too and other formats + pub content: PreviewContent, } /// A trait for a previewer that can preview entries. @@ -20,5 +29,5 @@ pub struct Preview { /// # Methods /// - `preview`: Preview an entry and produce a preview. pub trait Previewer { - fn preview(&self, e: Entry) -> Preview; + fn preview(&mut self, e: &Entry) -> Preview; } diff --git a/src/components/previewers/env.rs b/src/components/previewers/env.rs index 7abf21e..bbd4603 100644 --- a/src/components/previewers/env.rs +++ b/src/components/previewers/env.rs @@ -1,23 +1,39 @@ -use crate::components::display; +use std::collections::HashMap; + use crate::components::finders; -use crate::components::previewers; +use crate::components::previewers::{Preview, PreviewContent, Previewer}; -pub struct EnvVarPreviewer {} +pub struct EnvVarPreviewer { + cache: HashMap, +} impl EnvVarPreviewer { pub fn new() -> Self { - EnvVarPreviewer {} + EnvVarPreviewer { + cache: HashMap::new(), + } } } -impl previewers::Previewer for EnvVarPreviewer { - fn preview(&self, e: finders::Entry) -> previewers::Preview { - previewers::Preview { - title: e.name.clone(), - content: vec![display::StyledString { - text: e.preview.unwrap().clone(), - style: display::StyleGroup::Entry, - }], +impl Previewer for EnvVarPreviewer { + fn preview(&mut self, entry: &finders::Entry) -> Preview { + // check if we have that preview in the cache + if let Some(preview) = self.cache.get(entry) { + return preview.clone(); } + let preview = Preview { + title: entry.name.clone(), + content: if let Some(preview) = &entry.preview { + PreviewContent::PlainText(add_newline_after_colon(&preview)) + } else { + PreviewContent::Empty + }, + }; + self.cache.insert(entry.clone(), preview.clone()); + preview } } + +fn add_newline_after_colon(s: &str) -> String { + s.replace(":", ":\n") +} diff --git a/src/components/television.rs b/src/components/television.rs index d095921..bb49e21 100644 --- a/src/components/television.rs +++ b/src/components/television.rs @@ -16,7 +16,7 @@ use tracing::{debug, info}; use tokio::sync::mpsc::UnboundedSender; use crate::components::pickers::{env::EnvVarPicker, Picker}; -use crate::components::previewers::{Preview, Previewer}; +use crate::components::previewers::{Preview, PreviewContent, Previewer}; use crate::components::utils::centered_rect; use crate::components::Component; use crate::{action::Action, config::Config}; @@ -48,6 +48,7 @@ pub struct Television { action_tx: Option>, config: Config, channel: TvChannel, + current_pattern: String, current_pane: Pane, input: Input, results_list: ResultsList, @@ -58,49 +59,45 @@ const EMPTY_STRING: &str = ""; impl Television { pub fn new(channel: UnitTvChannel) -> Self { let mut tv_channel = get_tv_channel(channel); - match tv_channel { - // preload entries for the env picker - TvChannel::Env(ref mut picker) => { - picker.load_entries(EMPTY_STRING).unwrap(); - debug!("Loaded env picker"); - } - } + tv_channel.load_entries(EMPTY_STRING).unwrap(); + let results = tv_channel.entries().clone(); Self { action_tx: None, config: Config::default(), channel: tv_channel, + current_pattern: EMPTY_STRING.to_string(), current_pane: Pane::Input, input: Input::new(EMPTY_STRING.to_string()), results_list: ResultsList { - results: Vec::new(), + results, state: ListState::default(), }, } } - fn load_entries(&mut self, pattern: &str) -> Result<()> { - match &mut self.channel { - TvChannel::Env(picker) => picker.load_entries(pattern)?, - } - self.results_list.results = self.entries().clone(); + fn sync_channel(&mut self, pattern: &str) -> Result<()> { + self.channel.load_entries(pattern)?; + self.results_list.results = self.channel.entries().clone(); + self.results_list.state.select(Some(0)); Ok(()) } - fn entries(&self) -> &Vec { - match &self.channel { - TvChannel::Env(picker) => picker.entries(), - } - } - - fn clear(&mut self) { - match &mut self.channel { - TvChannel::Env(picker) => picker.clear(), - } + pub fn select_prev_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)); } - fn get_preview(&self, entry: Entry) -> Option { - match &self.channel { - TvChannel::Env(picker) => picker.get_preview(entry), + pub fn select_next_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)); } } @@ -202,25 +199,6 @@ 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 @@ -235,6 +213,8 @@ 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); +// preview +const DEFAULT_PREVIEW_TITLE_FG: Color = Color::Blue; impl Component for Television { fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { @@ -280,17 +260,18 @@ impl Component for Television { self.input.handle_action(&action); match action { Action::AddInputChar(_) | Action::DeletePrevChar | Action::DeleteNextChar => { - let pat = self.input.value().to_string(); - self.load_entries(&pat)?; + let new_pattern = self.input.value().to_string(); + if new_pattern != self.current_pattern { + self.current_pattern = new_pattern.clone(); + self.sync_channel(&new_pattern)?; + } } _ => {} } } Action::SelectNextEntry => self.select_next_entry(), Action::SelectPrevEntry => self.select_prev_entry(), - _ => { - info!("Unhandled action: {:?}", action); - } + _ => {} } Ok(None) } @@ -335,7 +316,7 @@ impl Component for Television { frame.render_widget(input_block, input_area); - let total_search_results = self.entries().len(); + let total_search_results = self.channel.entries().len(); let inner_input_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ @@ -384,32 +365,48 @@ impl Component for Television { } // top right block: preview title - let preview_file_path = Paragraph::new(" Preview Title ") - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(self.get_border_style(false)), - ) - .style(Style::default().fg(Color::Blue)) - .alignment(Alignment::Left); - - frame.render_widget(preview_file_path, preview_title_area); - - // file preview - let preview_outer_block = Block::default() - .title( - Title::from(" Preview ") - .position(Position::Top) - .alignment(Alignment::Center), - ) - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(self.get_border_style(Pane::Preview == self.current_pane)) - .style(Style::default()) - .padding(Padding::right(1)); - - frame.render_widget(preview_outer_block, preview_area); + // TODO: update this to always show the preview pane + if let Some(selected) = self.results_list.state.selected() { + let entry = &self.results_list.results[selected]; + let preview = self.channel.get_preview(entry); + let preview_title = Paragraph::new(Line::from(preview.title.clone())) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(self.get_border_style(false)), + ) + .style(Style::default().fg(DEFAULT_PREVIEW_TITLE_FG)) + .alignment(Alignment::Left); + frame.render_widget(preview_title, preview_title_area); + + // file preview + let preview_outer_block = Block::default() + .title( + Title::from(" Preview ") + .position(Position::Top) + .alignment(Alignment::Center), + ) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(self.get_border_style(Pane::Preview == self.current_pane)) + .style(Style::default()) + .padding(Padding::right(1)); + + let preview_inner_block = Block::default().style(Style::default()).padding(Padding { + top: 0, + right: 1, + bottom: 0, + left: 1, + }); + let inner = preview_outer_block.inner(preview_area); + frame.render_widget(preview_outer_block, preview_area); + + //app.preview_pane_height = inner.height; + let preview = self.build_preview(preview_inner_block, preview); + + frame.render_widget(preview, inner); + } Ok(()) } @@ -454,7 +451,7 @@ impl Television { } fn build_results_list<'a>(&'a self, results_block: Block<'a>) -> List<'a> { - List::new(self.entries().iter().map(|entry| { + List::new(self.channel.entries().iter().map(|entry| { let mut spans = Vec::new(); // optional icon if let Some(icon) = &entry.icon { @@ -530,6 +527,38 @@ impl Television { .highlight_symbol("> ") .block(results_block) } + + fn build_preview<'a>( + &'a mut self, + preview_block: Block<'a>, + preview: Preview, + ) -> Paragraph<'a> { + let preview_content = preview.content.clone(); + let text = match preview_content { + PreviewContent::PlainText(content) => { + let mut spans = Vec::new(); + for line in content.lines() { + spans.push(Span::styled( + line.to_string(), + Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + )); + } + Text::from(Line::from(spans)) + } + PreviewContent::HighlightedText(content) => { + let mut spans = Vec::new(); + for line in content.lines() { + spans.push(Span::styled( + line.to_string(), + Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + )); + } + Text::from(Line::from(spans)) + } + PreviewContent::Empty => Text::raw(EMPTY_STRING), + }; + Paragraph::new(text).block(preview_block) + } } /// This makes the `Action` type compatible with the `Input` logic.