diff --git a/Cargo.lock b/Cargo.lock index e3c6c94..244095d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -626,9 +626,9 @@ dependencies = [ [[package]] name = "devicons" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f85ba42bea802686a1532ab64b4b885b2df6e0f931d523b3e64a31ce68f4888d" +checksum = "f44d7af4053366d3bdc831abed4fdbf3adcd8e8f6401b52177c1fd2b79100083" dependencies = [ "lazy_static", ] @@ -1896,9 +1896,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ "memchr", "thiserror", @@ -1907,9 +1907,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" dependencies = [ "pest", "pest_generator", @@ -1917,9 +1917,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" dependencies = [ "pest", "pest_meta", @@ -1930,9 +1930,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" dependencies = [ "once_cell", "pest", @@ -2168,9 +2168,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" @@ -2419,7 +2419,7 @@ dependencies = [ [[package]] name = "television" -version = "0.1.3" +version = "0.1.4" dependencies = [ "anyhow", "better-panic", diff --git a/Cargo.toml b/Cargo.toml index 53892e2..870f5bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "television" -version = "0.1.3" +version = "0.1.4" edition = "2021" description = "The revolution will be televised." license = "MIT" @@ -75,6 +75,12 @@ anyhow = "1.0.86" vergen-gix = { version = "1.0.0", features = ["build", "cargo"] } +[profile.staging] +inherits = "dev" +opt-level = 3 +debug = true +lto = false + [profile.release] opt-level = 3 @@ -82,7 +88,7 @@ debug = "none" strip = "symbols" debug-assertions = false overflow-checks = false -lto = "fat" +lto = "thin" panic = "abort" [target.'cfg(target_os = "macos")'.dependencies] diff --git a/TODO.md b/TODO.md index 4d24e10..3839fe4 100644 --- a/TODO.md +++ b/TODO.md @@ -1,10 +1,17 @@ +# bugs +- [x] index out of bounds when resizing the terminal to a very small size +- [x] meta previews in cache are not terminal size aware + # tasks - [x] preview navigation -- [ ] add a way to open the selected file in the default editor (or maybe that should be achieved using pipes?) +- [ ] add a way to open the selected file in the default editor (or maybe that +should be achieved using pipes?) - [x] maybe filter out image types etc. for now - [x] return selected entry on exit - [x] piping output to another command -- [x] piping custom entries from stdin (e.g. `ls | tv`, what bout choosing previewers in that case? Some AUTO mode?) +- [x] piping custom entries from stdin (e.g. `ls | tv`, what bout choosing +previewers in that case? Some AUTO mode?) +- [x] documentation ## improvements - [x] async finder initialization @@ -12,29 +19,36 @@ - [x] use nucleo for env - [ ] better keymaps - [ ] mutualize placeholder previews in cache (really not a priority) -- [x] better abstractions for channels / separation / isolation so that others can contribute new ones easily +- [x] better abstractions for channels / separation / isolation so that others +can contribute new ones easily - [ ] channel selection in the UI (separate menu or top panel or something) - [x] only render highlighted lines that are visible - [x] only ever read a portion of the file for the temp preview - [ ] make layout an attribute of the channel? -- [x] I feel like the finder abstraction is a superfluous layer, maybe just use the channel directly? -- [x] support for images is implemented but do we really want that in the core? it's quite heavy -- [ ] use an icon for the prompt - [ ] profile using dyn Traits instead of an enum for channels (might degrade performance by storing on the heap) +- [x] I feel like the finder abstraction is a superfluous layer, maybe just use +the channel directly? +- [x] support for images is implemented but do we really want that in the core? +it's quite heavy +- [x] shrink entry names that are too long (from the middle) ## feature ideas -- [ ] some sort of iterative fuzzy file explorer (preview contents of folders on the right, enter to go in etc.) maybe - with mixed previews of files and folders +- [ ] some sort of iterative fuzzy file explorer (preview contents of folders +on the right, enter to go in etc.) maybe with mixed previews of files and +folders - [x] environment variables - [x] aliases - [ ] shell history - [x] text -- [ ] text in documents (pdfs, archives, ...) (rga, adapters) https://github.com/jrmuizel/pdf-extract +- [ ] text in documents (pdfs, archives, ...) (rga, adapters) +https://github.com/jrmuizel/pdf-extract - [x] fd - [ ] recent directories - [ ] git (commits, branches, status, diff, ...) - [ ] makefile commands - [ ] remote files (s3, ...) - [ ] custom actions as part of a channel (mappable) -- [ ] from one set of entries to another? (fuzzy-refine) +- [ ] from one set of entries to another? (fuzzy-refine) maybe piping +tv with itself? +- [ ] add a way of copying the selected entry name/value to the clipboard diff --git a/crates/television/action.rs b/crates/television/action.rs index ded6fe9..fce6c41 100644 --- a/crates/television/action.rs +++ b/crates/television/action.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use strum::Display; #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] -pub enum Action { +pub(crate) enum Action { // input actions AddInputChar(char), DeletePrevChar, diff --git a/crates/television/app.rs b/crates/television/app.rs index cc567c4..476ec96 100644 --- a/crates/television/app.rs +++ b/crates/television/app.rs @@ -67,7 +67,7 @@ use crate::{ render::{render, RenderingTask}, }; -pub struct App { +pub(crate) struct App { config: Config, // maybe move these two into config instead of passing them // via the cli? @@ -87,7 +87,7 @@ pub struct App { #[derive( Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, )] -pub enum Mode { +pub(crate) enum Mode { #[default] Help, Input, @@ -96,7 +96,7 @@ pub enum Mode { } impl App { - pub fn new( + pub(crate) fn new( channel: CliTvChannel, tick_rate: f64, frame_rate: f64, diff --git a/crates/television/channels.rs b/crates/television/channels.rs index 0a7f1b4..40abc50 100644 --- a/crates/television/channels.rs +++ b/crates/television/channels.rs @@ -84,7 +84,7 @@ pub trait TelevisionChannel: Send { /// #[allow(dead_code, clippy::module_name_repetitions)] #[derive(CliChannel)] -pub enum AvailableChannels { +pub(crate) enum AvailableChannels { Env(env::Channel), Files(files::Channel), Text(text::Channel), diff --git a/crates/television/channels/alias.rs b/crates/television/channels/alias.rs index 7bca95b..96a3904 100644 --- a/crates/television/channels/alias.rs +++ b/crates/television/channels/alias.rs @@ -16,7 +16,7 @@ struct Alias { value: String, } -pub struct Channel { +pub(crate) struct Channel { matcher: Nucleo, last_pattern: String, file_icon: FileIcon, @@ -66,7 +66,7 @@ fn get_raw_aliases(shell: &str) -> Vec { } impl Channel { - pub fn new() -> Self { + pub(crate) fn new() -> Self { let raw_shell = get_current_shell().unwrap_or("bash".to_string()); let shell = raw_shell.split('/').last().unwrap(); debug!("Current shell: {}", shell); diff --git a/crates/television/channels/env.rs b/crates/television/channels/env.rs index 7b65fa3..c2104c3 100644 --- a/crates/television/channels/env.rs +++ b/crates/television/channels/env.rs @@ -17,7 +17,7 @@ struct EnvVar { } #[allow(clippy::module_name_repetitions)] -pub struct Channel { +pub(crate) struct Channel { matcher: Nucleo, last_pattern: String, file_icon: FileIcon, @@ -29,7 +29,7 @@ const NUM_THREADS: usize = 1; const FILE_ICON_STR: &str = "config"; impl Channel { - pub fn new() -> Self { + pub(crate) fn new() -> Self { let matcher = Nucleo::new( Config::DEFAULT, Arc::new(|| {}), diff --git a/crates/television/channels/files.rs b/crates/television/channels/files.rs index 288de3a..4690f20 100644 --- a/crates/television/channels/files.rs +++ b/crates/television/channels/files.rs @@ -13,7 +13,7 @@ use crate::fuzzy::MATCHER; use crate::previewers::PreviewType; use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; -pub struct Channel { +pub(crate) struct Channel { matcher: Nucleo, last_pattern: String, result_count: u32, @@ -21,7 +21,7 @@ pub struct Channel { } impl Channel { - pub fn new() -> Self { + pub(crate) fn new() -> Self { let matcher = Nucleo::new( Config::DEFAULT.match_paths(), Arc::new(|| {}), diff --git a/crates/television/channels/stdin.rs b/crates/television/channels/stdin.rs index 463b43a..9862951 100644 --- a/crates/television/channels/stdin.rs +++ b/crates/television/channels/stdin.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::{io::BufRead, sync::Arc}; use devicons::FileIcon; @@ -10,7 +11,7 @@ use crate::previewers::PreviewType; use super::TelevisionChannel; -pub struct Channel { +pub(crate) struct Channel { matcher: Nucleo, last_pattern: String, result_count: u32, @@ -21,7 +22,7 @@ pub struct Channel { const NUM_THREADS: usize = 2; impl Channel { - pub fn new() -> Self { + pub(crate) fn new() -> Self { let mut lines = Vec::new(); for line in std::io::stdin().lock().lines().map_while(Result::ok) { debug!("Read line: {:?}", line); @@ -95,6 +96,12 @@ impl TelevisionChannel for Channel { let indices = indices.drain(..); let content = item.matcher_columns[0].to_string(); + let path = Path::new(&content); + let icon = if path.try_exists().unwrap_or(false) { + FileIcon::from(path) + } else { + icon + }; Entry::new(content.clone(), PreviewType::Basic) .with_name_match_ranges( indices.map(|i| (i, i + 1)).collect(), @@ -108,8 +115,19 @@ impl TelevisionChannel for Channel { let snapshot = self.matcher.snapshot(); snapshot.get_matched_item(index).map(|item| { let content = item.matcher_columns[0].to_string(); - Entry::new(content.clone(), PreviewType::Basic) - .with_icon(self.icon) + // if we recognize a file path, use a file icon + // and set the preview type to "Files" + let path = Path::new(&content); + if path.is_file() { + Entry::new(content.clone(), PreviewType::Files) + .with_icon(FileIcon::from(path)) + } else if path.is_dir() { + Entry::new(content.clone(), PreviewType::Directory) + .with_icon(FileIcon::from(path)) + } else { + Entry::new(content.clone(), PreviewType::Basic) + .with_icon(self.icon) + } }) } diff --git a/crates/television/channels/text.rs b/crates/television/channels/text.rs index 6d8fdd1..c8edbd1 100644 --- a/crates/television/channels/text.rs +++ b/crates/television/channels/text.rs @@ -39,7 +39,7 @@ impl CandidateLine { } #[allow(clippy::module_name_repetitions)] -pub struct Channel { +pub(crate) struct Channel { matcher: Nucleo, last_pattern: String, result_count: u32, @@ -47,7 +47,7 @@ pub struct Channel { } impl Channel { - pub fn new() -> Self { + pub(crate) fn new() -> Self { let matcher = Nucleo::new(Config::DEFAULT, Arc::new(|| {}), None, 1); // start loading files in the background tokio::spawn(load_candidates( @@ -139,6 +139,7 @@ impl TelevisionChannel for Channel { + ":" + &item.data.line_number.to_string(), ) + .with_icon(FileIcon::from(item.data.path.as_path())) .with_line_number(item.data.line_number) }) } diff --git a/crates/television/cli.rs b/crates/television/cli.rs index 3432ba3..bac017e 100644 --- a/crates/television/cli.rs +++ b/crates/television/cli.rs @@ -5,7 +5,7 @@ use crate::config::{get_config_dir, get_data_dir}; #[derive(Parser, Debug)] #[command(author, version = version(), about)] -pub struct Cli { +pub(crate) struct Cli { /// Which channel shall we watch? #[arg(value_enum, default_value = "files")] pub channel: CliTvChannel, @@ -28,7 +28,7 @@ const VERSION_MESSAGE: &str = concat!( ")" ); -pub fn version() -> String { +pub(crate) fn version() -> String { let author = clap::crate_authors!(); // let current_exe_path = PathBuf::from(clap::crate_name!()).display().to_string(); diff --git a/crates/television/config.rs b/crates/television/config.rs index 43b1a4a..e2ea471 100644 --- a/crates/television/config.rs +++ b/crates/television/config.rs @@ -20,7 +20,7 @@ const CONFIG: &str = include_str!("../../.config/config.toml"); #[allow(dead_code, clippy::module_name_repetitions)] #[derive(Clone, Debug, Deserialize, Default)] -pub struct AppConfig { +pub(crate) struct AppConfig { #[serde(default)] pub data_dir: PathBuf, #[serde(default)] @@ -28,7 +28,7 @@ pub struct AppConfig { } #[derive(Clone, Debug, Default, Deserialize)] -pub struct Config { +pub(crate) struct Config { #[allow(clippy::struct_field_names)] #[serde(default, flatten)] pub config: AppConfig, @@ -52,7 +52,7 @@ lazy_static! { } impl Config { - pub fn new() -> Result { + pub(crate) fn new() -> Result { //let default_config: Config = json5::from_str(CONFIG).unwrap(); let default_config: Config = toml::from_str(CONFIG).unwrap(); let data_dir = get_data_dir(); @@ -101,7 +101,7 @@ impl Config { } } -pub fn get_data_dir() -> PathBuf { +pub(crate) fn get_data_dir() -> PathBuf { let directory = if let Some(s) = DATA_FOLDER.clone() { s } else if let Some(proj_dirs) = project_directory() { @@ -112,7 +112,7 @@ pub fn get_data_dir() -> PathBuf { directory } -pub fn get_config_dir() -> PathBuf { +pub(crate) fn get_config_dir() -> PathBuf { let directory = if let Some(s) = CONFIG_FOLDER.clone() { s } else if let Some(proj_dirs) = project_directory() { @@ -128,7 +128,7 @@ fn project_directory() -> Option { } #[derive(Clone, Debug, Default, Deref, DerefMut)] -pub struct KeyBindings(pub HashMap>); +pub(crate) struct KeyBindings(pub HashMap>); impl<'de> Deserialize<'de> for KeyBindings { fn deserialize(deserializer: D) -> Result @@ -236,7 +236,7 @@ fn parse_key_code_with_modifiers( } #[allow(dead_code)] -pub fn key_event_to_string(key_event: &KeyEvent) -> String { +pub(crate) fn key_event_to_string(key_event: &KeyEvent) -> String { let char; let key_code = match key_event.code { KeyCode::Backspace => "backspace", @@ -299,7 +299,7 @@ pub fn key_event_to_string(key_event: &KeyEvent) -> String { key } -pub fn parse_key(raw: &str) -> Result { +pub(crate) fn parse_key(raw: &str) -> Result { if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() { @@ -316,7 +316,7 @@ pub fn parse_key(raw: &str) -> Result { Ok(convert_raw_event_to_key(key_event)) } -pub fn default_num_threads() -> NonZeroUsize { +pub(crate) fn default_num_threads() -> NonZeroUsize { // default to 1 thread if we can't determine the number of available threads let default = NonZeroUsize::MIN; // never use more than 32 threads to avoid startup overhead @@ -328,7 +328,7 @@ pub fn default_num_threads() -> NonZeroUsize { } #[derive(Clone, Debug, Default, Deref, DerefMut)] -pub struct Styles(pub HashMap>); +pub(crate) struct Styles(pub HashMap>); impl<'de> Deserialize<'de> for Styles { fn deserialize(deserializer: D) -> Result @@ -355,7 +355,7 @@ impl<'de> Deserialize<'de> for Styles { } } -pub fn parse_style(line: &str) -> Style { +pub(crate) fn parse_style(line: &str) -> Style { let (foreground, background) = line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len())); let foreground = process_color_string(foreground); diff --git a/crates/television/entry.rs b/crates/television/entry.rs index 93be66e..1beed0b 100644 --- a/crates/television/entry.rs +++ b/crates/television/entry.rs @@ -3,7 +3,7 @@ use devicons::FileIcon; use crate::previewers::PreviewType; #[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct Entry { +pub(crate) struct Entry { pub name: String, display_name: Option, pub value: Option, @@ -15,7 +15,7 @@ pub struct Entry { } impl Entry { - pub fn new(name: String, preview_type: PreviewType) -> Self { + pub(crate) fn new(name: String, preview_type: PreviewType) -> Self { Self { name, display_name: None, @@ -28,17 +28,17 @@ impl Entry { } } - pub fn with_display_name(mut self, display_name: String) -> Self { + pub(crate) fn with_display_name(mut self, display_name: String) -> Self { self.display_name = Some(display_name); self } - pub fn with_value(mut self, value: String) -> Self { + pub(crate) fn with_value(mut self, value: String) -> Self { self.value = Some(value); self } - pub fn with_name_match_ranges( + pub(crate) fn with_name_match_ranges( mut self, name_match_ranges: Vec<(u32, u32)>, ) -> Self { @@ -46,7 +46,7 @@ impl Entry { self } - pub fn with_value_match_ranges( + pub(crate) fn with_value_match_ranges( mut self, value_match_ranges: Vec<(u32, u32)>, ) -> Self { @@ -54,21 +54,21 @@ impl Entry { self } - pub fn with_icon(mut self, icon: FileIcon) -> Self { + pub(crate) fn with_icon(mut self, icon: FileIcon) -> Self { self.icon = Some(icon); self } - pub fn with_line_number(mut self, line_number: usize) -> Self { + pub(crate) fn with_line_number(mut self, line_number: usize) -> Self { self.line_number = Some(line_number); self } - pub fn display_name(&self) -> &str { + pub(crate) fn display_name(&self) -> &str { self.display_name.as_ref().unwrap_or(&self.name) } - pub fn stdout_repr(&self) -> String { + pub(crate) fn stdout_repr(&self) -> String { let mut repr = self.name.clone(); if let Some(line_number) = self.line_number { repr.push_str(&format!(":{line_number}")); diff --git a/crates/television/errors.rs b/crates/television/errors.rs index e5259fa..cba2461 100644 --- a/crates/television/errors.rs +++ b/crates/television/errors.rs @@ -3,7 +3,7 @@ use std::env; use color_eyre::Result; use tracing::error; -pub fn init() -> Result<()> { +pub(crate) fn init() -> Result<()> { let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() .panic_section(format!( "This is a bug. Consider reporting it at {}", diff --git a/crates/television/event.rs b/crates/television/event.rs index 5fa2010..b347e27 100644 --- a/crates/television/event.rs +++ b/crates/television/event.rs @@ -17,7 +17,7 @@ use tokio::sync::mpsc; use tracing::warn; #[derive(Debug, Clone, Copy)] -pub enum Event { +pub(crate) enum Event { Closed, Input(I), FocusLost, @@ -29,7 +29,7 @@ pub enum Event { #[derive( Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Hash, )] -pub enum Key { +pub(crate) enum Key { Backspace, Enter, Left, @@ -62,7 +62,7 @@ pub enum Key { } #[allow(clippy::module_name_repetitions)] -pub struct EventLoop { +pub(crate) struct EventLoop { pub rx: mpsc::UnboundedReceiver>, //tx: mpsc::UnboundedSender>, pub abort_tx: mpsc::UnboundedSender<()>, @@ -99,7 +99,7 @@ async fn poll_event(timeout: Duration) -> bool { } impl EventLoop { - pub fn new(tick_rate: f64, init: bool) -> Self { + pub(crate) fn new(tick_rate: f64, init: bool) -> Self { let (tx, rx) = mpsc::unbounded_channel(); let tx_c = tx.clone(); let tick_interval = @@ -162,7 +162,7 @@ impl EventLoop { } } -pub fn convert_raw_event_to_key(event: KeyEvent) -> Key { +pub(crate) fn convert_raw_event_to_key(event: KeyEvent) -> Key { match event.code { Backspace => match event.modifiers { KeyModifiers::CONTROL => Key::CtrlBackspace, diff --git a/crates/television/fuzzy.rs b/crates/television/fuzzy.rs index 5d83f63..9148bc9 100644 --- a/crates/television/fuzzy.rs +++ b/crates/television/fuzzy.rs @@ -1,7 +1,7 @@ use parking_lot::Mutex; use std::ops::DerefMut; -pub struct LazyMutex { +pub(crate) struct LazyMutex { inner: Mutex>, init: fn() -> T, } @@ -14,7 +14,7 @@ impl LazyMutex { } } - pub fn lock(&self) -> impl DerefMut + '_ { + pub(crate) fn lock(&self) -> impl DerefMut + '_ { parking_lot::MutexGuard::map(self.inner.lock(), |val| { val.get_or_insert_with(self.init) }) diff --git a/crates/television/logging.rs b/crates/television/logging.rs index 907551f..ec77ea9 100644 --- a/crates/television/logging.rs +++ b/crates/television/logging.rs @@ -8,7 +8,7 @@ lazy_static::lazy_static! { pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); } -pub fn init() -> Result<()> { +pub(crate) fn init() -> Result<()> { let directory = config::get_data_dir(); std::fs::create_dir_all(directory.clone())?; let log_path = directory.join(LOG_FILE.clone()); diff --git a/crates/television/main.rs b/crates/television/main.rs index 53e8531..d4496f2 100644 --- a/crates/television/main.rs +++ b/crates/television/main.rs @@ -55,7 +55,7 @@ async fn main() -> Result<()> { Ok(()) } -pub fn is_readable_stdin() -> bool { +pub(crate) fn is_readable_stdin() -> bool { use std::io::IsTerminal; #[cfg(unix)] diff --git a/crates/television/previewers.rs b/crates/television/previewers.rs index bc7f3d6..d80f379 100644 --- a/crates/television/previewers.rs +++ b/crates/television/previewers.rs @@ -4,26 +4,29 @@ use crate::entry::Entry; mod basic; mod cache; +mod directory; mod env; mod files; // previewer types -pub use basic::BasicPreviewer; -pub use env::EnvVarPreviewer; -pub use files::FilePreviewer; +pub(crate) use basic::BasicPreviewer; +pub(crate) use directory::DirectoryPreviewer; +pub(crate) use env::EnvVarPreviewer; +pub(crate) use files::FilePreviewer; //use ratatui_image::protocol::StatefulProtocol; use syntect::highlighting::Style; #[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] -pub enum PreviewType { +pub(crate) enum PreviewType { #[default] Basic, + Directory, EnvVar, Files, } #[derive(Clone)] -pub enum PreviewContent { +pub(crate) enum PreviewContent { Empty, FileTooLarge, HighlightedText(Vec>), @@ -44,7 +47,7 @@ pub const FILE_TOO_LARGE_MSG: &str = "File too large"; /// - `title`: The title of the preview. /// - `content`: The content of the preview. #[derive(Clone)] -pub struct Preview { +pub(crate) struct Preview { pub title: String, pub content: PreviewContent, } @@ -59,11 +62,11 @@ impl Default for Preview { } impl Preview { - pub fn new(title: String, content: PreviewContent) -> Self { + pub(crate) fn new(title: String, content: PreviewContent) -> Self { Preview { title, content } } - pub fn total_lines(&self) -> u16 { + pub(crate) fn total_lines(&self) -> u16 { match &self.content { PreviewContent::HighlightedText(lines) => lines.len() as u16, _ => 0, @@ -71,16 +74,18 @@ impl Preview { } } -pub struct Previewer { +pub(crate) struct Previewer { basic: BasicPreviewer, + directory: DirectoryPreviewer, file: FilePreviewer, env_var: EnvVarPreviewer, } impl Previewer { - pub fn new() -> Self { + pub(crate) fn new() -> Self { Previewer { basic: BasicPreviewer::new(), + directory: DirectoryPreviewer::new(), file: FilePreviewer::new(), env_var: EnvVarPreviewer::new(), } @@ -89,6 +94,7 @@ impl Previewer { pub async fn preview(&mut self, entry: &Entry) -> Arc { match entry.preview_type { PreviewType::Basic => self.basic.preview(entry), + PreviewType::Directory => self.directory.preview(entry), PreviewType::EnvVar => self.env_var.preview(entry), PreviewType::Files => self.file.preview(entry).await, } diff --git a/crates/television/previewers/basic.rs b/crates/television/previewers/basic.rs index f02f55b..f2af91b 100644 --- a/crates/television/previewers/basic.rs +++ b/crates/television/previewers/basic.rs @@ -3,14 +3,14 @@ use std::sync::Arc; use crate::entry::Entry; use crate::previewers::{Preview, PreviewContent}; -pub struct BasicPreviewer {} +pub(crate) struct BasicPreviewer {} impl BasicPreviewer { - pub fn new() -> Self { + pub(crate) fn new() -> Self { BasicPreviewer {} } - pub fn preview(&self, entry: &Entry) -> Arc { + pub(crate) fn preview(&self, entry: &Entry) -> Arc { Arc::new(Preview { title: entry.name.clone(), content: PreviewContent::PlainTextWrapped(entry.name.clone()), diff --git a/crates/television/previewers/cache.rs b/crates/television/previewers/cache.rs index 42fd622..ff15ab8 100644 --- a/crates/television/previewers/cache.rs +++ b/crates/television/previewers/cache.rs @@ -7,13 +7,43 @@ use tracing::debug; use crate::previewers::Preview; -/// TODO: add unit tests /// A ring buffer that also keeps track of the keys it contains to avoid duplicates. /// -/// I'm planning on using this as a backend LRU-cache for the preview cache. +/// This serves as a backend for the preview cache. /// Basic idea: /// - When a new key is pushed, if it's already in the buffer, do nothing. /// - If the buffer is full, remove the oldest key and push the new key. +/// +/// # Example +/// ```rust +/// let mut ring_set = RingSet::with_capacity(3); +/// // push 3 values into the ringset +/// assert_eq!(ring_set.push(1), None); +/// assert_eq!(ring_set.push(2), None); +/// assert_eq!(ring_set.push(3), None); +/// +/// // check that the values are in the buffer +/// assert!(ring_set.contains(&1)); +/// assert!(ring_set.contains(&2)); +/// assert!(ring_set.contains(&3)); +/// +/// // push an existing value (should do nothing) +/// assert_eq!(ring_set.push(1), None); +/// +/// // entries should still be there +/// assert!(ring_set.contains(&1)); +/// assert!(ring_set.contains(&2)); +/// assert!(ring_set.contains(&3)); +/// +/// // push a new value, should remove the oldest value (1) +/// assert_eq!(ring_set.push(4), Some(1)); +/// +/// // 1 is no longer there but 2 and 3 remain +/// assert!(!ring_set.contains(&1)); +/// assert!(ring_set.contains(&2)); +/// assert!(ring_set.contains(&3)); +/// assert!(ring_set.contains(&4)); +/// ``` struct RingSet { ring_buffer: VecDeque, known_keys: HashSet, @@ -24,7 +54,8 @@ impl RingSet where T: Eq + std::hash::Hash + Clone + std::fmt::Debug, { - pub fn with_capacity(capacity: usize) -> Self { + /// Create a new `RingSet` with the given capacity. + pub(crate) fn with_capacity(capacity: usize) -> Self { RingSet { ring_buffer: VecDeque::with_capacity(capacity), known_keys: HashSet::with_capacity(capacity), @@ -35,7 +66,7 @@ where /// Push a new item to the back of the buffer, removing the oldest item if the buffer is full. /// Returns the item that was removed, if any. /// If the item is already in the buffer, do nothing and return None. - pub fn push(&mut self, item: T) -> Option { + pub(crate) fn push(&mut self, item: T) -> Option { // If the key is already in the buffer, do nothing if self.contains(&item) { debug!("Key already in ring buffer: {:?}", item); @@ -67,34 +98,37 @@ where } } -/// Default size of the preview cache. +/// Default size of the preview cache: 100 entries. +/// /// This does seem kind of arbitrary for now, will need to play around with it. +/// At the moment, files over 4 MB are not previewed, so the cache size +/// shouldn't exceed 400 MB. const DEFAULT_PREVIEW_CACHE_SIZE: usize = 100; /// A cache for previews. /// The cache is implemented as an LRU cache with a fixed size. -pub struct PreviewCache { +pub(crate) struct PreviewCache { entries: HashMap>, ring_set: RingSet, } impl PreviewCache { /// Create a new preview cache with the given capacity. - pub fn new(capacity: usize) -> Self { + pub(crate) fn new(capacity: usize) -> Self { PreviewCache { entries: HashMap::new(), ring_set: RingSet::with_capacity(capacity), } } - pub fn get(&self, key: &str) -> Option> { + pub(crate) fn get(&self, key: &str) -> Option> { self.entries.get(key).cloned() } /// Insert a new preview into the cache. /// If the cache is full, the oldest entry will be removed. /// If the key is already in the cache, the preview will be updated. - pub fn insert(&mut self, key: String, preview: Arc) { + pub(crate) fn insert(&mut self, key: String, preview: Arc) { debug!("Inserting preview into cache: {}", key); self.entries.insert(key.clone(), preview.clone()); if let Some(oldest_key) = self.ring_set.push(key) { @@ -102,6 +136,24 @@ impl PreviewCache { self.entries.remove(&oldest_key); } } + + /// Get the preview for the given key, or insert a new preview if it doesn't exist. + pub(crate) fn get_or_insert( + &mut self, + key: String, + f: F, + ) -> Arc + where + F: FnOnce() -> Preview, + { + if let Some(preview) = self.get(&key) { + preview + } else { + let preview = Arc::new(f()); + self.insert(key, preview.clone()); + preview + } + } } impl Default for PreviewCache { @@ -109,3 +161,50 @@ impl Default for PreviewCache { PreviewCache::new(DEFAULT_PREVIEW_CACHE_SIZE) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ring_set() { + let mut ring_set = RingSet::with_capacity(3); + // push 3 values into the ringset + assert_eq!(ring_set.push(1), None); + assert_eq!(ring_set.push(2), None); + assert_eq!(ring_set.push(3), None); + + // check that the values are in the buffer + assert!(ring_set.contains(&1)); + assert!(ring_set.contains(&2)); + assert!(ring_set.contains(&3)); + + // push an existing value (should do nothing) + assert_eq!(ring_set.push(1), None); + + // entries should still be there + assert!(ring_set.contains(&1)); + assert!(ring_set.contains(&2)); + assert!(ring_set.contains(&3)); + + // push a new value, should remove the oldest value (1) + assert_eq!(ring_set.push(4), Some(1)); + + // 1 is no longer there but 2 and 3 remain + assert!(!ring_set.contains(&1)); + assert!(ring_set.contains(&2)); + assert!(ring_set.contains(&3)); + assert!(ring_set.contains(&4)); + + // push two new values, should remove 2 and 3 + assert_eq!(ring_set.push(5), Some(2)); + assert_eq!(ring_set.push(6), Some(3)); + + // 2 and 3 are no longer there but 4, 5 and 6 remain + assert!(!ring_set.contains(&2)); + assert!(!ring_set.contains(&3)); + assert!(ring_set.contains(&4)); + assert!(ring_set.contains(&5)); + assert!(ring_set.contains(&6)); + } +} diff --git a/crates/television/previewers/directory.rs b/crates/television/previewers/directory.rs new file mode 100644 index 0000000..187100a --- /dev/null +++ b/crates/television/previewers/directory.rs @@ -0,0 +1,52 @@ +use std::path::Path; +use std::sync::Arc; + +use devicons::FileIcon; + +use crate::entry::Entry; + +use crate::previewers::cache::PreviewCache; +use crate::previewers::{Preview, PreviewContent}; + +pub(crate) struct DirectoryPreviewer { + cache: PreviewCache, +} + +impl DirectoryPreviewer { + pub(crate) fn new() -> Self { + DirectoryPreviewer { + cache: PreviewCache::default(), + } + } + + pub(crate) fn preview(&mut self, entry: &Entry) -> Arc { + if let Some(preview) = self.cache.get(&entry.name) { + return preview; + } + let preview = Arc::new(build_preview(entry)); + self.cache.insert(entry.name.clone(), preview.clone()); + preview + } +} + +fn build_preview(entry: &Entry) -> Preview { + let dir_path = Path::new(&entry.name); + // get the list of files in the directory + let mut lines = vec![]; + if let Ok(entries) = std::fs::read_dir(dir_path) { + for entry in entries.flatten() { + if let Ok(file_name) = entry.file_name().into_string() { + lines.push(format!( + "{} {}", + FileIcon::from(&file_name), + &file_name + )); + } + } + } + + Preview { + title: entry.name.clone(), + content: PreviewContent::PlainText(lines), + } +} diff --git a/crates/television/previewers/env.rs b/crates/television/previewers/env.rs index 60b64ee..8eb174c 100644 --- a/crates/television/previewers/env.rs +++ b/crates/television/previewers/env.rs @@ -4,18 +4,18 @@ use std::sync::Arc; use crate::entry; use crate::previewers::{Preview, PreviewContent}; -pub struct EnvVarPreviewer { +pub(crate) struct EnvVarPreviewer { cache: HashMap>, } impl EnvVarPreviewer { - pub fn new() -> Self { + pub(crate) fn new() -> Self { EnvVarPreviewer { cache: HashMap::new(), } } - pub fn preview(&mut self, entry: &entry::Entry) -> Arc { + pub(crate) fn preview(&mut self, entry: &entry::Entry) -> Arc { // check if we have that preview in the cache if let Some(preview) = self.cache.get(entry) { return preview.clone(); diff --git a/crates/television/previewers/files.rs b/crates/television/previewers/files.rs index 4b279c9..944411e 100644 --- a/crates/television/previewers/files.rs +++ b/crates/television/previewers/files.rs @@ -24,7 +24,7 @@ use crate::utils::strings::preprocess_line; use super::cache::PreviewCache; -pub struct FilePreviewer { +pub(crate) struct FilePreviewer { cache: Arc>, syntax_set: Arc, syntax_theme: Arc, @@ -32,7 +32,7 @@ pub struct FilePreviewer { } impl FilePreviewer { - pub fn new() -> Self { + pub(crate) fn new() -> Self { let syntax_set = SyntaxSet::load_defaults_nonewlines(); let theme_set = ThemeSet::load_defaults(); //info!("getting image picker"); diff --git a/crates/television/render.rs b/crates/television/render.rs index c2e6eba..ec6f0d4 100644 --- a/crates/television/render.rs +++ b/crates/television/render.rs @@ -15,7 +15,7 @@ use crate::television::Television; use crate::{action::Action, config::Config, tui::Tui}; #[derive(Debug)] -pub enum RenderingTask { +pub(crate) enum RenderingTask { ClearScreen, Render, Resize(u16, u16), @@ -72,7 +72,7 @@ pub async fn render( // Rendering loop loop { select! { - _ = tokio::time::sleep(tokio::time::Duration::from_secs_f64(1.0 / frame_rate)) => { + () = tokio::time::sleep(tokio::time::Duration::from_secs_f64(1.0 / frame_rate)) => { action_tx.send(Action::Render)?; } maybe_task = render_rx.recv() => { @@ -83,13 +83,24 @@ pub async fn render( } RenderingTask::Render => { let mut television = television.lock().await; - tui.terminal.draw(|frame| { - if let Err(err) = television.draw(frame, frame.area()) { - warn!("Failed to draw: {:?}", err); - let _ = action_tx - .send(Action::Error(format!("Failed to draw: {err:?}"))); + if let Ok(size) = tui.size() { + // Ratatui uses u16s to encode terminal dimensions and its + // content for each terminal cell is stored linearly in a + // buffer with a u16 index which means we can't support + // terminal areas larger than u16::MAX. + if size.width.checked_mul(size.height).is_some() { + tui.terminal.draw(|frame| { + if let Err(err) = television.draw(frame, frame.area()) { + warn!("Failed to draw: {:?}", err); + let _ = action_tx + .send(Action::Error(format!("Failed to draw: {err:?}"))); + } + })?; + + } else { + warn!("Terminal area too large"); } - })?; + } } RenderingTask::Resize(w, h) => { tui.resize(Rect::new(0, 0, w, h))?; diff --git a/crates/television/television.rs b/crates/television/television.rs index 1d02439..78994cc 100644 --- a/crates/television/television.rs +++ b/crates/television/television.rs @@ -4,7 +4,7 @@ use ratatui::{ layout::{ Alignment, Constraint, Direction, Layout as RatatuiLayout, Rect, }, - style::{Color, Style}, + style::{Color, Style, Stylize}, text::{Line, Span}, widgets::{ block::{Position, Title}, @@ -15,7 +15,6 @@ use ratatui::{ use std::{collections::HashMap, str::FromStr}; use tokio::sync::mpsc::UnboundedSender; -use crate::channels::{CliTvChannel, TelevisionChannel}; use crate::entry::{Entry, ENTRY_PLACEHOLDER}; use crate::previewers::Previewer; use crate::ui::get_border_style; @@ -26,6 +25,10 @@ use crate::ui::preview::DEFAULT_PREVIEW_TITLE_FG; use crate::ui::results::build_results_list; use crate::utils::strings::EMPTY_STRING; use crate::{action::Action, config::Config}; +use crate::{ + channels::{CliTvChannel, TelevisionChannel}, + utils::strings::shrink_with_ellipsis, +}; #[derive(PartialEq, Copy, Clone)] enum Pane { @@ -36,7 +39,7 @@ enum Pane { static PANES: [Pane; 3] = [Pane::Input, Pane::Results, Pane::Preview]; -pub struct Television { +pub(crate) struct Television { action_tx: Option>, config: Config, channel: Box, @@ -48,15 +51,22 @@ pub struct Television { picker_view_offset: usize, results_area_height: u32, previewer: Previewer, - pub preview_scroll: Option, + pub(crate) preview_scroll: Option, pub(crate) preview_pane_height: u16, current_preview_total_lines: u16, - pub(crate) meta_paragraph_cache: HashMap>, + /// A cache for meta paragraphs (i.e. previews like "Not Supported", etc.). + /// + /// The key is a tuple of the preview name and the dimensions of the + /// preview pane. This is a little extra security to ensure meta previews + /// are rendered correctly even when resizing the terminal while still + /// benefiting from a cache mechanism. + pub(crate) meta_paragraph_cache: + HashMap<(String, u16, u16), Paragraph<'static>>, } impl Television { #[must_use] - pub fn new(cli_channel: CliTvChannel) -> Self { + pub(crate) fn new(cli_channel: CliTvChannel) -> Self { let mut tv_channel = cli_channel.to_channel(); tv_channel.find(EMPTY_STRING); @@ -86,13 +96,13 @@ impl Television { #[must_use] /// # Panics /// This method will panic if the index doesn't fit into an u32. - pub fn get_selected_entry(&self) -> Option { + pub(crate) fn get_selected_entry(&self) -> Option { self.picker_state .selected() .and_then(|i| self.channel.get_result(u32::try_from(i).unwrap())) } - pub fn select_prev_entry(&mut self) { + pub(crate) fn select_prev_entry(&mut self) { if self.channel.result_count() == 0 { return; } @@ -122,7 +132,7 @@ impl Television { } } - pub fn select_next_entry(&mut self) { + pub(crate) fn select_next_entry(&mut self) { if self.channel.result_count() == 0 { return; } @@ -155,7 +165,7 @@ impl Television { self.preview_scroll = None; } - pub fn scroll_preview_down(&mut self, offset: u16) { + pub(crate) fn scroll_preview_down(&mut self, offset: u16) { if self.preview_scroll.is_none() { self.preview_scroll = Some(0); } @@ -169,7 +179,7 @@ impl Television { } } - pub fn scroll_preview_up(&mut self, offset: u16) { + pub(crate) fn scroll_preview_up(&mut self, offset: u16) { if let Some(scroll) = self.preview_scroll { self.preview_scroll = Some(scroll.saturating_sub(offset)); } @@ -182,13 +192,13 @@ impl Television { .unwrap() } - pub fn next_pane(&mut self) { + pub(crate) fn next_pane(&mut self) { let current_index = self.get_current_pane_index(); let next_index = (current_index + 1) % PANES.len(); self.current_pane = PANES[next_index]; } - pub fn previous_pane(&mut self) { + pub(crate) fn previous_pane(&mut self) { let current_index = self.get_current_pane_index(); let previous_index = if current_index == 0 { PANES.len() - 1 @@ -207,7 +217,7 @@ impl Television { /// ┌───────────────────┐│ │ /// │ Search x ││ │ /// └───────────────────┘└─────────────┘ - pub fn move_to_pane_on_top(&mut self) { + pub(crate) fn move_to_pane_on_top(&mut self) { if self.current_pane == Pane::Input { self.current_pane = Pane::Results; } @@ -222,7 +232,7 @@ impl Television { /// ┌───────────────────┐│ │ /// │ Search ││ │ /// └───────────────────┘└─────────────┘ - pub fn move_to_pane_below(&mut self) { + pub(crate) fn move_to_pane_below(&mut self) { if self.current_pane == Pane::Results { self.current_pane = Pane::Input; } @@ -237,7 +247,7 @@ impl Television { /// ┌───────────────────┐│ │ /// │ Search x ││ │ /// └───────────────────┘└─────────────┘ - pub fn move_to_pane_right(&mut self) { + pub(crate) fn move_to_pane_right(&mut self) { match self.current_pane { Pane::Results | Pane::Input => { self.current_pane = Pane::Preview; @@ -255,22 +265,22 @@ impl Television { /// ┌───────────────────┐│ │ /// │ Search ││ │ /// └───────────────────┘└─────────────┘ - pub fn move_to_pane_left(&mut self) { + pub(crate) fn move_to_pane_left(&mut self) { if self.current_pane == Pane::Preview { self.current_pane = Pane::Results; } } #[must_use] - pub fn is_input_focused(&self) -> bool { + pub(crate) fn is_input_focused(&self) -> bool { Pane::Input == self.current_pane } } // Styles // input -const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200); -const DEFAULT_RESULTS_COUNT_FG: Color = Color::Rgb(150, 150, 150); +const DEFAULT_INPUT_FG: Color = Color::LightRed; +const DEFAULT_RESULTS_COUNT_FG: Color = Color::LightRed; impl Television { /// Register an action handler that can send actions for processing if necessary. @@ -282,7 +292,7 @@ impl Television { /// # Returns /// /// * `Result<()>` - An Ok result or an error. - pub fn register_action_handler( + pub(crate) fn register_action_handler( &mut self, tx: UnboundedSender, ) -> Result<()> { @@ -299,7 +309,10 @@ impl Television { /// # Returns /// /// * `Result<()>` - An Ok result or an error. - pub fn register_config_handler(&mut self, config: Config) -> Result<()> { + pub(crate) fn register_config_handler( + &mut self, + config: Config, + ) -> Result<()> { self.config = config; Ok(()) } @@ -388,7 +401,11 @@ impl Television { /// # Returns /// /// * `Result<()>` - An Ok result or an error. - pub fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + pub(crate) fn draw( + &mut self, + frame: &mut Frame, + area: Rect, + ) -> Result<()> { let layout = Layout::all_panes_centered(Dimensions::default(), area); //let layout = // Layout::results_only_centered(Dimensions::new(40, 60), area); @@ -446,11 +463,15 @@ impl Television { frame.render_widget(input_block, layout.input); + // split input block into 3 parts: prompt symbol, input, result count let inner_input_chunks = RatatuiLayout::default() .direction(Direction::Horizontal) .constraints([ + // prompt symbol Constraint::Length(2), + // input field Constraint::Fill(1), + // result count Constraint::Length( 3 * ((self.channel.total_count() as f32).log10().ceil() as u16 @@ -461,8 +482,11 @@ impl Television { .split(input_block_inner); let arrow_block = Block::default(); - let arrow = Paragraph::new(Span::styled("> ", Style::default())) - .block(arrow_block); + let arrow = Paragraph::new(Span::styled( + "> ", + Style::default().fg(DEFAULT_INPUT_FG).bold(), + )) + .block(arrow_block); frame.render_widget(arrow, inner_input_chunks[0]); let interactive_input_block = Block::default(); @@ -472,7 +496,7 @@ impl Television { let input = Paragraph::new(self.input.value()) .scroll((0, u16::try_from(scroll)?)) .block(interactive_input_block) - .style(Style::default().fg(DEFAULT_INPUT_FG)) + .style(Style::default().fg(DEFAULT_INPUT_FG).bold().italic()) .alignment(Alignment::Left); frame.render_widget(input, inner_input_chunks[1]); @@ -487,7 +511,7 @@ impl Television { }, self.channel.result_count(), ), - Style::default().fg(DEFAULT_RESULTS_COUNT_FG), + Style::default().fg(DEFAULT_RESULTS_COUNT_FG).italic(), )) .block(result_count_block) .alignment(Alignment::Right); @@ -519,14 +543,21 @@ impl Television { let mut preview_title_spans = Vec::new(); if let Some(icon) = &selected_entry.icon { preview_title_spans.push(Span::styled( - icon.to_string(), + { + let mut icon_str = String::from(" "); + icon_str.push(icon.icon); + icon_str.push(' '); + icon_str + }, Style::default().fg(Color::from_str(icon.color)?), )); - preview_title_spans.push(Span::raw(" ")); } preview_title_spans.push(Span::styled( - preview.title.clone(), - Style::default().fg(DEFAULT_PREVIEW_TITLE_FG), + shrink_with_ellipsis( + &preview.title, + (preview_title_area.width - 4) as usize, + ), + Style::default().fg(DEFAULT_PREVIEW_TITLE_FG).bold(), )); let preview_title = Paragraph::new(Line::from(preview_title_spans)) @@ -581,8 +612,7 @@ impl Television { &preview, selected_entry .line_number - // FIXME: this actually might panic in some edge cases - .map(|l| u16::try_from(l).unwrap()), + .map(|l| u16::try_from(l).unwrap_or(0)), ); frame.render_widget(preview_block, inner); //} diff --git a/crates/television/tui.rs b/crates/television/tui.rs index ef5cd46..792512b 100644 --- a/crates/television/tui.rs +++ b/crates/television/tui.rs @@ -16,7 +16,7 @@ use tokio::task::JoinHandle; use tracing::debug; #[allow(dead_code)] -pub struct Tui +pub(crate) struct Tui where W: Write, { @@ -30,7 +30,7 @@ impl Tui where W: Write, { - pub fn new(writer: W) -> Result { + pub(crate) fn new(writer: W) -> Result { Ok(Self { task: tokio::spawn(async {}), frame_rate: 60.0, @@ -38,16 +38,16 @@ where }) } - pub fn frame_rate(mut self, frame_rate: f64) -> Self { + pub(crate) fn frame_rate(mut self, frame_rate: f64) -> Self { self.frame_rate = frame_rate; self } - pub fn size(&self) -> Result { + pub(crate) fn size(&self) -> Result { Ok(self.terminal.size()?) } - pub fn enter(&mut self) -> Result<()> { + pub(crate) fn enter(&mut self) -> Result<()> { enable_raw_mode()?; let mut buffered_stderr = LineWriter::new(stderr()); execute!(buffered_stderr, EnterAlternateScreen)?; @@ -56,7 +56,7 @@ where Ok(()) } - pub fn exit(&mut self) -> Result<()> { + pub(crate) fn exit(&mut self) -> Result<()> { if is_raw_mode_enabled()? { debug!("Exiting terminal"); @@ -69,14 +69,14 @@ where Ok(()) } - pub fn suspend(&mut self) -> Result<()> { + pub(crate) fn suspend(&mut self) -> Result<()> { self.exit()?; #[cfg(not(windows))] signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; Ok(()) } - pub fn resume(&mut self) -> Result<()> { + pub(crate) fn resume(&mut self) -> Result<()> { self.enter()?; Ok(()) } diff --git a/crates/television/ui.rs b/crates/television/ui.rs index c1a66fd..3166840 100644 --- a/crates/television/ui.rs +++ b/crates/television/ui.rs @@ -15,11 +15,14 @@ pub mod results; //const DEFAULT_PREVIEW_GUTTER_FG: Color = Color::Rgb(70, 70, 70); //const DEFAULT_PREVIEW_GUTTER_SELECTED_FG: Color = Color::Rgb(255, 150, 150); -pub fn get_border_style(focused: bool) -> Style { - if focused { - Style::default().fg(Color::Green) - } else { - // TODO: make this depend on self.config - Style::default().fg(Color::Rgb(90, 90, 110)).dim() - } +pub(crate) fn get_border_style(focused: bool) -> Style { + Style::default().fg(Color::Blue) + + // NOTE: do we want to change the border color based on focus? Are we + // keeping the focus feature at all? + // if focused { + // Style::default().fg(Color::Green) + // } else { + // Style::default().fg(Color::Blue) + // } } diff --git a/crates/television/ui/input.rs b/crates/television/ui/input.rs index a4322a2..e780aba 100644 --- a/crates/television/ui/input.rs +++ b/crates/television/ui/input.rs @@ -6,7 +6,7 @@ pub mod backend; /// Different backends can be used to convert events into requests. #[allow(clippy::module_name_repetitions)] #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] -pub enum InputRequest { +pub(crate) enum InputRequest { SetCursor(usize), InsertChar(char), GoToPrevChar, @@ -24,7 +24,7 @@ pub enum InputRequest { } #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] -pub struct StateChanged { +pub(crate) struct StateChanged { pub value: bool, pub cursor: bool, } @@ -45,7 +45,7 @@ pub type InputResponse = Option; /// assert_eq!(input.to_string(), "Hello World"); /// ``` #[derive(Default, Debug, Clone)] -pub struct Input { +pub(crate) struct Input { value: String, cursor: usize, } @@ -53,14 +53,14 @@ pub struct Input { impl Input { /// Initialize a new instance with a given value /// Cursor will be set to the given value's length. - pub fn new(value: String) -> Self { + pub(crate) fn new(value: String) -> Self { let len = value.chars().count(); Self { value, cursor: len } } /// Set the value manually. /// Cursor will be set to the given value's length. - pub fn with_value(mut self, value: String) -> Self { + pub(crate) fn with_value(mut self, value: String) -> Self { self.cursor = value.chars().count(); self.value = value; self @@ -68,20 +68,20 @@ impl Input { /// Set the cursor manually. /// If the input is larger than the value length, it'll be auto adjusted. - pub fn with_cursor(mut self, cursor: usize) -> Self { + pub(crate) fn with_cursor(mut self, cursor: usize) -> Self { self.cursor = cursor.min(self.value.chars().count()); self } // Reset the cursor and value to default - pub fn reset(&mut self) { + pub(crate) fn reset(&mut self) { self.cursor = Default::default(); self.value = String::default(); } /// Handle request and emit response. #[allow(clippy::too_many_lines)] - pub fn handle(&mut self, req: InputRequest) -> InputResponse { + pub(crate) fn handle(&mut self, req: InputRequest) -> InputResponse { use InputRequest::{ DeleteLine, DeleteNextChar, DeleteNextWord, DeletePrevChar, DeletePrevWord, DeleteTillEnd, GoToEnd, GoToNextChar, @@ -328,17 +328,17 @@ impl Input { } /// Get a reference to the current value. - pub fn value(&self) -> &str { + pub(crate) fn value(&self) -> &str { self.value.as_str() } /// Get the currect cursor placement. - pub fn cursor(&self) -> usize { + pub(crate) fn cursor(&self) -> usize { self.cursor } /// Get the current cursor position with account for multispace characters. - pub fn visual_cursor(&self) -> usize { + pub(crate) fn visual_cursor(&self) -> usize { if self.cursor == 0 { return 0; } @@ -356,7 +356,7 @@ impl Input { } /// Get the scroll position with account for multispace characters. - pub fn visual_scroll(&self, width: usize) -> usize { + pub(crate) fn visual_scroll(&self, width: usize) -> usize { let scroll = self.visual_cursor().max(width) - width; let mut uscroll = 0; let mut chars = self.value().chars(); diff --git a/crates/television/ui/input/backend.rs b/crates/television/ui/input/backend.rs index c17bf62..1e66101 100644 --- a/crates/television/ui/input/backend.rs +++ b/crates/television/ui/input/backend.rs @@ -5,7 +5,7 @@ use ratatui::crossterm::event::{ /// Converts crossterm event into input requests. /// TODO: make these keybindings configurable. -pub fn to_input_request(evt: &CrosstermEvent) -> Option { +pub(crate) fn to_input_request(evt: &CrosstermEvent) -> Option { use InputRequest::*; use KeyCode::*; match evt { diff --git a/crates/television/ui/layout.rs b/crates/television/ui/layout.rs index a15f2d4..4eef517 100644 --- a/crates/television/ui/layout.rs +++ b/crates/television/ui/layout.rs @@ -1,13 +1,13 @@ use ratatui::layout; use ratatui::layout::{Constraint, Direction, Rect}; -pub struct Dimensions { +pub(crate) struct Dimensions { pub x: u16, pub y: u16, } impl Dimensions { - pub fn new(x: u16, y: u16) -> Self { + pub(crate) fn new(x: u16, y: u16) -> Self { Self { x, y } } } @@ -18,7 +18,7 @@ impl Default for Dimensions { } } -pub struct Layout { +pub(crate) struct Layout { pub results: Rect, pub input: Rect, pub preview_title: Option, @@ -26,7 +26,7 @@ pub struct Layout { } impl Layout { - pub fn new( + pub(crate) fn new( results: Rect, input: Rect, preview_title: Option, @@ -42,7 +42,7 @@ impl Layout { /// TODO: add diagram #[allow(dead_code)] - pub fn all_panes_centered(dimensions: Dimensions, area: Rect) -> Self { + pub(crate) fn all_panes_centered(dimensions: Dimensions, area: Rect) -> Self { let main_block = centered_rect(dimensions.x, dimensions.y, area); // split the main block into two vertical chunks let chunks = layout::Layout::default() @@ -75,7 +75,7 @@ impl Layout { /// TODO: add diagram #[allow(dead_code)] - pub fn results_only_centered(dimensions: Dimensions, area: Rect) -> Self { + pub(crate) fn results_only_centered(dimensions: Dimensions, area: Rect) -> Self { let main_block = centered_rect(dimensions.x, dimensions.y, area); // split the main block into two vertical chunks let chunks = layout::Layout::default() diff --git a/crates/television/ui/preview.rs b/crates/television/ui/preview.rs index 16fc5a1..9a8b0ab 100644 --- a/crates/television/ui/preview.rs +++ b/crates/television/ui/preview.rs @@ -20,7 +20,7 @@ impl Television { const FILL_CHAR_SLANTED: char = '╱'; const FILL_CHAR_EMPTY: char = ' '; - pub fn build_preview_paragraph<'b>( + pub(crate) fn build_preview_paragraph<'b>( &'b mut self, preview_block: Block<'b>, inner: Rect, @@ -34,10 +34,9 @@ impl Television { for (i, line) in content.iter().enumerate() { lines.push(Line::from(vec![ build_line_number_span(i + 1).style(Style::default().fg( - // FIXME: this actually might panic in some edge cases if matches!( target_line, - Some(l) if l == u16::try_from(i).unwrap() + 1 + Some(l) if l == u16::try_from(i).unwrap_or(0) + 1 ) { DEFAULT_PREVIEW_GUTTER_SELECTED_FG @@ -120,7 +119,7 @@ impl Television { } } - pub fn maybe_init_preview_scroll( + pub(crate) fn maybe_init_preview_scroll( &mut self, target_line: Option, height: u16, @@ -131,13 +130,17 @@ impl Television { } } - pub fn build_meta_preview_paragraph<'a>( + pub(crate) fn build_meta_preview_paragraph<'a>( &mut self, inner: Rect, message: &str, fill_char: char, ) -> Paragraph<'a> { - if let Some(paragraph) = self.meta_paragraph_cache.get(message) { + if let Some(paragraph) = self.meta_paragraph_cache.get(&( + message.to_string(), + inner.width, + inner.height, + )) { return paragraph.clone(); } let message_len = message.len(); @@ -187,8 +190,10 @@ impl Television { // Create a paragraph with the generated content let p = Paragraph::new(Text::from(lines)); - self.meta_paragraph_cache - .insert(message.to_string(), p.clone()); + self.meta_paragraph_cache.insert( + (message.to_string(), inner.width, inner.height), + p.clone(), + ); p } } diff --git a/crates/television/ui/results.rs b/crates/television/ui/results.rs index fe24792..fbba065 100644 --- a/crates/television/ui/results.rs +++ b/crates/television/ui/results.rs @@ -9,7 +9,7 @@ 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; -pub fn build_results_list<'a, 'b>( +pub(crate) fn build_results_list<'a, 'b>( results_block: Block<'b>, entries: &'a [Entry], ) -> List<'a> @@ -39,25 +39,22 @@ where last_match_end, start, ), - Style::default() - .fg(DEFAULT_RESULT_NAME_FG) - .bold() - .italic(), + Style::default().fg(DEFAULT_RESULT_NAME_FG), )); spans.push(Span::styled( slice_at_char_boundaries(&entry.name, start, end), - Style::default().fg(Color::Red).bold().italic(), + Style::default().fg(Color::Red), )); last_match_end = end; } spans.push(Span::styled( &entry.name[next_char_boundary(&entry.name, last_match_end)..], - Style::default().fg(DEFAULT_RESULT_NAME_FG).bold().italic(), + Style::default().fg(DEFAULT_RESULT_NAME_FG), )); } else { spans.push(Span::styled( entry.display_name(), - Style::default().fg(DEFAULT_RESULT_NAME_FG).bold().italic(), + Style::default().fg(DEFAULT_RESULT_NAME_FG), )); } // optional line number diff --git a/crates/television/utils/files.rs b/crates/television/utils/files.rs index 815f904..01299b9 100644 --- a/crates/television/utils/files.rs +++ b/crates/television/utils/files.rs @@ -11,7 +11,7 @@ lazy_static::lazy_static! { pub static ref DEFAULT_NUM_THREADS: usize = default_num_threads().into(); } -pub fn walk_builder(path: &Path, n_threads: usize) -> WalkBuilder { +pub(crate) fn walk_builder(path: &Path, n_threads: usize) -> WalkBuilder { let mut builder = WalkBuilder::new(path); // ft-based filtering @@ -23,19 +23,19 @@ pub fn walk_builder(path: &Path, n_threads: usize) -> WalkBuilder { builder } -pub fn get_file_size(path: &Path) -> Option { +pub(crate) fn get_file_size(path: &Path) -> Option { std::fs::metadata(path).ok().map(|m| m.len()) } #[derive(Debug)] -pub enum FileType { +pub(crate) enum FileType { Text, Image, Other, Unknown, } -pub fn is_not_text(bytes: &[u8]) -> Option { +pub(crate) fn is_not_text(bytes: &[u8]) -> Option { let infer = Infer::new(); match infer.get(bytes) { Some(t) => { @@ -56,11 +56,11 @@ pub fn is_not_text(bytes: &[u8]) -> Option { } } -pub fn is_valid_utf8(bytes: &[u8]) -> bool { +pub(crate) fn is_valid_utf8(bytes: &[u8]) -> bool { std::str::from_utf8(bytes).is_ok() } -pub fn is_known_text_extension(path: &Path) -> bool { +pub(crate) fn is_known_text_extension(path: &Path) -> bool { path.extension() .and_then(|ext| ext.to_str()) .is_some_and(|ext| KNOWN_TEXT_FILE_EXTENSIONS.contains(ext)) diff --git a/crates/television/utils/indices.rs b/crates/television/utils/indices.rs index 318277b..250c38f 100644 --- a/crates/television/utils/indices.rs +++ b/crates/television/utils/indices.rs @@ -1,4 +1,4 @@ -pub fn sep_name_and_value_indices( +pub(crate) fn sep_name_and_value_indices( indices: &mut Vec, name_len: u32, ) -> (Vec, Vec, bool, bool) { diff --git a/crates/television/utils/strings.rs b/crates/television/utils/strings.rs index cd44c32..daaf38e 100644 --- a/crates/television/utils/strings.rs +++ b/crates/television/utils/strings.rs @@ -1,7 +1,7 @@ use lazy_static::lazy_static; use std::fmt::Write; -pub fn next_char_boundary(s: &str, start: usize) -> usize { +pub(crate) fn next_char_boundary(s: &str, start: usize) -> usize { let mut i = start; while !s.is_char_boundary(i) { i += 1; @@ -9,7 +9,7 @@ pub fn next_char_boundary(s: &str, start: usize) -> usize { i } -pub fn prev_char_boundary(s: &str, start: usize) -> usize { +pub(crate) fn prev_char_boundary(s: &str, start: usize) -> usize { let mut i = start; while !s.is_char_boundary(i) { i -= 1; @@ -17,7 +17,7 @@ pub fn prev_char_boundary(s: &str, start: usize) -> usize { i } -pub fn slice_at_char_boundaries( +pub(crate) fn slice_at_char_boundaries( s: &str, start_byte_index: usize, end_byte_index: usize, @@ -26,7 +26,7 @@ pub fn slice_at_char_boundaries( ..next_char_boundary(s, end_byte_index)] } -pub fn slice_up_to_char_boundary(s: &str, byte_index: usize) -> &str { +pub(crate) fn slice_up_to_char_boundary(s: &str, byte_index: usize) -> &str { let mut char_index = byte_index; while !s.is_char_boundary(char_index) { char_index -= 1; @@ -64,7 +64,7 @@ const NULL_CHARACTER: char = '\x00'; const UNIT_SEPARATOR_CHARACTER: char = '\u{001F}'; const APPLICATION_PROGRAM_COMMAND_CHARACTER: char = '\u{009F}'; -pub fn replace_nonprintable(input: &[u8], tab_width: usize) -> String { +pub(crate) fn replace_nonprintable(input: &[u8], tab_width: usize) -> String { let mut output = String::new(); let mut idx = 0; @@ -110,7 +110,7 @@ pub fn replace_nonprintable(input: &[u8], tab_width: usize) -> String { const MAX_LINE_LENGTH: usize = 500; -pub fn preprocess_line(line: &str) -> String { +pub(crate) fn preprocess_line(line: &str) -> String { replace_nonprintable( { if line.len() > MAX_LINE_LENGTH { @@ -124,3 +124,15 @@ pub fn preprocess_line(line: &str) -> String { 2, ) } + +pub(crate) fn shrink_with_ellipsis(s: &str, max_length: usize) -> String { + if s.len() <= max_length { + return s.to_string(); + } + + let half_max_length = (max_length / 2) - 2; + let first_half = slice_up_to_char_boundary(s, half_max_length); + let second_half = + slice_at_char_boundaries(s, s.len() - half_max_length, s.len()); + format!("{first_half}…{second_half}") +} diff --git a/crates/television_derive/src/lib.rs b/crates/television_derive/src/lib.rs index b2bdd5e..b618fb7 100644 --- a/crates/television_derive/src/lib.rs +++ b/crates/television_derive/src/lib.rs @@ -36,7 +36,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { use clap::ValueEnum; #[derive(Debug, Clone, ValueEnum, Default, Copy)] - pub enum CliTvChannel { + pub(crate) enum CliTvChannel { #[default] #(#cli_enum_variants),* } @@ -67,7 +67,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { #cli_enum impl CliTvChannel { - pub fn to_channel(self) -> Box { + pub(crate) fn to_channel(self) -> Box { match self { #(#arms),* }