mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 19:45:23 +00:00
ux improvements
This commit is contained in:
parent
597838d56f
commit
b9c1d0780b
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2419,7 +2419,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "television"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"better-panic",
|
||||
|
@ -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
|
||||
|
15
README.md
15
README.md
@ -4,4 +4,19 @@
|
||||
|
||||
# 📺 television
|
||||
|
||||
```
|
||||
_______________
|
||||
|,----------. |\
|
||||
|| |=| |
|
||||
|| || | |
|
||||
|| . _o| | |
|
||||
|`-----------' |/
|
||||
~~~~~~~~~~~~~~~
|
||||
__ __ _ _
|
||||
/ /____ / /__ _ __(_)__ (_)__ ___
|
||||
/ __/ -_) / -_) |/ / (_-</ / _ \/ _ \
|
||||
\__/\__/_/\__/|___/_/___/_/\___/_//_/
|
||||
|
||||
```
|
||||
|
||||
The revolution will be televised.
|
||||
|
9
TODO.md
9
TODO.md
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -39,6 +39,18 @@ pub(crate) fn version() -> String {
|
||||
"\
|
||||
{VERSION_MESSAGE}
|
||||
|
||||
_______________
|
||||
|,----------. |\\
|
||||
|| |=| |
|
||||
|| || | |
|
||||
|| . _o| | |
|
||||
|`-----------' |/
|
||||
~~~~~~~~~~~~~~~
|
||||
__ __ _ _
|
||||
/ /____ / /__ _ __(_)__ (_)__ ___
|
||||
/ __/ -_) / -_) |/ / (_-</ / _ \\/ _ \\
|
||||
\\__/\\__/_/\\__/|___/_/___/_/\\___/_//_/
|
||||
|
||||
Authors: {author}
|
||||
|
||||
Config directory: {config_dir_path}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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!(
|
||||
|
@ -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);
|
||||
|
71
crates/television/ui/spinner.rs
Normal file
71
crates/television/ui/spinner.rs
Normal 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();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user