diff --git a/crates/television/app.rs b/crates/television/app.rs index 172ffe9..19d7a07 100644 --- a/crates/television/app.rs +++ b/crates/television/app.rs @@ -1,63 +1,64 @@ /** - The general idea - ┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ │ - │ rendering thread event thread main thread │ - │ │ - │ │ │ │ │ - │ │ - │ │ │ │ │ - │ │ - │ │ │ │ │ - │ ┌───────┴───────┐ │ - │ │ │ │ │ │ - │ │ receive event │ │ - │ │ │ │ │ │ - │ └───────┬───────┘ │ - │ │ │ │ │ - │ ▼ │ - │ │ ┌──────────────────┐ ┌──────────┴─────────┐ │ - │ │ │ │ │ │ - │ │ │send on `event_rx`├────────────►│ receive `event_rx` │ │ - │ │ │ │ │ │ - │ │ └──────────────────┘ └──────────┬─────────┘ │ - │ │ │ - │ │ ▼ │ - │ ┌────────────────────┐ │ - │ │ │ map to action │ │ - │ └──────────┬─────────┘ │ - │ │ ▼ │ - │ ┌────────────────────┐ │ - │ │ │ send on `action_tx`│ │ - │ └──────────┬─────────┘ │ - │ │ │ - │ │ - │ │ ┌──────────┴─────────┐ │ - │ │ receive `action_rx`│ │ - │ │ └──────────┬─────────┘ │ - │ ┌───────────┴────────────┐ ▼ │ - │ │ │ ┌────────────────────┐ │ - │ │ receive `render_rx` │◄────────────────────────────────────────────────┤ dispatch action │ │ - │ │ │ └──────────┬─────────┘ │ - │ └───────────┬────────────┘ │ │ - │ │ │ │ - │ ▼ ▼ │ - │ ┌────────────────────────┐ ┌────────────────────┐ │ - │ │ render components │ │ update components │ │ - │ └────────────────────────┘ └────────────────────┘ │ - │ │ - └──────────────────────────────────────────────────────────────────────────────────────────────────────┘ + The general idea +┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ │ +│ rendering thread event thread main thread │ +│ │ +│ │ │ │ │ +│ │ +│ │ │ │ │ +│ │ +│ │ │ │ │ +│ ┌───────┴───────┐ │ +│ │ │ │ │ │ +│ │ receive event │ │ +│ │ │ │ │ │ +│ └───────┬───────┘ │ +│ │ │ │ │ +│ ▼ │ +│ │ ┌──────────────────┐ ┌──────────┴─────────┐ │ +│ │ │ │ │ │ +│ │ │send on `event_rx`├────────────►│ receive `event_rx` │ │ +│ │ │ │ │ │ +│ │ └──────────────────┘ └──────────┬─────────┘ │ +│ │ │ +│ │ ▼ │ +│ ┌────────────────────┐ │ +│ │ │ map to action │ │ +│ └──────────┬─────────┘ │ +│ │ ▼ │ +│ ┌────────────────────┐ │ +│ │ │ send on `action_tx`│ │ +│ └──────────┬─────────┘ │ +│ │ │ +│ │ +│ │ ┌──────────┴─────────┐ │ +│ │ receive `action_rx`│ │ +│ │ └──────────┬─────────┘ │ +│ ┌───────────┴────────────┐ ▼ │ +│ │ │ ┌────────────────────┐ │ +│ │ receive `render_rx` │◄────────────────────────────────────────────────┤ dispatch action │ │ +│ │ │ └──────────┬─────────┘ │ +│ └───────────┬────────────┘ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌────────────────────────┐ ┌────────────────────┐ │ +│ │ render components │ │ update components │ │ +│ └────────────────────────┘ └────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ use std::sync::Arc; + use color_eyre::Result; use tokio::sync::{mpsc, Mutex}; use tracing::{debug, info}; use crate::channels::{CliTvChannel, TelevisionChannel}; -use crate::television::Television; +use crate::television::{Mode, Television}; use crate::{ action::Action, config::Config, @@ -132,7 +133,7 @@ impl App { frame_rate, is_output_tty, ) - .await + .await }); // event handling loop @@ -221,7 +222,7 @@ impl App { .television .lock() .await - .get_selected_entry()); + .get_selected_entry(Some(Mode::Channel))); } Action::ClearScreen => { self.render_tx.send(RenderingTask::ClearScreen)?; diff --git a/crates/television/channels.rs b/crates/television/channels.rs index a263319..aabe860 100644 --- a/crates/television/channels.rs +++ b/crates/television/channels.rs @@ -85,12 +85,24 @@ pub trait OnAir: Send { /// implement the `OnAir` trait for it. /// /// # Derive +/// ## `CliChannel` /// The `CliChannel` derive macro generates the necessary glue code to /// automatically create the corresponding `CliTvChannel` enum with unit /// variants that can be used to select the channel from the command line. /// It also generates the necessary glue code to automatically create a channel /// instance from the selected CLI enum variant. /// +/// ## `Broadcast` +/// The `Broadcast` derive macro generates the necessary glue code to +/// automatically forward method calls to the corresponding channel variant. +/// This allows to use the `OnAir` trait methods directly on the `TelevisionChannel` +/// enum. In a more straightforward way, it implements the `OnAir` trait for the +/// `TelevisionChannel` enum. +/// +/// ## `UnitChannel` +/// This macro generates an enum with unit variants that can be used instead +/// of carrying the actual channel instances around. It also generates the necessary +/// glue code to automatically create a channel instance from the selected enum variant. #[allow(dead_code, clippy::module_name_repetitions)] #[derive(UnitChannel, CliChannel, Broadcast)] pub enum TelevisionChannel { @@ -113,6 +125,7 @@ pub enum TelevisionChannel { /// The standard input channel. /// /// This channel allows to search through whatever is passed through stdin. + #[exclude_from_cli] Stdin(stdin::Channel), /// The alias channel. /// @@ -121,6 +134,7 @@ pub enum TelevisionChannel { /// The remote control channel. /// /// This channel allows to switch between different channels. + #[exclude_from_cli] RemoteControl(remote_control::RemoteControl), } diff --git a/crates/television/channels/alias.rs b/crates/television/channels/alias.rs index c63538e..a9aab27 100644 --- a/crates/television/channels/alias.rs +++ b/crates/television/channels/alias.rs @@ -9,6 +9,7 @@ use crate::entry::Entry; use crate::fuzzy::MATCHER; use crate::previewers::PreviewType; use crate::utils::indices::sep_name_and_value_indices; +use crate::utils::strings::preprocess_line; #[derive(Debug, Clone)] struct Alias { @@ -217,8 +218,8 @@ async fn load_aliases(injector: Injector) { if let Some(name) = parts.next() { if let Some(value) = parts.next() { return Some(Alias::new( - name.to_string(), - value.to_string(), + preprocess_line(name), + preprocess_line(value), )); } } diff --git a/crates/television/channels/env.rs b/crates/television/channels/env.rs index 7b74aa9..da80acd 100644 --- a/crates/television/channels/env.rs +++ b/crates/television/channels/env.rs @@ -10,6 +10,7 @@ use crate::entry::Entry; use crate::fuzzy::MATCHER; use crate::previewers::PreviewType; use crate::utils::indices::sep_name_and_value_indices; +use crate::utils::strings::preprocess_line; struct EnvVar { name: String, @@ -39,7 +40,7 @@ impl Channel { ); let injector = matcher.injector(); for (name, value) in std::env::vars() { - let _ = injector.push(EnvVar { name, value }, |e, cols| { + let _ = injector.push(EnvVar { name: preprocess_line(&name), value: preprocess_line(&value) }, |e, cols| { cols[0] = (e.name.clone() + &e.value).into(); }); } @@ -92,7 +93,7 @@ impl OnAir for Channel { .matched_items( offset ..(num_entries + offset) - .min(snapshot.matched_item_count()), + .min(snapshot.matched_item_count()), ) .map(move |item| { snapshot.pattern().column_pattern(0).indices( diff --git a/crates/television/channels/files.rs b/crates/television/channels/files.rs index 6bad96b..b1db775 100644 --- a/crates/television/channels/files.rs +++ b/crates/television/channels/files.rs @@ -3,20 +3,17 @@ use nucleo::{ pattern::{CaseMatching, Normalization}, Config, Injector, Nucleo, }; -use std::{os::unix::ffi::OsStrExt, path::PathBuf, sync::Arc}; - -use ignore::DirEntry; +use std::{path::PathBuf, sync::Arc}; use super::{OnAir, TelevisionChannel}; +use crate::entry::Entry; +use crate::fuzzy::MATCHER; use crate::previewers::PreviewType; use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; -use crate::{ - entry::Entry, utils::strings::proportion_of_printable_ascii_characters, -}; -use crate::{fuzzy::MATCHER, utils::strings::PRINTABLE_ASCII_THRESHOLD}; +use crate::utils::strings::preprocess_line; pub struct Channel { - matcher: Nucleo, + matcher: Nucleo, last_pattern: String, result_count: u32, total_count: u32, @@ -101,7 +98,7 @@ impl OnAir for Channel { .matched_items( offset ..(num_entries + offset) - .min(snapshot.matched_item_count()), + .min(snapshot.matched_item_count()), ) .map(move |item| { snapshot.pattern().column_pattern(0).indices( @@ -150,7 +147,7 @@ impl OnAir for Channel { } #[allow(clippy::unused_async)] -async fn load_files(paths: Vec, injector: Injector) { +async fn load_files(paths: Vec, injector: Injector) { if paths.is_empty() { return; } @@ -168,20 +165,9 @@ async fn load_files(paths: Vec, injector: Injector) { Box::new(move |result| { if let Ok(entry) = result { if entry.file_type().unwrap().is_file() { - let file_name = entry.file_name(); - if proportion_of_printable_ascii_characters( - file_name.as_bytes(), - ) < PRINTABLE_ASCII_THRESHOLD - { - return ignore::WalkState::Continue; - } - let _ = injector.push(entry, |e, cols| { - cols[0] = e - .path() - .strip_prefix(¤t_dir) - .unwrap() - .to_string_lossy() - .into(); + let file_path = preprocess_line(&*entry.path().strip_prefix(¤t_dir).unwrap().to_string_lossy()); + let _ = injector.push(file_path, |e, cols| { + cols[0] = e.clone().into(); }); } } diff --git a/crates/television/channels/git_repos.rs b/crates/television/channels/git_repos.rs index 988c83c..31347ea 100644 --- a/crates/television/channels/git_repos.rs +++ b/crates/television/channels/git_repos.rs @@ -1,15 +1,11 @@ use devicons::FileIcon; -use ignore::{overrides::OverrideBuilder, DirEntry}; +use ignore::overrides::OverrideBuilder; use nucleo::{ pattern::{CaseMatching, Normalization}, Config, Nucleo, }; -use parking_lot::Mutex; -use std::{collections::HashSet, path::PathBuf, sync::Arc}; -use tokio::{ - sync::{oneshot, watch}, - task::JoinHandle, -}; +use std::{path::PathBuf, sync::Arc}; +use tokio::task::JoinHandle; use tracing::debug; use crate::{ @@ -20,18 +16,16 @@ use crate::{ }; use crate::channels::OnAir; +use crate::utils::strings::preprocess_line; pub struct Channel { - matcher: Nucleo, + matcher: Nucleo, last_pattern: String, result_count: u32, total_count: u32, running: bool, icon: FileIcon, crawl_handle: JoinHandle<()>, - git_dirs_cache: Arc>>, - // TODO: implement cache validation/invalidation - cache_valid: Arc>, } impl Channel { @@ -42,15 +36,9 @@ impl Channel { None, 1, ); - let entry_cache = Arc::new(Mutex::new(HashSet::new())); - let cache_valid = Arc::new(Mutex::new(false)); - // start loading files in the background - // PERF: store the results somewhere in a cache let crawl_handle = tokio::spawn(crawl_for_repos( std::env::home_dir().expect("Could not get home directory"), matcher.injector(), - entry_cache.clone(), - cache_valid.clone(), )); Channel { matcher, @@ -60,8 +48,6 @@ impl Channel { running: false, icon: FileIcon::from("git"), crawl_handle, - git_dirs_cache: entry_cache, - cache_valid, } } @@ -201,9 +187,7 @@ fn get_ignored_paths() -> Vec { #[allow(clippy::unused_async)] async fn crawl_for_repos( starting_point: PathBuf, - injector: nucleo::Injector, - entry_cache: Arc>>, - cache_valid: Arc>, + injector: nucleo::Injector, ) { let mut walker_overrides_builder = OverrideBuilder::new(&starting_point); walker_overrides_builder.add(".git").unwrap(); @@ -217,29 +201,20 @@ async fn crawl_for_repos( walker.run(|| { let injector = injector.clone(); - let entry_cache = entry_cache.clone(); Box::new(move |result| { if let Ok(entry) = result { if entry.file_type().unwrap().is_dir() { - // if the dir is already in cache, skip it - let path = entry.path().to_string_lossy().to_string(); - if entry_cache.lock().contains(&path) { - return ignore::WalkState::Skip; - } - // if the entry is a .git directory, add its parent to the list - // of git repos and cache it + // if the entry is a .git directory, add its parent to the list of git repos if entry.path().ends_with(".git") { - let parent_path = entry + let parent_path = preprocess_line(&*entry .path() .parent() .unwrap() - .to_string_lossy() - .to_string(); + .to_string_lossy()); debug!("Found git repo: {:?}", parent_path); - let _ = injector.push(entry, |_e, cols| { - cols[0] = parent_path.clone().into(); + let _ = injector.push(parent_path, |e, cols| { + cols[0] = e.clone().into(); }); - entry_cache.lock().insert(parent_path); return ignore::WalkState::Skip; } } @@ -247,6 +222,4 @@ async fn crawl_for_repos( ignore::WalkState::Continue }) }); - - *cache_valid.lock() = true; } diff --git a/crates/television/channels/stdin.rs b/crates/television/channels/stdin.rs index 168c910..6364a2f 100644 --- a/crates/television/channels/stdin.rs +++ b/crates/television/channels/stdin.rs @@ -4,11 +4,11 @@ use std::{io::BufRead, sync::Arc}; use devicons::FileIcon; use nucleo::{Config, Nucleo}; +use super::OnAir; use crate::entry::Entry; use crate::fuzzy::MATCHER; use crate::previewers::PreviewType; - -use super::OnAir; +use crate::utils::strings::preprocess_line; pub struct Channel { matcher: Nucleo, @@ -25,7 +25,7 @@ impl Channel { pub fn new() -> Self { let mut lines = Vec::new(); for line in std::io::stdin().lock().lines().map_while(Result::ok) { - lines.push(line); + lines.push(preprocess_line(&line)); } let matcher = Nucleo::new( Config::DEFAULT, diff --git a/crates/television/entry.rs b/crates/television/entry.rs index 93be66e..448179a 100644 --- a/crates/television/entry.rs +++ b/crates/television/entry.rs @@ -1,6 +1,7 @@ use devicons::FileIcon; use crate::previewers::PreviewType; +use crate::utils::strings::preprocess_line; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct Entry { @@ -17,7 +18,7 @@ pub struct Entry { impl Entry { pub fn new(name: String, preview_type: PreviewType) -> Self { Self { - name, + name: preprocess_line(&name), display_name: None, value: None, name_match_ranges: None, @@ -29,12 +30,12 @@ impl Entry { } pub fn with_display_name(mut self, display_name: String) -> Self { - self.display_name = Some(display_name); + self.display_name = Some(preprocess_line(&display_name)); self } pub fn with_value(mut self, value: String) -> Self { - self.value = Some(value); + self.value = Some(preprocess_line(&value)); self } diff --git a/crates/television/picker.rs b/crates/television/picker.rs index 3ccd89f..ad91ea7 100644 --- a/crates/television/picker.rs +++ b/crates/television/picker.rs @@ -86,7 +86,7 @@ impl Picker { } } else { self.view_offset = total_items.saturating_sub(height - 2); - self.select(Some((total_items).saturating_sub(1))); + self.select(Some(total_items.saturating_sub(1))); self.relative_select(Some(height - 3)); } } diff --git a/crates/television/previewers/files.rs b/crates/television/previewers/files.rs index 4e161aa..c5cdcd2 100644 --- a/crates/television/previewers/files.rs +++ b/crates/television/previewers/files.rs @@ -165,7 +165,9 @@ impl FilePreviewer { entry_c.name ); let lines: Vec = - reader.lines().map_while(Result::ok).collect(); + reader.lines().map_while(Result::ok).map( + |line| preprocess_line(&line), + ).collect(); match syntax::compute_highlights_for_path( &PathBuf::from(&entry_c.name), diff --git a/crates/television/television.rs b/crates/television/television.rs index c3faed9..305aff3 100644 --- a/crates/television/television.rs +++ b/crates/television/television.rs @@ -108,8 +108,8 @@ impl Television { } #[must_use] - pub fn get_selected_entry(&mut self) -> Option { - match self.mode { + pub fn get_selected_entry(&mut self, mode: Option) -> Option { + match mode.unwrap_or(self.mode) { Mode::Channel => self.results_picker.selected().and_then(|i| { self.channel.get_result(u32::try_from(i).unwrap()) }), @@ -298,7 +298,7 @@ impl Television { Mode::SendToChannel => {} }, Action::SelectEntry => { - if let Some(entry) = self.get_selected_entry() { + if let Some(entry) = self.get_selected_entry(None) { match self.mode { Mode::Channel => self .action_tx @@ -378,7 +378,7 @@ impl Television { self.draw_input_box(f, &layout)?; let selected_entry = - self.get_selected_entry().unwrap_or(ENTRY_PLACEHOLDER); + self.get_selected_entry(Some(Mode::Channel)).unwrap_or(ENTRY_PLACEHOLDER); let preview = block_on(self.previewer.preview(&selected_entry)); // top right block: preview title diff --git a/crates/television/ui.rs b/crates/television/ui.rs index 6746a1a..3ba67f0 100644 --- a/crates/television/ui.rs +++ b/crates/television/ui.rs @@ -10,6 +10,7 @@ pub mod preview; mod remote_control; pub mod results; pub mod spinner; +mod mode; // input //const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200); //const DEFAULT_RESULTS_COUNT_FG: Color = Color::Rgb(150, 150, 150); diff --git a/crates/television/ui/help.rs b/crates/television/ui/help.rs index 5e95c1d..380e283 100644 --- a/crates/television/ui/help.rs +++ b/crates/television/ui/help.rs @@ -1,17 +1,18 @@ use crate::television::Television; use crate::ui::layout::Layout; use crate::ui::logo::build_logo_paragraph; +use crate::ui::mode::mode_color; use ratatui::layout::Rect; use ratatui::prelude::{Color, Style}; use ratatui::widgets::{Block, BorderType, Borders, Padding}; use ratatui::Frame; -pub fn draw_logo_block(f: &mut Frame, area: Rect) { +pub fn draw_logo_block(f: &mut Frame, area: Rect, color: Color) { let logo_block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(Color::Blue)) - .style(Style::default().fg(Color::Yellow)) + .style(Style::default().fg(color)) .padding(Padding::horizontal(1)); let logo_paragraph = build_logo_paragraph().block(logo_block); @@ -27,7 +28,7 @@ impl Television { ) -> color_eyre::Result<()> { self.draw_metadata_block(f, layout.help_bar_left); self.draw_keymaps_block(f, layout.help_bar_middle)?; - draw_logo_block(f, layout.help_bar_right); + draw_logo_block(f, layout.help_bar_right, mode_color(self.mode)); Ok(()) } diff --git a/crates/television/ui/keymap.rs b/crates/television/ui/keymap.rs index d3df4ea..ee1553b 100644 --- a/crates/television/ui/keymap.rs +++ b/crates/television/ui/keymap.rs @@ -7,6 +7,7 @@ use ratatui::{ }; use std::collections::HashMap; +use crate::ui::mode::mode_color; use crate::{ action::Action, event::Key, @@ -14,7 +15,6 @@ use crate::{ }; const ACTION_COLOR: Color = Color::DarkGray; -const KEY_COLOR: Color = Color::LightYellow; impl Television { pub fn build_keymap_table<'a>(&self) -> Result> { @@ -29,6 +29,7 @@ impl Television { fn build_keymap_table_for_channel<'a>(&self) -> Result> { let keymap = self.keymap_for_mode()?; + let key_color = mode_color(self.mode); // Results navigation let prev = keys_for_action(keymap, &Action::SelectPrevEntry); @@ -36,6 +37,7 @@ impl Television { let results_row = Row::new(build_cells_for_key_groups( "↕ Results navigation", vec![prev, next], + key_color, )); // Preview navigation @@ -46,6 +48,7 @@ impl Television { let preview_row = Row::new(build_cells_for_key_groups( "↕ Preview navigation", vec![up_keys, down_keys], + key_color, )); // Select entry @@ -53,6 +56,7 @@ impl Television { let select_entry_row = Row::new(build_cells_for_key_groups( "✓ Select entry", vec![select_entry_keys], + key_color, )); // Send to channel @@ -61,21 +65,23 @@ impl Television { let send_to_channel_row = Row::new(build_cells_for_key_groups( "⇉ Send results to", vec![send_to_channel_keys], + key_color, )); // Switch channels let switch_channels_keys = keys_for_action(keymap, &Action::ToggleRemoteControl); let switch_channels_row = Row::new(build_cells_for_key_groups( - "⨀ Remote control", + "⨀ Toggle Remote control", vec![switch_channels_keys], + key_color, )); // MISC line (quit, help, etc.) // Quit ⏼ let quit_keys = keys_for_action(keymap, &Action::Quit); let quit_row = - Row::new(build_cells_for_key_groups("⏼ Quit", vec![quit_keys])); + Row::new(build_cells_for_key_groups("⏼ Quit", vec![quit_keys], key_color)); let widths = vec![Constraint::Fill(1), Constraint::Fill(2)]; @@ -96,34 +102,38 @@ impl Television { &self, ) -> Result> { let keymap = self.keymap_for_mode()?; + let key_color = mode_color(self.mode); // Results navigation let prev = keys_for_action(keymap, &Action::SelectPrevEntry); let next = keys_for_action(keymap, &Action::SelectNextEntry); let results_row = Row::new(build_cells_for_key_groups( - "↕ Results", + "↕ Browse channels", vec![prev, next], + key_color, )); // Select entry let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry); let select_entry_row = Row::new(build_cells_for_key_groups( - "Select entry", + "✓ Select channel", vec![select_entry_keys], + key_color, )); // Switch channels let switch_channels_keys = keys_for_action(keymap, &Action::ToggleRemoteControl); let switch_channels_row = Row::new(build_cells_for_key_groups( - "Switch channels", + "⨀ Toggle Remote control", vec![switch_channels_keys], + key_color, )); // Quit let quit_keys = keys_for_action(keymap, &Action::Quit); let quit_row = - Row::new(build_cells_for_key_groups("Quit", vec![quit_keys])); + Row::new(build_cells_for_key_groups("⏼ Quit", vec![quit_keys], key_color)); Ok(Table::new( vec![results_row, select_entry_row, switch_channels_row, quit_row], @@ -173,6 +183,7 @@ impl Television { fn build_cells_for_key_groups( group_name: &str, key_groups: Vec>, + key_color: Color, ) -> Vec { if key_groups.is_empty() || key_groups.iter().all(Vec::is_empty) { @@ -190,13 +201,13 @@ fn build_cells_for_key_groups( let key_group_spans: Vec = non_empty_groups .map(|keys| { let key_group = keys.join(", "); - Span::styled(key_group, Style::default().fg(KEY_COLOR)) + Span::styled(key_group, Style::default().fg(key_color)) }) .collect(); key_group_spans.iter().enumerate().for_each(|(i, span)| { spans.push(span.clone()); if i < key_group_spans.len() - 1 { - spans.push(Span::styled(" / ", Style::default().fg(KEY_COLOR))); + spans.push(Span::styled(" / ", Style::default().fg(key_color))); } }); diff --git a/crates/television/ui/metadata.rs b/crates/television/ui/metadata.rs index 6d4e9c1..1719ed2 100644 --- a/crates/television/ui/metadata.rs +++ b/crates/television/ui/metadata.rs @@ -6,93 +6,92 @@ use ratatui::{ }; use crate::television::Television; +use crate::ui::mode::mode_color; -// television 0.1.6 -// target triple: aarch64-apple-darwin -// build: 1.82.0 (2024-10-24) -// current_channel: git_repos -// current_mode: channel + +const METADATA_FIELD_NAME_COLOR: Color = Color::DarkGray; +const METADATA_FIELD_VALUE_COLOR: Color = Color::Gray; impl Television { pub fn build_metadata_table<'a>(&self) -> Table<'a> { let version_row = Row::new(vec![ Cell::from(Span::styled( "version: ", - Style::default().fg(Color::DarkGray), + Style::default().fg(METADATA_FIELD_NAME_COLOR), )), Cell::from(Span::styled( env!("CARGO_PKG_VERSION"), - Style::default().fg(Color::LightYellow), + Style::default().fg(METADATA_FIELD_VALUE_COLOR), )), ]); let target_triple_row = Row::new(vec![ Cell::from(Span::styled( "target triple: ", - Style::default().fg(Color::DarkGray), + Style::default().fg(METADATA_FIELD_NAME_COLOR), )), Cell::from(Span::styled( env!("VERGEN_CARGO_TARGET_TRIPLE"), - Style::default().fg(Color::LightYellow), + Style::default().fg(METADATA_FIELD_VALUE_COLOR), )), ]); let build_row = Row::new(vec![ Cell::from(Span::styled( "build: ", - Style::default().fg(Color::DarkGray), + Style::default().fg(METADATA_FIELD_NAME_COLOR), )), Cell::from(Span::styled( env!("VERGEN_RUSTC_SEMVER"), - Style::default().fg(Color::LightYellow), + Style::default().fg(METADATA_FIELD_VALUE_COLOR), )), Cell::from(Span::styled( " (", - Style::default().fg(Color::DarkGray), + Style::default().fg(METADATA_FIELD_NAME_COLOR), )), Cell::from(Span::styled( env!("VERGEN_BUILD_DATE"), - Style::default().fg(Color::LightYellow), + Style::default().fg(METADATA_FIELD_VALUE_COLOR), )), Cell::from(Span::styled( ")", - Style::default().fg(Color::DarkGray), + Style::default().fg(METADATA_FIELD_NAME_COLOR), )), ]); let current_dir_row = Row::new(vec![ Cell::from(Span::styled( "current directory: ", - Style::default().fg(Color::DarkGray), + Style::default().fg(METADATA_FIELD_NAME_COLOR), )), Cell::from(Span::styled( std::env::current_dir() .expect("Could not get current directory") .display() .to_string(), - Style::default().fg(Color::LightYellow), + Style::default().fg(METADATA_FIELD_VALUE_COLOR), )), ]); let current_channel_row = Row::new(vec![ Cell::from(Span::styled( "current channel: ", - Style::default().fg(Color::DarkGray), + Style::default().fg(METADATA_FIELD_NAME_COLOR), )), Cell::from(Span::styled( self.current_channel().to_string(), - Style::default().fg(Color::LightYellow), + Style::default().fg(METADATA_FIELD_VALUE_COLOR), )), ]); let current_mode_row = Row::new(vec![ Cell::from(Span::styled( "current mode: ", - Style::default().fg(Color::DarkGray), + Style::default().fg(METADATA_FIELD_NAME_COLOR), )), Cell::from(Span::styled( self.mode.to_string(), - Style::default().fg(Color::LightYellow), + Style::default().fg(mode_color(self.mode)), )), ]); diff --git a/crates/television/ui/mode.rs b/crates/television/ui/mode.rs new file mode 100644 index 0000000..f21f887 --- /dev/null +++ b/crates/television/ui/mode.rs @@ -0,0 +1,15 @@ +use crate::television::Mode; +use ratatui::style::Color; + +const CHANNEL_COLOR: Color = Color::LightYellow; +const REMOTE_CONTROL_COLOR: Color = Color::LightMagenta; +const SEND_TO_CHANNEL_COLOR: Color = Color::LightCyan; + + +pub fn mode_color(mode: Mode) -> Color { + match mode { + Mode::Channel => CHANNEL_COLOR, + Mode::RemoteControl => REMOTE_CONTROL_COLOR, + Mode::SendToChannel => SEND_TO_CHANNEL_COLOR, + } +} \ No newline at end of file diff --git a/crates/television/ui/remote_control.rs b/crates/television/ui/remote_control.rs index c82e61c..e1fa569 100644 --- a/crates/television/ui/remote_control.rs +++ b/crates/television/ui/remote_control.rs @@ -2,7 +2,8 @@ use crate::channels::OnAir; use crate::television::Television; use crate::ui::get_border_style; use crate::ui::logo::build_remote_logo_paragraph; -use crate::ui::results::build_results_list; +use crate::ui::mode::mode_color; +use crate::ui::results::{build_results_list, ResultsListColors}; use color_eyre::eyre::Result; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::prelude::Style; @@ -32,7 +33,7 @@ impl Television { .split(*area); self.draw_rc_channels(f, &layout[0])?; self.draw_rc_input(f, &layout[1])?; - draw_rc_logo(f, layout[2]); + draw_rc_logo(f, layout[2], mode_color(self.mode)); Ok(()) } @@ -56,7 +57,7 @@ impl Television { ); let channel_list = - build_results_list(rc_block, &entries, ListDirection::TopToBottom); + build_results_list(rc_block, &entries, ListDirection::TopToBottom, Some(ResultsListColors::default().result_name_fg(mode_color(self.mode)))); f.render_stateful_widget( channel_list, @@ -132,12 +133,9 @@ impl Television { } } -fn draw_rc_logo(f: &mut Frame, area: Rect) { +fn draw_rc_logo(f: &mut Frame, area: Rect, color: Color) { let logo_block = Block::default() - // .borders(Borders::ALL) - // .border_type(BorderType::Rounded) - // .border_style(Style::default().fg(Color::Blue)) - .style(Style::default().fg(Color::Yellow)); + .style(Style::default().fg(color)); let logo_paragraph = build_remote_logo_paragraph() .alignment(Alignment::Center) diff --git a/crates/television/ui/results.rs b/crates/television/ui/results.rs index 26e6b15..15ad76a 100644 --- a/crates/television/ui/results.rs +++ b/crates/television/ui/results.rs @@ -19,14 +19,56 @@ const DEFAULT_RESULT_PREVIEW_FG: Color = Color::Rgb(150, 150, 150); const DEFAULT_RESULT_LINE_NUMBER_FG: Color = Color::Yellow; const DEFAULT_RESULT_SELECTED_BG: Color = Color::Rgb(50, 50, 50); +pub struct ResultsListColors { + pub result_name_fg: Color, + pub result_preview_fg: Color, + pub result_line_number_fg: Color, + pub result_selected_bg: Color, +} + +impl Default for ResultsListColors { + fn default() -> Self { + Self { + result_name_fg: DEFAULT_RESULT_NAME_FG, + result_preview_fg: DEFAULT_RESULT_PREVIEW_FG, + result_line_number_fg: DEFAULT_RESULT_LINE_NUMBER_FG, + result_selected_bg: DEFAULT_RESULT_SELECTED_BG, + } + } +} + +impl ResultsListColors { + pub fn result_name_fg(mut self, color: Color) -> Self { + self.result_name_fg = color; + self + } + + pub fn result_preview_fg(mut self, color: Color) -> Self { + self.result_preview_fg = color; + self + } + + pub fn result_line_number_fg(mut self, color: Color) -> Self { + self.result_line_number_fg = color; + self + } + + pub fn result_selected_bg(mut self, color: Color) -> Self { + self.result_selected_bg = color; + self + } +} + pub fn build_results_list<'a, 'b>( results_block: Block<'b>, entries: &'a [Entry], list_direction: ListDirection, + results_list_colors: Option, ) -> List<'a> where 'b: 'a, { + let results_list_colors = results_list_colors.unwrap_or_default(); List::new(entries.iter().map(|entry| { let mut spans = Vec::new(); // optional icon @@ -50,7 +92,7 @@ where last_match_end, start, ), - Style::default().fg(DEFAULT_RESULT_NAME_FG), + Style::default().fg(results_list_colors.result_name_fg), )); spans.push(Span::styled( slice_at_char_boundaries(&entry.name, start, end), @@ -60,19 +102,19 @@ where } spans.push(Span::styled( &entry.name[next_char_boundary(&entry.name, last_match_end)..], - Style::default().fg(DEFAULT_RESULT_NAME_FG), + Style::default().fg(results_list_colors.result_name_fg), )); } else { spans.push(Span::styled( entry.display_name(), - Style::default().fg(DEFAULT_RESULT_NAME_FG), + Style::default().fg(results_list_colors.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), + Style::default().fg(results_list_colors.result_line_number_fg), )); } // optional preview @@ -92,7 +134,7 @@ where last_match_end, start, ), - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + Style::default().fg(results_list_colors.result_preview_fg), )); spans.push(Span::styled( slice_at_char_boundaries(preview, start, end), @@ -105,22 +147,22 @@ where preview, preview_match_ranges.last().unwrap().1 as usize, )..], - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + Style::default().fg(results_list_colors.result_preview_fg), )); } } else { spans.push(Span::styled( preview, - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + Style::default().fg(results_list_colors.result_preview_fg), )); } } Line::from(spans) })) - .direction(list_direction) - .highlight_style(Style::default().bg(DEFAULT_RESULT_SELECTED_BG)) - .highlight_symbol("> ") - .block(results_block) + .direction(list_direction) + .highlight_style(Style::default().bg(results_list_colors.result_selected_bg)) + .highlight_symbol("> ") + .block(results_block) } impl Television { @@ -152,6 +194,7 @@ impl Television { results_block, &entries, ListDirection::BottomToTop, + None, ); f.render_stateful_widget( diff --git a/crates/television/utils/strings.rs b/crates/television/utils/strings.rs index 5b8ba50..d6a5ad8 100644 --- a/crates/television/utils/strings.rs +++ b/crates/television/utils/strings.rs @@ -1,5 +1,6 @@ use lazy_static::lazy_static; use std::fmt::Write; +use tracing::debug; pub fn next_char_boundary(s: &str, start: usize) -> usize { let mut i = start; @@ -53,7 +54,6 @@ lazy_static! { } pub const EMPTY_STRING: &str = ""; -pub const FOUR_SPACES: &str = " "; pub const TAB_WIDTH: usize = 4; const SPACE_CHARACTER: char = ' '; @@ -78,11 +78,12 @@ pub fn replace_nonprintable(input: &[u8], tab_width: usize) -> String { // space SPACE_CHARACTER => output.push(' '), // tab - TAB_CHARACTER => output.push_str(&" ".repeat(tab_width)), - // line feed - LINE_FEED_CHARACTER => { - output.push_str("␊\x0A"); + TAB_CHARACTER => { + output.push_str(&" ".repeat(tab_width)); } + // line feed + LINE_FEED_CHARACTER => {} + // ASCII control characters from 0x00 to 0x1F // + control characters from \u{007F} to \u{009F} NULL_CHARACTER..=UNIT_SEPARATOR_CHARACTER @@ -91,12 +92,15 @@ pub fn replace_nonprintable(input: &[u8], tab_width: usize) -> String { } // don't print BOMs BOM_CHARACTER => {} - // unicode characters above 0x0700 seem unstable with ratatui + // Unicode characters above 0x0700 seem unstable with ratatui c if c > '\u{0700}' => { output.push(*NULL_SYMBOL); } // everything else - c => output.push(c), + c => { + debug!("char: {:?}", c); + output.push(c) + } } } else { write!(output, "\\x{:02X}", input[idx]).ok(); @@ -123,7 +127,7 @@ pub fn proportion_of_printable_ascii_characters(buffer: &[u8]) -> f32 { printable as f32 / buffer.len() as f32 } -const MAX_LINE_LENGTH: usize = 500; +const MAX_LINE_LENGTH: usize = 300; pub fn preprocess_line(line: &str) -> String { replace_nonprintable( @@ -134,8 +138,8 @@ pub fn preprocess_line(line: &str) -> String { line } } - .trim_end_matches(['\r', '\n', '\0']) - .as_bytes(), + .trim_end_matches(['\r', '\n', '\0']) + .as_bytes(), TAB_WIDTH, ) } @@ -169,11 +173,16 @@ mod tests { #[test] fn test_replace_nonprintable_tab() { test_replace_nonprintable("Hello\tWorld!", "Hello World!"); + test_replace_nonprintable( + " -- AND +", + " -- AND", + ) } #[test] fn test_replace_nonprintable_line_feed() { - test_replace_nonprintable("Hello\nWorld!", "Hello␊\nWorld!"); + test_replace_nonprintable("Hello\nWorld!", "HelloWorld!"); } #[test] diff --git a/crates/television_derive/src/lib.rs b/crates/television_derive/src/lib.rs index 1f893ef..a95ea15 100644 --- a/crates/television_derive/src/lib.rs +++ b/crates/television_derive/src/lib.rs @@ -22,7 +22,10 @@ use quote::quote; /// ``` /// /// The `CliChannel` enum is used to select channels from the command line. -#[proc_macro_derive(CliChannel)] +/// +/// Any variant that should not be included in the CLI should be annotated with +/// `#[exclude_from_cli]`. +#[proc_macro_derive(CliChannel, attributes(exclude_from_cli))] pub fn cli_channel_derive(input: TokenStream) -> TokenStream { // Construct a representation of Rust code as a syntax tree // that we can manipulate @@ -32,8 +35,9 @@ pub fn cli_channel_derive(input: TokenStream) -> TokenStream { impl_cli_channel(&ast) } -/// List of variant names that should be ignored when generating the CliTvChannel enum. -const VARIANT_BLACKLIST: [&str; 2] = ["Stdin", "RemoteControl"]; +fn has_exclude_attr(attrs: &[syn::Attribute]) -> bool { + attrs.iter().any(|attr| attr.path().is_ident("exclude_from_cli")) +} fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { // check that the struct is an enum @@ -52,7 +56,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { // create the CliTvChannel enum let cli_enum_variants = variants .iter() - .filter(|v| !VARIANT_BLACKLIST.contains(&v.ident.to_string().as_str())) + .filter(|variant| !has_exclude_attr(&variant.attrs)) .map(|variant| { let variant_name = &variant.ident; quote! { @@ -74,7 +78,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { // Generate the match arms for the `to_channel` method let arms = variants.iter().filter( - |variant| !VARIANT_BLACKLIST.contains(&variant.ident.to_string().as_str()), + |variant| !has_exclude_attr(&variant.attrs) ).map(|variant| { let variant_name = &variant.ident;