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

@ -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)?;

View File

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

View File

@ -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<Alias>) {
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),
));
}
}

View File

@ -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(

View File

@ -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<DirEntry>,
matcher: Nucleo<String>,
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<PathBuf>, injector: Injector<DirEntry>) {
async fn load_files(paths: Vec<PathBuf>, injector: Injector<String>) {
if paths.is_empty() {
return;
}
@ -168,20 +165,9 @@ async fn load_files(paths: Vec<PathBuf>, injector: Injector<DirEntry>) {
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(&current_dir)
.unwrap()
.to_string_lossy()
.into();
let file_path = preprocess_line(&*entry.path().strip_prefix(&current_dir).unwrap().to_string_lossy());
let _ = injector.push(file_path, |e, cols| {
cols[0] = e.clone().into();
});
}
}

View File

@ -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<DirEntry>,
matcher: Nucleo<String>,
last_pattern: String,
result_count: u32,
total_count: u32,
running: bool,
icon: FileIcon,
crawl_handle: JoinHandle<()>,
git_dirs_cache: Arc<Mutex<HashSet<String>>>,
// TODO: implement cache validation/invalidation
cache_valid: Arc<Mutex<bool>>,
}
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<PathBuf> {
#[allow(clippy::unused_async)]
async fn crawl_for_repos(
starting_point: PathBuf,
injector: nucleo::Injector<DirEntry>,
entry_cache: Arc<Mutex<HashSet<String>>>,
cache_valid: Arc<Mutex<bool>>,
injector: nucleo::Injector<String>,
) {
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;
}

View File

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

View File

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

View File

@ -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));
}
}

View File

@ -165,7 +165,9 @@ impl FilePreviewer {
entry_c.name
);
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(
&PathBuf::from(&entry_c.name),

View File

@ -108,8 +108,8 @@ impl Television {
}
#[must_use]
pub fn get_selected_entry(&mut self) -> Option<Entry> {
match self.mode {
pub fn get_selected_entry(&mut self, mode: Option<Mode>) -> Option<Entry> {
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

View File

@ -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);

View File

@ -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(())
}

View File

@ -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<Table<'a>> {
@ -29,6 +29,7 @@ impl Television {
fn build_keymap_table_for_channel<'a>(&self) -> Result<Table<'a>> {
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<Table<'a>> {
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<Vec<String>>,
key_color: Color,
) -> Vec<Cell> {
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
.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)));
}
});

View File

@ -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)),
)),
]);

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::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)

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_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<ResultsListColors>,
) -> 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(

View File

@ -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]

View File

@ -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;