This commit is contained in:
Alexandre Pasmantier 2024-10-20 01:02:53 +02:00
parent 590fe14ee5
commit db3aa1ad49
24 changed files with 890 additions and 358 deletions

View File

@ -1,18 +1,22 @@
[keybindings.Input] [keybindings.Channel]
q = "Quit"
esc = "Quit" esc = "Quit"
ctrl-c = "Quit"
ctrl-z = "Suspend"
tab = "GoToNextPane"
backtab = "GoToPrevPane"
ctrl-left = "GoToPaneLeft"
ctrl-right = "GoToPaneRight"
down = "SelectNextEntry" down = "SelectNextEntry"
up = "SelectPrevEntry" up = "SelectPrevEntry"
ctrl-n = "SelectNextEntry" ctrl-n = "SelectNextEntry"
ctrl-p = "SelectPrevEntry" ctrl-p = "SelectPrevEntry"
ctrl-down = "ScrollPreviewDown" alt-down = "ScrollPreviewHalfPageDown"
ctrl-up = "ScrollPreviewUp"
ctrl-d = "ScrollPreviewHalfPageDown" ctrl-d = "ScrollPreviewHalfPageDown"
alt-up = "ScrollPreviewHalfPageUp"
ctrl-u = "ScrollPreviewHalfPageUp" ctrl-u = "ScrollPreviewHalfPageUp"
enter = "SelectEntry" enter = "SelectEntry"
ctrl-s = "ToChannelSelection"
[keybindings.ChannelSelection]
esc = "Quit"
down = "SelectNextEntry"
up = "SelectPrevEntry"
ctrl-n = "SelectNextEntry"
ctrl-p = "SelectPrevEntry"
enter = "SelectEntry"
ctrl-enter = "PipeInto"

View File

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::Display; use strum::Display;
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display)]
pub enum Action { pub enum Action {
// input actions // input actions
AddInputChar(char), AddInputChar(char),
@ -17,6 +17,7 @@ pub enum Action {
ClearScreen, ClearScreen,
// results actions // results actions
SelectEntry, SelectEntry,
SelectAndExit,
SelectNextEntry, SelectNextEntry,
SelectPrevEntry, SelectPrevEntry,
// navigation actions // navigation actions
@ -41,5 +42,6 @@ pub enum Action {
Error(String), Error(String),
NoOp, NoOp,
// channel actions // channel actions
SyncFinderResults, ToChannelSelection,
PipeInto,
} }

View File

@ -53,7 +53,6 @@
use std::sync::Arc; use std::sync::Arc;
use color_eyre::Result; use color_eyre::Result;
use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use tracing::{debug, info}; use tracing::{debug, info};
@ -76,7 +75,6 @@ pub struct App {
television: Arc<Mutex<Television>>, television: Arc<Mutex<Television>>,
should_quit: bool, should_quit: bool,
should_suspend: bool, should_suspend: bool,
mode: Mode,
action_tx: mpsc::UnboundedSender<Action>, action_tx: mpsc::UnboundedSender<Action>,
action_rx: mpsc::UnboundedReceiver<Action>, action_rx: mpsc::UnboundedReceiver<Action>,
event_rx: mpsc::UnboundedReceiver<Event<Key>>, event_rx: mpsc::UnboundedReceiver<Event<Key>>,
@ -84,17 +82,6 @@ pub struct App {
render_tx: mpsc::UnboundedSender<RenderingTask>, render_tx: mpsc::UnboundedSender<RenderingTask>,
} }
#[derive(
Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize,
)]
pub enum Mode {
#[default]
Help,
Input,
Preview,
Results,
}
impl App { impl App {
pub fn new( pub fn new(
channel: CliTvChannel, channel: CliTvChannel,
@ -114,7 +101,6 @@ impl App {
should_quit: false, should_quit: false,
should_suspend: false, should_suspend: false,
config: Config::new()?, config: Config::new()?,
mode: Mode::Input,
action_tx, action_tx,
action_rx, action_rx,
event_rx, event_rx,
@ -140,7 +126,6 @@ impl App {
let rendering_task = tokio::spawn(async move { let rendering_task = tokio::spawn(async move {
render( render(
render_rx, render_rx,
//render_tx,
action_tx_r, action_tx_r,
config_r, config_r,
television_r, television_r,
@ -178,28 +163,23 @@ impl App {
match event { match event {
Event::Input(keycode) => { Event::Input(keycode) => {
info!("{:?}", keycode); info!("{:?}", keycode);
// if the current component is the television // text input events
// and the mode is input, automatically handle match keycode {
// (these mappings aren't exposed to the user) Key::Backspace => return Action::DeletePrevChar,
if self.television.lock().await.is_input_focused() { Key::Delete => return Action::DeleteNextChar,
match keycode { Key::Left => return Action::GoToPrevChar,
Key::Backspace => return Action::DeletePrevChar, Key::Right => return Action::GoToNextChar,
Key::Delete => return Action::DeleteNextChar, Key::Home | Key::Ctrl('a') => {
Key::Left => return Action::GoToPrevChar, return Action::GoToInputStart
Key::Right => return Action::GoToNextChar,
Key::Home | Key::Ctrl('a') => {
return Action::GoToInputStart
}
Key::End | Key::Ctrl('e') => {
return Action::GoToInputEnd
}
Key::Char(c) => return Action::AddInputChar(c),
_ => {}
} }
Key::End | Key::Ctrl('e') => return Action::GoToInputEnd,
Key::Char(c) => return Action::AddInputChar(c),
_ => {}
} }
// get action based on keybindings
self.config self.config
.keybindings .keybindings
.get(&self.mode) .get(&self.television.lock().await.mode)
.and_then(|keymap| keymap.get(&keycode).cloned()) .and_then(|keymap| keymap.get(&keycode).cloned())
.unwrap_or(if let Key::Char(c) = keycode { .unwrap_or(if let Key::Char(c) = keycode {
Action::AddInputChar(c) Action::AddInputChar(c)
@ -234,7 +214,7 @@ impl App {
self.should_suspend = false; self.should_suspend = false;
self.render_tx.send(RenderingTask::Resume)?; self.render_tx.send(RenderingTask::Resume)?;
} }
Action::SelectEntry => { Action::SelectAndExit => {
self.should_quit = true; self.should_quit = true;
self.render_tx.send(RenderingTask::Quit)?; self.render_tx.send(RenderingTask::Quit)?;
return Ok(self return Ok(self

View File

@ -1,7 +1,9 @@
use crate::entry::Entry; use crate::entry::Entry;
use television_derive::CliChannel; use color_eyre::eyre::Result;
use television_derive::{CliChannel, TvChannel};
mod alias; mod alias;
pub mod channels;
mod env; mod env;
mod files; mod files;
mod git_repos; mod git_repos;
@ -87,8 +89,8 @@ pub trait TelevisionChannel: Send {
/// instance from the selected CLI enum variant. /// instance from the selected CLI enum variant.
/// ///
#[allow(dead_code, clippy::module_name_repetitions)] #[allow(dead_code, clippy::module_name_repetitions)]
#[derive(CliChannel)] #[derive(CliChannel, TvChannel)]
pub enum AvailableChannels { pub enum AvailableChannel {
Env(env::Channel), Env(env::Channel),
Files(files::Channel), Files(files::Channel),
GitRepos(git_repos::Channel), GitRepos(git_repos::Channel),
@ -96,3 +98,22 @@ pub enum AvailableChannels {
Stdin(stdin::Channel), Stdin(stdin::Channel),
Alias(alias::Channel), Alias(alias::Channel),
} }
/// NOTE: this could be generated by a derive macro
impl TryFrom<&Entry> for AvailableChannel {
type Error = String;
fn try_from(entry: &Entry) -> Result<Self, Self::Error> {
match entry.name.as_ref() {
"env" => Ok(AvailableChannel::Env(env::Channel::default())),
"files" => Ok(AvailableChannel::Files(files::Channel::default())),
"gitrepos" => {
Ok(AvailableChannel::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())),
_ => Err(format!("Unknown channel: {}", entry.name)),
}
}
}

View File

@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use devicons::FileIcon; use devicons::FileIcon;
use nucleo::{Config, Nucleo}; use nucleo::{Config, Injector, Nucleo};
use tracing::debug; use tracing::debug;
use crate::channels::TelevisionChannel; use crate::channels::TelevisionChannel;
@ -16,6 +16,12 @@ struct Alias {
value: String, value: String,
} }
impl Alias {
fn new(name: String, value: String) -> Self {
Self { name, value }
}
}
pub struct Channel { pub struct Channel {
matcher: Nucleo<Alias>, matcher: Nucleo<Alias>,
last_pattern: String, last_pattern: String,
@ -68,22 +74,6 @@ fn get_raw_aliases(shell: &str) -> Vec<String> {
impl Channel { impl Channel {
pub fn new() -> Self { pub fn new() -> Self {
let raw_shell = get_current_shell().unwrap_or("bash".to_string());
let shell = raw_shell.split('/').last().unwrap();
debug!("Current shell: {}", shell);
let raw_aliases = get_raw_aliases(shell);
debug!("Aliases: {:?}", raw_aliases);
let parsed_aliases = raw_aliases
.iter()
.map(|alias| {
let mut parts = alias.split('=');
let name = parts.next().unwrap().to_string();
let value = parts.next().unwrap().to_string();
Alias { name, value }
})
.collect::<Vec<_>>();
let matcher = Nucleo::new( let matcher = Nucleo::new(
Config::DEFAULT, Config::DEFAULT,
Arc::new(|| {}), Arc::new(|| {}),
@ -91,12 +81,7 @@ impl Channel {
1, 1,
); );
let injector = matcher.injector(); let injector = matcher.injector();
tokio::spawn(load_aliases(injector));
for alias in parsed_aliases {
let _ = injector.push(alias.clone(), |_, cols| {
cols[0] = (alias.name.clone() + &alias.value).into();
});
}
Self { Self {
matcher, matcher,
@ -108,7 +93,13 @@ impl Channel {
} }
} }
const MATCHER_TICK_TIMEOUT: u64 = 10; const MATCHER_TICK_TIMEOUT: u64 = 2;
}
impl Default for Channel {
fn default() -> Self {
Self::new()
}
} }
impl TelevisionChannel for Channel { impl TelevisionChannel for Channel {
@ -208,3 +199,33 @@ impl TelevisionChannel for Channel {
self.running self.running
} }
} }
#[allow(clippy::unused_async)]
async fn load_aliases(injector: Injector<Alias>) {
let raw_shell = get_current_shell().unwrap_or("bash".to_string());
let shell = raw_shell.split('/').last().unwrap();
debug!("Current shell: {}", shell);
let raw_aliases = get_raw_aliases(shell);
let parsed_aliases = raw_aliases
.iter()
.filter_map(|alias| {
let mut parts = alias.split('=');
if let Some(name) = parts.next() {
if let Some(value) = parts.next() {
return Some(Alias::new(
name.to_string(),
value.to_string(),
));
}
}
None
})
.collect::<Vec<_>>();
for alias in parsed_aliases {
let _ = injector.push(alias.clone(), |_, cols| {
cols[0] = (alias.name.clone() + &alias.value).into();
});
}
}

View File

@ -0,0 +1,135 @@
use std::sync::Arc;
use clap::ValueEnum;
use devicons::FileIcon;
use nucleo::{
pattern::{CaseMatching, Normalization},
Config, Nucleo,
};
use crate::{
channels::{CliTvChannel, TelevisionChannel},
entry::Entry,
fuzzy::MATCHER,
previewers::PreviewType,
};
pub struct SelectionChannel {
matcher: Nucleo<CliTvChannel>,
last_pattern: String,
result_count: u32,
total_count: u32,
running: bool,
}
const NUM_THREADS: usize = 1;
const CHANNEL_BLACKLIST: [CliTvChannel; 1] = [CliTvChannel::Stdin];
impl SelectionChannel {
pub fn new() -> Self {
let matcher = Nucleo::new(
Config::DEFAULT,
Arc::new(|| {}),
Some(NUM_THREADS),
1,
);
let injector = matcher.injector();
for variant in CliTvChannel::value_variants() {
if CHANNEL_BLACKLIST.contains(variant) {
continue;
}
let _ = injector.push(*variant, |e, cols| {
cols[0] = (*e).to_string().into();
});
}
SelectionChannel {
matcher,
last_pattern: String::new(),
result_count: 0,
total_count: 0,
running: false,
}
}
const MATCHER_TICK_TIMEOUT: u64 = 2;
}
const TV_ICON: FileIcon = FileIcon {
icon: '📺',
color: "#ffffff",
};
impl TelevisionChannel for SelectionChannel {
fn find(&mut self, pattern: &str) {
if pattern != self.last_pattern {
self.matcher.pattern.reparse(
0,
pattern,
CaseMatching::Smart,
Normalization::Smart,
pattern.starts_with(&self.last_pattern),
);
self.last_pattern = pattern.to_string();
}
}
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();
if status.changed {
self.result_count = snapshot.matched_item_count();
self.total_count = snapshot.item_count();
}
self.running = status.running;
let mut indices = Vec::new();
let mut matcher = MATCHER.lock();
snapshot
.matched_items(
offset
..(num_entries + offset)
.min(snapshot.matched_item_count()),
)
.map(move |item| {
snapshot.pattern().column_pattern(0).indices(
item.matcher_columns[0].slice(..),
&mut matcher,
&mut indices,
);
indices.sort_unstable();
indices.dedup();
let indices = indices.drain(..);
let name = item.matcher_columns[0].to_string();
Entry::new(name.clone(), PreviewType::Basic)
.with_name_match_ranges(
indices.map(|i| (i, i + 1)).collect(),
)
.with_icon(TV_ICON)
})
.collect()
}
fn get_result(&self, index: u32) -> Option<Entry> {
let snapshot = self.matcher.snapshot();
snapshot.get_matched_item(index).map(|item| {
let name = item.matcher_columns[0].to_string();
// TODO: Add new Previewer for Channel selection which displays a
// short description of the channel
Entry::new(name.clone(), PreviewType::Basic).with_icon(TV_ICON)
})
}
fn running(&self) -> bool {
self.running
}
}

View File

@ -53,7 +53,13 @@ impl Channel {
} }
} }
const MATCHER_TICK_TIMEOUT: u64 = 10; const MATCHER_TICK_TIMEOUT: u64 = 2;
}
impl Default for Channel {
fn default() -> Self {
Self::new()
}
} }
impl TelevisionChannel for Channel { impl TelevisionChannel for Channel {

View File

@ -3,7 +3,11 @@ use nucleo::{
pattern::{CaseMatching, Normalization}, pattern::{CaseMatching, Normalization},
Config, Injector, Nucleo, Config, Injector, Nucleo,
}; };
use std::{os::unix::ffi::OsStrExt, path::PathBuf, sync::Arc}; use std::{
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
sync::Arc,
};
use ignore::DirEntry; use ignore::DirEntry;
@ -26,7 +30,7 @@ pub struct Channel {
} }
impl Channel { impl Channel {
pub fn new() -> Self { pub fn new(starting_dir: &Path) -> Self {
let matcher = Nucleo::new( let matcher = Nucleo::new(
Config::DEFAULT.match_paths(), Config::DEFAULT.match_paths(),
Arc::new(|| {}), Arc::new(|| {}),
@ -35,7 +39,7 @@ impl Channel {
); );
// start loading files in the background // start loading files in the background
tokio::spawn(load_files( tokio::spawn(load_files(
std::env::current_dir().unwrap(), starting_dir.to_path_buf(),
matcher.injector(), matcher.injector(),
)); ));
Channel { Channel {
@ -47,7 +51,13 @@ impl Channel {
} }
} }
const MATCHER_TICK_TIMEOUT: u64 = 10; const MATCHER_TICK_TIMEOUT: u64 = 2;
}
impl Default for Channel {
fn default() -> Self {
Self::new(&std::env::current_dir().unwrap())
}
} }
impl TelevisionChannel for Channel { impl TelevisionChannel for Channel {

View File

@ -23,6 +23,7 @@ pub struct Channel {
result_count: u32, result_count: u32,
total_count: u32, total_count: u32,
running: bool, running: bool,
icon: FileIcon,
} }
impl Channel { impl Channel {
@ -44,10 +45,17 @@ impl Channel {
result_count: 0, result_count: 0,
total_count: 0, total_count: 0,
running: false, running: false,
icon: FileIcon::from("git"),
} }
} }
const MATCHER_TICK_TIMEOUT: u64 = 10; const MATCHER_TICK_TIMEOUT: u64 = 2;
}
impl Default for Channel {
fn default() -> Self {
Self::new()
}
} }
impl TelevisionChannel for Channel { impl TelevisionChannel for Channel {
@ -82,6 +90,7 @@ impl TelevisionChannel for Channel {
self.running = status.running; self.running = status.running;
let mut indices = Vec::new(); let mut indices = Vec::new();
let mut matcher = MATCHER.lock(); let mut matcher = MATCHER.lock();
let icon = self.icon;
snapshot snapshot
.matched_items( .matched_items(
@ -104,7 +113,7 @@ impl TelevisionChannel for Channel {
.with_name_match_ranges( .with_name_match_ranges(
indices.map(|i| (i, i + 1)).collect(), indices.map(|i| (i, i + 1)).collect(),
) )
.with_icon(FileIcon::from(&path)) .with_icon(icon)
}) })
.collect() .collect()
} }
@ -114,7 +123,7 @@ impl TelevisionChannel for Channel {
snapshot.get_matched_item(index).map(|item| { snapshot.get_matched_item(index).map(|item| {
let path = item.matcher_columns[0].to_string(); let path = item.matcher_columns[0].to_string();
Entry::new(path.clone(), PreviewType::Directory) Entry::new(path.clone(), PreviewType::Directory)
.with_icon(FileIcon::from(&path)) .with_icon(self.icon)
}) })
} }

View File

@ -26,7 +26,6 @@ impl Channel {
pub fn new() -> Self { pub fn new() -> Self {
let mut lines = Vec::new(); let mut lines = Vec::new();
for line in std::io::stdin().lock().lines().map_while(Result::ok) { for line in std::io::stdin().lock().lines().map_while(Result::ok) {
debug!("Read line: {:?}", line);
lines.push(line); lines.push(line);
} }
let matcher = Nucleo::new( let matcher = Nucleo::new(
@ -51,7 +50,13 @@ impl Channel {
} }
} }
const MATCHER_TICK_TIMEOUT: u64 = 10; const MATCHER_TICK_TIMEOUT: u64 = 2;
}
impl Default for Channel {
fn default() -> Self {
Self::new()
}
} }
impl TelevisionChannel for Channel { impl TelevisionChannel for Channel {

View File

@ -6,7 +6,7 @@ use nucleo::{
use std::{ use std::{
fs::File, fs::File,
io::{BufRead, Read, Seek}, io::{BufRead, Read, Seek},
path::PathBuf, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
@ -50,11 +50,11 @@ pub struct Channel {
} }
impl Channel { impl Channel {
pub fn new() -> Self { pub fn new(working_dir: &Path) -> Self {
let matcher = Nucleo::new(Config::DEFAULT, Arc::new(|| {}), None, 1); let matcher = Nucleo::new(Config::DEFAULT, Arc::new(|| {}), None, 1);
// start loading files in the background // start loading files in the background
tokio::spawn(load_candidates( tokio::spawn(load_candidates(
std::env::current_dir().unwrap(), working_dir.to_path_buf(),
matcher.injector(), matcher.injector(),
)); ));
Channel { Channel {
@ -66,7 +66,13 @@ impl Channel {
} }
} }
const MATCHER_TICK_TIMEOUT: u64 = 10; const MATCHER_TICK_TIMEOUT: u64 = 2;
}
impl Default for Channel {
fn default() -> Self {
Self::new(&std::env::current_dir().unwrap())
}
} }
impl TelevisionChannel for Channel { impl TelevisionChannel for Channel {

View File

@ -11,11 +11,10 @@ use tracing::{error, info};
use crate::{ use crate::{
action::Action, action::Action,
app::Mode,
event::{convert_raw_event_to_key, Key}, event::{convert_raw_event_to_key, Key},
television::Mode,
}; };
//const CONFIG: &str = include_str!("../.config/config.json5");
const CONFIG: &str = include_str!("../../.config/config.toml"); const CONFIG: &str = include_str!("../../.config/config.toml");
#[allow(dead_code, clippy::module_name_repetitions)] #[allow(dead_code, clippy::module_name_repetitions)]
@ -523,9 +522,9 @@ mod tests {
let c = Config::new()?; let c = Config::new()?;
assert_eq!( assert_eq!(
c.keybindings c.keybindings
.get(&Mode::Input) .get(&Mode::Channel)
.unwrap() .unwrap()
.get(&parse_key("<q>").unwrap()) .get(&parse_key("esc").unwrap())
.unwrap(), .unwrap(),
&Action::Quit &Action::Quit
); );

View File

@ -1,4 +1,5 @@
use std::{ use std::{
fmt::Display,
future::Future, future::Future,
pin::Pin, pin::Pin,
task::{Context, Poll as TaskPoll}, task::{Context, Poll as TaskPoll},
@ -14,7 +15,7 @@ use crossterm::event::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::warn; use tracing::{debug, warn};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum Event<I> { pub enum Event<I> {
@ -36,6 +37,7 @@ pub enum Key {
Right, Right,
Up, Up,
Down, Down,
CtrlSpace,
CtrlBackspace, CtrlBackspace,
CtrlEnter, CtrlEnter,
CtrlLeft, CtrlLeft,
@ -43,8 +45,14 @@ pub enum Key {
CtrlUp, CtrlUp,
CtrlDown, CtrlDown,
CtrlDelete, CtrlDelete,
AltSpace,
AltEnter,
AltBackspace, AltBackspace,
AltDelete, AltDelete,
AltUp,
AltDown,
AltLeft,
AltRight,
Home, Home,
End, End,
PageUp, PageUp,
@ -61,6 +69,49 @@ pub enum Key {
Tab, Tab,
} }
impl Display for Key {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Key::Backspace => write!(f, "Backspace"),
Key::Enter => write!(f, "Enter"),
Key::Left => write!(f, ""),
Key::Right => write!(f, ""),
Key::Up => write!(f, ""),
Key::Down => write!(f, ""),
Key::CtrlSpace => write!(f, "Ctrl-Space"),
Key::CtrlBackspace => write!(f, "Ctrl-Backspace"),
Key::CtrlEnter => write!(f, "Ctrl-Enter"),
Key::CtrlLeft => write!(f, "Ctrl-←"),
Key::CtrlRight => write!(f, "Ctrl-→"),
Key::CtrlUp => write!(f, "Ctrl-↑"),
Key::CtrlDown => write!(f, "Ctrl-↓"),
Key::CtrlDelete => write!(f, "Ctrl-Del"),
Key::AltSpace => write!(f, "Alt+Space"),
Key::AltEnter => write!(f, "Alt+Enter"),
Key::AltBackspace => write!(f, "Alt+Backspace"),
Key::AltDelete => write!(f, "Alt+Delete"),
Key::AltUp => write!(f, "Alt↑"),
Key::AltDown => write!(f, "Alt↓"),
Key::AltLeft => write!(f, "Alt←"),
Key::AltRight => write!(f, "Alt→"),
Key::Home => write!(f, "Home"),
Key::End => write!(f, "End"),
Key::PageUp => write!(f, "PageUp"),
Key::PageDown => write!(f, "PageDown"),
Key::BackTab => write!(f, "BackTab"),
Key::Delete => write!(f, "Delete"),
Key::Insert => write!(f, "Insert"),
Key::F(k) => write!(f, "F{k}"),
Key::Char(c) => write!(f, "{c}"),
Key::Alt(c) => write!(f, "Alt+{c}"),
Key::Ctrl(c) => write!(f, "Ctrl-{c}"),
Key::Null => write!(f, "Null"),
Key::Esc => write!(f, "Esc"),
Key::Tab => write!(f, "Tab"),
}
}
}
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
pub struct EventLoop { pub struct EventLoop {
pub rx: mpsc::UnboundedReceiver<Event<Key>>, pub rx: mpsc::UnboundedReceiver<Event<Key>>,
@ -163,6 +214,7 @@ impl EventLoop {
} }
pub fn convert_raw_event_to_key(event: KeyEvent) -> Key { pub fn convert_raw_event_to_key(event: KeyEvent) -> Key {
debug!("Raw event: {:?}", event);
match event.code { match event.code {
Backspace => match event.modifiers { Backspace => match event.modifiers {
KeyModifiers::CONTROL => Key::CtrlBackspace, KeyModifiers::CONTROL => Key::CtrlBackspace,
@ -176,22 +228,27 @@ pub fn convert_raw_event_to_key(event: KeyEvent) -> Key {
}, },
Enter => match event.modifiers { Enter => match event.modifiers {
KeyModifiers::CONTROL => Key::CtrlEnter, KeyModifiers::CONTROL => Key::CtrlEnter,
KeyModifiers::ALT => Key::AltEnter,
_ => Key::Enter, _ => Key::Enter,
}, },
Up => match event.modifiers { Up => match event.modifiers {
KeyModifiers::CONTROL => Key::CtrlUp, KeyModifiers::CONTROL => Key::CtrlUp,
KeyModifiers::ALT => Key::AltUp,
_ => Key::Up, _ => Key::Up,
}, },
Down => match event.modifiers { Down => match event.modifiers {
KeyModifiers::CONTROL => Key::CtrlDown, KeyModifiers::CONTROL => Key::CtrlDown,
KeyModifiers::ALT => Key::AltDown,
_ => Key::Down, _ => Key::Down,
}, },
Left => match event.modifiers { Left => match event.modifiers {
KeyModifiers::CONTROL => Key::CtrlLeft, KeyModifiers::CONTROL => Key::CtrlLeft,
KeyModifiers::ALT => Key::AltLeft,
_ => Key::Left, _ => Key::Left,
}, },
Right => match event.modifiers { Right => match event.modifiers {
KeyModifiers::CONTROL => Key::CtrlRight, KeyModifiers::CONTROL => Key::CtrlRight,
KeyModifiers::ALT => Key::AltRight,
_ => Key::Right, _ => Key::Right,
}, },
Home => Key::Home, Home => Key::Home,
@ -203,6 +260,12 @@ pub fn convert_raw_event_to_key(event: KeyEvent) -> Key {
Insert => Key::Insert, Insert => Key::Insert,
F(k) => Key::F(k), F(k) => Key::F(k),
Esc => Key::Esc, Esc => Key::Esc,
Char(' ') => match event.modifiers {
KeyModifiers::NONE | KeyModifiers::SHIFT => Key::Char(' '),
KeyModifiers::CONTROL => Key::CtrlSpace,
KeyModifiers::ALT => Key::AltSpace,
_ => Key::Null,
},
Char(c) => match event.modifiers { Char(c) => match event.modifiers {
KeyModifiers::NONE | KeyModifiers::SHIFT => Key::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT => Key::Char(c),
KeyModifiers::CONTROL => Key::Ctrl(c), KeyModifiers::CONTROL => Key::Ctrl(c),

View File

@ -7,6 +7,7 @@ mod cache;
mod directory; mod directory;
mod env; mod env;
mod files; mod files;
mod meta;
// previewer types // previewer types
pub use basic::BasicPreviewer; pub use basic::BasicPreviewer;
@ -99,7 +100,7 @@ impl Previewer {
pub async fn preview(&mut self, entry: &Entry) -> Arc<Preview> { pub async fn preview(&mut self, entry: &Entry) -> Arc<Preview> {
match entry.preview_type { match entry.preview_type {
PreviewType::Basic => self.basic.preview(entry), PreviewType::Basic => self.basic.preview(entry),
PreviewType::Directory => self.directory.preview(entry), PreviewType::Directory => self.directory.preview(entry).await,
PreviewType::EnvVar => self.env_var.preview(entry), PreviewType::EnvVar => self.env_var.preview(entry),
PreviewType::Files => self.file.preview(entry).await, PreviewType::Files => self.file.preview(entry).await,
} }

View File

@ -3,37 +3,50 @@ use std::sync::Arc;
use devicons::FileIcon; use devicons::FileIcon;
use termtree::Tree; use termtree::Tree;
use tokio::sync::Mutex;
use crate::entry::Entry; use crate::entry::Entry;
use crate::previewers::cache::PreviewCache; use crate::previewers::cache::PreviewCache;
use crate::previewers::{Preview, PreviewContent}; use crate::previewers::{meta, Preview, PreviewContent};
use crate::utils::files::walk_builder; use crate::utils::files::walk_builder;
pub struct DirectoryPreviewer { pub struct DirectoryPreviewer {
cache: PreviewCache, cache: Arc<Mutex<PreviewCache>>,
} }
impl DirectoryPreviewer { impl DirectoryPreviewer {
pub fn new() -> Self { pub fn new() -> Self {
DirectoryPreviewer { DirectoryPreviewer {
cache: PreviewCache::default(), cache: Arc::new(Mutex::new(PreviewCache::default())),
} }
} }
pub fn preview(&mut self, entry: &Entry) -> Arc<Preview> { pub async fn preview(&mut self, entry: &Entry) -> Arc<Preview> {
if let Some(preview) = self.cache.get(&entry.name) { if let Some(preview) = self.cache.lock().await.get(&entry.name) {
return preview; return preview;
} }
let preview = Arc::new(build_tree_preview(entry)); let preview = meta::loading(&entry.name);
self.cache.insert(entry.name.clone(), preview.clone()); self.cache
.lock()
.await
.insert(entry.name.clone(), preview.clone());
let entry_c = entry.clone();
let cache = self.cache.clone();
tokio::spawn(async move {
let preview = Arc::new(build_tree_preview(&entry_c));
cache
.lock()
.await
.insert(entry_c.name.clone(), preview.clone());
});
preview preview
} }
} }
fn build_tree_preview(entry: &Entry) -> Preview { fn build_tree_preview(entry: &Entry) -> Preview {
let path = Path::new(&entry.name); let path = Path::new(&entry.name);
let tree = tree(path).unwrap(); let tree = tree(path, MAX_DEPTH, FIRST_LEVEL_MAX_ENTRIES, &mut 0);
let tree_string = tree.to_string(); let tree_string = tree.to_string();
Preview { Preview {
title: entry.name.clone(), title: entry.name.clone(),
@ -47,33 +60,53 @@ fn build_tree_preview(entry: &Entry) -> Preview {
} }
fn label<P: AsRef<Path>>(p: P, strip: &str) -> String { fn label<P: AsRef<Path>>(p: P, strip: &str) -> String {
//let path = p.as_ref().file_name().unwrap().to_str().unwrap().to_owned(); let icon = FileIcon::from(&p);
let path = p.as_ref().strip_prefix(strip).unwrap(); let path = p.as_ref().strip_prefix(strip).unwrap();
let icon = FileIcon::from(&path);
format!("{} {}", icon, path.display()) format!("{} {}", icon, path.display())
} }
/// PERF: (urgent) change to use the ignore crate here const MAX_DEPTH: u8 = 4;
fn tree<P: AsRef<Path>>(p: P) -> std::io::Result<Tree<String>> { const FIRST_LEVEL_MAX_ENTRIES: u8 = 30;
let result = std::fs::read_dir(&p)? const NESTED_MAX_ENTRIES: u8 = 10;
.filter_map(std::result::Result::ok) const MAX_ENTRIES: u8 = 200;
.fold(
Tree::new(label( fn tree<P: AsRef<Path>>(
p.as_ref(), p: P,
p.as_ref().parent().unwrap().to_str().unwrap(), max_depth: u8,
)), nested_max_entries: u8,
|mut root, entry| { total_entry_count: &mut u8,
let m = entry.metadata().unwrap(); ) -> Tree<String> {
if m.is_dir() { let mut root = Tree::new(label(
root.push(tree(entry.path()).unwrap()); p.as_ref(),
} else { p.as_ref().parent().unwrap().to_str().unwrap(),
root.push(Tree::new(label( ));
entry.path(), let w = walk_builder(p.as_ref(), 1, None).max_depth(Some(1)).build();
p.as_ref().to_str().unwrap(), let mut level_entry_count: u8 = 0;
)));
} for path in w.skip(1).filter_map(std::result::Result::ok) {
root let m = path.metadata().unwrap();
}, if m.is_dir() && max_depth > 1 {
); root.push(tree(
Ok(result) path.path(),
max_depth - 1,
NESTED_MAX_ENTRIES,
total_entry_count,
));
} else {
root.push(Tree::new(label(
path.path(),
p.as_ref().to_str().unwrap(),
)));
}
level_entry_count += 1;
*total_entry_count += 1;
if level_entry_count >= nested_max_entries
|| *total_entry_count >= MAX_ENTRIES
{
root.push(Tree::new(String::from("...")));
break;
}
}
root
} }

View File

@ -15,7 +15,7 @@ use syntect::{
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::entry; use crate::entry;
use crate::previewers::{Preview, PreviewContent}; use crate::previewers::{meta, Preview, PreviewContent};
use crate::utils::files::FileType; use crate::utils::files::FileType;
use crate::utils::files::{get_file_size, is_known_text_extension}; use crate::utils::files::{get_file_size, is_known_text_extension};
use crate::utils::strings::{ use crate::utils::strings::{
@ -63,7 +63,7 @@ impl FilePreviewer {
if get_file_size(&path_buf).map_or(false, |s| s > Self::MAX_FILE_SIZE) if get_file_size(&path_buf).map_or(false, |s| s > Self::MAX_FILE_SIZE)
{ {
debug!("File too large: {:?}", entry.name); debug!("File too large: {:?}", entry.name);
let preview = file_too_large(&entry.name); let preview = meta::file_too_large(&entry.name);
self.cache_preview(entry.name.clone(), preview.clone()) self.cache_preview(entry.name.clone(), preview.clone())
.await; .await;
return preview; return preview;
@ -94,7 +94,7 @@ impl FilePreviewer {
} }
Err(e) => { Err(e) => {
warn!("Error opening file: {:?}", e); warn!("Error opening file: {:?}", e);
let p = not_supported(&entry.name); let p = meta::not_supported(&entry.name);
self.cache_preview(entry.name.clone(), p.clone()) self.cache_preview(entry.name.clone(), p.clone())
.await; .await;
p p
@ -105,7 +105,7 @@ impl FilePreviewer {
debug!("Previewing image file: {:?}", entry.name); debug!("Previewing image file: {:?}", entry.name);
// insert a loading preview into the cache // insert a loading preview into the cache
//let preview = loading(&entry.name); //let preview = loading(&entry.name);
let preview = not_supported(&entry.name); let preview = meta::not_supported(&entry.name);
self.cache_preview(entry.name.clone(), preview.clone()) self.cache_preview(entry.name.clone(), preview.clone())
.await; .await;
//// compute the image preview in the background //// compute the image preview in the background
@ -114,14 +114,14 @@ impl FilePreviewer {
} }
FileType::Other => { FileType::Other => {
debug!("Previewing other file: {:?}", entry.name); debug!("Previewing other file: {:?}", entry.name);
let preview = not_supported(&entry.name); let preview = meta::not_supported(&entry.name);
self.cache_preview(entry.name.clone(), preview.clone()) self.cache_preview(entry.name.clone(), preview.clone())
.await; .await;
preview preview
} }
FileType::Unknown => { FileType::Unknown => {
debug!("Unknown file type: {:?}", entry.name); debug!("Unknown file type: {:?}", entry.name);
let preview = not_supported(&entry.name); let preview = meta::not_supported(&entry.name);
self.cache_preview(entry.name.clone(), preview.clone()) self.cache_preview(entry.name.clone(), preview.clone())
.await; .await;
preview preview
@ -225,8 +225,9 @@ impl FilePreviewer {
let mut buffer = [0u8; 256]; let mut buffer = [0u8; 256];
if let Ok(bytes_read) = f.read(&mut buffer) { if let Ok(bytes_read) = f.read(&mut buffer) {
if bytes_read > 0 if bytes_read > 0
&& proportion_of_printable_ascii_characters(&buffer) && proportion_of_printable_ascii_characters(
> PRINTABLE_ASCII_THRESHOLD &buffer[..bytes_read],
) > PRINTABLE_ASCII_THRESHOLD
{ {
file_type = FileType::Text; file_type = FileType::Text;
} }
@ -266,7 +267,7 @@ fn plain_text_preview(title: &str, reader: BufReader<&File>) -> Arc<Preview> {
Ok(line) => lines.push(preprocess_line(&line)), Ok(line) => lines.push(preprocess_line(&line)),
Err(e) => { Err(e) => {
warn!("Error reading file: {:?}", e); warn!("Error reading file: {:?}", e);
return not_supported(title); return meta::not_supported(title);
} }
} }
if lines.len() >= TEMP_PLAIN_TEXT_PREVIEW_HEIGHT { if lines.len() >= TEMP_PLAIN_TEXT_PREVIEW_HEIGHT {
@ -279,25 +280,6 @@ fn plain_text_preview(title: &str, reader: BufReader<&File>) -> Arc<Preview> {
)) ))
} }
fn not_supported(title: &str) -> Arc<Preview> {
Arc::new(Preview::new(
title.to_string(),
PreviewContent::NotSupported,
))
}
fn file_too_large(title: &str) -> Arc<Preview> {
Arc::new(Preview::new(
title.to_string(),
PreviewContent::FileTooLarge,
))
}
#[allow(dead_code)]
fn loading(title: &str) -> Arc<Preview> {
Arc::new(Preview::new(title.to_string(), PreviewContent::Loading))
}
fn compute_highlights( fn compute_highlights(
file_path: &Path, file_path: &Path,
lines: Vec<String>, lines: Vec<String>,

View File

@ -0,0 +1,21 @@
use crate::previewers::{Preview, PreviewContent};
use std::sync::Arc;
pub fn not_supported(title: &str) -> Arc<Preview> {
Arc::new(Preview::new(
title.to_string(),
PreviewContent::NotSupported,
))
}
pub fn file_too_large(title: &str) -> Arc<Preview> {
Arc::new(Preview::new(
title.to_string(),
PreviewContent::FileTooLarge,
))
}
#[allow(dead_code)]
pub fn loading(title: &str) -> Arc<Preview> {
Arc::new(Preview::new(title.to_string(), PreviewContent::Loading))
}

View File

@ -8,21 +8,25 @@ use ratatui::{
text::{Line, Span}, text::{Line, Span},
widgets::{ widgets::{
block::{Position, Title}, block::{Position, Title},
Block, BorderType, Borders, ListState, Padding, Paragraph, Block, BorderType, Borders, ListState, Padding, Paragraph, Wrap,
}, },
Frame, Frame,
}; };
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use tracing::debug;
use crate::ui::get_border_style;
use crate::ui::input::actions::InputActionHandler;
use crate::ui::input::Input; use crate::ui::input::Input;
use crate::ui::layout::{Dimensions, Layout}; use crate::ui::layout::{Dimensions, Layout};
use crate::ui::preview::DEFAULT_PREVIEW_TITLE_FG; use crate::ui::preview::DEFAULT_PREVIEW_TITLE_FG;
use crate::ui::results::build_results_list; use crate::ui::results::build_results_list;
use crate::utils::strings::EMPTY_STRING; use crate::utils::strings::EMPTY_STRING;
use crate::{action::Action, config::Config}; use crate::{action::Action, config::Config};
use crate::{channels::channels::SelectionChannel, ui::get_border_style};
use crate::{
channels::AvailableChannel, ui::input::actions::InputActionHandler,
};
use crate::{ use crate::{
channels::{CliTvChannel, TelevisionChannel}, channels::{CliTvChannel, TelevisionChannel},
utils::strings::shrink_with_ellipsis, utils::strings::shrink_with_ellipsis,
@ -33,21 +37,18 @@ use crate::{
}; };
use crate::{previewers::Previewer, ui::spinner::SpinnerState}; use crate::{previewers::Previewer, ui::spinner::SpinnerState};
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
enum Pane { pub enum Mode {
Results, Channel,
Preview, ChannelSelection,
Input,
} }
static PANES: [Pane; 3] = [Pane::Input, Pane::Results, Pane::Preview];
pub struct Television { pub struct Television {
action_tx: Option<UnboundedSender<Action>>, action_tx: Option<UnboundedSender<Action>>,
config: Config, pub config: Config,
channel: Box<dyn TelevisionChannel>, channel: AvailableChannel,
current_pattern: String, current_pattern: String,
current_pane: Pane, pub mode: Mode,
input: Input, input: Input,
picker_state: ListState, picker_state: ListState,
relative_picker_state: ListState, relative_picker_state: ListState,
@ -82,7 +83,7 @@ impl Television {
config: Config::default(), config: Config::default(),
channel: tv_channel, channel: tv_channel,
current_pattern: EMPTY_STRING.to_string(), current_pattern: EMPTY_STRING.to_string(),
current_pane: Pane::Input, mode: Mode::Channel,
input: Input::new(EMPTY_STRING.to_string()), input: Input::new(EMPTY_STRING.to_string()),
picker_state: ListState::default(), picker_state: ListState::default(),
relative_picker_state: ListState::default(), relative_picker_state: ListState::default(),
@ -98,6 +99,14 @@ impl Television {
} }
} }
pub fn change_channel(&mut self, channel: Box<dyn TelevisionChannel>) {
self.reset_preview_scroll();
self.reset_results_selection();
self.current_pattern = EMPTY_STRING.to_string();
self.input.reset();
self.channel = channel;
}
fn find(&mut self, pattern: &str) { fn find(&mut self, pattern: &str) {
self.channel.find(pattern); self.channel.find(pattern);
} }
@ -194,95 +203,10 @@ impl Television {
} }
} }
fn get_current_pane_index(&self) -> usize { fn reset_results_selection(&mut self) {
PANES self.picker_state.select(Some(0));
.iter() self.relative_picker_state.select(Some(0));
.position(|pane| *pane == self.current_pane) self.picker_view_offset = 0;
.unwrap()
}
pub fn next_pane(&mut self) {
let current_index = self.get_current_pane_index();
let next_index = (current_index + 1) % PANES.len();
self.current_pane = PANES[next_index];
}
pub fn previous_pane(&mut self) {
let current_index = self.get_current_pane_index();
let previous_index = if current_index == 0 {
PANES.len() - 1
} else {
current_index - 1
};
self.current_pane = PANES[previous_index];
}
/// ┌───────────────────┐┌─────────────┐
/// │ Results ││ Preview │
/// │ ││ │
/// │ ││ │
/// │ ││ │
/// └───────────────────┘│ │
/// ┌───────────────────┐│ │
/// │ Search x ││ │
/// └───────────────────┘└─────────────┘
pub fn move_to_pane_on_top(&mut self) {
if self.current_pane == Pane::Input {
self.current_pane = Pane::Results;
}
}
/// ┌───────────────────┐┌─────────────┐
/// │ Results x ││ Preview │
/// │ ││ │
/// │ ││ │
/// │ ││ │
/// └───────────────────┘│ │
/// ┌───────────────────┐│ │
/// │ Search ││ │
/// └───────────────────┘└─────────────┘
pub fn move_to_pane_below(&mut self) {
if self.current_pane == Pane::Results {
self.current_pane = Pane::Input;
}
}
/// ┌───────────────────┐┌─────────────┐
/// │ Results x ││ Preview │
/// │ ││ │
/// │ ││ │
/// │ ││ │
/// └───────────────────┘│ │
/// ┌───────────────────┐│ │
/// │ Search x ││ │
/// └───────────────────┘└─────────────┘
pub fn move_to_pane_right(&mut self) {
match self.current_pane {
Pane::Results | Pane::Input => {
self.current_pane = Pane::Preview;
}
Pane::Preview => {}
}
}
/// ┌───────────────────┐┌─────────────┐
/// │ Results ││ Preview x │
/// │ ││ │
/// │ ││ │
/// │ ││ │
/// └───────────────────┘│ │
/// ┌───────────────────┐│ │
/// │ Search ││ │
/// └───────────────────┘└─────────────┘
pub fn move_to_pane_left(&mut self) {
if self.current_pane == Pane::Preview {
self.current_pane = Pane::Results;
}
}
#[must_use]
pub fn is_input_focused(&self) -> bool {
Pane::Input == self.current_pane
} }
} }
@ -334,24 +258,6 @@ impl Television {
/// * `Result<Option<Action>>` - An action to be processed or none. /// * `Result<Option<Action>>` - An action to be processed or none.
pub async fn update(&mut self, action: Action) -> Result<Option<Action>> { pub async fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::GoToPaneUp => {
self.move_to_pane_on_top();
}
Action::GoToPaneDown => {
self.move_to_pane_below();
}
Action::GoToPaneLeft => {
self.move_to_pane_left();
}
Action::GoToPaneRight => {
self.move_to_pane_right();
}
Action::GoToNextPane => {
self.next_pane();
}
Action::GoToPrevPane => {
self.previous_pane();
}
// handle input actions // handle input actions
Action::AddInputChar(_) Action::AddInputChar(_)
| Action::DeletePrevChar | Action::DeletePrevChar
@ -359,9 +265,7 @@ impl Television {
| Action::GoToInputEnd | Action::GoToInputEnd
| Action::GoToInputStart | Action::GoToInputStart
| Action::GoToNextChar | Action::GoToNextChar
| Action::GoToPrevChar | Action::GoToPrevChar => {
if self.is_input_focused() =>
{
self.input.handle_action(&action); self.input.handle_action(&action);
match action { match action {
Action::AddInputChar(_) Action::AddInputChar(_)
@ -372,9 +276,7 @@ impl Television {
self.current_pattern.clone_from(&new_pattern); self.current_pattern.clone_from(&new_pattern);
self.find(&new_pattern); self.find(&new_pattern);
self.reset_preview_scroll(); self.reset_preview_scroll();
self.picker_state.select(Some(0)); self.reset_results_selection();
self.relative_picker_state.select(Some(0));
self.picker_view_offset = 0;
} }
} }
_ => {} _ => {}
@ -392,6 +294,31 @@ impl Television {
Action::ScrollPreviewUp => self.scroll_preview_up(1), Action::ScrollPreviewUp => self.scroll_preview_up(1),
Action::ScrollPreviewHalfPageDown => self.scroll_preview_down(20), Action::ScrollPreviewHalfPageDown => self.scroll_preview_down(20),
Action::ScrollPreviewHalfPageUp => self.scroll_preview_up(20), Action::ScrollPreviewHalfPageUp => self.scroll_preview_up(20),
Action::ToChannelSelection => {
self.mode = Mode::ChannelSelection;
let selection_channel = Box::new(SelectionChannel::new());
self.change_channel(selection_channel);
}
Action::SelectEntry => {
if let Some(entry) = self.get_selected_entry() {
match self.mode {
Mode::Channel => self
.action_tx
.as_ref()
.unwrap()
.send(Action::SelectAndExit)?,
Mode::ChannelSelection => {
self.mode = Mode::Channel;
let new_channel =
AvailableChannel::from_entry(&entry)?;
self.change_channel(new_channel);
}
}
}
}
Action::PipeInto => {
if let Some(entry) = self.get_selected_entry() {}
}
_ => {} _ => {}
} }
Ok(None) Ok(None)
@ -407,10 +334,28 @@ impl Television {
/// # Returns /// # Returns
/// ///
/// * `Result<()>` - An Ok result or an error. /// * `Result<()>` - An Ok result or an error.
pub fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { pub fn draw(&mut self, f: &mut Frame, area: Rect) -> Result<()> {
let layout = Layout::all_panes_centered(Dimensions::default(), area); let layout = Layout::build(
//let layout = &Dimensions::default(),
//Layout::results_only_centered(Dimensions::new(40, 60), area); area,
match self.mode {
Mode::Channel => true,
Mode::ChannelSelection => false,
},
);
let help_block = Block::default()
.borders(Borders::NONE)
.style(Style::default())
.padding(Padding::uniform(1));
let help_text = self
.build_help_paragraph(layout.help_bar.width.saturating_sub(4))?
.style(Style::default().fg(Color::DarkGray).italic())
.alignment(Alignment::Center)
.block(help_block);
f.render_widget(help_text, layout.help_bar);
self.results_area_height = u32::from(layout.results.height); self.results_area_height = u32::from(layout.results.height);
if let Some(preview_window) = layout.preview_window { if let Some(preview_window) = layout.preview_window {
@ -426,7 +371,7 @@ impl Television {
) )
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(get_border_style(Pane::Results == self.current_pane)) .border_style(get_border_style(false))
.style(Style::default()) .style(Style::default())
.padding(Padding::right(1)); .padding(Padding::right(1));
@ -438,12 +383,12 @@ impl Television {
} }
let entries = self.channel.results( let entries = self.channel.results(
(layout.results.height - 2).into(), (layout.results.height.saturating_sub(2)).into(),
u32::try_from(self.picker_view_offset)?, u32::try_from(self.picker_view_offset)?,
); );
let results_list = build_results_list(results_block, &entries); let results_list = build_results_list(results_block, &entries);
frame.render_stateful_widget( f.render_stateful_widget(
results_list, results_list,
layout.results, layout.results,
&mut self.relative_picker_state, &mut self.relative_picker_state,
@ -458,12 +403,12 @@ impl Television {
) )
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(get_border_style(Pane::Input == self.current_pane)) .border_style(get_border_style(false))
.style(Style::default()); .style(Style::default());
let input_block_inner = input_block.inner(layout.input); let input_block_inner = input_block.inner(layout.input);
frame.render_widget(input_block, layout.input); f.render_widget(input_block, layout.input);
// split input block into 3 parts: prompt symbol, input, result count // split input block into 3 parts: prompt symbol, input, result count
let inner_input_chunks = RatatuiLayout::default() let inner_input_chunks = RatatuiLayout::default()
@ -491,7 +436,7 @@ impl Television {
Style::default().fg(DEFAULT_INPUT_FG).bold(), Style::default().fg(DEFAULT_INPUT_FG).bold(),
)) ))
.block(arrow_block); .block(arrow_block);
frame.render_widget(arrow, inner_input_chunks[0]); f.render_widget(arrow, inner_input_chunks[0]);
let interactive_input_block = Block::default(); let interactive_input_block = Block::default();
// keep 2 for borders and 1 for cursor // keep 2 for borders and 1 for cursor
@ -502,10 +447,10 @@ impl Television {
.block(interactive_input_block) .block(interactive_input_block)
.style(Style::default().fg(DEFAULT_INPUT_FG).bold().italic()) .style(Style::default().fg(DEFAULT_INPUT_FG).bold().italic())
.alignment(Alignment::Left); .alignment(Alignment::Left);
frame.render_widget(input, inner_input_chunks[1]); f.render_widget(input, inner_input_chunks[1]);
if self.channel.running() { if self.channel.running() {
frame.render_stateful_widget( f.render_stateful_widget(
self.spinner, self.spinner,
inner_input_chunks[3], inner_input_chunks[3],
&mut self.spinner_state, &mut self.spinner_state,
@ -527,21 +472,19 @@ impl Television {
)) ))
.block(result_count_block) .block(result_count_block)
.alignment(Alignment::Right); .alignment(Alignment::Right);
frame.render_widget(result_count, inner_input_chunks[2]); f.render_widget(result_count, inner_input_chunks[2]);
if let Pane::Input = self.current_pane { // Make the cursor visible and ask tui-rs to put it at the
// Make the cursor visible and ask tui-rs to put it at the // specified coordinates after rendering
// specified coordinates after rendering f.set_cursor_position((
frame.set_cursor_position(( // Put cursor past the end of the input text
// Put cursor past the end of the input text inner_input_chunks[1].x
inner_input_chunks[1].x + u16::try_from(
+ u16::try_from( self.input.visual_cursor().max(scroll) - scroll,
self.input.visual_cursor().max(scroll) - scroll, )?,
)?, // Move one line down, from the border to the input line
// Move one line down, from the border to the input line inner_input_chunks[1].y,
inner_input_chunks[1].y, ));
));
}
if layout.preview_title.is_some() || layout.preview_window.is_some() { if layout.preview_title.is_some() || layout.preview_window.is_some() {
let selected_entry = let selected_entry =
@ -567,7 +510,7 @@ impl Television {
preview_title_spans.push(Span::styled( preview_title_spans.push(Span::styled(
shrink_with_ellipsis( shrink_with_ellipsis(
&preview.title, &preview.title,
(preview_title_area.width - 4) as usize, preview_title_area.width.saturating_sub(4) as usize,
), ),
Style::default().fg(DEFAULT_PREVIEW_TITLE_FG).bold(), Style::default().fg(DEFAULT_PREVIEW_TITLE_FG).bold(),
)); ));
@ -580,7 +523,7 @@ impl Television {
.border_style(get_border_style(false)), .border_style(get_border_style(false)),
) )
.alignment(Alignment::Left); .alignment(Alignment::Left);
frame.render_widget(preview_title, preview_title_area); f.render_widget(preview_title, preview_title_area);
} }
if let Some(preview_area) = layout.preview_window { if let Some(preview_area) = layout.preview_window {
@ -593,9 +536,7 @@ impl Television {
) )
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(get_border_style( .border_style(get_border_style(false))
Pane::Preview == self.current_pane,
))
.style(Style::default()) .style(Style::default())
.padding(Padding::right(1)); .padding(Padding::right(1));
@ -608,7 +549,7 @@ impl Television {
left: 1, left: 1,
}); });
let inner = preview_outer_block.inner(preview_area); let inner = preview_outer_block.inner(preview_area);
frame.render_widget(preview_outer_block, preview_area); f.render_widget(preview_outer_block, preview_area);
//if let PreviewContent::Image(img) = &preview.content { //if let PreviewContent::Image(img) = &preview.content {
// let image_component = StatefulImage::new(None); // let image_component = StatefulImage::new(None);
@ -626,7 +567,7 @@ impl Television {
.line_number .line_number
.map(|l| u16::try_from(l).unwrap_or(0)), .map(|l| u16::try_from(l).unwrap_or(0)),
); );
frame.render_widget(preview_block, inner); f.render_widget(preview_block, inner);
//} //}
} }
} }

View File

@ -1,5 +1,6 @@
use ratatui::style::{Color, Style, Stylize}; use ratatui::style::{Color, Style, Stylize};
pub mod help;
pub mod input; pub mod input;
pub mod layout; pub mod layout;
pub mod preview; pub mod preview;

View File

@ -0,0 +1,190 @@
use color_eyre::eyre::{OptionExt, Result};
use ratatui::{
style::{Color, Style},
text::{Line, Span},
widgets::Paragraph,
};
use tracing::debug;
use crate::{
action::Action,
config::Config,
event::Key,
television::{Mode, Television},
};
const SEPARATOR: &str = " ";
const ACTION_COLOR: Color = Color::DarkGray;
const KEY_COLOR: Color = Color::LightYellow;
impl Television {
pub fn build_help_paragraph<'a>(
&self,
width: u16,
) -> Result<Paragraph<'a>> {
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))
}
}

View File

@ -19,6 +19,7 @@ impl Default for Dimensions {
} }
pub struct Layout { pub struct Layout {
pub help_bar: Rect,
pub results: Rect, pub results: Rect,
pub input: Rect, pub input: Rect,
pub preview_title: Option<Rect>, pub preview_title: Option<Rect>,
@ -27,12 +28,14 @@ pub struct Layout {
impl Layout { impl Layout {
pub fn new( pub fn new(
help_bar: Rect,
results: Rect, results: Rect,
input: Rect, input: Rect,
preview_title: Option<Rect>, preview_title: Option<Rect>,
preview_window: Option<Rect>, preview_window: Option<Rect>,
) -> Self { ) -> Self {
Self { Self {
help_bar,
results, results,
input, input,
preview_title, preview_title,
@ -40,56 +43,56 @@ impl Layout {
} }
} }
/// TODO: add diagram pub fn build(
#[allow(dead_code)] dimensions: &Dimensions,
pub fn all_panes_centered(
dimensions: Dimensions,
area: Rect, area: Rect,
with_preview: bool,
) -> Self { ) -> Self {
let main_block = centered_rect(dimensions.x, dimensions.y, area); 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
let chunks = layout::Layout::default() let hz_chunks = layout::Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Vertical)
.constraints([ .constraints([Constraint::Fill(1), Constraint::Length(5)])
Constraint::Percentage(50),
Constraint::Percentage(50),
])
.split(main_block); .split(main_block);
// left block: results + input field if with_preview {
let left_chunks = layout::Layout::default() // split the main block into two vertical chunks
.direction(Direction::Vertical) let vt_chunks = layout::Layout::default()
.constraints([Constraint::Min(10), Constraint::Length(3)]) .direction(Direction::Horizontal)
.split(chunks[0]); .constraints([
Constraint::Percentage(50),
Constraint::Percentage(50),
])
.split(hz_chunks[0]);
// right block: preview title + preview // left block: results + input field
let right_chunks = layout::Layout::default() let left_chunks = layout::Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(10)]) .constraints([Constraint::Min(10), Constraint::Length(3)])
.split(chunks[1]); .split(vt_chunks[0]);
Self::new( // right block: preview title + preview
left_chunks[0], let right_chunks = layout::Layout::default()
left_chunks[1], .direction(Direction::Vertical)
Some(right_chunks[0]), .constraints([Constraint::Length(3), Constraint::Min(10)])
Some(right_chunks[1]), .split(vt_chunks[1]);
)
}
/// TODO: add diagram Self::new(
#[allow(dead_code)] hz_chunks[1],
pub fn results_only_centered( left_chunks[0],
dimensions: Dimensions, left_chunks[1],
area: Rect, Some(right_chunks[0]),
) -> Self { Some(right_chunks[1]),
let main_block = centered_rect(dimensions.x, dimensions.y, area); )
// split the main block into two vertical chunks } else {
let chunks = layout::Layout::default() // split the main block into two vertical chunks
.direction(Direction::Vertical) let chunks = layout::Layout::default()
.constraints([Constraint::Min(10), Constraint::Length(3)]) .direction(Direction::Vertical)
.split(main_block); .constraints([Constraint::Min(10), Constraint::Length(3)])
.split(hz_chunks[0]);
Self::new(chunks[0], chunks[1], None, None) Self::new(hz_chunks[1], chunks[0], chunks[1], None, None)
}
} }
} }

View File

@ -361,6 +361,7 @@ lazy_static! {
"tmLanguage", "tmLanguage",
"tmpl", "tmpl",
"tmTheme", "tmTheme",
"toml",
"tpl", "tpl",
"ts", "ts",
"tsv", "tsv",

View File

@ -145,7 +145,7 @@ pub fn shrink_with_ellipsis(s: &str, max_length: usize) -> String {
return s.to_string(); return s.to_string();
} }
let half_max_length = (max_length / 2) - 2; let half_max_length = (max_length / 2).saturating_sub(2);
let first_half = slice_up_to_char_boundary(s, half_max_length); let first_half = slice_up_to_char_boundary(s, half_max_length);
let second_half = let second_half =
slice_at_char_boundaries(s, s.len() - half_max_length, s.len()); slice_at_char_boundaries(s, s.len() - half_max_length, s.len());

View File

@ -34,8 +34,11 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
}); });
let cli_enum = quote! { let cli_enum = quote! {
use clap::ValueEnum; use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use strum::Display;
use std::default::Default;
#[derive(Debug, Clone, ValueEnum, Default, Copy)] #[derive(Debug, Clone, ValueEnum, Default, Copy, PartialEq, Eq, Serialize, Deserialize, Display)]
pub enum CliTvChannel { pub enum CliTvChannel {
#[default] #[default]
#(#cli_enum_variants),* #(#cli_enum_variants),*
@ -53,7 +56,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
let inner_type = &fields.unnamed[0].ty; let inner_type = &fields.unnamed[0].ty;
quote! { quote! {
CliTvChannel::#variant_name => Box::new(#inner_type::new()) CliTvChannel::#variant_name => AvailableChannel::#variant_name(#inner_type::default())
} }
} else { } else {
panic!("Enum variants should have exactly one unnamed field."); panic!("Enum variants should have exactly one unnamed field.");
@ -67,7 +70,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
#cli_enum #cli_enum
impl CliTvChannel { impl CliTvChannel {
pub fn to_channel(self) -> Box<dyn TelevisionChannel> { pub fn to_channel(self) -> AvailableChannel {
match self { match self {
#(#arms),* #(#arms),*
} }
@ -77,3 +80,98 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
gen.into() gen.into()
} }
/// This macro generates the TelevisionChannel trait implementation for the
/// given enum.
#[proc_macro_derive(TvChannel)]
pub fn tv_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_tv_channel(&ast)
}
fn impl_tv_channel(ast: &syn::DeriveInput) -> TokenStream {
// check that the struct is an enum
let variants = if let syn::Data::Enum(data_enum) = &ast.data {
&data_enum.variants
} else {
panic!("#[derive(TvChannel)] is only defined for enums");
};
// check that the enum has at least one variant
assert!(
!variants.is_empty(),
"#[derive(TvChannel)] requires at least one variant"
);
// Generate the trait implementation for the TelevisionChannel trait
// FIXME: fix this
let trait_impl = quote! {
impl TelevisionChannel for AvailableChannel {
fn find(&mut self, pattern: &str) {
match self {
#(
AvailableChannel::#variants(_) => {
self.find(pattern);
}
)*
}
}
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
match self {
#(
AvailableChannel::#variants(_) => {
self.results(num_entries, offset)
}
)*
}
}
fn get_result(&self, index: u32) -> Option<Entry> {
match self {
#(
AvailableChannel::#variants(_) => {
self.get_result(index)
}
)*
}
}
fn result_count(&self) -> u32 {
match self {
#(
AvailableChannel::#variants(_) => {
self.result_count()
}
)*
}
}
fn total_count(&self) -> u32 {
match self {
#(
AvailableChannel::#variants(_) => {
self.total_count()
}
)*
}
}
fn running(&self) -> bool {
match self {
#(
AvailableChannel::#variants(_) => {
self.running()
}
)*
}
}
}
};
trait_impl.into()
}