mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 19:45:23 +00:00
progress
This commit is contained in:
parent
590fe14ee5
commit
db3aa1ad49
@ -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"
|
||||||
|
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
135
crates/television/channels/channels.rs
Normal file
135
crates/television/channels/channels.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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>,
|
||||||
|
21
crates/television/previewers/meta.rs
Normal file
21
crates/television/previewers/meta.rs
Normal 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))
|
||||||
|
}
|
@ -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);
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
190
crates/television/ui/help.rs
Normal file
190
crates/television/ui/help.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -361,6 +361,7 @@ lazy_static! {
|
|||||||
"tmLanguage",
|
"tmLanguage",
|
||||||
"tmpl",
|
"tmpl",
|
||||||
"tmTheme",
|
"tmTheme",
|
||||||
|
"toml",
|
||||||
"tpl",
|
"tpl",
|
||||||
"ts",
|
"ts",
|
||||||
"tsv",
|
"tsv",
|
||||||
|
@ -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());
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user