mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-08 04:25: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]]
|
[[package]]
|
||||||
name = "television"
|
name = "television"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"better-panic",
|
"better-panic",
|
||||||
|
@ -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
|
||||||
|
15
README.md
15
README.md
@ -4,4 +4,19 @@
|
|||||||
|
|
||||||
# 📺 television
|
# 📺 television
|
||||||
|
|
||||||
|
```
|
||||||
|
_______________
|
||||||
|
|,----------. |\
|
||||||
|
|| |=| |
|
||||||
|
|| || | |
|
||||||
|
|| . _o| | |
|
||||||
|
|`-----------' |/
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
__ __ _ _
|
||||||
|
/ /____ / /__ _ __(_)__ (_)__ ___
|
||||||
|
/ __/ -_) / -_) |/ / (_-</ / _ \/ _ \
|
||||||
|
\__/\__/_/\__/|___/_/___/_/\___/_//_/
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
The revolution will be televised.
|
The revolution will be televised.
|
||||||
|
9
TODO.md
9
TODO.md
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)]
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) => {
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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!(
|
||||||
|
@ -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);
|
||||||
|
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