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]]
name = "television"
version = "0.1.4"
version = "0.1.5"
dependencies = [
"anyhow",
"better-panic",

View File

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

View File

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

View File

@ -4,8 +4,8 @@
# tasks
- [x] preview navigation
- [ ] add a way to open the selected file in the default editor (or maybe that
should be achieved using pipes?)
- [x] add a way to open the selected file in the default editor (or maybe that
should be achieved using pipes?) --> use xargs for that
- [x] maybe filter out image types etc. for now
- [x] return selected entry on exit
- [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?
it's quite heavy
- [x] shrink entry names that are too long (from the middle)
- [ ] make the preview toggleable
## feature ideas
- [ ] 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
tv with itself?
- [ ] 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.
fn total_count(&self) -> u32;
/// Check if the channel is currently running.
fn running(&self) -> bool;
}
/// The available television channels.

View File

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

View File

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

View File

@ -18,6 +18,7 @@ pub(crate) struct Channel {
last_pattern: String,
result_count: u32,
total_count: u32,
running: bool,
}
impl Channel {
@ -38,6 +39,7 @@ impl Channel {
last_pattern: String::new(),
result_count: 0,
total_count: 0,
running: false,
}
}
@ -73,6 +75,7 @@ impl TelevisionChannel for Channel {
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();
@ -110,6 +113,10 @@ impl TelevisionChannel for Channel {
.with_icon(FileIcon::from(&path))
})
}
fn running(&self) -> bool {
self.running
}
}
#[allow(clippy::unused_async)]

View File

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

View File

@ -44,6 +44,7 @@ pub(crate) struct Channel {
last_pattern: String,
result_count: u32,
total_count: u32,
running: bool,
}
impl Channel {
@ -59,6 +60,7 @@ impl Channel {
last_pattern: String::new(),
result_count: 0,
total_count: 0,
running: false,
}
}
@ -94,6 +96,7 @@ impl TelevisionChannel for Channel {
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();
@ -143,8 +146,18 @@ impl TelevisionChannel for Channel {
.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)]
async fn load_candidates(path: PathBuf, injector: Injector<CandidateLine>) {
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| {
if let Ok(entry) = result {
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
match File::open(entry.path()) {
Ok(file) => {

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ pub mod input;
pub mod layout;
pub mod preview;
pub mod results;
pub mod spinner;
// input
//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();
}
}