ux improvements

This commit is contained in:
Alexandre Pasmantier 2024-10-17 00:24:41 +02:00
parent 597838d56f
commit b9c1d0780b
15 changed files with 185 additions and 9 deletions

2
Cargo.lock generated
View File

@ -2419,7 +2419,7 @@ dependencies = [
[[package]] [[package]]
name = "television" name = "television"
version = "0.1.4" version = "0.1.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"better-panic", "better-panic",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "television" name = "television"
version = "0.1.4" version = "0.1.5"
edition = "2021" edition = "2021"
description = "The revolution will be televised." description = "The revolution will be televised."
license = "MIT" license = "MIT"
@ -81,6 +81,10 @@ opt-level = 3
debug = true debug = true
lto = false lto = false
[profile.profiling]
inherits = "release"
debug = true
[profile.release] [profile.release]
opt-level = 3 opt-level = 3

View File

@ -4,4 +4,19 @@
# 📺 television # 📺 television
```
_______________
|,----------. |\
|| |=| |
|| || | |
|| . _o| | |
|`-----------' |/
~~~~~~~~~~~~~~~
__ __ _ _
/ /____ / /__ _ __(_)__ (_)__ ___
/ __/ -_) / -_) |/ / (_-</ / _ \/ _ \
\__/\__/_/\__/|___/_/___/_/\___/_//_/
```
The revolution will be televised. The revolution will be televised.

View File

@ -4,8 +4,8 @@
# tasks # tasks
- [x] preview navigation - [x] preview navigation
- [ ] add a way to open the selected file in the default editor (or maybe that - [x] add a way to open the selected file in the default editor (or maybe that
should be achieved using pipes?) should be achieved using pipes?) --> use xargs for that
- [x] maybe filter out image types etc. for now - [x] maybe filter out image types etc. for now
- [x] return selected entry on exit - [x] return selected entry on exit
- [x] piping output to another command - [x] piping output to another command
@ -31,6 +31,7 @@ the channel directly?
- [x] support for images is implemented but do we really want that in the core? - [x] support for images is implemented but do we really want that in the core?
it's quite heavy it's quite heavy
- [x] shrink entry names that are too long (from the middle) - [x] shrink entry names that are too long (from the middle)
- [ ] make the preview toggleable
## feature ideas ## feature ideas
- [ ] some sort of iterative fuzzy file explorer (preview contents of folders - [ ] some sort of iterative fuzzy file explorer (preview contents of folders
@ -51,4 +52,6 @@ https://github.com/jrmuizel/pdf-extract
- [ ] from one set of entries to another? (fuzzy-refine) maybe piping - [ ] from one set of entries to another? (fuzzy-refine) maybe piping
tv with itself? tv with itself?
- [ ] add a way of copying the selected entry name/value to the clipboard - [ ] add a way of copying the selected entry name/value to the clipboard
- [ ] have a keybind to send all current entries to stdout ... oorrrrr to another channel??
- [ ] action menu on the bottom: send to channel, copy to clipboard, send to stdout, ... maybe with tab to navigate
between possible actions (defined per channel, not all channels can pipe to all channels)

View File

@ -64,6 +64,9 @@ pub trait TelevisionChannel: Send {
/// Get the total number of entries currently available. /// Get the total number of entries currently available.
fn total_count(&self) -> u32; fn total_count(&self) -> u32;
/// Check if the channel is currently running.
fn running(&self) -> bool;
} }
/// The available television channels. /// The available television channels.

View File

@ -22,6 +22,7 @@ pub(crate) struct Channel {
file_icon: FileIcon, file_icon: FileIcon,
result_count: u32, result_count: u32,
total_count: u32, total_count: u32,
running: bool,
} }
const NUM_THREADS: usize = 1; const NUM_THREADS: usize = 1;
@ -103,6 +104,7 @@ impl Channel {
file_icon: FileIcon::from(FILE_ICON_STR), file_icon: FileIcon::from(FILE_ICON_STR),
result_count: 0, result_count: 0,
total_count: 0, total_count: 0,
running: false,
} }
} }
@ -130,6 +132,7 @@ impl TelevisionChannel for Channel {
self.result_count = snapshot.matched_item_count(); self.result_count = snapshot.matched_item_count();
self.total_count = snapshot.item_count(); self.total_count = snapshot.item_count();
} }
self.running = status.running;
let mut col_indices = Vec::new(); let mut col_indices = Vec::new();
let mut matcher = MATCHER.lock(); let mut matcher = MATCHER.lock();
let icon = self.file_icon; let icon = self.file_icon;
@ -200,4 +203,8 @@ impl TelevisionChannel for Channel {
fn total_count(&self) -> u32 { fn total_count(&self) -> u32 {
self.total_count self.total_count
} }
fn running(&self) -> bool {
self.running
}
} }

View File

@ -23,6 +23,7 @@ pub(crate) struct Channel {
file_icon: FileIcon, file_icon: FileIcon,
result_count: u32, result_count: u32,
total_count: u32, total_count: u32,
running: bool,
} }
const NUM_THREADS: usize = 1; const NUM_THREADS: usize = 1;
@ -48,6 +49,7 @@ impl Channel {
file_icon: FileIcon::from(FILE_ICON_STR), file_icon: FileIcon::from(FILE_ICON_STR),
result_count: 0, result_count: 0,
total_count: 0, total_count: 0,
running: false,
} }
} }
@ -83,6 +85,7 @@ impl TelevisionChannel for Channel {
self.result_count = snapshot.matched_item_count(); self.result_count = snapshot.matched_item_count();
self.total_count = snapshot.item_count(); self.total_count = snapshot.item_count();
} }
self.running = status.running;
let mut col_indices = Vec::new(); let mut col_indices = Vec::new();
let mut matcher = MATCHER.lock(); let mut matcher = MATCHER.lock();
let icon = self.file_icon; let icon = self.file_icon;
@ -147,4 +150,8 @@ impl TelevisionChannel for Channel {
.with_icon(self.file_icon) .with_icon(self.file_icon)
}) })
} }
fn running(&self) -> bool {
self.running
}
} }

View File

@ -18,6 +18,7 @@ pub(crate) struct Channel {
last_pattern: String, last_pattern: String,
result_count: u32, result_count: u32,
total_count: u32, total_count: u32,
running: bool,
} }
impl Channel { impl Channel {
@ -38,6 +39,7 @@ impl Channel {
last_pattern: String::new(), last_pattern: String::new(),
result_count: 0, result_count: 0,
total_count: 0, total_count: 0,
running: false,
} }
} }
@ -73,6 +75,7 @@ impl TelevisionChannel for Channel {
self.result_count = snapshot.matched_item_count(); self.result_count = snapshot.matched_item_count();
self.total_count = snapshot.item_count(); self.total_count = snapshot.item_count();
} }
self.running = status.running;
let mut indices = Vec::new(); let mut indices = Vec::new();
let mut matcher = MATCHER.lock(); let mut matcher = MATCHER.lock();
@ -110,6 +113,10 @@ impl TelevisionChannel for Channel {
.with_icon(FileIcon::from(&path)) .with_icon(FileIcon::from(&path))
}) })
} }
fn running(&self) -> bool {
self.running
}
} }
#[allow(clippy::unused_async)] #[allow(clippy::unused_async)]

View File

@ -17,6 +17,7 @@ pub(crate) struct Channel {
result_count: u32, result_count: u32,
total_count: u32, total_count: u32,
icon: FileIcon, icon: FileIcon,
running: bool,
} }
const NUM_THREADS: usize = 2; const NUM_THREADS: usize = 2;
@ -46,6 +47,7 @@ impl Channel {
result_count: 0, result_count: 0,
total_count: 0, total_count: 0,
icon: FileIcon::from("nu"), icon: FileIcon::from("nu"),
running: false,
} }
} }
@ -75,6 +77,7 @@ impl TelevisionChannel for Channel {
self.result_count = snapshot.matched_item_count(); self.result_count = snapshot.matched_item_count();
self.total_count = snapshot.item_count(); self.total_count = snapshot.item_count();
} }
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; let icon = self.icon;
@ -138,4 +141,8 @@ impl TelevisionChannel for Channel {
fn total_count(&self) -> u32 { fn total_count(&self) -> u32 {
self.total_count self.total_count
} }
fn running(&self) -> bool {
self.running
}
} }

View File

@ -44,6 +44,7 @@ pub(crate) struct Channel {
last_pattern: String, last_pattern: String,
result_count: u32, result_count: u32,
total_count: u32, total_count: u32,
running: bool,
} }
impl Channel { impl Channel {
@ -59,6 +60,7 @@ impl Channel {
last_pattern: String::new(), last_pattern: String::new(),
result_count: 0, result_count: 0,
total_count: 0, total_count: 0,
running: false,
} }
} }
@ -94,6 +96,7 @@ impl TelevisionChannel for Channel {
self.result_count = snapshot.matched_item_count(); self.result_count = snapshot.matched_item_count();
self.total_count = snapshot.item_count(); self.total_count = snapshot.item_count();
} }
self.running = status.running;
let mut indices = Vec::new(); let mut indices = Vec::new();
let mut matcher = MATCHER.lock(); let mut matcher = MATCHER.lock();
@ -143,8 +146,18 @@ impl TelevisionChannel for Channel {
.with_line_number(item.data.line_number) .with_line_number(item.data.line_number)
}) })
} }
fn running(&self) -> bool {
self.running
}
} }
/// The maximum file size we're willing to search in.
///
/// This is to prevent taking humongous amounts of memory when searching in
/// a lot of files (e.g. starting tv in $HOME).
const MAX_FILE_SIZE: u64 = 4 * 1024 * 1024;
#[allow(clippy::unused_async)] #[allow(clippy::unused_async)]
async fn load_candidates(path: PathBuf, injector: Injector<CandidateLine>) { async fn load_candidates(path: PathBuf, injector: Injector<CandidateLine>) {
let current_dir = std::env::current_dir().unwrap(); let current_dir = std::env::current_dir().unwrap();
@ -156,6 +169,11 @@ async fn load_candidates(path: PathBuf, injector: Injector<CandidateLine>) {
Box::new(move |result| { Box::new(move |result| {
if let Ok(entry) = result { if let Ok(entry) = result {
if entry.file_type().unwrap().is_file() { if entry.file_type().unwrap().is_file() {
if let Ok(m) = entry.metadata() {
if m.len() > MAX_FILE_SIZE {
return ignore::WalkState::Continue;
}
}
// iterate over the lines of the file // iterate over the lines of the file
match File::open(entry.path()) { match File::open(entry.path()) {
Ok(file) => { Ok(file) => {

View File

@ -39,6 +39,18 @@ pub(crate) fn version() -> String {
"\ "\
{VERSION_MESSAGE} {VERSION_MESSAGE}
_______________
|,----------. |\\
|| |=| |
|| || | |
|| . _o| | |
|`-----------' |/
~~~~~~~~~~~~~~~
__ __ _ _
/ /____ / /__ _ __(_)__ (_)__ ___
/ __/ -_) / -_) |/ / (_-</ / _ \\/ _ \\
\\__/\\__/_/\\__/|___/_/___/_/\\___/_//_/
Authors: {author} Authors: {author}
Config directory: {config_dir_path} Config directory: {config_dir_path}

View File

@ -7,7 +7,7 @@ use directories::ProjectDirs;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use ratatui::style::{Color, Modifier, Style}; use ratatui::style::{Color, Modifier, Style};
use serde::{de::Deserializer, Deserialize}; use serde::{de::Deserializer, Deserialize};
use tracing::error; use tracing::{error, info};
use crate::{ use crate::{
action::Action, action::Action,
@ -120,6 +120,7 @@ pub(crate) fn get_config_dir() -> PathBuf {
} else { } else {
PathBuf::from(".").join(".config") PathBuf::from(".").join(".config")
}; };
info!("Using config directory: {:?}", directory);
directory directory
} }

View File

@ -15,8 +15,6 @@ use ratatui::{
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use crate::entry::{Entry, ENTRY_PLACEHOLDER};
use crate::previewers::Previewer;
use crate::ui::get_border_style; use crate::ui::get_border_style;
use crate::ui::input::actions::InputActionHandler; use crate::ui::input::actions::InputActionHandler;
use crate::ui::input::Input; use crate::ui::input::Input;
@ -29,6 +27,11 @@ use crate::{
channels::{CliTvChannel, TelevisionChannel}, channels::{CliTvChannel, TelevisionChannel},
utils::strings::shrink_with_ellipsis, utils::strings::shrink_with_ellipsis,
}; };
use crate::{
entry::{Entry, ENTRY_PLACEHOLDER},
ui::spinner::Spinner,
};
use crate::{previewers::Previewer, ui::spinner::SpinnerState};
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone)]
enum Pane { enum Pane {
@ -62,6 +65,8 @@ pub(crate) struct Television {
/// benefiting from a cache mechanism. /// benefiting from a cache mechanism.
pub(crate) meta_paragraph_cache: pub(crate) meta_paragraph_cache:
HashMap<(String, u16, u16), Paragraph<'static>>, HashMap<(String, u16, u16), Paragraph<'static>>,
spinner: Spinner,
spinner_state: SpinnerState,
} }
impl Television { impl Television {
@ -70,6 +75,9 @@ impl Television {
let mut tv_channel = cli_channel.to_channel(); let mut tv_channel = cli_channel.to_channel();
tv_channel.find(EMPTY_STRING); tv_channel.find(EMPTY_STRING);
let spinner = Spinner::default();
let spinner_state = SpinnerState::from(&spinner);
Self { Self {
action_tx: None, action_tx: None,
config: Config::default(), config: Config::default(),
@ -86,6 +94,8 @@ impl Television {
preview_pane_height: 0, preview_pane_height: 0,
current_preview_total_lines: 0, current_preview_total_lines: 0,
meta_paragraph_cache: HashMap::new(), meta_paragraph_cache: HashMap::new(),
spinner,
spinner_state,
} }
} }
@ -408,7 +418,7 @@ impl Television {
) -> Result<()> { ) -> Result<()> {
let layout = Layout::all_panes_centered(Dimensions::default(), area); let layout = Layout::all_panes_centered(Dimensions::default(), area);
//let layout = //let layout =
// Layout::results_only_centered(Dimensions::new(40, 60), area); //Layout::results_only_centered(Dimensions::new(40, 60), area);
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 {
@ -478,6 +488,8 @@ impl Television {
+ 1) + 1)
+ 3, + 3,
), ),
// spinner
Constraint::Length(1),
]) ])
.split(input_block_inner); .split(input_block_inner);
@ -500,6 +512,14 @@ impl Television {
.alignment(Alignment::Left); .alignment(Alignment::Left);
frame.render_widget(input, inner_input_chunks[1]); frame.render_widget(input, inner_input_chunks[1]);
if self.channel.running() {
frame.render_stateful_widget(
self.spinner,
inner_input_chunks[3],
&mut self.spinner_state,
);
}
let result_count_block = Block::default(); let result_count_block = Block::default();
let result_count = Paragraph::new(Span::styled( let result_count = Paragraph::new(Span::styled(
format!( format!(

View File

@ -4,6 +4,7 @@ pub mod input;
pub mod layout; pub mod layout;
pub mod preview; pub mod preview;
pub mod results; pub mod results;
pub mod spinner;
// input // input
//const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200); //const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200);

View File

@ -0,0 +1,71 @@
use ratatui::{
buffer::Buffer, layout::Rect, style::Style, widgets::StatefulWidget,
};
//const FRAMES: &[char] = &[
// '⠄', '⠆', '⠇', '⠋', '⠙', '⠸', '⠰', '⠠', '⠰', '⠸', '⠙', '⠋', '⠇', '⠆',
//];
const FRAMES: &[&str] = &["", "", "", "", "", "", "", "", "", ""];
/// A spinner widget.
#[derive(Debug, Clone, Copy)]
pub struct Spinner {
frames: &'static [&'static str],
}
impl Spinner {
pub fn new(frames: &'static [&str]) -> Spinner {
Spinner { frames }
}
pub fn frame(&self, index: usize) -> &str {
self.frames[index]
}
}
impl Default for Spinner {
fn default() -> Spinner {
Spinner::new(FRAMES)
}
}
#[derive(Debug)]
pub struct SpinnerState {
pub current_frame: usize,
total_frames: usize,
}
impl SpinnerState {
pub fn new(total_frames: usize) -> SpinnerState {
SpinnerState {
current_frame: 0,
total_frames,
}
}
fn tick(&mut self) {
self.current_frame = (self.current_frame + 1) % self.total_frames;
}
}
impl From<&Spinner> for SpinnerState {
fn from(spinner: &Spinner) -> SpinnerState {
SpinnerState::new(spinner.frames.len())
}
}
impl StatefulWidget for Spinner {
type State = SpinnerState;
/// Renders the spinner in the given area.
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
buf.set_string(
area.left(),
area.top(),
self.frame(state.current_frame),
Style::default(),
);
state.tick();
}
}