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]
q = "Quit"
[keybindings.Channel]
esc = "Quit"
ctrl-c = "Quit"
ctrl-z = "Suspend"
tab = "GoToNextPane"
backtab = "GoToPrevPane"
ctrl-left = "GoToPaneLeft"
ctrl-right = "GoToPaneRight"
down = "SelectNextEntry"
up = "SelectPrevEntry"
ctrl-n = "SelectNextEntry"
ctrl-p = "SelectPrevEntry"
ctrl-down = "ScrollPreviewDown"
ctrl-up = "ScrollPreviewUp"
alt-down = "ScrollPreviewHalfPageDown"
ctrl-d = "ScrollPreviewHalfPageDown"
alt-up = "ScrollPreviewHalfPageUp"
ctrl-u = "ScrollPreviewHalfPageUp"
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 strum::Display;
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display)]
pub enum Action {
// input actions
AddInputChar(char),
@ -17,6 +17,7 @@ pub enum Action {
ClearScreen,
// results actions
SelectEntry,
SelectAndExit,
SelectNextEntry,
SelectPrevEntry,
// navigation actions
@ -41,5 +42,6 @@ pub enum Action {
Error(String),
NoOp,
// channel actions
SyncFinderResults,
ToChannelSelection,
PipeInto,
}

View File

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

View File

@ -1,7 +1,9 @@
use crate::entry::Entry;
use television_derive::CliChannel;
use color_eyre::eyre::Result;
use television_derive::{CliChannel, TvChannel};
mod alias;
pub mod channels;
mod env;
mod files;
mod git_repos;
@ -87,8 +89,8 @@ pub trait TelevisionChannel: Send {
/// instance from the selected CLI enum variant.
///
#[allow(dead_code, clippy::module_name_repetitions)]
#[derive(CliChannel)]
pub enum AvailableChannels {
#[derive(CliChannel, TvChannel)]
pub enum AvailableChannel {
Env(env::Channel),
Files(files::Channel),
GitRepos(git_repos::Channel),
@ -96,3 +98,22 @@ pub enum AvailableChannels {
Stdin(stdin::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 devicons::FileIcon;
use nucleo::{Config, Nucleo};
use nucleo::{Config, Injector, Nucleo};
use tracing::debug;
use crate::channels::TelevisionChannel;
@ -16,6 +16,12 @@ struct Alias {
value: String,
}
impl Alias {
fn new(name: String, value: String) -> Self {
Self { name, value }
}
}
pub struct Channel {
matcher: Nucleo<Alias>,
last_pattern: String,
@ -68,22 +74,6 @@ fn get_raw_aliases(shell: &str) -> Vec<String> {
impl Channel {
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(
Config::DEFAULT,
Arc::new(|| {}),
@ -91,12 +81,7 @@ impl Channel {
1,
);
let injector = matcher.injector();
for alias in parsed_aliases {
let _ = injector.push(alias.clone(), |_, cols| {
cols[0] = (alias.name.clone() + &alias.value).into();
});
}
tokio::spawn(load_aliases(injector));
Self {
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 {
@ -208,3 +199,33 @@ impl TelevisionChannel for Channel {
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 {

View File

@ -3,7 +3,11 @@ use nucleo::{
pattern::{CaseMatching, Normalization},
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;
@ -26,7 +30,7 @@ pub struct Channel {
}
impl Channel {
pub fn new() -> Self {
pub fn new(starting_dir: &Path) -> Self {
let matcher = Nucleo::new(
Config::DEFAULT.match_paths(),
Arc::new(|| {}),
@ -35,7 +39,7 @@ impl Channel {
);
// start loading files in the background
tokio::spawn(load_files(
std::env::current_dir().unwrap(),
starting_dir.to_path_buf(),
matcher.injector(),
));
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 {

View File

@ -23,6 +23,7 @@ pub struct Channel {
result_count: u32,
total_count: u32,
running: bool,
icon: FileIcon,
}
impl Channel {
@ -44,10 +45,17 @@ impl Channel {
result_count: 0,
total_count: 0,
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 {
@ -82,6 +90,7 @@ impl TelevisionChannel for Channel {
self.running = status.running;
let mut indices = Vec::new();
let mut matcher = MATCHER.lock();
let icon = self.icon;
snapshot
.matched_items(
@ -104,7 +113,7 @@ impl TelevisionChannel for Channel {
.with_name_match_ranges(
indices.map(|i| (i, i + 1)).collect(),
)
.with_icon(FileIcon::from(&path))
.with_icon(icon)
})
.collect()
}
@ -114,7 +123,7 @@ impl TelevisionChannel for Channel {
snapshot.get_matched_item(index).map(|item| {
let path = item.matcher_columns[0].to_string();
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 {
let mut lines = Vec::new();
for line in std::io::stdin().lock().lines().map_while(Result::ok) {
debug!("Read line: {:?}", line);
lines.push(line);
}
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 {

View File

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

View File

@ -11,11 +11,10 @@ use tracing::{error, info};
use crate::{
action::Action,
app::Mode,
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");
#[allow(dead_code, clippy::module_name_repetitions)]
@ -523,9 +522,9 @@ mod tests {
let c = Config::new()?;
assert_eq!(
c.keybindings
.get(&Mode::Input)
.get(&Mode::Channel)
.unwrap()
.get(&parse_key("<q>").unwrap())
.get(&parse_key("esc").unwrap())
.unwrap(),
&Action::Quit
);

View File

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

View File

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

View File

@ -3,37 +3,50 @@ use std::sync::Arc;
use devicons::FileIcon;
use termtree::Tree;
use tokio::sync::Mutex;
use crate::entry::Entry;
use crate::previewers::cache::PreviewCache;
use crate::previewers::{Preview, PreviewContent};
use crate::previewers::{meta, Preview, PreviewContent};
use crate::utils::files::walk_builder;
pub struct DirectoryPreviewer {
cache: PreviewCache,
cache: Arc<Mutex<PreviewCache>>,
}
impl DirectoryPreviewer {
pub fn new() -> Self {
DirectoryPreviewer {
cache: PreviewCache::default(),
cache: Arc::new(Mutex::new(PreviewCache::default())),
}
}
pub fn preview(&mut self, entry: &Entry) -> Arc<Preview> {
if let Some(preview) = self.cache.get(&entry.name) {
pub async fn preview(&mut self, entry: &Entry) -> Arc<Preview> {
if let Some(preview) = self.cache.lock().await.get(&entry.name) {
return preview;
}
let preview = Arc::new(build_tree_preview(entry));
self.cache.insert(entry.name.clone(), preview.clone());
let preview = meta::loading(&entry.name);
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
}
}
fn build_tree_preview(entry: &Entry) -> Preview {
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();
Preview {
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 {
//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 icon = FileIcon::from(&path);
format!("{} {}", icon, path.display())
}
/// PERF: (urgent) change to use the ignore crate here
fn tree<P: AsRef<Path>>(p: P) -> std::io::Result<Tree<String>> {
let result = std::fs::read_dir(&p)?
.filter_map(std::result::Result::ok)
.fold(
Tree::new(label(
const MAX_DEPTH: u8 = 4;
const FIRST_LEVEL_MAX_ENTRIES: u8 = 30;
const NESTED_MAX_ENTRIES: u8 = 10;
const MAX_ENTRIES: u8 = 200;
fn tree<P: AsRef<Path>>(
p: P,
max_depth: u8,
nested_max_entries: u8,
total_entry_count: &mut u8,
) -> Tree<String> {
let mut root = Tree::new(label(
p.as_ref(),
p.as_ref().parent().unwrap().to_str().unwrap(),
)),
|mut root, entry| {
let m = entry.metadata().unwrap();
if m.is_dir() {
root.push(tree(entry.path()).unwrap());
));
let w = walk_builder(p.as_ref(), 1, None).max_depth(Some(1)).build();
let mut level_entry_count: u8 = 0;
for path in w.skip(1).filter_map(std::result::Result::ok) {
let m = path.metadata().unwrap();
if m.is_dir() && max_depth > 1 {
root.push(tree(
path.path(),
max_depth - 1,
NESTED_MAX_ENTRIES,
total_entry_count,
));
} else {
root.push(Tree::new(label(
entry.path(),
path.path(),
p.as_ref().to_str().unwrap(),
)));
}
root
},
);
Ok(result)
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 crate::entry;
use crate::previewers::{Preview, PreviewContent};
use crate::previewers::{meta, Preview, PreviewContent};
use crate::utils::files::FileType;
use crate::utils::files::{get_file_size, is_known_text_extension};
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)
{
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())
.await;
return preview;
@ -94,7 +94,7 @@ impl FilePreviewer {
}
Err(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())
.await;
p
@ -105,7 +105,7 @@ impl FilePreviewer {
debug!("Previewing image file: {:?}", entry.name);
// insert a loading preview into the cache
//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())
.await;
//// compute the image preview in the background
@ -114,14 +114,14 @@ impl FilePreviewer {
}
FileType::Other => {
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())
.await;
preview
}
FileType::Unknown => {
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())
.await;
preview
@ -225,8 +225,9 @@ impl FilePreviewer {
let mut buffer = [0u8; 256];
if let Ok(bytes_read) = f.read(&mut buffer) {
if bytes_read > 0
&& proportion_of_printable_ascii_characters(&buffer)
> PRINTABLE_ASCII_THRESHOLD
&& proportion_of_printable_ascii_characters(
&buffer[..bytes_read],
) > PRINTABLE_ASCII_THRESHOLD
{
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)),
Err(e) => {
warn!("Error reading file: {:?}", e);
return not_supported(title);
return meta::not_supported(title);
}
}
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(
file_path: &Path,
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},
widgets::{
block::{Position, Title},
Block, BorderType, Borders, ListState, Padding, Paragraph,
Block, BorderType, Borders, ListState, Padding, Paragraph, Wrap,
},
Frame,
};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, str::FromStr};
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::layout::{Dimensions, Layout};
use crate::ui::preview::DEFAULT_PREVIEW_TITLE_FG;
use crate::ui::results::build_results_list;
use crate::utils::strings::EMPTY_STRING;
use crate::{action::Action, config::Config};
use crate::{channels::channels::SelectionChannel, ui::get_border_style};
use crate::{
channels::AvailableChannel, ui::input::actions::InputActionHandler,
};
use crate::{
channels::{CliTvChannel, TelevisionChannel},
utils::strings::shrink_with_ellipsis,
@ -33,21 +37,18 @@ use crate::{
};
use crate::{previewers::Previewer, ui::spinner::SpinnerState};
#[derive(PartialEq, Copy, Clone)]
enum Pane {
Results,
Preview,
Input,
#[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
pub enum Mode {
Channel,
ChannelSelection,
}
static PANES: [Pane; 3] = [Pane::Input, Pane::Results, Pane::Preview];
pub struct Television {
action_tx: Option<UnboundedSender<Action>>,
config: Config,
channel: Box<dyn TelevisionChannel>,
pub config: Config,
channel: AvailableChannel,
current_pattern: String,
current_pane: Pane,
pub mode: Mode,
input: Input,
picker_state: ListState,
relative_picker_state: ListState,
@ -82,7 +83,7 @@ impl Television {
config: Config::default(),
channel: tv_channel,
current_pattern: EMPTY_STRING.to_string(),
current_pane: Pane::Input,
mode: Mode::Channel,
input: Input::new(EMPTY_STRING.to_string()),
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) {
self.channel.find(pattern);
}
@ -194,95 +203,10 @@ impl Television {
}
}
fn get_current_pane_index(&self) -> usize {
PANES
.iter()
.position(|pane| *pane == self.current_pane)
.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
fn reset_results_selection(&mut self) {
self.picker_state.select(Some(0));
self.relative_picker_state.select(Some(0));
self.picker_view_offset = 0;
}
}
@ -334,24 +258,6 @@ impl Television {
/// * `Result<Option<Action>>` - An action to be processed or none.
pub async fn update(&mut self, action: Action) -> Result<Option<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
Action::AddInputChar(_)
| Action::DeletePrevChar
@ -359,9 +265,7 @@ impl Television {
| Action::GoToInputEnd
| Action::GoToInputStart
| Action::GoToNextChar
| Action::GoToPrevChar
if self.is_input_focused() =>
{
| Action::GoToPrevChar => {
self.input.handle_action(&action);
match action {
Action::AddInputChar(_)
@ -372,9 +276,7 @@ impl Television {
self.current_pattern.clone_from(&new_pattern);
self.find(&new_pattern);
self.reset_preview_scroll();
self.picker_state.select(Some(0));
self.relative_picker_state.select(Some(0));
self.picker_view_offset = 0;
self.reset_results_selection();
}
}
_ => {}
@ -392,6 +294,31 @@ impl Television {
Action::ScrollPreviewUp => self.scroll_preview_up(1),
Action::ScrollPreviewHalfPageDown => self.scroll_preview_down(20),
Action::ScrollPreviewHalfPageUp => self.scroll_preview_up(20),
Action::ToChannelSelection => {
self.mode = Mode::ChannelSelection;
let selection_channel = 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)
@ -407,10 +334,28 @@ impl Television {
/// # Returns
///
/// * `Result<()>` - An Ok result or an error.
pub fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let layout = Layout::all_panes_centered(Dimensions::default(), area);
//let layout =
//Layout::results_only_centered(Dimensions::new(40, 60), area);
pub fn draw(&mut self, f: &mut Frame, area: Rect) -> Result<()> {
let layout = Layout::build(
&Dimensions::default(),
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);
if let Some(preview_window) = layout.preview_window {
@ -426,7 +371,7 @@ impl Television {
)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(get_border_style(Pane::Results == self.current_pane))
.border_style(get_border_style(false))
.style(Style::default())
.padding(Padding::right(1));
@ -438,12 +383,12 @@ impl Television {
}
let entries = self.channel.results(
(layout.results.height - 2).into(),
(layout.results.height.saturating_sub(2)).into(),
u32::try_from(self.picker_view_offset)?,
);
let results_list = build_results_list(results_block, &entries);
frame.render_stateful_widget(
f.render_stateful_widget(
results_list,
layout.results,
&mut self.relative_picker_state,
@ -458,12 +403,12 @@ impl Television {
)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(get_border_style(Pane::Input == self.current_pane))
.border_style(get_border_style(false))
.style(Style::default());
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
let inner_input_chunks = RatatuiLayout::default()
@ -491,7 +436,7 @@ impl Television {
Style::default().fg(DEFAULT_INPUT_FG).bold(),
))
.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();
// keep 2 for borders and 1 for cursor
@ -502,10 +447,10 @@ impl Television {
.block(interactive_input_block)
.style(Style::default().fg(DEFAULT_INPUT_FG).bold().italic())
.alignment(Alignment::Left);
frame.render_widget(input, inner_input_chunks[1]);
f.render_widget(input, inner_input_chunks[1]);
if self.channel.running() {
frame.render_stateful_widget(
f.render_stateful_widget(
self.spinner,
inner_input_chunks[3],
&mut self.spinner_state,
@ -527,12 +472,11 @@ impl Television {
))
.block(result_count_block)
.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
// specified coordinates after rendering
frame.set_cursor_position((
f.set_cursor_position((
// Put cursor past the end of the input text
inner_input_chunks[1].x
+ u16::try_from(
@ -541,7 +485,6 @@ impl Television {
// Move one line down, from the border to the input line
inner_input_chunks[1].y,
));
}
if layout.preview_title.is_some() || layout.preview_window.is_some() {
let selected_entry =
@ -567,7 +510,7 @@ impl Television {
preview_title_spans.push(Span::styled(
shrink_with_ellipsis(
&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(),
));
@ -580,7 +523,7 @@ impl Television {
.border_style(get_border_style(false)),
)
.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 {
@ -593,9 +536,7 @@ impl Television {
)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(get_border_style(
Pane::Preview == self.current_pane,
))
.border_style(get_border_style(false))
.style(Style::default())
.padding(Padding::right(1));
@ -608,7 +549,7 @@ impl Television {
left: 1,
});
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 {
// let image_component = StatefulImage::new(None);
@ -626,7 +567,7 @@ impl Television {
.line_number
.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};
pub mod help;
pub mod input;
pub mod layout;
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 help_bar: Rect,
pub results: Rect,
pub input: Rect,
pub preview_title: Option<Rect>,
@ -27,12 +28,14 @@ pub struct Layout {
impl Layout {
pub fn new(
help_bar: Rect,
results: Rect,
input: Rect,
preview_title: Option<Rect>,
preview_window: Option<Rect>,
) -> Self {
Self {
help_bar,
results,
input,
preview_title,
@ -40,56 +43,56 @@ impl Layout {
}
}
/// TODO: add diagram
#[allow(dead_code)]
pub fn all_panes_centered(
dimensions: Dimensions,
pub fn build(
dimensions: &Dimensions,
area: Rect,
with_preview: bool,
) -> Self {
let main_block = centered_rect(dimensions.x, dimensions.y, area);
// split the main block into two vertical chunks
let chunks = layout::Layout::default()
let hz_chunks = layout::Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Fill(1), Constraint::Length(5)])
.split(main_block);
if with_preview {
// split the main block into two vertical chunks
let vt_chunks = layout::Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(50),
Constraint::Percentage(50),
])
.split(main_block);
.split(hz_chunks[0]);
// left block: results + input field
let left_chunks = layout::Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(10), Constraint::Length(3)])
.split(chunks[0]);
.split(vt_chunks[0]);
// right block: preview title + preview
let right_chunks = layout::Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(10)])
.split(chunks[1]);
.split(vt_chunks[1]);
Self::new(
hz_chunks[1],
left_chunks[0],
left_chunks[1],
Some(right_chunks[0]),
Some(right_chunks[1]),
)
}
/// TODO: add diagram
#[allow(dead_code)]
pub fn results_only_centered(
dimensions: Dimensions,
area: Rect,
) -> Self {
let main_block = centered_rect(dimensions.x, dimensions.y, area);
} else {
// split the main block into two vertical chunks
let chunks = layout::Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(10), Constraint::Length(3)])
.split(main_block);
.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",
"tmpl",
"tmTheme",
"toml",
"tpl",
"ts",
"tsv",

View File

@ -145,7 +145,7 @@ pub fn shrink_with_ellipsis(s: &str, max_length: usize) -> 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 second_half =
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! {
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 {
#[default]
#(#cli_enum_variants),*
@ -53,7 +56,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
let inner_type = &fields.unnamed[0].ty;
quote! {
CliTvChannel::#variant_name => Box::new(#inner_type::new())
CliTvChannel::#variant_name => AvailableChannel::#variant_name(#inner_type::default())
}
} else {
panic!("Enum variants should have exactly one unnamed field.");
@ -67,7 +70,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
#cli_enum
impl CliTvChannel {
pub fn to_channel(self) -> Box<dyn TelevisionChannel> {
pub fn to_channel(self) -> AvailableChannel {
match self {
#(#arms),*
}
@ -77,3 +80,98 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
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()
}