new things

This commit is contained in:
Alexandre Pasmantier 2024-10-21 00:14:13 +02:00
parent 10f302546d
commit 5556515240
23 changed files with 528 additions and 287 deletions

View File

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

View File

@ -42,6 +42,6 @@ pub enum Action {
Error(String),
NoOp,
// channel actions
ToChannelSelection,
PipeInto,
ToggleChannelSelection,
SendToChannel,
}

View File

@ -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<Self> {

View File

@ -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<Self, Self::Error> {
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)),
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<DirEntry>,
@ -24,6 +25,7 @@ pub struct Channel {
total_count: u32,
running: bool,
icon: FileIcon,
crawl_cancellation_tx: watch::Sender<bool>,
}
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<Entry> {
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<DirEntry>,
cancellation_rx: watch::Receiver<bool>,
) {
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")

View File

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

View File

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

View File

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

View File

@ -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<F>(
&mut self,
key: String,
f: F,
) -> Arc<Preview>
pub fn get_or_insert<F>(&mut self, key: String, f: F) -> Arc<Preview>
where
F: FnOnce() -> Preview,
{

View File

@ -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<Mutex<PreviewCache>>,
syntax_set: Arc<SyntaxSet>,
syntax_theme: Arc<Theme>,
pub syntax_set: Arc<SyntaxSet>,
pub syntax_theme: Arc<Theme>,
//image_picker: Arc<Mutex<Picker>>,
}
@ -168,7 +168,7 @@ impl FilePreviewer {
let lines: Vec<String> =
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<Preview> {
PreviewContent::PlainText(lines),
))
}
fn compute_highlights(
file_path: &Path,
lines: Vec<String>,
syntax_set: &SyntaxSet,
syntax_theme: &Theme,
) -> Result<Vec<Vec<(Style, String)>>> {
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)
}

View File

@ -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<UnboundedSender<Action>>,
pub config: Config,
channel: AvailableChannel,
channel: TelevisionChannel,
last_channel: Option<UnitChannel>,
last_channel_results: Vec<Entry>,
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<u16>,
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<Entry> {
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<Option<Action>>` - An action to be processed or none.
pub async fn update(&mut self, action: Action) -> Result<Option<Action>> {
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 => {
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;
let selection_channel =
AvailableChannel::Channel(SelectionChannel::new());
self.change_channel(selection_channel);
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);

View File

@ -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<Paragraph<'a>> {
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<Paragraph<'a>> {
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<Paragraph<'a>> {
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<Key, Action>> {
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<String>>,
) -> Vec<Span> {
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<Span> = 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<Key, Action>,
action: Action,
) -> Vec<String> {
keymap
.iter()
.filter(|(_key, act)| **act == action)
.map(|(key, _act)| format!("{key}"))
.collect()
}

View File

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

View File

@ -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<syntect::highlighting::Color>,
) -> Span<'a> {

View File

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

View File

@ -1,3 +1,4 @@
pub mod files;
pub mod indices;
pub mod strings;
pub mod syntax;

View File

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

View File

@ -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<String>,
syntax_set: &SyntaxSet,
syntax_theme: &Theme,
) -> color_eyre::Result<Vec<Vec<(Style, String)>>> {
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<Vec<(Style, &'a str)>> {
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)?)
}
}
}

View File

@ -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<TelevisionChannel> implementation
let into_impl = quote! {
impl Into<TelevisionChannel> 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()
}