mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 11:35:25 +00:00
new things
This commit is contained in:
parent
10f302546d
commit
5556515240
@ -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"
|
||||
|
||||
|
@ -42,6 +42,6 @@ pub enum Action {
|
||||
Error(String),
|
||||
NoOp,
|
||||
// channel actions
|
||||
ToChannelSelection,
|
||||
PipeInto,
|
||||
ToggleChannelSelection,
|
||||
SendToChannel,
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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!()
|
||||
}
|
||||
}
|
||||
|
@ -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!()
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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")
|
||||
|
@ -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!()
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
{
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)])
|
||||
|
@ -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> {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod files;
|
||||
pub mod indices;
|
||||
pub mod strings;
|
||||
pub mod syntax;
|
||||
|
@ -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) {
|
||||
|
57
crates/television/utils/syntax.rs
Normal file
57
crates/television/utils/syntax.rs
Normal 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)?)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user