remote controls

This commit is contained in:
alexpasmantier 2024-10-27 23:30:50 +01:00
parent 76d32a01c2
commit 8223e073a0
20 changed files with 256 additions and 196 deletions

View File

@ -52,12 +52,13 @@
*/ */
use std::sync::Arc; use std::sync::Arc;
use color_eyre::Result; use color_eyre::Result;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use tracing::{debug, info}; use tracing::{debug, info};
use crate::channels::{CliTvChannel, TelevisionChannel}; use crate::channels::{CliTvChannel, TelevisionChannel};
use crate::television::Television; use crate::television::{Mode, Television};
use crate::{ use crate::{
action::Action, action::Action,
config::Config, config::Config,
@ -221,7 +222,7 @@ impl App {
.television .television
.lock() .lock()
.await .await
.get_selected_entry()); .get_selected_entry(Some(Mode::Channel)));
} }
Action::ClearScreen => { Action::ClearScreen => {
self.render_tx.send(RenderingTask::ClearScreen)?; self.render_tx.send(RenderingTask::ClearScreen)?;

View File

@ -85,12 +85,24 @@ pub trait OnAir: Send {
/// implement the `OnAir` trait for it. /// implement the `OnAir` trait for it.
/// ///
/// # Derive /// # Derive
/// ## `CliChannel`
/// The `CliChannel` derive macro generates the necessary glue code to /// The `CliChannel` derive macro generates the necessary glue code to
/// automatically create the corresponding `CliTvChannel` enum with unit /// automatically create the corresponding `CliTvChannel` enum with unit
/// variants that can be used to select the channel from the command line. /// 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 /// It also generates the necessary glue code to automatically create a channel
/// instance from the selected CLI enum variant. /// 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)] #[allow(dead_code, clippy::module_name_repetitions)]
#[derive(UnitChannel, CliChannel, Broadcast)] #[derive(UnitChannel, CliChannel, Broadcast)]
pub enum TelevisionChannel { pub enum TelevisionChannel {
@ -113,6 +125,7 @@ pub enum TelevisionChannel {
/// The standard input channel. /// The standard input channel.
/// ///
/// This channel allows to search through whatever is passed through stdin. /// This channel allows to search through whatever is passed through stdin.
#[exclude_from_cli]
Stdin(stdin::Channel), Stdin(stdin::Channel),
/// The alias channel. /// The alias channel.
/// ///
@ -121,6 +134,7 @@ pub enum TelevisionChannel {
/// The remote control channel. /// The remote control channel.
/// ///
/// This channel allows to switch between different channels. /// This channel allows to switch between different channels.
#[exclude_from_cli]
RemoteControl(remote_control::RemoteControl), RemoteControl(remote_control::RemoteControl),
} }

View File

@ -9,6 +9,7 @@ use crate::entry::Entry;
use crate::fuzzy::MATCHER; use crate::fuzzy::MATCHER;
use crate::previewers::PreviewType; use crate::previewers::PreviewType;
use crate::utils::indices::sep_name_and_value_indices; use crate::utils::indices::sep_name_and_value_indices;
use crate::utils::strings::preprocess_line;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Alias { struct Alias {
@ -217,8 +218,8 @@ async fn load_aliases(injector: Injector<Alias>) {
if let Some(name) = parts.next() { if let Some(name) = parts.next() {
if let Some(value) = parts.next() { if let Some(value) = parts.next() {
return Some(Alias::new( return Some(Alias::new(
name.to_string(), preprocess_line(name),
value.to_string(), preprocess_line(value),
)); ));
} }
} }

View File

@ -10,6 +10,7 @@ use crate::entry::Entry;
use crate::fuzzy::MATCHER; use crate::fuzzy::MATCHER;
use crate::previewers::PreviewType; use crate::previewers::PreviewType;
use crate::utils::indices::sep_name_and_value_indices; use crate::utils::indices::sep_name_and_value_indices;
use crate::utils::strings::preprocess_line;
struct EnvVar { struct EnvVar {
name: String, name: String,
@ -39,7 +40,7 @@ impl Channel {
); );
let injector = matcher.injector(); let injector = matcher.injector();
for (name, value) in std::env::vars() { 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(); cols[0] = (e.name.clone() + &e.value).into();
}); });
} }

View File

@ -3,20 +3,17 @@ use nucleo::{
pattern::{CaseMatching, Normalization}, pattern::{CaseMatching, Normalization},
Config, Injector, Nucleo, Config, Injector, Nucleo,
}; };
use std::{os::unix::ffi::OsStrExt, path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use ignore::DirEntry;
use super::{OnAir, TelevisionChannel}; use super::{OnAir, TelevisionChannel};
use crate::entry::Entry;
use crate::fuzzy::MATCHER;
use crate::previewers::PreviewType; use crate::previewers::PreviewType;
use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS};
use crate::{ use crate::utils::strings::preprocess_line;
entry::Entry, utils::strings::proportion_of_printable_ascii_characters,
};
use crate::{fuzzy::MATCHER, utils::strings::PRINTABLE_ASCII_THRESHOLD};
pub struct Channel { pub struct Channel {
matcher: Nucleo<DirEntry>, matcher: Nucleo<String>,
last_pattern: String, last_pattern: String,
result_count: u32, result_count: u32,
total_count: u32, total_count: u32,
@ -150,7 +147,7 @@ impl OnAir for Channel {
} }
#[allow(clippy::unused_async)] #[allow(clippy::unused_async)]
async fn load_files(paths: Vec<PathBuf>, injector: Injector<DirEntry>) { async fn load_files(paths: Vec<PathBuf>, injector: Injector<String>) {
if paths.is_empty() { if paths.is_empty() {
return; return;
} }
@ -168,20 +165,9 @@ async fn load_files(paths: Vec<PathBuf>, injector: Injector<DirEntry>) {
Box::new(move |result| { Box::new(move |result| {
if let Ok(entry) = result { if let Ok(entry) = result {
if entry.file_type().unwrap().is_file() { if entry.file_type().unwrap().is_file() {
let file_name = entry.file_name(); let file_path = preprocess_line(&*entry.path().strip_prefix(&current_dir).unwrap().to_string_lossy());
if proportion_of_printable_ascii_characters( let _ = injector.push(file_path, |e, cols| {
file_name.as_bytes(), cols[0] = e.clone().into();
) < PRINTABLE_ASCII_THRESHOLD
{
return ignore::WalkState::Continue;
}
let _ = injector.push(entry, |e, cols| {
cols[0] = e
.path()
.strip_prefix(&current_dir)
.unwrap()
.to_string_lossy()
.into();
}); });
} }
} }

View File

@ -1,15 +1,11 @@
use devicons::FileIcon; use devicons::FileIcon;
use ignore::{overrides::OverrideBuilder, DirEntry}; use ignore::overrides::OverrideBuilder;
use nucleo::{ use nucleo::{
pattern::{CaseMatching, Normalization}, pattern::{CaseMatching, Normalization},
Config, Nucleo, Config, Nucleo,
}; };
use parking_lot::Mutex; use std::{path::PathBuf, sync::Arc};
use std::{collections::HashSet, path::PathBuf, sync::Arc}; use tokio::task::JoinHandle;
use tokio::{
sync::{oneshot, watch},
task::JoinHandle,
};
use tracing::debug; use tracing::debug;
use crate::{ use crate::{
@ -20,18 +16,16 @@ use crate::{
}; };
use crate::channels::OnAir; use crate::channels::OnAir;
use crate::utils::strings::preprocess_line;
pub struct Channel { pub struct Channel {
matcher: Nucleo<DirEntry>, matcher: Nucleo<String>,
last_pattern: String, last_pattern: String,
result_count: u32, result_count: u32,
total_count: u32, total_count: u32,
running: bool, running: bool,
icon: FileIcon, icon: FileIcon,
crawl_handle: JoinHandle<()>, crawl_handle: JoinHandle<()>,
git_dirs_cache: Arc<Mutex<HashSet<String>>>,
// TODO: implement cache validation/invalidation
cache_valid: Arc<Mutex<bool>>,
} }
impl Channel { impl Channel {
@ -42,15 +36,9 @@ impl Channel {
None, None,
1, 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( let crawl_handle = tokio::spawn(crawl_for_repos(
std::env::home_dir().expect("Could not get home directory"), std::env::home_dir().expect("Could not get home directory"),
matcher.injector(), matcher.injector(),
entry_cache.clone(),
cache_valid.clone(),
)); ));
Channel { Channel {
matcher, matcher,
@ -60,8 +48,6 @@ impl Channel {
running: false, running: false,
icon: FileIcon::from("git"), icon: FileIcon::from("git"),
crawl_handle, crawl_handle,
git_dirs_cache: entry_cache,
cache_valid,
} }
} }
@ -201,9 +187,7 @@ fn get_ignored_paths() -> Vec<PathBuf> {
#[allow(clippy::unused_async)] #[allow(clippy::unused_async)]
async fn crawl_for_repos( async fn crawl_for_repos(
starting_point: PathBuf, starting_point: PathBuf,
injector: nucleo::Injector<DirEntry>, injector: nucleo::Injector<String>,
entry_cache: Arc<Mutex<HashSet<String>>>,
cache_valid: Arc<Mutex<bool>>,
) { ) {
let mut walker_overrides_builder = OverrideBuilder::new(&starting_point); let mut walker_overrides_builder = OverrideBuilder::new(&starting_point);
walker_overrides_builder.add(".git").unwrap(); walker_overrides_builder.add(".git").unwrap();
@ -217,29 +201,20 @@ async fn crawl_for_repos(
walker.run(|| { walker.run(|| {
let injector = injector.clone(); let injector = injector.clone();
let entry_cache = entry_cache.clone();
Box::new(move |result| { Box::new(move |result| {
if let Ok(entry) = result { if let Ok(entry) = result {
if entry.file_type().unwrap().is_dir() { if entry.file_type().unwrap().is_dir() {
// if the dir is already in cache, skip it // if the entry is a .git directory, add its parent to the list of git repos
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 entry.path().ends_with(".git") { if entry.path().ends_with(".git") {
let parent_path = entry let parent_path = preprocess_line(&*entry
.path() .path()
.parent() .parent()
.unwrap() .unwrap()
.to_string_lossy() .to_string_lossy());
.to_string();
debug!("Found git repo: {:?}", parent_path); debug!("Found git repo: {:?}", parent_path);
let _ = injector.push(entry, |_e, cols| { let _ = injector.push(parent_path, |e, cols| {
cols[0] = parent_path.clone().into(); cols[0] = e.clone().into();
}); });
entry_cache.lock().insert(parent_path);
return ignore::WalkState::Skip; return ignore::WalkState::Skip;
} }
} }
@ -247,6 +222,4 @@ async fn crawl_for_repos(
ignore::WalkState::Continue ignore::WalkState::Continue
}) })
}); });
*cache_valid.lock() = true;
} }

View File

@ -4,11 +4,11 @@ use std::{io::BufRead, sync::Arc};
use devicons::FileIcon; use devicons::FileIcon;
use nucleo::{Config, Nucleo}; use nucleo::{Config, Nucleo};
use super::OnAir;
use crate::entry::Entry; use crate::entry::Entry;
use crate::fuzzy::MATCHER; use crate::fuzzy::MATCHER;
use crate::previewers::PreviewType; use crate::previewers::PreviewType;
use crate::utils::strings::preprocess_line;
use super::OnAir;
pub struct Channel { pub struct Channel {
matcher: Nucleo<String>, matcher: Nucleo<String>,
@ -25,7 +25,7 @@ impl Channel {
pub fn new() -> Self { pub fn new() -> Self {
let mut lines = Vec::new(); let mut lines = Vec::new();
for line in std::io::stdin().lock().lines().map_while(Result::ok) { for line in std::io::stdin().lock().lines().map_while(Result::ok) {
lines.push(line); lines.push(preprocess_line(&line));
} }
let matcher = Nucleo::new( let matcher = Nucleo::new(
Config::DEFAULT, Config::DEFAULT,

View File

@ -1,6 +1,7 @@
use devicons::FileIcon; use devicons::FileIcon;
use crate::previewers::PreviewType; use crate::previewers::PreviewType;
use crate::utils::strings::preprocess_line;
#[derive(Clone, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Entry { pub struct Entry {
@ -17,7 +18,7 @@ pub struct Entry {
impl Entry { impl Entry {
pub fn new(name: String, preview_type: PreviewType) -> Self { pub fn new(name: String, preview_type: PreviewType) -> Self {
Self { Self {
name, name: preprocess_line(&name),
display_name: None, display_name: None,
value: None, value: None,
name_match_ranges: None, name_match_ranges: None,
@ -29,12 +30,12 @@ impl Entry {
} }
pub fn with_display_name(mut self, display_name: String) -> Self { 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 self
} }
pub fn with_value(mut self, value: String) -> Self { pub fn with_value(mut self, value: String) -> Self {
self.value = Some(value); self.value = Some(preprocess_line(&value));
self self
} }

View File

@ -86,7 +86,7 @@ impl Picker {
} }
} else { } else {
self.view_offset = total_items.saturating_sub(height - 2); 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)); self.relative_select(Some(height - 3));
} }
} }

View File

@ -165,7 +165,9 @@ impl FilePreviewer {
entry_c.name entry_c.name
); );
let lines: Vec<String> = let lines: Vec<String> =
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( match syntax::compute_highlights_for_path(
&PathBuf::from(&entry_c.name), &PathBuf::from(&entry_c.name),

View File

@ -108,8 +108,8 @@ impl Television {
} }
#[must_use] #[must_use]
pub fn get_selected_entry(&mut self) -> Option<Entry> { pub fn get_selected_entry(&mut self, mode: Option<Mode>) -> Option<Entry> {
match self.mode { match mode.unwrap_or(self.mode) {
Mode::Channel => self.results_picker.selected().and_then(|i| { Mode::Channel => self.results_picker.selected().and_then(|i| {
self.channel.get_result(u32::try_from(i).unwrap()) self.channel.get_result(u32::try_from(i).unwrap())
}), }),
@ -298,7 +298,7 @@ impl Television {
Mode::SendToChannel => {} Mode::SendToChannel => {}
}, },
Action::SelectEntry => { Action::SelectEntry => {
if let Some(entry) = self.get_selected_entry() { if let Some(entry) = self.get_selected_entry(None) {
match self.mode { match self.mode {
Mode::Channel => self Mode::Channel => self
.action_tx .action_tx
@ -378,7 +378,7 @@ impl Television {
self.draw_input_box(f, &layout)?; self.draw_input_box(f, &layout)?;
let selected_entry = 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)); let preview = block_on(self.previewer.preview(&selected_entry));
// top right block: preview title // top right block: preview title

View File

@ -10,6 +10,7 @@ pub mod preview;
mod remote_control; mod remote_control;
pub mod results; pub mod results;
pub mod spinner; pub mod spinner;
mod mode;
// input // input
//const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200); //const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200);
//const DEFAULT_RESULTS_COUNT_FG: Color = Color::Rgb(150, 150, 150); //const DEFAULT_RESULTS_COUNT_FG: Color = Color::Rgb(150, 150, 150);

View File

@ -1,17 +1,18 @@
use crate::television::Television; use crate::television::Television;
use crate::ui::layout::Layout; use crate::ui::layout::Layout;
use crate::ui::logo::build_logo_paragraph; use crate::ui::logo::build_logo_paragraph;
use crate::ui::mode::mode_color;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::prelude::{Color, Style}; use ratatui::prelude::{Color, Style};
use ratatui::widgets::{Block, BorderType, Borders, Padding}; use ratatui::widgets::{Block, BorderType, Borders, Padding};
use ratatui::Frame; 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() let logo_block = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Blue)) .border_style(Style::default().fg(Color::Blue))
.style(Style::default().fg(Color::Yellow)) .style(Style::default().fg(color))
.padding(Padding::horizontal(1)); .padding(Padding::horizontal(1));
let logo_paragraph = build_logo_paragraph().block(logo_block); let logo_paragraph = build_logo_paragraph().block(logo_block);
@ -27,7 +28,7 @@ impl Television {
) -> color_eyre::Result<()> { ) -> color_eyre::Result<()> {
self.draw_metadata_block(f, layout.help_bar_left); self.draw_metadata_block(f, layout.help_bar_left);
self.draw_keymaps_block(f, layout.help_bar_middle)?; 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(()) Ok(())
} }

View File

@ -7,6 +7,7 @@ use ratatui::{
}; };
use std::collections::HashMap; use std::collections::HashMap;
use crate::ui::mode::mode_color;
use crate::{ use crate::{
action::Action, action::Action,
event::Key, event::Key,
@ -14,7 +15,6 @@ use crate::{
}; };
const ACTION_COLOR: Color = Color::DarkGray; const ACTION_COLOR: Color = Color::DarkGray;
const KEY_COLOR: Color = Color::LightYellow;
impl Television { impl Television {
pub fn build_keymap_table<'a>(&self) -> Result<Table<'a>> { pub fn build_keymap_table<'a>(&self) -> Result<Table<'a>> {
@ -29,6 +29,7 @@ impl Television {
fn build_keymap_table_for_channel<'a>(&self) -> Result<Table<'a>> { fn build_keymap_table_for_channel<'a>(&self) -> Result<Table<'a>> {
let keymap = self.keymap_for_mode()?; let keymap = self.keymap_for_mode()?;
let key_color = mode_color(self.mode);
// Results navigation // Results navigation
let prev = keys_for_action(keymap, &Action::SelectPrevEntry); let prev = keys_for_action(keymap, &Action::SelectPrevEntry);
@ -36,6 +37,7 @@ impl Television {
let results_row = Row::new(build_cells_for_key_groups( let results_row = Row::new(build_cells_for_key_groups(
"↕ Results navigation", "↕ Results navigation",
vec![prev, next], vec![prev, next],
key_color,
)); ));
// Preview navigation // Preview navigation
@ -46,6 +48,7 @@ impl Television {
let preview_row = Row::new(build_cells_for_key_groups( let preview_row = Row::new(build_cells_for_key_groups(
"↕ Preview navigation", "↕ Preview navigation",
vec![up_keys, down_keys], vec![up_keys, down_keys],
key_color,
)); ));
// Select entry // Select entry
@ -53,6 +56,7 @@ impl Television {
let select_entry_row = Row::new(build_cells_for_key_groups( let select_entry_row = Row::new(build_cells_for_key_groups(
"✓ Select entry", "✓ Select entry",
vec![select_entry_keys], vec![select_entry_keys],
key_color,
)); ));
// Send to channel // Send to channel
@ -61,21 +65,23 @@ impl Television {
let send_to_channel_row = Row::new(build_cells_for_key_groups( let send_to_channel_row = Row::new(build_cells_for_key_groups(
"⇉ Send results to", "⇉ Send results to",
vec![send_to_channel_keys], vec![send_to_channel_keys],
key_color,
)); ));
// Switch channels // Switch channels
let switch_channels_keys = let switch_channels_keys =
keys_for_action(keymap, &Action::ToggleRemoteControl); keys_for_action(keymap, &Action::ToggleRemoteControl);
let switch_channels_row = Row::new(build_cells_for_key_groups( let switch_channels_row = Row::new(build_cells_for_key_groups(
"Remote control", "Toggle Remote control",
vec![switch_channels_keys], vec![switch_channels_keys],
key_color,
)); ));
// MISC line (quit, help, etc.) // MISC line (quit, help, etc.)
// Quit ⏼ // Quit ⏼
let quit_keys = keys_for_action(keymap, &Action::Quit); let quit_keys = keys_for_action(keymap, &Action::Quit);
let quit_row = 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)]; let widths = vec![Constraint::Fill(1), Constraint::Fill(2)];
@ -96,34 +102,38 @@ impl Television {
&self, &self,
) -> Result<Table<'a>> { ) -> Result<Table<'a>> {
let keymap = self.keymap_for_mode()?; let keymap = self.keymap_for_mode()?;
let key_color = mode_color(self.mode);
// Results navigation // Results navigation
let prev = keys_for_action(keymap, &Action::SelectPrevEntry); let prev = keys_for_action(keymap, &Action::SelectPrevEntry);
let next = keys_for_action(keymap, &Action::SelectNextEntry); let next = keys_for_action(keymap, &Action::SelectNextEntry);
let results_row = Row::new(build_cells_for_key_groups( let results_row = Row::new(build_cells_for_key_groups(
"Results", "Browse channels",
vec![prev, next], vec![prev, next],
key_color,
)); ));
// Select entry // Select entry
let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry); let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry);
let select_entry_row = Row::new(build_cells_for_key_groups( let select_entry_row = Row::new(build_cells_for_key_groups(
"Select entry", "✓ Select channel",
vec![select_entry_keys], vec![select_entry_keys],
key_color,
)); ));
// Switch channels // Switch channels
let switch_channels_keys = let switch_channels_keys =
keys_for_action(keymap, &Action::ToggleRemoteControl); keys_for_action(keymap, &Action::ToggleRemoteControl);
let switch_channels_row = Row::new(build_cells_for_key_groups( let switch_channels_row = Row::new(build_cells_for_key_groups(
"Switch channels", "⨀ Toggle Remote control",
vec![switch_channels_keys], vec![switch_channels_keys],
key_color,
)); ));
// Quit // Quit
let quit_keys = keys_for_action(keymap, &Action::Quit); let quit_keys = keys_for_action(keymap, &Action::Quit);
let quit_row = 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( Ok(Table::new(
vec![results_row, select_entry_row, switch_channels_row, quit_row], vec![results_row, select_entry_row, switch_channels_row, quit_row],
@ -173,6 +183,7 @@ impl Television {
fn build_cells_for_key_groups( fn build_cells_for_key_groups(
group_name: &str, group_name: &str,
key_groups: Vec<Vec<String>>, key_groups: Vec<Vec<String>>,
key_color: Color,
) -> Vec<Cell> { ) -> Vec<Cell> {
if key_groups.is_empty() || key_groups.iter().all(Vec::is_empty) 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<Span> = non_empty_groups let key_group_spans: Vec<Span> = non_empty_groups
.map(|keys| { .map(|keys| {
let key_group = keys.join(", "); let key_group = keys.join(", ");
Span::styled(key_group, Style::default().fg(KEY_COLOR)) Span::styled(key_group, Style::default().fg(key_color))
}) })
.collect(); .collect();
key_group_spans.iter().enumerate().for_each(|(i, span)| { key_group_spans.iter().enumerate().for_each(|(i, span)| {
spans.push(span.clone()); spans.push(span.clone());
if i < key_group_spans.len() - 1 { 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)));
} }
}); });

View File

@ -6,93 +6,92 @@ use ratatui::{
}; };
use crate::television::Television; use crate::television::Television;
use crate::ui::mode::mode_color;
// television 0.1.6
// target triple: aarch64-apple-darwin const METADATA_FIELD_NAME_COLOR: Color = Color::DarkGray;
// build: 1.82.0 (2024-10-24) const METADATA_FIELD_VALUE_COLOR: Color = Color::Gray;
// current_channel: git_repos
// current_mode: channel
impl Television { impl Television {
pub fn build_metadata_table<'a>(&self) -> Table<'a> { pub fn build_metadata_table<'a>(&self) -> Table<'a> {
let version_row = Row::new(vec![ let version_row = Row::new(vec![
Cell::from(Span::styled( Cell::from(Span::styled(
"version: ", "version: ",
Style::default().fg(Color::DarkGray), Style::default().fg(METADATA_FIELD_NAME_COLOR),
)), )),
Cell::from(Span::styled( Cell::from(Span::styled(
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
Style::default().fg(Color::LightYellow), Style::default().fg(METADATA_FIELD_VALUE_COLOR),
)), )),
]); ]);
let target_triple_row = Row::new(vec![ let target_triple_row = Row::new(vec![
Cell::from(Span::styled( Cell::from(Span::styled(
"target triple: ", "target triple: ",
Style::default().fg(Color::DarkGray), Style::default().fg(METADATA_FIELD_NAME_COLOR),
)), )),
Cell::from(Span::styled( Cell::from(Span::styled(
env!("VERGEN_CARGO_TARGET_TRIPLE"), env!("VERGEN_CARGO_TARGET_TRIPLE"),
Style::default().fg(Color::LightYellow), Style::default().fg(METADATA_FIELD_VALUE_COLOR),
)), )),
]); ]);
let build_row = Row::new(vec![ let build_row = Row::new(vec![
Cell::from(Span::styled( Cell::from(Span::styled(
"build: ", "build: ",
Style::default().fg(Color::DarkGray), Style::default().fg(METADATA_FIELD_NAME_COLOR),
)), )),
Cell::from(Span::styled( Cell::from(Span::styled(
env!("VERGEN_RUSTC_SEMVER"), env!("VERGEN_RUSTC_SEMVER"),
Style::default().fg(Color::LightYellow), Style::default().fg(METADATA_FIELD_VALUE_COLOR),
)), )),
Cell::from(Span::styled( Cell::from(Span::styled(
" (", " (",
Style::default().fg(Color::DarkGray), Style::default().fg(METADATA_FIELD_NAME_COLOR),
)), )),
Cell::from(Span::styled( Cell::from(Span::styled(
env!("VERGEN_BUILD_DATE"), env!("VERGEN_BUILD_DATE"),
Style::default().fg(Color::LightYellow), Style::default().fg(METADATA_FIELD_VALUE_COLOR),
)), )),
Cell::from(Span::styled( Cell::from(Span::styled(
")", ")",
Style::default().fg(Color::DarkGray), Style::default().fg(METADATA_FIELD_NAME_COLOR),
)), )),
]); ]);
let current_dir_row = Row::new(vec![ let current_dir_row = Row::new(vec![
Cell::from(Span::styled( Cell::from(Span::styled(
"current directory: ", "current directory: ",
Style::default().fg(Color::DarkGray), Style::default().fg(METADATA_FIELD_NAME_COLOR),
)), )),
Cell::from(Span::styled( Cell::from(Span::styled(
std::env::current_dir() std::env::current_dir()
.expect("Could not get current directory") .expect("Could not get current directory")
.display() .display()
.to_string(), .to_string(),
Style::default().fg(Color::LightYellow), Style::default().fg(METADATA_FIELD_VALUE_COLOR),
)), )),
]); ]);
let current_channel_row = Row::new(vec![ let current_channel_row = Row::new(vec![
Cell::from(Span::styled( Cell::from(Span::styled(
"current channel: ", "current channel: ",
Style::default().fg(Color::DarkGray), Style::default().fg(METADATA_FIELD_NAME_COLOR),
)), )),
Cell::from(Span::styled( Cell::from(Span::styled(
self.current_channel().to_string(), self.current_channel().to_string(),
Style::default().fg(Color::LightYellow), Style::default().fg(METADATA_FIELD_VALUE_COLOR),
)), )),
]); ]);
let current_mode_row = Row::new(vec![ let current_mode_row = Row::new(vec![
Cell::from(Span::styled( Cell::from(Span::styled(
"current mode: ", "current mode: ",
Style::default().fg(Color::DarkGray), Style::default().fg(METADATA_FIELD_NAME_COLOR),
)), )),
Cell::from(Span::styled( Cell::from(Span::styled(
self.mode.to_string(), self.mode.to_string(),
Style::default().fg(Color::LightYellow), Style::default().fg(mode_color(self.mode)),
)), )),
]); ]);

View File

@ -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,
}
}

View File

@ -2,7 +2,8 @@ use crate::channels::OnAir;
use crate::television::Television; use crate::television::Television;
use crate::ui::get_border_style; use crate::ui::get_border_style;
use crate::ui::logo::build_remote_logo_paragraph; 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 color_eyre::eyre::Result;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::prelude::Style; use ratatui::prelude::Style;
@ -32,7 +33,7 @@ impl Television {
.split(*area); .split(*area);
self.draw_rc_channels(f, &layout[0])?; self.draw_rc_channels(f, &layout[0])?;
self.draw_rc_input(f, &layout[1])?; self.draw_rc_input(f, &layout[1])?;
draw_rc_logo(f, layout[2]); draw_rc_logo(f, layout[2], mode_color(self.mode));
Ok(()) Ok(())
} }
@ -56,7 +57,7 @@ impl Television {
); );
let channel_list = 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( f.render_stateful_widget(
channel_list, 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() let logo_block = Block::default()
// .borders(Borders::ALL) .style(Style::default().fg(color));
// .border_type(BorderType::Rounded)
// .border_style(Style::default().fg(Color::Blue))
.style(Style::default().fg(Color::Yellow));
let logo_paragraph = build_remote_logo_paragraph() let logo_paragraph = build_remote_logo_paragraph()
.alignment(Alignment::Center) .alignment(Alignment::Center)

View File

@ -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_LINE_NUMBER_FG: Color = Color::Yellow;
const DEFAULT_RESULT_SELECTED_BG: Color = Color::Rgb(50, 50, 50); 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>( pub fn build_results_list<'a, 'b>(
results_block: Block<'b>, results_block: Block<'b>,
entries: &'a [Entry], entries: &'a [Entry],
list_direction: ListDirection, list_direction: ListDirection,
results_list_colors: Option<ResultsListColors>,
) -> List<'a> ) -> List<'a>
where where
'b: 'a, 'b: 'a,
{ {
let results_list_colors = results_list_colors.unwrap_or_default();
List::new(entries.iter().map(|entry| { List::new(entries.iter().map(|entry| {
let mut spans = Vec::new(); let mut spans = Vec::new();
// optional icon // optional icon
@ -50,7 +92,7 @@ where
last_match_end, last_match_end,
start, start,
), ),
Style::default().fg(DEFAULT_RESULT_NAME_FG), Style::default().fg(results_list_colors.result_name_fg),
)); ));
spans.push(Span::styled( spans.push(Span::styled(
slice_at_char_boundaries(&entry.name, start, end), slice_at_char_boundaries(&entry.name, start, end),
@ -60,19 +102,19 @@ where
} }
spans.push(Span::styled( spans.push(Span::styled(
&entry.name[next_char_boundary(&entry.name, last_match_end)..], &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 { } else {
spans.push(Span::styled( spans.push(Span::styled(
entry.display_name(), entry.display_name(),
Style::default().fg(DEFAULT_RESULT_NAME_FG), Style::default().fg(results_list_colors.result_name_fg),
)); ));
} }
// optional line number // optional line number
if let Some(line_number) = entry.line_number { if let Some(line_number) = entry.line_number {
spans.push(Span::styled( spans.push(Span::styled(
format!(":{line_number}"), format!(":{line_number}"),
Style::default().fg(DEFAULT_RESULT_LINE_NUMBER_FG), Style::default().fg(results_list_colors.result_line_number_fg),
)); ));
} }
// optional preview // optional preview
@ -92,7 +134,7 @@ where
last_match_end, last_match_end,
start, start,
), ),
Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), Style::default().fg(results_list_colors.result_preview_fg),
)); ));
spans.push(Span::styled( spans.push(Span::styled(
slice_at_char_boundaries(preview, start, end), slice_at_char_boundaries(preview, start, end),
@ -105,20 +147,20 @@ where
preview, preview,
preview_match_ranges.last().unwrap().1 as usize, 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 { } else {
spans.push(Span::styled( spans.push(Span::styled(
preview, preview,
Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), Style::default().fg(results_list_colors.result_preview_fg),
)); ));
} }
} }
Line::from(spans) Line::from(spans)
})) }))
.direction(list_direction) .direction(list_direction)
.highlight_style(Style::default().bg(DEFAULT_RESULT_SELECTED_BG)) .highlight_style(Style::default().bg(results_list_colors.result_selected_bg))
.highlight_symbol("> ") .highlight_symbol("> ")
.block(results_block) .block(results_block)
} }
@ -152,6 +194,7 @@ impl Television {
results_block, results_block,
&entries, &entries,
ListDirection::BottomToTop, ListDirection::BottomToTop,
None,
); );
f.render_stateful_widget( f.render_stateful_widget(

View File

@ -1,5 +1,6 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::fmt::Write; use std::fmt::Write;
use tracing::debug;
pub fn next_char_boundary(s: &str, start: usize) -> usize { pub fn next_char_boundary(s: &str, start: usize) -> usize {
let mut i = start; let mut i = start;
@ -53,7 +54,6 @@ lazy_static! {
} }
pub const EMPTY_STRING: &str = ""; pub const EMPTY_STRING: &str = "";
pub const FOUR_SPACES: &str = " ";
pub const TAB_WIDTH: usize = 4; pub const TAB_WIDTH: usize = 4;
const SPACE_CHARACTER: char = ' '; const SPACE_CHARACTER: char = ' ';
@ -78,11 +78,12 @@ pub fn replace_nonprintable(input: &[u8], tab_width: usize) -> String {
// space // space
SPACE_CHARACTER => output.push(' '), SPACE_CHARACTER => output.push(' '),
// tab // tab
TAB_CHARACTER => output.push_str(&" ".repeat(tab_width)), TAB_CHARACTER => {
// line feed output.push_str(&" ".repeat(tab_width));
LINE_FEED_CHARACTER => {
output.push_str("\x0A");
} }
// line feed
LINE_FEED_CHARACTER => {}
// ASCII control characters from 0x00 to 0x1F // ASCII control characters from 0x00 to 0x1F
// + control characters from \u{007F} to \u{009F} // + control characters from \u{007F} to \u{009F}
NULL_CHARACTER..=UNIT_SEPARATOR_CHARACTER NULL_CHARACTER..=UNIT_SEPARATOR_CHARACTER
@ -91,12 +92,15 @@ pub fn replace_nonprintable(input: &[u8], tab_width: usize) -> String {
} }
// don't print BOMs // don't print BOMs
BOM_CHARACTER => {} BOM_CHARACTER => {}
// unicode characters above 0x0700 seem unstable with ratatui // Unicode characters above 0x0700 seem unstable with ratatui
c if c > '\u{0700}' => { c if c > '\u{0700}' => {
output.push(*NULL_SYMBOL); output.push(*NULL_SYMBOL);
} }
// everything else // everything else
c => output.push(c), c => {
debug!("char: {:?}", c);
output.push(c)
}
} }
} else { } else {
write!(output, "\\x{:02X}", input[idx]).ok(); 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 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 { pub fn preprocess_line(line: &str) -> String {
replace_nonprintable( replace_nonprintable(
@ -169,11 +173,16 @@ mod tests {
#[test] #[test]
fn test_replace_nonprintable_tab() { fn test_replace_nonprintable_tab() {
test_replace_nonprintable("Hello\tWorld!", "Hello World!"); test_replace_nonprintable("Hello\tWorld!", "Hello World!");
test_replace_nonprintable(
" -- AND
",
" -- AND",
)
} }
#[test] #[test]
fn test_replace_nonprintable_line_feed() { fn test_replace_nonprintable_line_feed() {
test_replace_nonprintable("Hello\nWorld!", "Hello␊\nWorld!"); test_replace_nonprintable("Hello\nWorld!", "HelloWorld!");
} }
#[test] #[test]

View File

@ -22,7 +22,10 @@ use quote::quote;
/// ``` /// ```
/// ///
/// The `CliChannel` enum is used to select channels from the command line. /// 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 { pub fn cli_channel_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree // Construct a representation of Rust code as a syntax tree
// that we can manipulate // that we can manipulate
@ -32,8 +35,9 @@ pub fn cli_channel_derive(input: TokenStream) -> TokenStream {
impl_cli_channel(&ast) impl_cli_channel(&ast)
} }
/// List of variant names that should be ignored when generating the CliTvChannel enum. fn has_exclude_attr(attrs: &[syn::Attribute]) -> bool {
const VARIANT_BLACKLIST: [&str; 2] = ["Stdin", "RemoteControl"]; attrs.iter().any(|attr| attr.path().is_ident("exclude_from_cli"))
}
fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
// check that the struct is an enum // check that the struct is an enum
@ -52,7 +56,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
// create the CliTvChannel enum // create the CliTvChannel enum
let cli_enum_variants = variants let cli_enum_variants = variants
.iter() .iter()
.filter(|v| !VARIANT_BLACKLIST.contains(&v.ident.to_string().as_str())) .filter(|variant| !has_exclude_attr(&variant.attrs))
.map(|variant| { .map(|variant| {
let variant_name = &variant.ident; let variant_name = &variant.ident;
quote! { quote! {
@ -74,7 +78,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
// Generate the match arms for the `to_channel` method // Generate the match arms for the `to_channel` method
let arms = variants.iter().filter( let arms = variants.iter().filter(
|variant| !VARIANT_BLACKLIST.contains(&variant.ident.to_string().as_str()), |variant| !has_exclude_attr(&variant.attrs)
).map(|variant| { ).map(|variant| {
let variant_name = &variant.ident; let variant_name = &variant.ident;