From 555651524035bd41f834fe5dcabd518d65af375f Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier Date: Mon, 21 Oct 2024 00:14:13 +0200 Subject: [PATCH] new things --- .config/config.toml | 5 +- crates/television/action.rs | 4 +- crates/television/app.rs | 5 +- crates/television/channels.rs | 27 +- crates/television/channels/alias.rs | 8 +- crates/television/channels/channels.rs | 8 +- crates/television/channels/env.rs | 8 +- crates/television/channels/files.rs | 8 +- crates/television/channels/git_repos.rs | 37 ++- crates/television/channels/stdin.rs | 9 +- crates/television/channels/text.rs | 8 +- crates/television/main.rs | 4 +- crates/television/previewers/cache.rs | 6 +- crates/television/previewers/files.rs | 40 +-- crates/television/television.rs | 79 +++-- crates/television/ui/help.rs | 394 ++++++++++++++---------- crates/television/ui/layout.rs | 2 +- crates/television/ui/preview.rs | 6 +- crates/television/ui/results.rs | 3 +- crates/television/utils.rs | 1 + crates/television/utils/strings.rs | 1 - crates/television/utils/syntax.rs | 57 ++++ crates/television_derive/src/lib.rs | 95 +++++- 23 files changed, 528 insertions(+), 287 deletions(-) create mode 100644 crates/television/utils/syntax.rs diff --git a/.config/config.toml b/.config/config.toml index c902023..522dae5 100644 --- a/.config/config.toml +++ b/.config/config.toml @@ -9,7 +9,8 @@ ctrl-d = "ScrollPreviewHalfPageDown" alt-up = "ScrollPreviewHalfPageUp" ctrl-u = "ScrollPreviewHalfPageUp" enter = "SelectEntry" -ctrl-s = "ToChannelSelection" +ctrl-enter = "SendToChannel" +ctrl-s = "ToggleChannelSelection" [keybindings.ChannelSelection] esc = "Quit" @@ -18,5 +19,5 @@ up = "SelectPrevEntry" ctrl-n = "SelectNextEntry" ctrl-p = "SelectPrevEntry" enter = "SelectEntry" -ctrl-enter = "PipeInto" +ctrl-s = "ToggleChannelSelection" diff --git a/crates/television/action.rs b/crates/television/action.rs index f9484ea..c6e2af9 100644 --- a/crates/television/action.rs +++ b/crates/television/action.rs @@ -42,6 +42,6 @@ pub enum Action { Error(String), NoOp, // channel actions - ToChannelSelection, - PipeInto, + ToggleChannelSelection, + SendToChannel, } diff --git a/crates/television/app.rs b/crates/television/app.rs index bc638d7..88f58be 100644 --- a/crates/television/app.rs +++ b/crates/television/app.rs @@ -52,11 +52,12 @@ */ use std::sync::Arc; + use color_eyre::Result; use tokio::sync::{mpsc, Mutex}; use tracing::{debug, info}; -use crate::channels::{AvailableChannel, CliTvChannel}; +use crate::channels::{TelevisionChannel, CliTvChannel}; use crate::television::Television; use crate::{ action::Action, @@ -84,7 +85,7 @@ pub struct App { impl App { pub fn new( - channel: AvailableChannel, + channel: TelevisionChannel, tick_rate: f64, frame_rate: f64, ) -> Result { diff --git a/crates/television/channels.rs b/crates/television/channels.rs index 930fe3f..5bd6302 100644 --- a/crates/television/channels.rs +++ b/crates/television/channels.rs @@ -1,6 +1,6 @@ use crate::entry::Entry; use color_eyre::eyre::Result; -use television_derive::{CliChannel, TvChannel}; +use television_derive::{CliChannel, Broadcast, UnitChannel}; mod alias; pub mod channels; @@ -13,7 +13,7 @@ mod text; /// The interface that all television channels must implement. /// /// # Important -/// The `TelevisionChannel` requires the `Send` trait to be implemented as +/// The `OnAir` requires the `Send` trait to be implemented as /// well. This is necessary to allow the channels to be used in a /// multithreaded environment. /// @@ -47,7 +47,7 @@ mod text; /// fn total_count(&self) -> u32; /// ``` /// -pub trait TelevisionChannel: Send { +pub trait OnAir: Send { /// Find entries that match the given pattern. /// /// This method does not return anything and instead typically stores the @@ -70,6 +70,9 @@ pub trait TelevisionChannel: Send { /// Check if the channel is currently running. fn running(&self) -> bool; + + /// Turn off + fn shutdown(&self); } /// The available television channels. @@ -89,8 +92,8 @@ pub trait TelevisionChannel: Send { /// instance from the selected CLI enum variant. /// #[allow(dead_code, clippy::module_name_repetitions)] -#[derive(CliChannel, TvChannel)] -pub enum AvailableChannel { +#[derive(UnitChannel, CliChannel, Broadcast)] +pub enum TelevisionChannel { Env(env::Channel), Files(files::Channel), GitRepos(git_repos::Channel), @@ -101,19 +104,19 @@ pub enum AvailableChannel { } /// NOTE: this could be generated by a derive macro -impl TryFrom<&Entry> for AvailableChannel { +impl TryFrom<&Entry> for TelevisionChannel { type Error = String; fn try_from(entry: &Entry) -> Result { match entry.name.to_ascii_lowercase().as_ref() { - "env" => Ok(AvailableChannel::Env(env::Channel::default())), - "files" => Ok(AvailableChannel::Files(files::Channel::default())), + "env" => Ok(TelevisionChannel::Env(env::Channel::default())), + "files" => Ok(TelevisionChannel::Files(files::Channel::default())), "gitrepos" => { - Ok(AvailableChannel::GitRepos(git_repos::Channel::default())) + Ok(TelevisionChannel::GitRepos(git_repos::Channel::default())) } - "text" => Ok(AvailableChannel::Text(text::Channel::default())), - "stdin" => Ok(AvailableChannel::Stdin(stdin::Channel::default())), - "alias" => Ok(AvailableChannel::Alias(alias::Channel::default())), + "text" => Ok(TelevisionChannel::Text(text::Channel::default())), + "stdin" => Ok(TelevisionChannel::Stdin(stdin::Channel::default())), + "alias" => Ok(TelevisionChannel::Alias(alias::Channel::default())), _ => Err(format!("Unknown channel: {}", entry.name)), } } diff --git a/crates/television/channels/alias.rs b/crates/television/channels/alias.rs index f10d9f6..c0b7519 100644 --- a/crates/television/channels/alias.rs +++ b/crates/television/channels/alias.rs @@ -4,7 +4,7 @@ use devicons::FileIcon; use nucleo::{Config, Injector, Nucleo}; use tracing::debug; -use crate::channels::TelevisionChannel; +use crate::channels::OnAir; use crate::entry::Entry; use crate::fuzzy::MATCHER; use crate::previewers::PreviewType; @@ -102,7 +102,7 @@ impl Default for Channel { } } -impl TelevisionChannel for Channel { +impl OnAir for Channel { fn find(&mut self, pattern: &str) { if pattern != self.last_pattern { self.matcher.pattern.reparse( @@ -198,6 +198,10 @@ impl TelevisionChannel for Channel { fn running(&self) -> bool { self.running } + + fn shutdown(&self) { + todo!() + } } #[allow(clippy::unused_async)] diff --git a/crates/television/channels/channels.rs b/crates/television/channels/channels.rs index 8b36c53..9b77378 100644 --- a/crates/television/channels/channels.rs +++ b/crates/television/channels/channels.rs @@ -8,7 +8,7 @@ use nucleo::{ }; use crate::{ - channels::{AvailableChannel, CliTvChannel, TelevisionChannel}, + channels::{CliTvChannel, OnAir}, entry::Entry, fuzzy::MATCHER, previewers::PreviewType, @@ -61,7 +61,7 @@ const TV_ICON: FileIcon = FileIcon { color: "#ffffff", }; -impl TelevisionChannel for SelectionChannel { +impl OnAir for SelectionChannel { fn find(&mut self, pattern: &str) { if pattern != self.last_pattern { self.matcher.pattern.reparse( @@ -133,4 +133,8 @@ impl TelevisionChannel for SelectionChannel { fn running(&self) -> bool { self.running } + + fn shutdown(&self) { + todo!() + } } diff --git a/crates/television/channels/env.rs b/crates/television/channels/env.rs index ca4994d..0c945f1 100644 --- a/crates/television/channels/env.rs +++ b/crates/television/channels/env.rs @@ -5,7 +5,7 @@ use nucleo::{ }; use std::sync::Arc; -use super::TelevisionChannel; +use super::OnAir; use crate::entry::Entry; use crate::fuzzy::MATCHER; use crate::previewers::PreviewType; @@ -62,7 +62,7 @@ impl Default for Channel { } } -impl TelevisionChannel for Channel { +impl OnAir for Channel { fn find(&mut self, pattern: &str) { if pattern != self.last_pattern { self.matcher.pattern.reparse( @@ -160,4 +160,8 @@ impl TelevisionChannel for Channel { fn running(&self) -> bool { self.running } + + fn shutdown(&self) { + todo!() + } } diff --git a/crates/television/channels/files.rs b/crates/television/channels/files.rs index 8ae0365..a01b8e4 100644 --- a/crates/television/channels/files.rs +++ b/crates/television/channels/files.rs @@ -11,7 +11,7 @@ use std::{ use ignore::DirEntry; -use super::TelevisionChannel; +use super::OnAir; use crate::previewers::PreviewType; use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; use crate::{ @@ -60,7 +60,7 @@ impl Default for Channel { } } -impl TelevisionChannel for Channel { +impl OnAir for Channel { fn find(&mut self, pattern: &str) { if pattern != self.last_pattern { self.matcher.pattern.reparse( @@ -131,6 +131,10 @@ impl TelevisionChannel for Channel { fn running(&self) -> bool { self.running } + + fn shutdown(&self) { + todo!() + } } #[allow(clippy::unused_async)] diff --git a/crates/television/channels/git_repos.rs b/crates/television/channels/git_repos.rs index 4c67b6d..e1fb12f 100644 --- a/crates/television/channels/git_repos.rs +++ b/crates/television/channels/git_repos.rs @@ -1,11 +1,12 @@ use std::sync::Arc; - +use color_eyre::owo_colors::OwoColorize; use devicons::FileIcon; use ignore::{overrides::OverrideBuilder, DirEntry}; use nucleo::{ pattern::{CaseMatching, Normalization}, Config, Nucleo, }; +use tokio::sync::{oneshot, watch}; use tracing::debug; use crate::{ @@ -15,7 +16,7 @@ use crate::{ utils::files::{walk_builder, DEFAULT_NUM_THREADS}, }; -use crate::channels::TelevisionChannel; +use crate::channels::OnAir; pub struct Channel { matcher: Nucleo, @@ -24,6 +25,7 @@ pub struct Channel { total_count: u32, running: bool, icon: FileIcon, + crawl_cancellation_tx: watch::Sender, } impl Channel { @@ -35,9 +37,11 @@ impl Channel { 1, ); // start loading files in the background + let (tx, rx) = watch::channel(false); tokio::spawn(crawl_for_repos( std::env::home_dir().expect("Could not get home directory"), matcher.injector(), + rx, )); Channel { matcher, @@ -46,6 +50,7 @@ impl Channel { total_count: 0, running: false, icon: FileIcon::from("git"), + crawl_cancellation_tx: tx, } } @@ -58,7 +63,7 @@ impl Default for Channel { } } -impl TelevisionChannel for Channel { +impl OnAir for Channel { fn find(&mut self, pattern: &str) { if pattern != self.last_pattern { self.matcher.pattern.reparse( @@ -72,14 +77,6 @@ impl TelevisionChannel for Channel { } } - fn result_count(&self) -> u32 { - self.result_count - } - - fn total_count(&self) -> u32 { - self.total_count - } - fn results(&mut self, num_entries: u32, offset: u32) -> Vec { let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT); let snapshot = self.matcher.snapshot(); @@ -127,15 +124,28 @@ impl TelevisionChannel for Channel { }) } + fn result_count(&self) -> u32 { + self.result_count + } + + fn total_count(&self) -> u32 { + self.total_count + } + fn running(&self) -> bool { self.running } + + fn shutdown(&self) { + self.crawl_cancellation_tx.send(true).unwrap(); + } } #[allow(clippy::unused_async)] async fn crawl_for_repos( starting_point: std::path::PathBuf, injector: nucleo::Injector, + cancellation_rx: watch::Receiver, ) { let mut walker_overrides_builder = OverrideBuilder::new(&starting_point); walker_overrides_builder.add(".git").unwrap(); @@ -148,7 +158,12 @@ async fn crawl_for_repos( walker.run(|| { let injector = injector.clone(); + let cancellation_rx = cancellation_rx.clone(); Box::new(move |result| { + if let Ok(true) = cancellation_rx.has_changed() { + debug!("Crawling for git repos cancelled"); + return ignore::WalkState::Quit; + } if let Ok(entry) = result { if entry.file_type().unwrap().is_dir() && entry.path().ends_with(".git") diff --git a/crates/television/channels/stdin.rs b/crates/television/channels/stdin.rs index 4478a5f..9a95ca1 100644 --- a/crates/television/channels/stdin.rs +++ b/crates/television/channels/stdin.rs @@ -3,13 +3,12 @@ use std::{io::BufRead, sync::Arc}; use devicons::FileIcon; use nucleo::{Config, Nucleo}; -use tracing::debug; use crate::entry::Entry; use crate::fuzzy::MATCHER; use crate::previewers::PreviewType; -use super::TelevisionChannel; +use super::OnAir; pub struct Channel { matcher: Nucleo, @@ -59,7 +58,7 @@ impl Default for Channel { } } -impl TelevisionChannel for Channel { +impl OnAir for Channel { // maybe this could be sort of automatic with a blanket impl (making Finder generic over // its matcher type or something) fn find(&mut self, pattern: &str) { @@ -150,4 +149,8 @@ impl TelevisionChannel for Channel { fn running(&self) -> bool { self.running } + + fn shutdown(&self) { + todo!() + } } diff --git a/crates/television/channels/text.rs b/crates/television/channels/text.rs index 8dd7494..d66f79f 100644 --- a/crates/television/channels/text.rs +++ b/crates/television/channels/text.rs @@ -12,7 +12,7 @@ use std::{ use tracing::{debug, info}; -use super::TelevisionChannel; +use super::OnAir; use crate::previewers::PreviewType; use crate::utils::{ files::{is_not_text, walk_builder, DEFAULT_NUM_THREADS}, @@ -75,7 +75,7 @@ impl Default for Channel { } } -impl TelevisionChannel for Channel { +impl OnAir for Channel { fn find(&mut self, pattern: &str) { if pattern != self.last_pattern { self.matcher.pattern.reparse( @@ -158,6 +158,10 @@ impl TelevisionChannel for Channel { fn running(&self) -> bool { self.running } + + fn shutdown(&self) { + todo!() + } } /// The maximum file size we're willing to search in. diff --git a/crates/television/main.rs b/crates/television/main.rs index 877f8cc..e724bf6 100644 --- a/crates/television/main.rs +++ b/crates/television/main.rs @@ -1,6 +1,6 @@ use std::io::{stdout, IsTerminal, Write}; -use channels::AvailableChannel; +use channels::TelevisionChannel; use clap::Parser; use color_eyre::Result; use tracing::{debug, info}; @@ -37,7 +37,7 @@ async fn main() -> Result<()> { { if is_readable_stdin() { debug!("Using stdin channel"); - AvailableChannel::Stdin(StdinChannel::default()) + TelevisionChannel::Stdin(StdinChannel::default()) } else { debug!("Using {:?} channel", args.channel); args.channel.to_channel() diff --git a/crates/television/previewers/cache.rs b/crates/television/previewers/cache.rs index efb9a94..b20d81c 100644 --- a/crates/television/previewers/cache.rs +++ b/crates/television/previewers/cache.rs @@ -138,11 +138,7 @@ impl PreviewCache { } /// Get the preview for the given key, or insert a new preview if it doesn't exist. - pub fn get_or_insert( - &mut self, - key: String, - f: F, - ) -> Arc + pub fn get_or_insert(&mut self, key: String, f: F) -> Arc where F: FnOnce() -> Preview, { diff --git a/crates/television/previewers/files.rs b/crates/television/previewers/files.rs index 41c72fc..193edab 100644 --- a/crates/television/previewers/files.rs +++ b/crates/television/previewers/files.rs @@ -14,6 +14,7 @@ use syntect::{ }; use tracing::{debug, warn}; +use super::cache::PreviewCache; use crate::entry; use crate::previewers::{meta, Preview, PreviewContent}; use crate::utils::files::FileType; @@ -22,13 +23,12 @@ use crate::utils::strings::{ preprocess_line, proportion_of_printable_ascii_characters, PRINTABLE_ASCII_THRESHOLD, }; - -use super::cache::PreviewCache; +use crate::utils::syntax; pub struct FilePreviewer { cache: Arc>, - syntax_set: Arc, - syntax_theme: Arc, + pub syntax_set: Arc, + pub syntax_theme: Arc, //image_picker: Arc>, } @@ -168,7 +168,7 @@ impl FilePreviewer { let lines: Vec = reader.lines().map_while(Result::ok).collect(); - match compute_highlights( + match syntax::compute_highlights_for_path( &PathBuf::from(&entry_c.name), lines, &syntax_set, @@ -279,33 +279,3 @@ fn plain_text_preview(title: &str, reader: BufReader<&File>) -> Arc { PreviewContent::PlainText(lines), )) } - -fn compute_highlights( - file_path: &Path, - lines: Vec, - syntax_set: &SyntaxSet, - syntax_theme: &Theme, -) -> Result>> { - let syntax = - syntax_set - .find_syntax_for_file(file_path)? - .unwrap_or_else(|| { - warn!( - "No syntax found for {:?}, defaulting to plain text", - file_path - ); - syntax_set.find_syntax_plain_text() - }); - let mut highlighter = HighlightLines::new(syntax, syntax_theme); - let mut highlighted_lines = Vec::new(); - for line in lines { - let hl_regions = highlighter.highlight_line(&line, syntax_set)?; - highlighted_lines.push( - hl_regions - .iter() - .map(|(style, text)| (*style, (*text).to_string())) - .collect(), - ); - } - Ok(highlighted_lines) -} diff --git a/crates/television/television.rs b/crates/television/television.rs index 3899e42..672f83d 100644 --- a/crates/television/television.rs +++ b/crates/television/television.rs @@ -15,7 +15,6 @@ use ratatui::{ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, str::FromStr}; use tokio::sync::mpsc::UnboundedSender; -use tracing::debug; use crate::ui::input::Input; use crate::ui::layout::{Dimensions, Layout}; @@ -25,10 +24,10 @@ use crate::utils::strings::EMPTY_STRING; use crate::{action::Action, config::Config}; use crate::{channels::channels::SelectionChannel, ui::get_border_style}; use crate::{ - channels::AvailableChannel, ui::input::actions::InputActionHandler, + channels::TelevisionChannel, ui::input::actions::InputActionHandler, }; use crate::{ - channels::{CliTvChannel, TelevisionChannel}, + channels::{OnAir, UnitChannel}, utils::strings::shrink_with_ellipsis, }; use crate::{ @@ -41,12 +40,15 @@ use crate::{previewers::Previewer, ui::spinner::SpinnerState}; pub enum Mode { Channel, ChannelSelection, + SendToChannel, } pub struct Television { action_tx: Option>, pub config: Config, - channel: AvailableChannel, + channel: TelevisionChannel, + last_channel: Option, + last_channel_results: Vec, current_pattern: String, pub mode: Mode, input: Input, @@ -54,7 +56,7 @@ pub struct Television { relative_picker_state: ListState, picker_view_offset: usize, results_area_height: u32, - previewer: Previewer, + pub previewer: Previewer, pub preview_scroll: Option, pub preview_pane_height: u16, current_preview_total_lines: u16, @@ -71,7 +73,7 @@ pub struct Television { impl Television { #[must_use] - pub fn new(mut channel: AvailableChannel) -> Self { + pub fn new(mut channel: TelevisionChannel) -> Self { channel.find(EMPTY_STRING); let spinner = Spinner::default(); @@ -81,6 +83,8 @@ impl Television { action_tx: None, config: Config::default(), channel, + last_channel: None, + last_channel_results: Vec::new(), current_pattern: EMPTY_STRING.to_string(), mode: Mode::Channel, input: Input::new(EMPTY_STRING.to_string()), @@ -98,7 +102,7 @@ impl Television { } } - pub fn change_channel(&mut self, channel: AvailableChannel) { + pub fn change_channel(&mut self, channel: TelevisionChannel) { self.reset_preview_scroll(); self.reset_results_selection(); self.current_pattern = EMPTY_STRING.to_string(); @@ -106,13 +110,17 @@ impl Television { self.channel = channel; } + fn backup_current_channel(&mut self) { + self.last_channel = Some(UnitChannel::from(&self.channel)); + self.last_channel_results = + self.channel.results(self.channel.result_count(), 0); + } + fn find(&mut self, pattern: &str) { self.channel.find(pattern); } #[must_use] - /// # Panics - /// This method will panic if the index doesn't fit into an u32. pub fn get_selected_entry(&self) -> Option { self.picker_state .selected() @@ -218,11 +226,9 @@ impl Television { /// Register an action handler that can send actions for processing if necessary. /// /// # Arguments - /// /// * `tx` - An unbounded sender that can send actions. /// /// # Returns - /// /// * `Result<()>` - An Ok result or an error. pub fn register_action_handler( &mut self, @@ -235,11 +241,9 @@ impl Television { /// Register a configuration handler that provides configuration settings if necessary. /// /// # Arguments - /// /// * `config` - Configuration settings. /// /// # Returns - /// /// * `Result<()>` - An Ok result or an error. pub fn register_config_handler(&mut self, config: Config) -> Result<()> { self.config = config; @@ -249,11 +253,9 @@ impl Television { /// Update the state of the component based on a received action. /// /// # Arguments - /// /// * `action` - An action that may modify the state of the television. /// /// # Returns - /// /// * `Result>` - An action to be processed or none. pub async fn update(&mut self, action: Action) -> Result> { match action { @@ -293,11 +295,28 @@ impl Television { Action::ScrollPreviewUp => self.scroll_preview_up(1), Action::ScrollPreviewHalfPageDown => self.scroll_preview_down(20), Action::ScrollPreviewHalfPageUp => self.scroll_preview_up(20), - Action::ToChannelSelection => { - self.mode = Mode::ChannelSelection; - let selection_channel = - AvailableChannel::Channel(SelectionChannel::new()); - self.change_channel(selection_channel); + Action::ToggleChannelSelection => { + // TODO: continue this + match self.mode { + // if we're in channel mode, backup current channel and + // switch to channel selection + Mode::Channel => { + self.backup_current_channel(); + self.mode = Mode::ChannelSelection; + self.change_channel(TelevisionChannel::Channel( + SelectionChannel::new(), + )); + } + // if we're in channel selection, switch to channel mode + // and restore the last channel if there is one + Mode::ChannelSelection => { + self.mode = Mode::Channel; + if let Some(last_channel) = self.last_channel.take() { + self.change_channel(last_channel.into()); + } + } + Mode::SendToChannel => {} + } } Action::SelectEntry => { if let Some(entry) = self.get_selected_entry() { @@ -309,16 +328,17 @@ impl Television { .send(Action::SelectAndExit)?, Mode::ChannelSelection => { if let Ok(new_channel) = - AvailableChannel::try_from(&entry) + TelevisionChannel::try_from(&entry) { self.mode = Mode::Channel; self.change_channel(new_channel); } } + Mode::SendToChannel => {} } } } - Action::PipeInto => { + Action::SendToChannel => { if let Some(entry) = self.get_selected_entry() {} } _ => {} @@ -329,20 +349,24 @@ impl Television { /// Render the television on the screen. /// /// # Arguments - /// /// * `f` - A frame used for rendering. /// * `area` - The area in which the television should be drawn. /// /// # Returns - /// /// * `Result<()>` - An Ok result or an error. pub fn draw(&mut self, f: &mut Frame, area: Rect) -> Result<()> { + let dimensions = match self.mode { + Mode::Channel => &Dimensions::default(), + Mode::ChannelSelection | Mode::SendToChannel => { + &Dimensions::new(30, 70) + } + }; let layout = Layout::build( - &Dimensions::default(), + dimensions, area, match self.mode { Mode::Channel => true, - Mode::ChannelSelection => false, + Mode::ChannelSelection | Mode::SendToChannel => false, }, ); @@ -352,9 +376,10 @@ impl Television { .padding(Padding::uniform(1)); let help_text = self - .build_help_paragraph(layout.help_bar.width.saturating_sub(4))? + .build_help_paragraph()? .style(Style::default().fg(Color::DarkGray).italic()) .alignment(Alignment::Center) + .wrap(Wrap { trim: true }) .block(help_block); f.render_widget(help_text, layout.help_bar); diff --git a/crates/television/ui/help.rs b/crates/television/ui/help.rs index f197d89..7313c7e 100644 --- a/crates/television/ui/help.rs +++ b/crates/television/ui/help.rs @@ -4,11 +4,10 @@ use ratatui::{ text::{Line, Span}, widgets::Paragraph, }; -use tracing::debug; +use std::collections::HashMap; use crate::{ action::Action, - config::Config, event::Key, television::{Mode, Television}, }; @@ -18,173 +17,242 @@ const ACTION_COLOR: Color = Color::DarkGray; const KEY_COLOR: Color = Color::LightYellow; impl Television { - pub fn build_help_paragraph<'a>( + pub fn build_help_paragraph<'a>(&self) -> Result> { + match self.mode { + Mode::Channel => self.build_help_paragraph_for_channel(), + Mode::ChannelSelection => { + self.build_help_paragraph_for_channel_selection() + } + Mode::SendToChannel => self.build_help_paragraph_for_channel(), + } + } + + fn build_help_paragraph_for_channel<'a>(&self) -> Result> { + let keymap = self.keymap_for_mode()?; + let mut lines = Vec::new(); + + // NAVIGATION and SELECTION line + let mut ns_line = Line::default(); + + // Results navigation + let prev = keys_for_action(keymap, Action::SelectPrevEntry); + let next = keys_for_action(keymap, Action::SelectNextEntry); + let results_spans = + build_spans_for_key_groups("↕ Results", vec![prev, next]); + + ns_line.extend(results_spans); + ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + + // Preview navigation + let up_keys = keys_for_action(keymap, Action::ScrollPreviewHalfPageUp); + let down_keys = + keys_for_action(keymap, Action::ScrollPreviewHalfPageDown); + let preview_spans = + build_spans_for_key_groups("↕ Preview", vec![up_keys, down_keys]); + + ns_line.extend(preview_spans); + ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + + // Send to channel + let send_to_channel_keys = + keys_for_action(keymap, Action::SendToChannel); + // TODO: add send icon + let send_to_channel_spans = + build_spans_for_key_groups("Send to", vec![send_to_channel_keys]); + + ns_line.extend(send_to_channel_spans); + ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + + // Select entry + let select_entry_keys = keys_for_action(keymap, Action::SelectEntry); + let select_entry_spans = build_spans_for_key_groups( + "Select entry", + vec![select_entry_keys], + ); + + ns_line.extend(select_entry_spans); + ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + + // Switch channels + let switch_channels_keys = + keys_for_action(keymap, Action::ToggleChannelSelection); + let switch_channels_spans = build_spans_for_key_groups( + "Switch channels", + vec![switch_channels_keys], + ); + + ns_line.extend(switch_channels_spans); + lines.push(ns_line); + + // MISC line (quit, help, etc.) + // let mut misc_line = Line::default(); + // + // // Quit + // let quit_keys = keys_for_action(keymap, Action::Quit); + // let quit_spans = build_spans_for_key_groups("Quit", vec![quit_keys]); + // + // misc_line.extend(quit_spans); + // + // lines.push(misc_line); + + Ok(Paragraph::new(lines)) + } + + fn build_help_paragraph_for_channel_selection<'a>( &self, - width: u16, ) -> Result> { + let keymap = self.keymap_for_mode()?; + let mut lines = Vec::new(); + + // NAVIGATION + SELECTION line + let mut ns_line = Line::default(); + + // Results navigation + let prev = keys_for_action(keymap, Action::SelectPrevEntry); + let next = keys_for_action(keymap, Action::SelectNextEntry); + let results_spans = + build_spans_for_key_groups("↕ Results", vec![prev, next]); + + ns_line.extend(results_spans); + ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + + // Select entry + let select_entry_keys = keys_for_action(keymap, Action::SelectEntry); + let select_entry_spans = build_spans_for_key_groups( + "Select entry", + vec![select_entry_keys], + ); + + ns_line.extend(select_entry_spans); + ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + + // Switch channels + let switch_channels_keys = + keys_for_action(keymap, Action::ToggleChannelSelection); + let switch_channels_spans = build_spans_for_key_groups( + "Switch channels", + vec![switch_channels_keys], + ); + + ns_line.extend(switch_channels_spans); + + lines.push(ns_line); + + // MISC line (quit, help, etc.) + // let mut misc_line = Line::default(); + + // Quit + // let quit_keys = keys_for_action(keymap, Action::Quit); + // let quit_spans = build_spans_for_key_groups("Quit", vec![quit_keys]); + + // misc_line.extend(quit_spans); + + // lines.push(misc_line); + + Ok(Paragraph::new(lines)) + } + + /// Get the keymap for the current mode. + /// + /// # Returns + /// A reference to the keymap for the current mode. + fn keymap_for_mode(&self) -> Result<&HashMap> { let keymap = self .config .keybindings .get(&self.mode) .ok_or_eyre("No keybindings found for the current Mode")?; - - let mut help_spans = Vec::new(); - - // Results navigation - let prev: Vec<_> = keymap - .iter() - .filter(|(_key, action)| **action == Action::SelectPrevEntry) - .map(|(key, _action)| format!("{key}")) - .collect(); - - let next: Vec<_> = keymap - .iter() - .filter(|(_key, action)| **action == Action::SelectNextEntry) - .map(|(key, _action)| format!("{key}")) - .collect(); - - let results_spans = vec![ - Span::styled("↕ Results: [", Style::default().fg(ACTION_COLOR)), - Span::styled(prev.join(", "), Style::default().fg(KEY_COLOR)), - Span::styled(" | ", Style::default().fg(ACTION_COLOR)), - Span::styled(next.join(", "), Style::default().fg(KEY_COLOR)), - Span::styled("]", Style::default().fg(ACTION_COLOR)), - ]; - - help_spans.extend(results_spans); - help_spans.push(Span::styled(SEPARATOR, Style::default())); - - if self.mode == Mode::Channel { - // Preview navigation - let up: Vec<_> = keymap - .iter() - .filter(|(_key, action)| { - **action == Action::ScrollPreviewHalfPageUp - }) - .map(|(key, _action)| format!("{key}")) - .collect(); - - let down: Vec<_> = keymap - .iter() - .filter(|(_key, action)| { - **action == Action::ScrollPreviewHalfPageDown - }) - .map(|(key, _action)| format!("{key}")) - .collect(); - - let preview_spans = vec![ - Span::styled( - "↕ Preview: [", - Style::default().fg(ACTION_COLOR), - ), - Span::styled(up.join(", "), Style::default().fg(KEY_COLOR)), - Span::styled(" | ", Style::default().fg(ACTION_COLOR)), - Span::styled(down.join(", "), Style::default().fg(KEY_COLOR)), - Span::styled("]", Style::default().fg(ACTION_COLOR)), - ]; - - help_spans.extend(preview_spans); - help_spans.push(Span::styled(SEPARATOR, Style::default())); - - // Channels - let channels: Vec<_> = keymap - .iter() - .filter(|(_key, action)| { - **action == Action::ToChannelSelection - }) - .map(|(key, _action)| format!("{key}")) - .collect(); - - let channels_spans = vec![ - Span::styled("Channels: [", Style::default().fg(ACTION_COLOR)), - Span::styled( - channels.join(", "), - Style::default().fg(KEY_COLOR), - ), - Span::styled("]", Style::default().fg(ACTION_COLOR)), - ]; - - help_spans.extend(channels_spans); - help_spans.push(Span::styled(SEPARATOR, Style::default())); - } - - if self.mode == Mode::ChannelSelection { - // Pipe into - let channels: Vec<_> = keymap - .iter() - .filter(|(_key, action)| **action == Action::PipeInto) - .map(|(key, _action)| format!("{key}")) - .collect(); - - let channels_spans = vec![ - Span::styled( - "Pipe into: [", - Style::default().fg(ACTION_COLOR), - ), - Span::styled( - channels.join(", "), - Style::default().fg(KEY_COLOR), - ), - Span::styled("]", Style::default().fg(ACTION_COLOR)), - ]; - - help_spans.extend(channels_spans); - help_spans.push(Span::styled(SEPARATOR, Style::default())); - - // Select Channel - let select: Vec<_> = keymap - .iter() - .filter(|(_key, action)| **action == Action::SelectEntry) - .map(|(key, _action)| format!("{key}")) - .collect(); - - let select_spans = vec![ - Span::styled("Select: [", Style::default().fg(ACTION_COLOR)), - Span::styled( - select.join(", "), - Style::default().fg(KEY_COLOR), - ), - Span::styled("]", Style::default().fg(ACTION_COLOR)), - ]; - - help_spans.extend(select_spans); - help_spans.push(Span::styled(SEPARATOR, Style::default())); - } - - // Quit - let quit: Vec<_> = keymap - .iter() - .filter(|(_key, action)| **action == Action::Quit) - .map(|(key, _action)| format!("{key}")) - .collect(); - - let quit_spans = vec![ - Span::styled("Quit: [", Style::default().fg(ACTION_COLOR)), - Span::styled(quit.join(", "), Style::default().fg(KEY_COLOR)), - Span::styled("]", Style::default().fg(ACTION_COLOR)), - ]; - - help_spans.extend(quit_spans); - - // arrange lines depending on the width - let mut lines = Vec::new(); - let mut current_line = Line::default(); - let mut current_width = 0; - - for span in help_spans { - let span_width = span.content.chars().count() as u16; - if current_width + span_width > width { - lines.push(current_line); - current_line = Line::default(); - current_width = 0; - } - - current_line.push_span(span); - current_width += span_width; - } - - lines.push(current_line); - - Ok(Paragraph::new(lines)) + Ok(keymap) } } + +/// Build the corresponding spans for a group of keys. +/// +/// # Arguments +/// - `group_name`: The name of the group. +/// - `key_groups`: A vector of vectors of strings representing the keys for each group. +/// Each vector of strings represents a group of alternate keys for a given `Action`. +/// +/// # Returns +/// A vector of `Span`s representing the key groups. +/// +/// # Example +/// ```rust +/// use ratatui::text::Span; +/// use television::ui::help::build_spans_for_key_groups; +/// +/// let key_groups = vec![ +/// // alternate keys for the `SelectNextEntry` action +/// vec!["j".to_string(), "n".to_string()], +/// // alternate keys for the `SelectPrevEntry` action +/// vec!["k".to_string(), "p".to_string()], +/// ]; +/// let spans = build_spans_for_key_groups("↕ Results", key_groups); +/// +/// assert_eq!(spans.len(), 5); +/// ``` +fn build_spans_for_key_groups( + group_name: &str, + key_groups: Vec>, +) -> Vec { + if key_groups.is_empty() || key_groups.iter().all(|keys| keys.is_empty()) { + return vec![]; + } + let non_empty_groups = key_groups.iter().filter(|keys| !keys.is_empty()); + let mut spans = vec![ + Span::styled( + group_name.to_owned() + ": ", + Style::default().fg(ACTION_COLOR), + ), + Span::styled("[", Style::default().fg(KEY_COLOR)), + ]; + let key_group_spans: Vec = non_empty_groups + .map(|keys| { + let key_group = keys.join(", "); + 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))); + spans +} + +/// Get the keys for a given action. +/// +/// # Arguments +/// - `keymap`: A hashmap of keybindings. +/// - `action`: The action to get the keys for. +/// +/// # Returns +/// A vector of strings representing the keys for the given action. +/// +/// # Example +/// ```rust +/// use std::collections::HashMap; +/// use television::action::Action; +/// use television::ui::help::keys_for_action; +/// +/// let mut keymap = HashMap::new(); +/// keymap.insert('j', Action::SelectNextEntry); +/// keymap.insert('k', Action::SelectPrevEntry); +/// +/// let keys = keys_for_action(&keymap, Action::SelectNextEntry); +/// +/// assert_eq!(keys, vec!["j"]); +/// ``` +fn keys_for_action( + keymap: &HashMap, + action: Action, +) -> Vec { + keymap + .iter() + .filter(|(_key, act)| **act == action) + .map(|(key, _act)| format!("{key}")) + .collect() +} diff --git a/crates/television/ui/layout.rs b/crates/television/ui/layout.rs index 3519335..d31ff59 100644 --- a/crates/television/ui/layout.rs +++ b/crates/television/ui/layout.rs @@ -49,7 +49,7 @@ impl Layout { with_preview: bool, ) -> Self { let main_block = centered_rect(dimensions.x, dimensions.y, area); - // split the main block into two vertical chunks + // split the main block into two vertical chunks (help bar + rest) let hz_chunks = layout::Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Fill(1), Constraint::Length(5)]) diff --git a/crates/television/ui/preview.rs b/crates/television/ui/preview.rs index c92b635..5ced656 100644 --- a/crates/television/ui/preview.rs +++ b/crates/television/ui/preview.rs @@ -2,7 +2,7 @@ use crate::previewers::{ Preview, PreviewContent, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG, }; use crate::television::Television; -use crate::utils::strings::{EMPTY_STRING, FOUR_SPACES}; +use crate::utils::strings::EMPTY_STRING; use ratatui::layout::{Alignment, Rect}; use ratatui::prelude::{Color, Line, Modifier, Span, Style, Stylize, Text}; use ratatui::widgets::{Block, Paragraph, Wrap}; @@ -235,7 +235,7 @@ fn compute_paragraph_from_highlighted_lines( ))) .chain(l.iter().cloned().map(|sr| { convert_syn_region_to_span( - &(sr.0, sr.1.replace('\t', FOUR_SPACES)), + &(sr.0, sr.1), if line_specifier.is_some() && i == line_specifier.unwrap() - 1 { @@ -257,7 +257,7 @@ fn compute_paragraph_from_highlighted_lines( Paragraph::new(preview_lines) } -fn convert_syn_region_to_span<'a>( +pub fn convert_syn_region_to_span<'a>( syn_region: &(syntect::highlighting::Style, String), background: Option, ) -> Span<'a> { diff --git a/crates/television/ui/results.rs b/crates/television/ui/results.rs index 73496ce..b19fbf7 100644 --- a/crates/television/ui/results.rs +++ b/crates/television/ui/results.rs @@ -8,6 +8,7 @@ use std::str::FromStr; const DEFAULT_RESULT_NAME_FG: Color = Color::Blue; const DEFAULT_RESULT_PREVIEW_FG: Color = Color::Rgb(150, 150, 150); const DEFAULT_RESULT_LINE_NUMBER_FG: Color = Color::Yellow; +const DEFAULT_RESULT_SELECTED_BG: Color = Color::Rgb(50, 50, 50); pub fn build_results_list<'a, 'b>( results_block: Block<'b>, @@ -107,7 +108,7 @@ where Line::from(spans) })) .direction(ListDirection::BottomToTop) - .highlight_style(Style::default().bg(Color::Rgb(50, 50, 50))) + .highlight_style(Style::default().bg(DEFAULT_RESULT_SELECTED_BG)) .highlight_symbol("> ") .block(results_block) } diff --git a/crates/television/utils.rs b/crates/television/utils.rs index d11f5e9..67cc40a 100644 --- a/crates/television/utils.rs +++ b/crates/television/utils.rs @@ -1,3 +1,4 @@ pub mod files; pub mod indices; pub mod strings; +pub mod syntax; diff --git a/crates/television/utils/strings.rs b/crates/television/utils/strings.rs index f36ba31..5b8ba50 100644 --- a/crates/television/utils/strings.rs +++ b/crates/television/utils/strings.rs @@ -154,7 +154,6 @@ pub fn shrink_with_ellipsis(s: &str, max_length: usize) -> String { #[cfg(test)] mod tests { - use super::*; fn test_replace_nonprintable(input: &str, expected: &str) { diff --git a/crates/television/utils/syntax.rs b/crates/television/utils/syntax.rs new file mode 100644 index 0000000..75bb8fc --- /dev/null +++ b/crates/television/utils/syntax.rs @@ -0,0 +1,57 @@ +use std::path::Path; +use syntect::easy::HighlightLines; +use syntect::highlighting::{Style, Theme}; +use syntect::parsing::SyntaxSet; +use tracing::warn; + +pub fn compute_highlights_for_path( + file_path: &Path, + lines: Vec, + syntax_set: &SyntaxSet, + syntax_theme: &Theme, +) -> color_eyre::Result>> { + let syntax = + syntax_set + .find_syntax_for_file(file_path)? + .unwrap_or_else(|| { + warn!( + "No syntax found for {:?}, defaulting to plain text", + file_path + ); + syntax_set.find_syntax_plain_text() + }); + let mut highlighter = HighlightLines::new(syntax, syntax_theme); + let mut highlighted_lines = Vec::new(); + for line in lines { + let hl_regions = highlighter.highlight_line(&line, syntax_set)?; + highlighted_lines.push( + hl_regions + .iter() + .map(|(style, text)| (*style, (*text).to_string())) + .collect(), + ); + } + Ok(highlighted_lines) +} + +pub fn compute_highlights_for_line<'a>( + line: &'a str, + syntax_set: &SyntaxSet, + syntax_theme: &Theme, + file_path: &str, +) -> color_eyre::Result> { + let syntax = syntax_set.find_syntax_for_file(file_path)?; + match syntax { + None => { + warn!( + "No syntax found for path {:?}, defaulting to plain text", + file_path + ); + Ok(vec![(Style::default(), line)]) + } + Some(syntax) => { + let mut highlighter = HighlightLines::new(syntax, syntax_theme); + Ok(highlighter.highlight_line(line, syntax_set)?) + } + } +} diff --git a/crates/television_derive/src/lib.rs b/crates/television_derive/src/lib.rs index 1b2c839..f6fd831 100644 --- a/crates/television_derive/src/lib.rs +++ b/crates/television_derive/src/lib.rs @@ -63,7 +63,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { let inner_type = &fields.unnamed[0].ty; quote! { - CliTvChannel::#variant_name => AvailableChannel::#variant_name(#inner_type::default()) + CliTvChannel::#variant_name => TelevisionChannel::#variant_name(#inner_type::default()) } } else { panic!("Enum variants should have exactly one unnamed field."); @@ -77,7 +77,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { #cli_enum impl CliTvChannel { - pub fn to_channel(self) -> AvailableChannel { + pub fn to_channel(self) -> TelevisionChannel { match self { #(#arms),* } @@ -88,9 +88,9 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream { gen.into() } -/// This macro generates the TelevisionChannel trait implementation for the +/// This macro generates the `OnAir` trait implementation for the /// given enum. -#[proc_macro_derive(TvChannel)] +#[proc_macro_derive(Broadcast)] pub fn tv_channel_derive(input: TokenStream) -> TokenStream { // Construct a representation of Rust code as a syntax tree // that we can manipulate @@ -105,13 +105,13 @@ fn impl_tv_channel(ast: &syn::DeriveInput) -> TokenStream { let variants = if let syn::Data::Enum(data_enum) = &ast.data { &data_enum.variants } else { - panic!("#[derive(TvChannel)] is only defined for enums"); + panic!("#[derive(OnAir)] is only defined for enums"); }; // Ensure the enum has at least one variant assert!( !variants.is_empty(), - "#[derive(TvChannel)] requires at least one variant" + "#[derive(OnAir)] requires at least one variant" ); let enum_name = &ast.ident; @@ -120,7 +120,7 @@ fn impl_tv_channel(ast: &syn::DeriveInput) -> TokenStream { // Generate the trait implementation for the TelevisionChannel trait let trait_impl = quote! { - impl TelevisionChannel for #enum_name { + impl OnAir for #enum_name { fn find(&mut self, pattern: &str) { match self { #( @@ -180,8 +180,89 @@ fn impl_tv_channel(ast: &syn::DeriveInput) -> TokenStream { )* } } + + fn shutdown(&self) { + match self { + #( + #enum_name::#variant_names(ref channel) => { + channel.shutdown() + } + )* + } + } } }; trait_impl.into() } + +#[proc_macro_derive(UnitChannel)] +pub fn unit_channel_derive(input: TokenStream) -> TokenStream { + // Construct a representation of Rust code as a syntax tree + // that we can manipulate + let ast = syn::parse(input).unwrap(); + + // Build the trait implementation + impl_unit_channel(&ast) +} + +fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream { + // Ensure the struct is an enum + let variants = if let syn::Data::Enum(data_enum) = &ast.data { + &data_enum.variants + } else { + panic!("#[derive(UnitChannel)] is only defined for enums"); + }; + + // Ensure the enum has at least one variant + assert!( + !variants.is_empty(), + "#[derive(UnitChannel)] requires at least one variant" + ); + + let variant_names: Vec<_> = variants.iter().map(|v| &v.ident).collect(); + + // Generate a unit enum from the given enum + let unit_enum = quote! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum UnitChannel { + #( + #variant_names, + )* + } + }; + + // Generate Into implementation + let into_impl = quote! { + impl Into for UnitChannel { + fn into(self) -> TelevisionChannel { + match self { + #( + UnitChannel::#variant_names => TelevisionChannel::#variant_names(Default::default()), + )* + } + } + } + }; + + // Generate From<&TelevisionChannel> implementation + let from_impl = quote! { + impl From<&TelevisionChannel> for UnitChannel { + fn from(channel: &TelevisionChannel) -> Self { + match channel { + #( + TelevisionChannel::#variant_names(_) => UnitChannel::#variant_names, + )* + } + } + } + }; + + let gen = quote! { + #unit_enum + #into_impl + #from_impl + }; + + gen.into() +}