mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-07 20:15:23 +00:00
refactor(draw): clearing out mut operations from rendering critical path, avoiding mutexes and perf improvements (#322)
This commit is contained in:
parent
eaab4e966b
commit
be80496549
@ -203,3 +203,4 @@ toggle_preview = "ctrl-o"
|
|||||||
# controls which keybinding should trigger tv
|
# controls which keybinding should trigger tv
|
||||||
# for command history
|
# for command history
|
||||||
"command_history" = "ctrl-r"
|
"command_history" = "ctrl-r"
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
pub mod main {
|
pub mod main {
|
||||||
pub mod draw;
|
pub mod draw;
|
||||||
pub mod results_list_benchmark;
|
pub mod draw_results_list;
|
||||||
}
|
}
|
||||||
pub use main::*;
|
pub use main::*;
|
||||||
|
|
||||||
criterion::criterion_main!(results_list_benchmark::benches, draw::benches,);
|
criterion::criterion_main!(draw_results_list::benches, draw::benches,);
|
||||||
|
@ -3,6 +3,7 @@ use ratatui::backend::TestBackend;
|
|||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use television::action::Action;
|
||||||
use television::channels::OnAir;
|
use television::channels::OnAir;
|
||||||
use television::channels::{files::Channel, TelevisionChannel};
|
use television::channels::{files::Channel, TelevisionChannel};
|
||||||
use television::config::Config;
|
use television::config::Config;
|
||||||
@ -22,24 +23,30 @@ fn draw(c: &mut Criterion) {
|
|||||||
let config = Config::new().unwrap();
|
let config = Config::new().unwrap();
|
||||||
let backend = TestBackend::new(width, height);
|
let backend = TestBackend::new(width, height);
|
||||||
let terminal = Terminal::new(backend).unwrap();
|
let terminal = Terminal::new(backend).unwrap();
|
||||||
|
let (tx, _) = tokio::sync::mpsc::unbounded_channel();
|
||||||
let mut channel =
|
let mut channel =
|
||||||
TelevisionChannel::Files(Channel::new(vec![
|
TelevisionChannel::Files(Channel::new(vec![
|
||||||
PathBuf::from("."),
|
PathBuf::from("."),
|
||||||
]));
|
]));
|
||||||
channel.find("television");
|
channel.find("television");
|
||||||
// Wait for the channel to finish loading
|
// Wait for the channel to finish loading
|
||||||
|
let mut tv = Television::new(tx, channel, config, None);
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
// tick the matcher
|
// tick the matcher
|
||||||
let _ = channel.results(10, 0);
|
let _ = tv.channel.results(10, 0);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
let mut tv = Television::new(channel, config, None);
|
|
||||||
tv.select_next_entry(10);
|
tv.select_next_entry(10);
|
||||||
|
let _ = tv.update_preview_state(
|
||||||
|
&tv.get_selected_entry(None).unwrap(),
|
||||||
|
);
|
||||||
|
tv.update(&Action::Tick).unwrap();
|
||||||
(tv, terminal)
|
(tv, terminal)
|
||||||
},
|
},
|
||||||
// Measurement
|
// Measurement
|
||||||
|(mut tv, mut terminal)| async move {
|
|(tv, mut terminal)| async move {
|
||||||
tv.draw(
|
television::draw::draw(
|
||||||
|
black_box(&tv.dump_context()),
|
||||||
black_box(&mut terminal.get_frame()),
|
black_box(&mut terminal.get_frame()),
|
||||||
black_box(Rect::new(0, 0, width, height)),
|
black_box(Rect::new(0, 0, width, height)),
|
||||||
)
|
)
|
||||||
|
@ -4,14 +4,12 @@ use ratatui::layout::Alignment;
|
|||||||
use ratatui::prelude::{Line, Style};
|
use ratatui::prelude::{Line, Style};
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
use ratatui::widgets::{Block, BorderType, Borders, ListDirection, Padding};
|
use ratatui::widgets::{Block, BorderType, Borders, ListDirection, Padding};
|
||||||
use rustc_hash::FxHashMap;
|
|
||||||
use television::channels::entry::merge_ranges;
|
use television::channels::entry::merge_ranges;
|
||||||
use television::channels::entry::{Entry, PreviewType};
|
use television::channels::entry::{Entry, PreviewType};
|
||||||
use television::screen::colors::ResultsColorscheme;
|
use television::screen::colors::ResultsColorscheme;
|
||||||
use television::screen::results::build_results_list;
|
use television::screen::results::build_results_list;
|
||||||
|
|
||||||
pub fn results_list_benchmark(c: &mut Criterion) {
|
pub fn draw_results_list(c: &mut Criterion) {
|
||||||
let mut icon_color_cache = FxHashMap::default();
|
|
||||||
// FIXME: there's probably a way to have this as a benchmark asset
|
// FIXME: there's probably a way to have this as a benchmark asset
|
||||||
// possible as a JSON file and to load it for the benchmark using Serde
|
// possible as a JSON file and to load it for the benchmark using Serde
|
||||||
// I don't know how exactly right now just having it here instead
|
// I don't know how exactly right now just having it here instead
|
||||||
@ -656,11 +654,10 @@ pub fn results_list_benchmark(c: &mut Criterion) {
|
|||||||
None,
|
None,
|
||||||
ListDirection::BottomToTop,
|
ListDirection::BottomToTop,
|
||||||
false,
|
false,
|
||||||
&mut icon_color_cache,
|
|
||||||
&colorscheme,
|
&colorscheme,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
criterion_group!(benches, results_list_benchmark);
|
criterion_group!(benches, draw_results_list);
|
@ -323,7 +323,7 @@ fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream {
|
|||||||
|
|
||||||
// Generate a unit enum from the given enum
|
// Generate a unit enum from the given enum
|
||||||
let unit_enum = quote! {
|
let unit_enum = quote! {
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumString)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Hash)]
|
||||||
#[strum(serialize_all = "kebab_case")]
|
#[strum(serialize_all = "kebab_case")]
|
||||||
pub enum UnitChannel {
|
pub enum UnitChannel {
|
||||||
#(
|
#(
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::screen::mode::Mode;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::mpsc;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info, trace};
|
||||||
|
|
||||||
use crate::channels::entry::Entry;
|
use crate::channels::entry::Entry;
|
||||||
use crate::channels::TelevisionChannel;
|
use crate::channels::TelevisionChannel;
|
||||||
use crate::config::{parse_key, Config};
|
use crate::config::{parse_key, Config};
|
||||||
use crate::keymap::Keymap;
|
use crate::keymap::Keymap;
|
||||||
use crate::television::Television;
|
use crate::television::{Mode, Television};
|
||||||
use crate::{
|
use crate::{
|
||||||
action::Action,
|
action::Action,
|
||||||
event::{Event, EventLoop, Key},
|
event::{Event, EventLoop, Key},
|
||||||
@ -23,9 +21,8 @@ pub struct App {
|
|||||||
// maybe move these two into config instead of passing them
|
// maybe move these two into config instead of passing them
|
||||||
// via the cli?
|
// via the cli?
|
||||||
tick_rate: f64,
|
tick_rate: f64,
|
||||||
frame_rate: f64,
|
|
||||||
/// The television instance that handles channels and entries.
|
/// The television instance that handles channels and entries.
|
||||||
television: Arc<Mutex<Television>>,
|
television: Television,
|
||||||
/// A flag that indicates whether the application should quit during the next frame.
|
/// A flag that indicates whether the application should quit during the next frame.
|
||||||
should_quit: bool,
|
should_quit: bool,
|
||||||
/// A flag that indicates whether the application should suspend during the next frame.
|
/// A flag that indicates whether the application should suspend during the next frame.
|
||||||
@ -92,7 +89,6 @@ impl App {
|
|||||||
let (render_tx, _) = mpsc::unbounded_channel();
|
let (render_tx, _) = mpsc::unbounded_channel();
|
||||||
let (_, event_rx) = mpsc::unbounded_channel();
|
let (_, event_rx) = mpsc::unbounded_channel();
|
||||||
let (event_abort_tx, _) = mpsc::unbounded_channel();
|
let (event_abort_tx, _) = mpsc::unbounded_channel();
|
||||||
let frame_rate = config.config.frame_rate;
|
|
||||||
let tick_rate = config.config.tick_rate;
|
let tick_rate = config.config.tick_rate;
|
||||||
let keymap = Keymap::from(&config.keybindings).with_mode_mappings(
|
let keymap = Keymap::from(&config.keybindings).with_mode_mappings(
|
||||||
Mode::Channel,
|
Mode::Channel,
|
||||||
@ -106,12 +102,11 @@ impl App {
|
|||||||
)?;
|
)?;
|
||||||
debug!("{:?}", keymap);
|
debug!("{:?}", keymap);
|
||||||
let television =
|
let television =
|
||||||
Arc::new(Mutex::new(Television::new(channel, config, input)));
|
Television::new(action_tx.clone(), channel, config, input);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
keymap,
|
keymap,
|
||||||
tick_rate,
|
tick_rate,
|
||||||
frame_rate,
|
|
||||||
television,
|
television,
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
should_suspend: false,
|
should_suspend: false,
|
||||||
@ -148,19 +143,10 @@ impl App {
|
|||||||
let (render_tx, render_rx) = mpsc::unbounded_channel();
|
let (render_tx, render_rx) = mpsc::unbounded_channel();
|
||||||
self.render_tx = render_tx.clone();
|
self.render_tx = render_tx.clone();
|
||||||
let action_tx_r = self.action_tx.clone();
|
let action_tx_r = self.action_tx.clone();
|
||||||
let television_r = self.television.clone();
|
|
||||||
let frame_rate = self.frame_rate;
|
|
||||||
let rendering_task = tokio::spawn(async move {
|
let rendering_task = tokio::spawn(async move {
|
||||||
render(
|
render(render_rx, action_tx_r, is_output_tty).await
|
||||||
render_rx,
|
|
||||||
action_tx_r,
|
|
||||||
television_r,
|
|
||||||
frame_rate,
|
|
||||||
is_output_tty,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
});
|
});
|
||||||
render_tx.send(RenderingTask::Render)?;
|
self.action_tx.send(Action::Render)?;
|
||||||
|
|
||||||
// event handling loop
|
// event handling loop
|
||||||
debug!("Starting event handling loop");
|
debug!("Starting event handling loop");
|
||||||
@ -168,14 +154,11 @@ impl App {
|
|||||||
loop {
|
loop {
|
||||||
// handle event and convert to action
|
// handle event and convert to action
|
||||||
if let Some(event) = self.event_rx.recv().await {
|
if let Some(event) = self.event_rx.recv().await {
|
||||||
let action = self.convert_event_to_action(event).await;
|
let action = self.convert_event_to_action(event);
|
||||||
action_tx.send(action)?;
|
action_tx.send(action)?;
|
||||||
// it's fine to send a rendering task here, because the rendering loop
|
|
||||||
// batches and deduplicates rendering tasks
|
|
||||||
render_tx.send(RenderingTask::Render)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let action_outcome = self.handle_actions().await?;
|
let action_outcome = self.handle_actions()?;
|
||||||
|
|
||||||
if self.should_quit {
|
if self.should_quit {
|
||||||
// send a termination signal to the event loop
|
// send a termination signal to the event loop
|
||||||
@ -199,7 +182,7 @@ impl App {
|
|||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// The action that corresponds to the given event.
|
/// The action that corresponds to the given event.
|
||||||
async fn convert_event_to_action(&self, event: Event<Key>) -> Action {
|
fn convert_event_to_action(&self, event: Event<Key>) -> Action {
|
||||||
match event {
|
match event {
|
||||||
Event::Input(keycode) => {
|
Event::Input(keycode) => {
|
||||||
info!("{:?}", keycode);
|
info!("{:?}", keycode);
|
||||||
@ -219,7 +202,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
// get action based on keybindings
|
// get action based on keybindings
|
||||||
self.keymap
|
self.keymap
|
||||||
.get(&self.television.lock().await.mode)
|
.get(&self.television.mode)
|
||||||
.and_then(|keymap| keymap.get(&keycode).cloned())
|
.and_then(|keymap| keymap.get(&keycode).cloned())
|
||||||
.unwrap_or(if let Key::Char(c) = keycode {
|
.unwrap_or(if let Key::Char(c) = keycode {
|
||||||
Action::AddInputChar(c)
|
Action::AddInputChar(c)
|
||||||
@ -246,10 +229,10 @@ impl App {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// If an error occurs during the execution of the application.
|
/// If an error occurs during the execution of the application.
|
||||||
async fn handle_actions(&mut self) -> Result<ActionOutcome> {
|
fn handle_actions(&mut self) -> Result<ActionOutcome> {
|
||||||
while let Ok(action) = self.action_rx.try_recv() {
|
while let Ok(action) = self.action_rx.try_recv() {
|
||||||
if action != Action::Tick && action != Action::Render {
|
if action != Action::Tick {
|
||||||
debug!("{action:?}");
|
trace!("{action:?}");
|
||||||
}
|
}
|
||||||
match action {
|
match action {
|
||||||
Action::Quit => {
|
Action::Quit => {
|
||||||
@ -269,15 +252,13 @@ impl App {
|
|||||||
self.render_tx.send(RenderingTask::Quit)?;
|
self.render_tx.send(RenderingTask::Quit)?;
|
||||||
if let Some(entries) = self
|
if let Some(entries) = self
|
||||||
.television
|
.television
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.get_selected_entries(Some(Mode::Channel))
|
.get_selected_entries(Some(Mode::Channel))
|
||||||
{
|
{
|
||||||
return Ok(ActionOutcome::Entries(entries));
|
return Ok(ActionOutcome::Entries(entries));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(ActionOutcome::Input(
|
return Ok(ActionOutcome::Input(
|
||||||
self.television.lock().await.current_pattern.clone(),
|
self.television.current_pattern.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Action::SelectPassthrough(passthrough) => {
|
Action::SelectPassthrough(passthrough) => {
|
||||||
@ -285,8 +266,6 @@ impl App {
|
|||||||
self.render_tx.send(RenderingTask::Quit)?;
|
self.render_tx.send(RenderingTask::Quit)?;
|
||||||
if let Some(entries) = self
|
if let Some(entries) = self
|
||||||
.television
|
.television
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.get_selected_entries(Some(Mode::Channel))
|
.get_selected_entries(Some(Mode::Channel))
|
||||||
{
|
{
|
||||||
return Ok(ActionOutcome::Passthrough(
|
return Ok(ActionOutcome::Passthrough(
|
||||||
@ -303,14 +282,14 @@ impl App {
|
|||||||
self.render_tx.send(RenderingTask::Resize(w, h))?;
|
self.render_tx.send(RenderingTask::Resize(w, h))?;
|
||||||
}
|
}
|
||||||
Action::Render => {
|
Action::Render => {
|
||||||
self.render_tx.send(RenderingTask::Render)?;
|
self.render_tx.send(RenderingTask::Render(
|
||||||
|
self.television.dump_context(),
|
||||||
|
))?;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
// forward action to the television handler
|
// forward action to the television handler
|
||||||
if let Some(action) =
|
if let Some(action) = self.television.update(&action)? {
|
||||||
self.television.lock().await.update(action.clone()).await?
|
|
||||||
{
|
|
||||||
self.action_tx.send(action)?;
|
self.action_tx.send(action)?;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -264,8 +264,8 @@ const MAX_FILE_SIZE: u64 = 4 * 1024 * 1024;
|
|||||||
/// This is a soft limit, we might go over it a bit.
|
/// This is a soft limit, we might go over it a bit.
|
||||||
///
|
///
|
||||||
/// A typical line should take somewhere around 100 bytes in memory (for utf8 english text),
|
/// A typical line should take somewhere around 100 bytes in memory (for utf8 english text),
|
||||||
/// so this should take around 100 x `5_000_000` = 500MB of memory.
|
/// so this should take around 100 x `10_000_000` = 1GB of memory.
|
||||||
const MAX_LINES_IN_MEM: usize = 5_000_000;
|
const MAX_LINES_IN_MEM: usize = 10_000_000;
|
||||||
|
|
||||||
#[allow(clippy::unused_async)]
|
#[allow(clippy::unused_async)]
|
||||||
async fn crawl_for_candidates(
|
async fn crawl_for_candidates(
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
use crate::action::Action;
|
use crate::action::Action;
|
||||||
use crate::event::{convert_raw_event_to_key, Key};
|
use crate::event::{convert_raw_event_to_key, Key};
|
||||||
use crate::screen::mode::Mode;
|
use crate::television::Mode;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use std::hash::Hash;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Hash)]
|
||||||
pub enum Binding {
|
pub enum Binding {
|
||||||
SingleKey(Key),
|
SingleKey(Key),
|
||||||
MultipleKeys(Vec<Key>),
|
MultipleKeys(Vec<Key>),
|
||||||
@ -28,9 +29,16 @@ impl Display for Binding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
pub struct KeyBindings(pub FxHashMap<Mode, FxHashMap<Action, Binding>>);
|
pub struct KeyBindings(pub FxHashMap<Mode, FxHashMap<Action, Binding>>);
|
||||||
|
|
||||||
|
impl Hash for KeyBindings {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
// we're not actually using this for hashing, so this really only is a placeholder
|
||||||
|
state.write_u8(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Deref for KeyBindings {
|
impl Deref for KeyBindings {
|
||||||
type Target = FxHashMap<Mode, FxHashMap<Action, Binding>>;
|
type Target = FxHashMap<Mode, FxHashMap<Action, Binding>>;
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#![allow(clippy::module_name_repetitions, clippy::ref_option)]
|
#![allow(clippy::module_name_repetitions, clippy::ref_option)]
|
||||||
use std::{env, path::PathBuf};
|
use std::{env, hash::Hash, path::PathBuf};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
@ -11,7 +11,7 @@ use serde::Deserialize;
|
|||||||
use shell_integration::ShellIntegrationConfig;
|
use shell_integration::ShellIntegrationConfig;
|
||||||
pub use themes::Theme;
|
pub use themes::Theme;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
use ui::UiConfig;
|
pub use ui::UiConfig;
|
||||||
|
|
||||||
mod keybindings;
|
mod keybindings;
|
||||||
mod previewers;
|
mod previewers;
|
||||||
@ -22,7 +22,7 @@ mod ui;
|
|||||||
const DEFAULT_CONFIG: &str = include_str!("../../.config/config.toml");
|
const DEFAULT_CONFIG: &str = include_str!("../../.config/config.toml");
|
||||||
|
|
||||||
#[allow(dead_code, clippy::module_name_repetitions)]
|
#[allow(dead_code, clippy::module_name_repetitions)]
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
#[derive(Clone, Debug, Deserialize, Default, PartialEq)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
#[serde(default = "get_data_dir")]
|
#[serde(default = "get_data_dir")]
|
||||||
@ -35,8 +35,17 @@ pub struct AppConfig {
|
|||||||
pub tick_rate: f64,
|
pub tick_rate: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Hash for AppConfig {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.data_dir.hash(state);
|
||||||
|
self.config_dir.hash(state);
|
||||||
|
self.frame_rate.to_bits().hash(state);
|
||||||
|
self.tick_rate.to_bits().hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
#[derive(Clone, Debug, Deserialize, Default, PartialEq, Hash)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// General application configuration
|
/// General application configuration
|
||||||
@ -77,7 +86,6 @@ lazy_static! {
|
|||||||
const CONFIG_FILE_NAME: &str = "config.toml";
|
const CONFIG_FILE_NAME: &str = "config.toml";
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
// FIXME: default management is a bit of a mess right now
|
|
||||||
#[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]
|
#[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
// Load the default_config values as base defaults
|
// Load the default_config values as base defaults
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::preview::{previewers, PreviewerConfig};
|
use crate::preview::{previewers, PreviewerConfig};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
#[derive(Clone, Debug, Deserialize, Default, PartialEq, Hash)]
|
||||||
pub struct PreviewersConfig {
|
pub struct PreviewersConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub basic: BasicPreviewerConfig,
|
pub basic: BasicPreviewerConfig,
|
||||||
@ -17,10 +17,10 @@ impl From<PreviewersConfig> for PreviewerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
#[derive(Clone, Debug, Deserialize, Default, PartialEq, Hash)]
|
||||||
pub struct BasicPreviewerConfig {}
|
pub struct BasicPreviewerConfig {}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Hash)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct FilePreviewerConfig {
|
pub struct FilePreviewerConfig {
|
||||||
//pub max_file_size: u64,
|
//pub max_file_size: u64,
|
||||||
@ -36,5 +36,5 @@ impl Default for FilePreviewerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
#[derive(Clone, Debug, Deserialize, Default, PartialEq, Hash)]
|
||||||
pub struct EnvVarPreviewerConfig {}
|
pub struct EnvVarPreviewerConfig {}
|
||||||
|
@ -1,14 +1,24 @@
|
|||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
use crate::config::parse_key;
|
use crate::config::parse_key;
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
#[derive(Clone, Debug, Deserialize, Default, PartialEq)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct ShellIntegrationConfig {
|
pub struct ShellIntegrationConfig {
|
||||||
pub commands: FxHashMap<String, String>,
|
pub commands: FxHashMap<String, String>,
|
||||||
pub keybindings: FxHashMap<String, String>,
|
pub keybindings: FxHashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Hash for ShellIntegrationConfig {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
// we're not actually using this for hashing, so this really only is a placeholder
|
||||||
|
state.write_u8(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const SMART_AUTOCOMPLETE_CONFIGURATION_KEY: &str = "smart_autocomplete";
|
const SMART_AUTOCOMPLETE_CONFIGURATION_KEY: &str = "smart_autocomplete";
|
||||||
const COMMAND_HISTORY_CONFIGURATION_KEY: &str = "command_history";
|
const COMMAND_HISTORY_CONFIGURATION_KEY: &str = "command_history";
|
||||||
const DEFAULT_SHELL_AUTOCOMPLETE_KEY: char = 'T';
|
const DEFAULT_SHELL_AUTOCOMPLETE_KEY: char = 'T';
|
||||||
|
@ -6,7 +6,7 @@ use super::themes::DEFAULT_THEME;
|
|||||||
|
|
||||||
const DEFAULT_UI_SCALE: u16 = 100;
|
const DEFAULT_UI_SCALE: u16 = 100;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Hash)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct UiConfig {
|
pub struct UiConfig {
|
||||||
pub use_nerd_font_icons: bool,
|
pub use_nerd_font_icons: bool,
|
||||||
|
265
television/draw.rs
Normal file
265
television/draw.rs
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
use std::{hash::Hash, time::Instant};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use ratatui::{layout::Rect, Frame};
|
||||||
|
use rustc_hash::FxHashSet;
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
action::Action,
|
||||||
|
channels::{
|
||||||
|
entry::{Entry, PreviewType, ENTRY_PLACEHOLDER},
|
||||||
|
UnitChannel,
|
||||||
|
},
|
||||||
|
config::Config,
|
||||||
|
picker::Picker,
|
||||||
|
preview::PreviewState,
|
||||||
|
screen::{
|
||||||
|
colors::Colorscheme, help::draw_help_bar, input::draw_input_box,
|
||||||
|
keybindings::build_keybindings_table, layout::Layout,
|
||||||
|
preview::draw_preview_content_block,
|
||||||
|
remote_control::draw_remote_control, results::draw_results_list,
|
||||||
|
spinner::Spinner,
|
||||||
|
},
|
||||||
|
television::{Message, Mode},
|
||||||
|
utils::metadata::AppMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct ChannelState {
|
||||||
|
pub current_channel: UnitChannel,
|
||||||
|
pub selected_entries: FxHashSet<Entry>,
|
||||||
|
pub total_count: u32,
|
||||||
|
pub running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelState {
|
||||||
|
pub fn new(
|
||||||
|
current_channel: UnitChannel,
|
||||||
|
selected_entries: FxHashSet<Entry>,
|
||||||
|
total_count: u32,
|
||||||
|
running: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
current_channel,
|
||||||
|
selected_entries,
|
||||||
|
total_count,
|
||||||
|
running,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for ChannelState {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.current_channel.hash(state);
|
||||||
|
self.selected_entries
|
||||||
|
.iter()
|
||||||
|
.for_each(|entry| entry.hash(state));
|
||||||
|
self.total_count.hash(state);
|
||||||
|
self.running.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||||
|
pub struct TvState {
|
||||||
|
pub mode: Mode,
|
||||||
|
pub selected_entry: Option<Entry>,
|
||||||
|
pub results_area_height: u16,
|
||||||
|
pub results_picker: Picker,
|
||||||
|
pub rc_picker: Picker,
|
||||||
|
pub channel_state: ChannelState,
|
||||||
|
pub spinner: Spinner,
|
||||||
|
pub preview_state: PreviewState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TvState {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new(
|
||||||
|
mode: Mode,
|
||||||
|
selected_entry: Option<Entry>,
|
||||||
|
results_area_height: u16,
|
||||||
|
results_picker: Picker,
|
||||||
|
rc_picker: Picker,
|
||||||
|
channel_state: ChannelState,
|
||||||
|
spinner: Spinner,
|
||||||
|
preview_state: PreviewState,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
mode,
|
||||||
|
selected_entry,
|
||||||
|
results_area_height,
|
||||||
|
results_picker,
|
||||||
|
rc_picker,
|
||||||
|
channel_state,
|
||||||
|
spinner,
|
||||||
|
preview_state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Ctx {
|
||||||
|
pub tv_state: TvState,
|
||||||
|
pub config: Config,
|
||||||
|
pub colorscheme: Colorscheme,
|
||||||
|
pub app_metadata: AppMetadata,
|
||||||
|
pub tv_tx_handle: Sender<Message>,
|
||||||
|
pub instant: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ctx {
|
||||||
|
pub fn new(
|
||||||
|
tv_state: TvState,
|
||||||
|
config: Config,
|
||||||
|
colorscheme: Colorscheme,
|
||||||
|
app_metadata: AppMetadata,
|
||||||
|
tv_tx_handle: Sender<Message>,
|
||||||
|
instant: Instant,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
tv_state,
|
||||||
|
config,
|
||||||
|
colorscheme,
|
||||||
|
app_metadata,
|
||||||
|
tv_tx_handle,
|
||||||
|
instant,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Ctx {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.tv_state == other.tv_state
|
||||||
|
&& self.config == other.config
|
||||||
|
&& self.colorscheme == other.colorscheme
|
||||||
|
&& self.app_metadata == other.app_metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Ctx {}
|
||||||
|
|
||||||
|
impl Hash for Ctx {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.tv_state.hash(state);
|
||||||
|
self.config.hash(state);
|
||||||
|
self.colorscheme.hash(state);
|
||||||
|
self.app_metadata.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Ctx {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.instant.cmp(&other.instant))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Ctx {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.instant.cmp(&other.instant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result<()> {
|
||||||
|
let selected_entry = ctx
|
||||||
|
.tv_state
|
||||||
|
.selected_entry
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(ENTRY_PLACEHOLDER);
|
||||||
|
|
||||||
|
let show_preview = ctx.config.ui.show_preview_panel
|
||||||
|
&& !matches!(selected_entry.preview_type, PreviewType::None);
|
||||||
|
let show_remote = !matches!(ctx.tv_state.mode, Mode::Channel);
|
||||||
|
|
||||||
|
let layout =
|
||||||
|
Layout::build(area, &ctx.config.ui, show_remote, show_preview);
|
||||||
|
|
||||||
|
// help bar (metadata, keymaps, logo)
|
||||||
|
draw_help_bar(
|
||||||
|
f,
|
||||||
|
&layout.help_bar,
|
||||||
|
ctx.tv_state.channel_state.current_channel,
|
||||||
|
build_keybindings_table(
|
||||||
|
&ctx.config.keybindings.to_displayable(),
|
||||||
|
ctx.tv_state.mode,
|
||||||
|
&ctx.colorscheme,
|
||||||
|
),
|
||||||
|
ctx.tv_state.mode,
|
||||||
|
&ctx.app_metadata,
|
||||||
|
&ctx.colorscheme,
|
||||||
|
);
|
||||||
|
|
||||||
|
if layout.results.height.saturating_sub(2)
|
||||||
|
!= ctx.tv_state.results_area_height
|
||||||
|
{
|
||||||
|
ctx.tv_tx_handle.try_send(Message::ResultListHeightChanged(
|
||||||
|
layout.results.height.saturating_sub(2),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// results list
|
||||||
|
draw_results_list(
|
||||||
|
f,
|
||||||
|
layout.results,
|
||||||
|
&ctx.tv_state.results_picker.entries,
|
||||||
|
&ctx.tv_state.channel_state.selected_entries,
|
||||||
|
&mut ctx.tv_state.results_picker.relative_state.clone(),
|
||||||
|
ctx.config.ui.input_bar_position,
|
||||||
|
ctx.config.ui.use_nerd_font_icons,
|
||||||
|
&ctx.colorscheme,
|
||||||
|
&ctx.config
|
||||||
|
.keybindings
|
||||||
|
.get(&ctx.tv_state.mode)
|
||||||
|
.unwrap()
|
||||||
|
.get(&Action::ToggleHelp)
|
||||||
|
// just display the first keybinding
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
&ctx.config
|
||||||
|
.keybindings
|
||||||
|
.get(&ctx.tv_state.mode)
|
||||||
|
.unwrap()
|
||||||
|
.get(&Action::TogglePreview)
|
||||||
|
// just display the first keybinding
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// input box
|
||||||
|
draw_input_box(
|
||||||
|
f,
|
||||||
|
layout.input,
|
||||||
|
ctx.tv_state.results_picker.total_items,
|
||||||
|
ctx.tv_state.channel_state.total_count,
|
||||||
|
&ctx.tv_state.results_picker.input,
|
||||||
|
&ctx.tv_state.results_picker.state,
|
||||||
|
ctx.tv_state.channel_state.running,
|
||||||
|
&ctx.tv_state.spinner,
|
||||||
|
&ctx.colorscheme,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if show_preview {
|
||||||
|
draw_preview_content_block(
|
||||||
|
f,
|
||||||
|
layout.preview_window.unwrap(),
|
||||||
|
&ctx.tv_state.preview_state,
|
||||||
|
ctx.config.ui.use_nerd_font_icons,
|
||||||
|
&ctx.colorscheme,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remote control
|
||||||
|
if show_remote {
|
||||||
|
draw_remote_control(
|
||||||
|
f,
|
||||||
|
layout.remote_control.unwrap(),
|
||||||
|
&ctx.tv_state.rc_picker.entries,
|
||||||
|
ctx.config.ui.use_nerd_font_icons,
|
||||||
|
&mut ctx.tv_state.rc_picker.state.clone(),
|
||||||
|
&mut ctx.tv_state.rc_picker.input.clone(),
|
||||||
|
&ctx.tv_state.mode,
|
||||||
|
&ctx.colorscheme,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -195,7 +195,8 @@ impl EventLoop {
|
|||||||
tx.send(Event::FocusGained).unwrap_or_else(|_| warn!("Unable to send FocusGained event"));
|
tx.send(Event::FocusGained).unwrap_or_else(|_| warn!("Unable to send FocusGained event"));
|
||||||
},
|
},
|
||||||
Ok(crossterm::event::Event::Resize(x, y)) => {
|
Ok(crossterm::event::Event::Resize(x, y)) => {
|
||||||
tx.send(Event::Resize(x, y)).unwrap_or_else(|_| warn!("Unable to send Resize event"));
|
let (_, (new_x, new_y)) = flush_resize_events((x, y));
|
||||||
|
tx.send(Event::Resize(new_x, new_y)).unwrap_or_else(|_| warn!("Unable to send Resize event"));
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -214,6 +215,22 @@ impl EventLoop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resize events can occur in batches.
|
||||||
|
// With a simple loop they can be flushed.
|
||||||
|
// This function will keep the first and last resize event.
|
||||||
|
fn flush_resize_events(first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) {
|
||||||
|
let mut last_resize = first_resize;
|
||||||
|
while let Ok(true) = crossterm::event::poll(Duration::from_millis(50)) {
|
||||||
|
if let Ok(crossterm::event::Event::Resize(x, y)) =
|
||||||
|
crossterm::event::read()
|
||||||
|
{
|
||||||
|
last_resize = (x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(first_resize, last_resize)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn convert_raw_event_to_key(event: KeyEvent) -> Key {
|
pub fn convert_raw_event_to_key(event: KeyEvent) -> Key {
|
||||||
debug!("Raw event: {:?}", event);
|
debug!("Raw event: {:?}", event);
|
||||||
if event.kind == KeyEventKind::Release {
|
if event.kind == KeyEventKind::Release {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use crate::screen::mode::Mode;
|
use crate::television::Mode;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::action::Action;
|
use crate::action::Action;
|
||||||
|
@ -4,6 +4,7 @@ pub mod cable;
|
|||||||
pub mod channels;
|
pub mod channels;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod draw;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
@ -57,8 +57,6 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
config.config.tick_rate =
|
config.config.tick_rate =
|
||||||
args.tick_rate.unwrap_or(config.config.tick_rate);
|
args.tick_rate.unwrap_or(config.config.tick_rate);
|
||||||
config.config.frame_rate =
|
|
||||||
args.frame_rate.unwrap_or(config.config.frame_rate);
|
|
||||||
if args.no_preview {
|
if args.no_preview {
|
||||||
config.ui.show_preview_panel = false;
|
config.ui.show_preview_panel = false;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
use crate::utils::{input::Input, strings::EMPTY_STRING};
|
use crate::{
|
||||||
|
channels::entry::Entry,
|
||||||
|
utils::{input::Input, strings::EMPTY_STRING},
|
||||||
|
};
|
||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||||
pub struct Picker {
|
pub struct Picker {
|
||||||
pub(crate) state: ListState,
|
pub(crate) state: ListState,
|
||||||
pub(crate) relative_state: ListState,
|
pub(crate) relative_state: ListState,
|
||||||
inverted: bool,
|
inverted: bool,
|
||||||
pub(crate) input: Input,
|
pub(crate) input: Input,
|
||||||
|
pub entries: Vec<Entry>,
|
||||||
|
pub total_items: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Picker {
|
impl Default for Picker {
|
||||||
@ -22,6 +27,8 @@ impl Picker {
|
|||||||
relative_state: ListState::default(),
|
relative_state: ListState::default(),
|
||||||
inverted: false,
|
inverted: false,
|
||||||
input: Input::new(input.unwrap_or(EMPTY_STRING.to_string())),
|
input: Input::new(input.unwrap_or(EMPTY_STRING.to_string())),
|
||||||
|
entries: Vec::new(),
|
||||||
|
total_items: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +106,7 @@ impl Picker {
|
|||||||
let selected = self.selected().unwrap_or(0);
|
let selected = self.selected().unwrap_or(0);
|
||||||
let relative_selected = self.relative_selected().unwrap_or(0);
|
let relative_selected = self.relative_selected().unwrap_or(0);
|
||||||
self.select(Some(selected.saturating_add(1) % total_items));
|
self.select(Some(selected.saturating_add(1) % total_items));
|
||||||
self.relative_select(Some((relative_selected + 1).min(height)));
|
self.relative_select(Some((relative_selected + 1).min(height - 1)));
|
||||||
if self.selected().unwrap() == 0 {
|
if self.selected().unwrap() == 0 {
|
||||||
self.relative_select(Some(0));
|
self.relative_select(Some(0));
|
||||||
}
|
}
|
||||||
@ -111,7 +118,7 @@ impl Picker {
|
|||||||
self.select(Some((selected + (total_items - 1)) % total_items));
|
self.select(Some((selected + (total_items - 1)) % total_items));
|
||||||
self.relative_select(Some(relative_selected.saturating_sub(1)));
|
self.relative_select(Some(relative_selected.saturating_sub(1)));
|
||||||
if self.selected().unwrap() == total_items - 1 {
|
if self.selected().unwrap() == total_items - 1 {
|
||||||
self.relative_select(Some(height));
|
self.relative_select(Some((height - 1).min(total_items - 1)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,7 +136,7 @@ mod tests {
|
|||||||
let mut picker = Picker::default();
|
let mut picker = Picker::default();
|
||||||
picker.select(Some(0));
|
picker.select(Some(0));
|
||||||
picker.relative_select(Some(0));
|
picker.relative_select(Some(0));
|
||||||
picker.select_next(1, 4, 2);
|
picker.select_next(1, 4, 3);
|
||||||
assert_eq!(picker.selected(), Some(1), "selected");
|
assert_eq!(picker.selected(), Some(1), "selected");
|
||||||
assert_eq!(picker.relative_selected(), Some(1), "relative_selected");
|
assert_eq!(picker.relative_selected(), Some(1), "relative_selected");
|
||||||
}
|
}
|
||||||
@ -143,7 +150,7 @@ mod tests {
|
|||||||
let mut picker = Picker::default();
|
let mut picker = Picker::default();
|
||||||
picker.select(Some(1));
|
picker.select(Some(1));
|
||||||
picker.relative_select(Some(1));
|
picker.relative_select(Some(1));
|
||||||
picker.select_next(1, 4, 2);
|
picker.select_next(1, 4, 3);
|
||||||
assert_eq!(picker.selected(), Some(2), "selected");
|
assert_eq!(picker.selected(), Some(2), "selected");
|
||||||
assert_eq!(picker.relative_selected(), Some(2), "relative_selected");
|
assert_eq!(picker.relative_selected(), Some(2), "relative_selected");
|
||||||
}
|
}
|
||||||
@ -157,7 +164,7 @@ mod tests {
|
|||||||
let mut picker = Picker::default();
|
let mut picker = Picker::default();
|
||||||
picker.select(Some(2));
|
picker.select(Some(2));
|
||||||
picker.relative_select(Some(2));
|
picker.relative_select(Some(2));
|
||||||
picker.select_next(1, 4, 2);
|
picker.select_next(1, 4, 3);
|
||||||
assert_eq!(picker.selected(), Some(3), "selected");
|
assert_eq!(picker.selected(), Some(3), "selected");
|
||||||
assert_eq!(picker.relative_selected(), Some(2), "relative_selected");
|
assert_eq!(picker.relative_selected(), Some(2), "relative_selected");
|
||||||
}
|
}
|
||||||
@ -171,7 +178,7 @@ mod tests {
|
|||||||
let mut picker = Picker::default();
|
let mut picker = Picker::default();
|
||||||
picker.select(Some(3));
|
picker.select(Some(3));
|
||||||
picker.relative_select(Some(2));
|
picker.relative_select(Some(2));
|
||||||
picker.select_next(1, 4, 2);
|
picker.select_next(1, 4, 3);
|
||||||
assert_eq!(picker.selected(), Some(0), "selected");
|
assert_eq!(picker.selected(), Some(0), "selected");
|
||||||
assert_eq!(picker.relative_selected(), Some(0), "relative_selected");
|
assert_eq!(picker.relative_selected(), Some(0), "relative_selected");
|
||||||
}
|
}
|
||||||
@ -185,7 +192,7 @@ mod tests {
|
|||||||
let mut picker = Picker::default();
|
let mut picker = Picker::default();
|
||||||
picker.select(Some(2));
|
picker.select(Some(2));
|
||||||
picker.relative_select(Some(2));
|
picker.relative_select(Some(2));
|
||||||
picker.select_next(1, 3, 2);
|
picker.select_next(1, 3, 4);
|
||||||
assert_eq!(picker.selected(), Some(0), "selected");
|
assert_eq!(picker.selected(), Some(0), "selected");
|
||||||
assert_eq!(picker.relative_selected(), Some(0), "relative_selected");
|
assert_eq!(picker.relative_selected(), Some(0), "relative_selected");
|
||||||
}
|
}
|
||||||
@ -199,7 +206,7 @@ mod tests {
|
|||||||
let mut picker = Picker::default();
|
let mut picker = Picker::default();
|
||||||
picker.select(Some(1));
|
picker.select(Some(1));
|
||||||
picker.relative_select(Some(1));
|
picker.relative_select(Some(1));
|
||||||
picker.select_prev(1, 4, 2);
|
picker.select_prev(1, 4, 3);
|
||||||
assert_eq!(picker.selected(), Some(0), "selected");
|
assert_eq!(picker.selected(), Some(0), "selected");
|
||||||
assert_eq!(picker.relative_selected(), Some(0), "relative_selected");
|
assert_eq!(picker.relative_selected(), Some(0), "relative_selected");
|
||||||
}
|
}
|
||||||
@ -213,7 +220,7 @@ mod tests {
|
|||||||
let mut picker = Picker::default();
|
let mut picker = Picker::default();
|
||||||
picker.select(Some(0));
|
picker.select(Some(0));
|
||||||
picker.relative_select(Some(0));
|
picker.relative_select(Some(0));
|
||||||
picker.select_prev(1, 4, 2);
|
picker.select_prev(1, 4, 3);
|
||||||
assert_eq!(picker.selected(), Some(3), "selected");
|
assert_eq!(picker.selected(), Some(3), "selected");
|
||||||
assert_eq!(picker.relative_selected(), Some(2), "relative_selected");
|
assert_eq!(picker.relative_selected(), Some(2), "relative_selected");
|
||||||
}
|
}
|
||||||
@ -227,7 +234,7 @@ mod tests {
|
|||||||
let mut picker = Picker::default();
|
let mut picker = Picker::default();
|
||||||
picker.select(Some(3));
|
picker.select(Some(3));
|
||||||
picker.relative_select(Some(2));
|
picker.relative_select(Some(2));
|
||||||
picker.select_prev(1, 4, 2);
|
picker.select_prev(1, 4, 3);
|
||||||
assert_eq!(picker.selected(), Some(2), "selected");
|
assert_eq!(picker.selected(), Some(2), "selected");
|
||||||
assert_eq!(picker.relative_selected(), Some(1), "relative_selected");
|
assert_eq!(picker.relative_selected(), Some(1), "relative_selected");
|
||||||
}
|
}
|
||||||
@ -241,7 +248,7 @@ mod tests {
|
|||||||
let mut picker = Picker::default();
|
let mut picker = Picker::default();
|
||||||
picker.select(Some(2));
|
picker.select(Some(2));
|
||||||
picker.relative_select(Some(2));
|
picker.relative_select(Some(2));
|
||||||
picker.select_prev(1, 4, 2);
|
picker.select_prev(1, 4, 3);
|
||||||
assert_eq!(picker.selected(), Some(1), "selected");
|
assert_eq!(picker.selected(), Some(1), "selected");
|
||||||
assert_eq!(picker.relative_selected(), Some(1), "relative_selected");
|
assert_eq!(picker.relative_selected(), Some(1), "relative_selected");
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ pub use previewers::env::EnvVarPreviewerConfig;
|
|||||||
pub use previewers::files::FilePreviewer;
|
pub use previewers::files::FilePreviewer;
|
||||||
pub use previewers::files::FilePreviewerConfig;
|
pub use previewers::files::FilePreviewerConfig;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq, Hash)]
|
||||||
pub enum PreviewContent {
|
pub enum PreviewContent {
|
||||||
Empty,
|
Empty,
|
||||||
FileTooLarge,
|
FileTooLarge,
|
||||||
@ -60,7 +60,7 @@ pub const TIMEOUT_MSG: &str = "Preview timed out";
|
|||||||
/// # Fields
|
/// # Fields
|
||||||
/// - `title`: The title of the preview.
|
/// - `title`: The title of the preview.
|
||||||
/// - `content`: The content of the preview.
|
/// - `content`: The content of the preview.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq, Hash)]
|
||||||
pub struct Preview {
|
pub struct Preview {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub content: PreviewContent,
|
pub content: PreviewContent,
|
||||||
@ -99,19 +99,58 @@ impl Preview {
|
|||||||
total_lines,
|
total_lines,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn total_lines(&self) -> u16 {
|
#[derive(Debug, Default, Clone, PartialEq, Hash)]
|
||||||
match &self.content {
|
pub struct PreviewState {
|
||||||
PreviewContent::SyntectHighlightedText(hl_lines) => {
|
pub preview: Arc<Preview>,
|
||||||
hl_lines.lines.len().try_into().unwrap_or(u16::MAX)
|
pub scroll: u16,
|
||||||
|
pub target_line: Option<u16>,
|
||||||
}
|
}
|
||||||
PreviewContent::PlainText(lines) => {
|
|
||||||
lines.len().try_into().unwrap_or(u16::MAX)
|
const PREVIEW_MIN_SCROLL_LINES: u16 = 3;
|
||||||
|
|
||||||
|
impl PreviewState {
|
||||||
|
pub fn new(
|
||||||
|
preview: Arc<Preview>,
|
||||||
|
scroll: u16,
|
||||||
|
target_line: Option<u16>,
|
||||||
|
) -> Self {
|
||||||
|
PreviewState {
|
||||||
|
preview,
|
||||||
|
scroll,
|
||||||
|
target_line,
|
||||||
}
|
}
|
||||||
PreviewContent::AnsiText(text) => {
|
|
||||||
text.lines().count().try_into().unwrap_or(u16::MAX)
|
|
||||||
}
|
}
|
||||||
_ => 0,
|
|
||||||
|
pub fn scroll_down(&mut self, offset: u16) {
|
||||||
|
self.scroll = self.scroll.saturating_add(offset).min(
|
||||||
|
self.preview
|
||||||
|
.total_lines
|
||||||
|
.saturating_sub(PREVIEW_MIN_SCROLL_LINES),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_up(&mut self, offset: u16) {
|
||||||
|
self.scroll = self.scroll.saturating_sub(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.preview = Arc::new(Preview::default());
|
||||||
|
self.scroll = 0;
|
||||||
|
self.target_line = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
preview: Arc<Preview>,
|
||||||
|
scroll: u16,
|
||||||
|
target_line: Option<u16>,
|
||||||
|
) {
|
||||||
|
if self.preview.title != preview.title {
|
||||||
|
self.preview = preview;
|
||||||
|
self.scroll = scroll;
|
||||||
|
self.target_line = target_line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,7 +189,7 @@ impl PreviewerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const REQUEST_STACK_SIZE: usize = 20;
|
const REQUEST_STACK_SIZE: usize = 10;
|
||||||
|
|
||||||
impl Previewer {
|
impl Previewer {
|
||||||
pub fn new(config: Option<PreviewerConfig>) -> Self {
|
pub fn new(config: Option<PreviewerConfig>) -> Self {
|
||||||
@ -174,15 +213,10 @@ impl Previewer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cached(&self, entry: &Entry) -> Option<Arc<Preview>> {
|
// we could use a target scroll here to make the previewer
|
||||||
match &entry.preview_type {
|
// faster, but since it's already running in the background and quite
|
||||||
PreviewType::Files => self.file.cached(entry),
|
// fast for most standard file sizes, plus we're caching the previews,
|
||||||
PreviewType::Command(_) => self.command.cached(entry),
|
// I'm not sure the extra complexity is worth it.
|
||||||
PreviewType::Basic | PreviewType::EnvVar => None,
|
|
||||||
PreviewType::None => Some(Arc::new(Preview::default())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn preview(&mut self, entry: &Entry) -> Option<Arc<Preview>> {
|
pub fn preview(&mut self, entry: &Entry) -> Option<Arc<Preview>> {
|
||||||
// if we haven't acknowledged the request yet, acknowledge it
|
// if we haven't acknowledged the request yet, acknowledge it
|
||||||
self.requests.push(entry.clone());
|
self.requests.push(entry.clone());
|
||||||
@ -190,9 +224,10 @@ impl Previewer {
|
|||||||
if let Some(preview) = self.dispatch_request(entry) {
|
if let Some(preview) = self.dispatch_request(entry) {
|
||||||
return Some(preview);
|
return Some(preview);
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup request stack and return the most recent preview available
|
// lookup request stack and return the most recent preview available
|
||||||
for request in self.requests.back_to_front() {
|
for request in self.requests.back_to_front() {
|
||||||
if let Some(preview) = self.cached(&request) {
|
if let Some(preview) = self.dispatch_request(&request) {
|
||||||
return Some(preview);
|
return Some(preview);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ use std::sync::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use syntect::{highlighting::Theme, parsing::SyntaxSet};
|
use syntect::{highlighting::Theme, parsing::SyntaxSet};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, trace, warn};
|
||||||
|
|
||||||
use crate::channels::entry;
|
use crate::channels::entry;
|
||||||
use crate::preview::cache::PreviewCache;
|
use crate::preview::cache::PreviewCache;
|
||||||
@ -80,7 +80,7 @@ impl FilePreviewer {
|
|||||||
|
|
||||||
pub fn preview(&mut self, entry: &entry::Entry) -> Option<Arc<Preview>> {
|
pub fn preview(&mut self, entry: &entry::Entry) -> Option<Arc<Preview>> {
|
||||||
if let Some(preview) = self.cached(entry) {
|
if let Some(preview) = self.cached(entry) {
|
||||||
debug!("Preview cache hit for {:?}", entry.name);
|
trace!("Preview cache hit for {:?}", entry.name);
|
||||||
if preview.partial_offset.is_some() {
|
if preview.partial_offset.is_some() {
|
||||||
// preview is partial, spawn a task to compute the next chunk
|
// preview is partial, spawn a task to compute the next chunk
|
||||||
// and return the partial preview
|
// and return the partial preview
|
||||||
@ -90,7 +90,7 @@ impl FilePreviewer {
|
|||||||
Some(preview)
|
Some(preview)
|
||||||
} else {
|
} else {
|
||||||
// preview is not in cache, spawn a task to compute the preview
|
// preview is not in cache, spawn a task to compute the preview
|
||||||
debug!("Preview cache miss for {:?}", entry.name);
|
trace!("Preview cache miss for {:?}", entry.name);
|
||||||
self.handle_preview_request(entry, None);
|
self.handle_preview_request(entry, None);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ impl FilePreviewer {
|
|||||||
partial_preview: Option<Arc<Preview>>,
|
partial_preview: Option<Arc<Preview>>,
|
||||||
) {
|
) {
|
||||||
if self.in_flight_previews.lock().contains(&entry.name) {
|
if self.in_flight_previews.lock().contains(&entry.name) {
|
||||||
debug!("Preview already in flight for {:?}", entry.name);
|
trace!("Preview already in flight for {:?}", entry.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
|
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
|
||||||
@ -139,7 +139,7 @@ impl FilePreviewer {
|
|||||||
|
|
||||||
/// The size of the buffer used to read the file in bytes.
|
/// The size of the buffer used to read the file in bytes.
|
||||||
/// This ends up being the max size of partial previews.
|
/// This ends up being the max size of partial previews.
|
||||||
const PARTIAL_BUFREAD_SIZE: usize = 64 * 1024;
|
const PARTIAL_BUFREAD_SIZE: usize = 5 * 1024 * 1024;
|
||||||
|
|
||||||
pub fn try_preview(
|
pub fn try_preview(
|
||||||
entry: &entry::Entry,
|
entry: &entry::Entry,
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use crossterm::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate};
|
||||||
|
use crossterm::{execute, queue};
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use std::{
|
use std::io::{stderr, stdout, LineWriter};
|
||||||
io::{stderr, stdout, LineWriter},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::television::Television;
|
use crate::draw::Ctx;
|
||||||
use crate::{action::Action, tui::Tui};
|
use crate::{action::Action, draw::draw, tui::Tui};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
|
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
|
||||||
pub enum RenderingTask {
|
pub enum RenderingTask {
|
||||||
ClearScreen,
|
ClearScreen,
|
||||||
Render,
|
Render(Ctx),
|
||||||
Resize(u16, u16),
|
Resize(u16, u16),
|
||||||
Resume,
|
Resume,
|
||||||
Suspend,
|
Suspend,
|
||||||
@ -39,8 +38,6 @@ impl IoStream {
|
|||||||
pub async fn render(
|
pub async fn render(
|
||||||
mut render_rx: mpsc::UnboundedReceiver<RenderingTask>,
|
mut render_rx: mpsc::UnboundedReceiver<RenderingTask>,
|
||||||
action_tx: mpsc::UnboundedSender<Action>,
|
action_tx: mpsc::UnboundedSender<Action>,
|
||||||
television: Arc<Mutex<Television>>,
|
|
||||||
frame_rate: f64,
|
|
||||||
is_output_tty: bool,
|
is_output_tty: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let stream = if is_output_tty {
|
let stream = if is_output_tty {
|
||||||
@ -50,21 +47,15 @@ pub async fn render(
|
|||||||
debug!("Rendering to stderr");
|
debug!("Rendering to stderr");
|
||||||
IoStream::BufferedStderr.to_stream()
|
IoStream::BufferedStderr.to_stream()
|
||||||
};
|
};
|
||||||
let mut tui = Tui::new(stream)?.frame_rate(frame_rate);
|
let mut tui = Tui::new(stream)?;
|
||||||
|
|
||||||
debug!("Entering tui");
|
debug!("Entering tui");
|
||||||
tui.enter()?;
|
tui.enter()?;
|
||||||
|
|
||||||
debug!("Registering action handler");
|
let mut buffer = Vec::with_capacity(256);
|
||||||
television
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.register_action_handler(action_tx.clone())?;
|
|
||||||
|
|
||||||
let mut buffer = Vec::with_capacity(128);
|
|
||||||
|
|
||||||
// Rendering loop
|
// Rendering loop
|
||||||
'rendering: while render_rx.recv_many(&mut buffer, 128).await > 0 {
|
'rendering: while render_rx.recv_many(&mut buffer, 256).await > 0 {
|
||||||
// deduplicate events
|
// deduplicate events
|
||||||
buffer.sort_unstable();
|
buffer.sort_unstable();
|
||||||
buffer.dedup();
|
buffer.dedup();
|
||||||
@ -73,17 +64,17 @@ pub async fn render(
|
|||||||
RenderingTask::ClearScreen => {
|
RenderingTask::ClearScreen => {
|
||||||
tui.terminal.clear()?;
|
tui.terminal.clear()?;
|
||||||
}
|
}
|
||||||
RenderingTask::Render => {
|
RenderingTask::Render(context) => {
|
||||||
if let Ok(size) = tui.size() {
|
if let Ok(size) = tui.size() {
|
||||||
// Ratatui uses `u16`s to encode terminal dimensions and its
|
// Ratatui uses `u16`s to encode terminal dimensions and its
|
||||||
// content for each terminal cell is stored linearly in a
|
// content for each terminal cell is stored linearly in a
|
||||||
// buffer with a `u16` index which means we can't support
|
// buffer with a `u16` index which means we can't support
|
||||||
// terminal areas larger than `u16::MAX`.
|
// terminal areas larger than `u16::MAX`.
|
||||||
if size.width.checked_mul(size.height).is_some() {
|
if size.width.checked_mul(size.height).is_some() {
|
||||||
let mut television = television.lock().await;
|
queue!(stderr(), BeginSynchronizedUpdate).ok();
|
||||||
tui.terminal.draw(|frame| {
|
tui.terminal.draw(|frame| {
|
||||||
if let Err(err) =
|
if let Err(err) =
|
||||||
television.draw(frame, frame.area())
|
draw(&context, frame, frame.area())
|
||||||
{
|
{
|
||||||
warn!("Failed to draw: {:?}", err);
|
warn!("Failed to draw: {:?}", err);
|
||||||
let _ = action_tx.send(Action::Error(
|
let _ = action_tx.send(Action::Error(
|
||||||
@ -91,6 +82,7 @@ pub async fn render(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
execute!(stderr(), EndSynchronizedUpdate).ok();
|
||||||
} else {
|
} else {
|
||||||
warn!("Terminal area too large");
|
warn!("Terminal area too large");
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct Colorscheme {
|
pub struct Colorscheme {
|
||||||
pub general: GeneralColorscheme,
|
pub general: GeneralColorscheme,
|
||||||
pub help: HelpColorscheme,
|
pub help: HelpColorscheme,
|
||||||
@ -10,19 +10,19 @@ pub struct Colorscheme {
|
|||||||
pub mode: ModeColorscheme,
|
pub mode: ModeColorscheme,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct GeneralColorscheme {
|
pub struct GeneralColorscheme {
|
||||||
pub border_fg: Color,
|
pub border_fg: Color,
|
||||||
pub background: Option<Color>,
|
pub background: Option<Color>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct HelpColorscheme {
|
pub struct HelpColorscheme {
|
||||||
pub metadata_field_name_fg: Color,
|
pub metadata_field_name_fg: Color,
|
||||||
pub metadata_field_value_fg: Color,
|
pub metadata_field_value_fg: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct ResultsColorscheme {
|
pub struct ResultsColorscheme {
|
||||||
pub result_name_fg: Color,
|
pub result_name_fg: Color,
|
||||||
pub result_preview_fg: Color,
|
pub result_preview_fg: Color,
|
||||||
@ -32,7 +32,7 @@ pub struct ResultsColorscheme {
|
|||||||
pub match_foreground_color: Color,
|
pub match_foreground_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct PreviewColorscheme {
|
pub struct PreviewColorscheme {
|
||||||
pub title_fg: Color,
|
pub title_fg: Color,
|
||||||
pub highlight_bg: Color,
|
pub highlight_bg: Color,
|
||||||
@ -41,13 +41,13 @@ pub struct PreviewColorscheme {
|
|||||||
pub gutter_selected_fg: Color,
|
pub gutter_selected_fg: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct InputColorscheme {
|
pub struct InputColorscheme {
|
||||||
pub input_fg: Color,
|
pub input_fg: Color,
|
||||||
pub results_count_fg: Color,
|
pub results_count_fg: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct ModeColorscheme {
|
pub struct ModeColorscheme {
|
||||||
pub channel: Color,
|
pub channel: Color,
|
||||||
pub remote_control: Color,
|
pub remote_control: Color,
|
||||||
|
@ -3,7 +3,8 @@ use crate::channels::UnitChannel;
|
|||||||
use crate::screen::colors::{Colorscheme, GeneralColorscheme};
|
use crate::screen::colors::{Colorscheme, GeneralColorscheme};
|
||||||
use crate::screen::logo::build_logo_paragraph;
|
use crate::screen::logo::build_logo_paragraph;
|
||||||
use crate::screen::metadata::build_metadata_table;
|
use crate::screen::metadata::build_metadata_table;
|
||||||
use crate::screen::mode::{mode_color, Mode};
|
use crate::screen::mode::mode_color;
|
||||||
|
use crate::television::Mode;
|
||||||
use crate::utils::metadata::AppMetadata;
|
use crate::utils::metadata::AppMetadata;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::prelude::{Color, Style};
|
use ratatui::prelude::{Color, Style};
|
||||||
|
@ -10,10 +10,7 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::screen::{
|
use crate::screen::{colors::Colorscheme, spinner::Spinner};
|
||||||
colors::Colorscheme,
|
|
||||||
spinner::{Spinner, SpinnerState},
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: refactor arguments (e.g. use a struct for the spinner+state, same
|
// TODO: refactor arguments (e.g. use a struct for the spinner+state, same
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@ -22,11 +19,10 @@ pub fn draw_input_box(
|
|||||||
rect: Rect,
|
rect: Rect,
|
||||||
results_count: u32,
|
results_count: u32,
|
||||||
total_count: u32,
|
total_count: u32,
|
||||||
input_state: &mut Input,
|
input_state: &Input,
|
||||||
results_picker_state: &mut ListState,
|
results_picker_state: &ListState,
|
||||||
matcher_running: bool,
|
matcher_running: bool,
|
||||||
spinner: &Spinner,
|
spinner: &Spinner,
|
||||||
spinner_state: &mut SpinnerState,
|
|
||||||
colorscheme: &Colorscheme,
|
colorscheme: &Colorscheme,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let input_block = Block::default()
|
let input_block = Block::default()
|
||||||
@ -89,11 +85,7 @@ pub fn draw_input_box(
|
|||||||
f.render_widget(input, inner_input_chunks[1]);
|
f.render_widget(input, inner_input_chunks[1]);
|
||||||
|
|
||||||
if matcher_running {
|
if matcher_running {
|
||||||
f.render_stateful_widget(
|
f.render_widget(spinner, inner_input_chunks[3]);
|
||||||
spinner,
|
|
||||||
inner_input_chunks[3],
|
|
||||||
spinner_state,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let result_count_block = Block::default();
|
let result_count_block = Block::default();
|
||||||
@ -119,8 +111,9 @@ pub fn draw_input_box(
|
|||||||
// specified coordinates after rendering
|
// specified coordinates after rendering
|
||||||
f.set_cursor_position((
|
f.set_cursor_position((
|
||||||
// Put cursor past the end of the input text
|
// Put cursor past the end of the input text
|
||||||
inner_input_chunks[1].x
|
inner_input_chunks[1].x.saturating_add(u16::try_from(
|
||||||
+ u16::try_from(input_state.visual_cursor().max(scroll) - scroll)?,
|
input_state.visual_cursor().max(scroll) - scroll,
|
||||||
|
)?),
|
||||||
// Move one line down, from the border to the input line
|
// Move one line down, from the border to the input line
|
||||||
inner_input_chunks[1].y,
|
inner_input_chunks[1].y,
|
||||||
));
|
));
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use crate::screen::{colors::Colorscheme, mode::Mode};
|
use crate::screen::colors::Colorscheme;
|
||||||
|
use crate::television::Mode;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Constraint,
|
layout::Constraint,
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
|
@ -4,6 +4,8 @@ use ratatui::layout;
|
|||||||
use ratatui::layout::{Constraint, Direction, Rect};
|
use ratatui::layout::{Constraint, Direction, Rect};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::config::UiConfig;
|
||||||
|
|
||||||
pub struct Dimensions {
|
pub struct Dimensions {
|
||||||
pub x: u16,
|
pub x: u16,
|
||||||
pub y: u16,
|
pub y: u16,
|
||||||
@ -44,7 +46,7 @@ impl HelpBarLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq)]
|
#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq, Hash)]
|
||||||
pub enum InputPosition {
|
pub enum InputPosition {
|
||||||
#[serde(rename = "top")]
|
#[serde(rename = "top")]
|
||||||
Top,
|
Top,
|
||||||
@ -62,7 +64,7 @@ impl Display for InputPosition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, Default)]
|
#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq, Hash)]
|
||||||
pub enum PreviewTitlePosition {
|
pub enum PreviewTitlePosition {
|
||||||
#[serde(rename = "top")]
|
#[serde(rename = "top")]
|
||||||
#[default]
|
#[default]
|
||||||
@ -107,19 +109,20 @@ impl Layout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(
|
pub fn build(
|
||||||
dimensions: &Dimensions,
|
|
||||||
area: Rect,
|
area: Rect,
|
||||||
with_remote: bool,
|
ui_config: &UiConfig,
|
||||||
with_help_bar: bool,
|
show_remote: bool,
|
||||||
with_preview: bool,
|
show_preview: bool,
|
||||||
input_position: InputPosition,
|
//
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let show_preview = show_preview && ui_config.show_preview_panel;
|
||||||
|
let dimensions = Dimensions::from(ui_config.ui_scale);
|
||||||
let main_block = centered_rect(dimensions.x, dimensions.y, area);
|
let main_block = centered_rect(dimensions.x, dimensions.y, area);
|
||||||
// split the main block into two vertical chunks (help bar + rest)
|
// split the main block into two vertical chunks (help bar + rest)
|
||||||
let main_rect: Rect;
|
let main_rect: Rect;
|
||||||
let help_bar_layout: Option<HelpBarLayout>;
|
let help_bar_layout: Option<HelpBarLayout>;
|
||||||
|
|
||||||
if with_help_bar {
|
if ui_config.show_help_bar {
|
||||||
let hz_chunks = layout::Layout::default()
|
let hz_chunks = layout::Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Max(9), Constraint::Fill(1)])
|
.constraints([Constraint::Max(9), Constraint::Fill(1)])
|
||||||
@ -152,10 +155,10 @@ impl Layout {
|
|||||||
// split the main block into 1, 2, or 3 vertical chunks
|
// split the main block into 1, 2, or 3 vertical chunks
|
||||||
// (results + preview + remote)
|
// (results + preview + remote)
|
||||||
let mut constraints = vec![Constraint::Fill(1)];
|
let mut constraints = vec![Constraint::Fill(1)];
|
||||||
if with_preview {
|
if show_preview {
|
||||||
constraints.push(Constraint::Fill(1));
|
constraints.push(Constraint::Fill(1));
|
||||||
}
|
}
|
||||||
if with_remote {
|
if show_remote {
|
||||||
// in order to fit with the help bar logo
|
// in order to fit with the help bar logo
|
||||||
constraints.push(Constraint::Length(24));
|
constraints.push(Constraint::Length(24));
|
||||||
}
|
}
|
||||||
@ -170,28 +173,28 @@ impl Layout {
|
|||||||
|
|
||||||
let left_chunks = layout::Layout::default()
|
let left_chunks = layout::Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(match input_position {
|
.constraints(match ui_config.input_bar_position {
|
||||||
InputPosition::Top => {
|
InputPosition::Top => {
|
||||||
results_constraints.into_iter().rev().collect()
|
results_constraints.into_iter().rev().collect()
|
||||||
}
|
}
|
||||||
InputPosition::Bottom => results_constraints,
|
InputPosition::Bottom => results_constraints,
|
||||||
})
|
})
|
||||||
.split(vt_chunks[0]);
|
.split(vt_chunks[0]);
|
||||||
let (input, results) = match input_position {
|
let (input, results) = match ui_config.input_bar_position {
|
||||||
InputPosition::Bottom => (left_chunks[1], left_chunks[0]),
|
InputPosition::Bottom => (left_chunks[1], left_chunks[0]),
|
||||||
InputPosition::Top => (left_chunks[0], left_chunks[1]),
|
InputPosition::Top => (left_chunks[0], left_chunks[1]),
|
||||||
};
|
};
|
||||||
|
|
||||||
// right block: preview title + preview
|
// right block: preview title + preview
|
||||||
let mut remote_idx = 1;
|
let mut remote_idx = 1;
|
||||||
let preview_window = if with_preview {
|
let preview_window = if show_preview {
|
||||||
remote_idx += 1;
|
remote_idx += 1;
|
||||||
Some(vt_chunks[1])
|
Some(vt_chunks[1])
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let remote_control = if with_remote {
|
let remote_control = if show_remote {
|
||||||
Some(vt_chunks[remote_idx])
|
Some(vt_chunks[remote_idx])
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use crate::channels::UnitChannel;
|
use crate::channels::UnitChannel;
|
||||||
use crate::screen::{
|
use crate::screen::{colors::Colorscheme, mode::mode_color};
|
||||||
colors::Colorscheme,
|
use crate::television::Mode;
|
||||||
mode::{mode_color, Mode},
|
|
||||||
};
|
|
||||||
use crate::utils::metadata::AppMetadata;
|
use crate::utils::metadata::AppMetadata;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Constraint,
|
layout::Constraint,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::screen::colors::ModeColorscheme;
|
use crate::{screen::colors::ModeColorscheme, television::Mode};
|
||||||
|
|
||||||
pub fn mode_color(mode: Mode, colorscheme: &ModeColorscheme) -> Color {
|
pub fn mode_color(mode: Mode, colorscheme: &ModeColorscheme) -> Color {
|
||||||
match mode {
|
match mode {
|
||||||
@ -10,11 +9,3 @@ pub fn mode_color(mode: Mode, colorscheme: &ModeColorscheme) -> Color {
|
|||||||
Mode::SendToChannel => colorscheme.send_to_channel,
|
Mode::SendToChannel => colorscheme.send_to_channel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Mode shouldn't be in the screen crate
|
|
||||||
#[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
|
|
||||||
pub enum Mode {
|
|
||||||
Channel,
|
|
||||||
RemoteControl,
|
|
||||||
SendToChannel,
|
|
||||||
}
|
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
use crate::channels::entry::Entry;
|
use crate::preview::PreviewState;
|
||||||
use crate::preview::{
|
use crate::preview::{
|
||||||
ansi::IntoText, Preview, PreviewContent, FILE_TOO_LARGE_MSG, LOADING_MSG,
|
ansi::IntoText, PreviewContent, FILE_TOO_LARGE_MSG, LOADING_MSG,
|
||||||
PREVIEW_NOT_SUPPORTED_MSG, TIMEOUT_MSG,
|
PREVIEW_NOT_SUPPORTED_MSG, TIMEOUT_MSG,
|
||||||
};
|
};
|
||||||
use crate::screen::{
|
use crate::screen::colors::{Colorscheme, PreviewColorscheme};
|
||||||
cache::RenderedPreviewCache,
|
|
||||||
colors::{Colorscheme, PreviewColorscheme},
|
|
||||||
};
|
|
||||||
use crate::utils::strings::{
|
use crate::utils::strings::{
|
||||||
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
||||||
EMPTY_STRING,
|
EMPTY_STRING,
|
||||||
@ -20,19 +17,46 @@ use ratatui::{
|
|||||||
prelude::{Color, Line, Modifier, Span, Style, Stylize, Text},
|
prelude::{Color, Line, Modifier, Span, Style, Stylize, Text},
|
||||||
};
|
};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
const FILL_CHAR_SLANTED: char = '╱';
|
const FILL_CHAR_SLANTED: char = '╱';
|
||||||
const FILL_CHAR_EMPTY: char = ' ';
|
const FILL_CHAR_EMPTY: char = ' ';
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn draw_preview_content_block(
|
||||||
|
f: &mut Frame,
|
||||||
|
rect: Rect,
|
||||||
|
preview_state: &PreviewState,
|
||||||
|
use_nerd_font_icons: bool,
|
||||||
|
colorscheme: &Colorscheme,
|
||||||
|
) -> Result<()> {
|
||||||
|
let inner = draw_content_outer_block(
|
||||||
|
f,
|
||||||
|
rect,
|
||||||
|
colorscheme,
|
||||||
|
preview_state.preview.icon,
|
||||||
|
&preview_state.preview.title,
|
||||||
|
use_nerd_font_icons,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// render the preview content
|
||||||
|
let rp = build_preview_paragraph(
|
||||||
|
inner,
|
||||||
|
&preview_state.preview.content,
|
||||||
|
preview_state.target_line,
|
||||||
|
preview_state.scroll,
|
||||||
|
colorscheme,
|
||||||
|
);
|
||||||
|
f.render_widget(rp, inner);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_preview_paragraph<'a>(
|
pub fn build_preview_paragraph<'a>(
|
||||||
inner: Rect,
|
inner: Rect,
|
||||||
preview_content: PreviewContent,
|
preview_content: &'a PreviewContent,
|
||||||
target_line: Option<u16>,
|
target_line: Option<u16>,
|
||||||
preview_scroll: u16,
|
preview_scroll: u16,
|
||||||
colorscheme: Colorscheme,
|
colorscheme: &'a Colorscheme,
|
||||||
) -> Paragraph<'a> {
|
) -> Paragraph<'a> {
|
||||||
let preview_block =
|
let preview_block =
|
||||||
Block::default().style(Style::default()).padding(Padding {
|
Block::default().style(Style::default()).padding(Padding {
|
||||||
@ -58,14 +82,16 @@ pub fn build_preview_paragraph<'a>(
|
|||||||
preview_block,
|
preview_block,
|
||||||
colorscheme.preview,
|
colorscheme.preview,
|
||||||
)
|
)
|
||||||
|
.scroll((preview_scroll, 0))
|
||||||
}
|
}
|
||||||
PreviewContent::SyntectHighlightedText(highlighted_lines) => {
|
PreviewContent::SyntectHighlightedText(highlighted_lines) => {
|
||||||
build_syntect_highlighted_paragraph(
|
build_syntect_highlighted_paragraph(
|
||||||
highlighted_lines.lines,
|
&highlighted_lines.lines,
|
||||||
preview_block,
|
preview_block,
|
||||||
target_line,
|
target_line,
|
||||||
preview_scroll,
|
preview_scroll,
|
||||||
colorscheme.preview,
|
colorscheme.preview,
|
||||||
|
inner.height,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// meta
|
// meta
|
||||||
@ -104,12 +130,11 @@ pub fn build_preview_paragraph<'a>(
|
|||||||
const ANSI_BEFORE_CONTEXT_SIZE: u16 = 10;
|
const ANSI_BEFORE_CONTEXT_SIZE: u16 = 10;
|
||||||
const ANSI_CONTEXT_SIZE: usize = 150;
|
const ANSI_CONTEXT_SIZE: usize = 150;
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
fn build_ansi_text_paragraph<'a>(
|
||||||
fn build_ansi_text_paragraph(
|
text: &'a str,
|
||||||
text: String,
|
preview_block: Block<'a>,
|
||||||
preview_block: Block,
|
|
||||||
preview_scroll: u16,
|
preview_scroll: u16,
|
||||||
) -> Paragraph {
|
) -> Paragraph<'a> {
|
||||||
let lines = text.lines();
|
let lines = text.lines();
|
||||||
let skip =
|
let skip =
|
||||||
preview_scroll.saturating_sub(ANSI_BEFORE_CONTEXT_SIZE) as usize;
|
preview_scroll.saturating_sub(ANSI_BEFORE_CONTEXT_SIZE) as usize;
|
||||||
@ -137,14 +162,13 @@ fn build_ansi_text_paragraph(
|
|||||||
.scroll((preview_scroll, 0))
|
.scroll((preview_scroll, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
fn build_plain_text_paragraph<'a>(
|
||||||
fn build_plain_text_paragraph(
|
text: &'a [String],
|
||||||
text: Vec<String>,
|
preview_block: Block<'a>,
|
||||||
preview_block: Block<'_>,
|
|
||||||
target_line: Option<u16>,
|
target_line: Option<u16>,
|
||||||
preview_scroll: u16,
|
preview_scroll: u16,
|
||||||
colorscheme: PreviewColorscheme,
|
colorscheme: PreviewColorscheme,
|
||||||
) -> Paragraph<'_> {
|
) -> Paragraph<'a> {
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
for (i, line) in text.iter().enumerate() {
|
for (i, line) in text.iter().enumerate() {
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
@ -179,12 +203,11 @@ fn build_plain_text_paragraph(
|
|||||||
.scroll((preview_scroll, 0))
|
.scroll((preview_scroll, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
fn build_plain_text_wrapped_paragraph<'a>(
|
||||||
fn build_plain_text_wrapped_paragraph(
|
text: &'a str,
|
||||||
text: String,
|
preview_block: Block<'a>,
|
||||||
preview_block: Block<'_>,
|
|
||||||
colorscheme: PreviewColorscheme,
|
colorscheme: PreviewColorscheme,
|
||||||
) -> Paragraph<'_> {
|
) -> Paragraph<'a> {
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
for line in text.lines() {
|
for line in text.lines() {
|
||||||
lines.push(Line::styled(
|
lines.push(Line::styled(
|
||||||
@ -198,22 +221,24 @@ fn build_plain_text_wrapped_paragraph(
|
|||||||
.wrap(Wrap { trim: true })
|
.wrap(Wrap { trim: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
fn build_syntect_highlighted_paragraph<'a>(
|
||||||
fn build_syntect_highlighted_paragraph(
|
highlighted_lines: &'a [Vec<(syntect::highlighting::Style, String)>],
|
||||||
highlighted_lines: Vec<Vec<(syntect::highlighting::Style, String)>>,
|
preview_block: Block<'a>,
|
||||||
preview_block: Block,
|
|
||||||
target_line: Option<u16>,
|
target_line: Option<u16>,
|
||||||
preview_scroll: u16,
|
preview_scroll: u16,
|
||||||
colorscheme: PreviewColorscheme,
|
colorscheme: PreviewColorscheme,
|
||||||
) -> Paragraph {
|
height: u16,
|
||||||
|
) -> Paragraph<'a> {
|
||||||
compute_paragraph_from_highlighted_lines(
|
compute_paragraph_from_highlighted_lines(
|
||||||
&highlighted_lines,
|
highlighted_lines,
|
||||||
target_line.map(|l| l as usize),
|
target_line.map(|l| l as usize),
|
||||||
|
preview_scroll,
|
||||||
colorscheme,
|
colorscheme,
|
||||||
|
height,
|
||||||
)
|
)
|
||||||
.block(preview_block)
|
.block(preview_block)
|
||||||
.alignment(Alignment::Left)
|
.alignment(Alignment::Left)
|
||||||
.scroll((preview_scroll, 0))
|
//.scroll((preview_scroll, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_meta_preview_paragraph<'a>(
|
pub fn build_meta_preview_paragraph<'a>(
|
||||||
@ -325,109 +350,6 @@ fn draw_content_outer_block(
|
|||||||
Ok(inner)
|
Ok(inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn draw_preview_content_block(
|
|
||||||
f: &mut Frame,
|
|
||||||
rect: Rect,
|
|
||||||
entry: &Entry,
|
|
||||||
preview: &Option<Arc<Preview>>,
|
|
||||||
rendered_preview_cache: &Arc<Mutex<RenderedPreviewCache<'static>>>,
|
|
||||||
preview_scroll: u16,
|
|
||||||
use_nerd_font_icons: bool,
|
|
||||||
colorscheme: &Colorscheme,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let Some(preview) = preview {
|
|
||||||
let inner = draw_content_outer_block(
|
|
||||||
f,
|
|
||||||
rect,
|
|
||||||
colorscheme,
|
|
||||||
preview.icon,
|
|
||||||
&preview.title,
|
|
||||||
use_nerd_font_icons,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// check if the rendered preview content is already in the cache
|
|
||||||
let cache_key = compute_cache_key(entry);
|
|
||||||
if let Some(rp) =
|
|
||||||
rendered_preview_cache.lock().unwrap().get(&cache_key)
|
|
||||||
{
|
|
||||||
// we got a hit, render the cached preview content
|
|
||||||
let p = rp.paragraph.as_ref().clone();
|
|
||||||
f.render_widget(p.scroll((preview_scroll, 0)), inner);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// render the preview content and cache it
|
|
||||||
let rp = build_preview_paragraph(
|
|
||||||
//preview_inner_block,
|
|
||||||
inner,
|
|
||||||
preview.content.clone(),
|
|
||||||
entry.line_number.map(|l| u16::try_from(l).unwrap_or(0)),
|
|
||||||
preview_scroll,
|
|
||||||
colorscheme.clone(),
|
|
||||||
);
|
|
||||||
// only cache the preview content if it's not a partial preview
|
|
||||||
// and the preview title matches the entry name
|
|
||||||
if preview.partial_offset.is_none() && preview.title == entry.name {
|
|
||||||
rendered_preview_cache.lock().unwrap().insert(
|
|
||||||
cache_key,
|
|
||||||
preview.icon,
|
|
||||||
&preview.title,
|
|
||||||
&Arc::new(rp.clone()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
f.render_widget(rp.scroll((preview_scroll, 0)), inner);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// else if last_preview exists
|
|
||||||
if let Some(last_preview) =
|
|
||||||
&rendered_preview_cache.lock().unwrap().last_preview
|
|
||||||
{
|
|
||||||
let inner = draw_content_outer_block(
|
|
||||||
f,
|
|
||||||
rect,
|
|
||||||
colorscheme,
|
|
||||||
last_preview.icon,
|
|
||||||
&last_preview.title,
|
|
||||||
use_nerd_font_icons,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
f.render_widget(
|
|
||||||
last_preview
|
|
||||||
.paragraph
|
|
||||||
.as_ref()
|
|
||||||
.clone()
|
|
||||||
.scroll((preview_scroll, 0)),
|
|
||||||
inner,
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// otherwise render empty preview
|
|
||||||
let inner = draw_content_outer_block(
|
|
||||||
f,
|
|
||||||
rect,
|
|
||||||
colorscheme,
|
|
||||||
None,
|
|
||||||
"",
|
|
||||||
use_nerd_font_icons,
|
|
||||||
)?;
|
|
||||||
let preview_outer_block = Block::default()
|
|
||||||
.title_top(Line::from(Span::styled(
|
|
||||||
" Preview ",
|
|
||||||
Style::default().fg(colorscheme.preview.title_fg),
|
|
||||||
)))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(Style::default().fg(colorscheme.general.border_fg))
|
|
||||||
.style(
|
|
||||||
Style::default()
|
|
||||||
.bg(colorscheme.general.background.unwrap_or_default()),
|
|
||||||
)
|
|
||||||
.padding(Padding::new(0, 1, 1, 0));
|
|
||||||
f.render_widget(preview_outer_block, inner);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_line_number_span<'a>(line_number: usize) -> Span<'a> {
|
fn build_line_number_span<'a>(line_number: usize) -> Span<'a> {
|
||||||
Span::from(format!("{line_number:5} "))
|
Span::from(format!("{line_number:5} "))
|
||||||
}
|
}
|
||||||
@ -435,11 +357,15 @@ fn build_line_number_span<'a>(line_number: usize) -> Span<'a> {
|
|||||||
fn compute_paragraph_from_highlighted_lines(
|
fn compute_paragraph_from_highlighted_lines(
|
||||||
highlighted_lines: &[Vec<(syntect::highlighting::Style, String)>],
|
highlighted_lines: &[Vec<(syntect::highlighting::Style, String)>],
|
||||||
line_specifier: Option<usize>,
|
line_specifier: Option<usize>,
|
||||||
|
preview_scroll: u16,
|
||||||
colorscheme: PreviewColorscheme,
|
colorscheme: PreviewColorscheme,
|
||||||
|
height: u16,
|
||||||
) -> Paragraph<'static> {
|
) -> Paragraph<'static> {
|
||||||
let preview_lines: Vec<Line> = highlighted_lines
|
let preview_lines: Vec<Line> = highlighted_lines
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
.skip(preview_scroll.saturating_sub(1).into())
|
||||||
|
.take(height.into())
|
||||||
.map(|(i, l)| {
|
.map(|(i, l)| {
|
||||||
let line_number =
|
let line_number =
|
||||||
build_line_number_span(i + 1).style(Style::default().fg(
|
build_line_number_span(i + 1).style(Style::default().fg(
|
||||||
@ -501,11 +427,3 @@ fn convert_syn_color_to_ratatui_color(
|
|||||||
) -> Color {
|
) -> Color {
|
||||||
Color::Rgb(color.r, color.g, color.b)
|
Color::Rgb(color.r, color.g, color.b)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_cache_key(entry: &Entry) -> String {
|
|
||||||
let mut cache_key = entry.name.clone();
|
|
||||||
if let Some(line_number) = entry.line_number {
|
|
||||||
cache_key.push_str(&line_number.to_string());
|
|
||||||
}
|
|
||||||
cache_key
|
|
||||||
}
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
use rustc_hash::FxHashMap;
|
|
||||||
|
|
||||||
use crate::channels::entry::Entry;
|
use crate::channels::entry::Entry;
|
||||||
use crate::screen::colors::{Colorscheme, GeneralColorscheme};
|
use crate::screen::colors::{Colorscheme, GeneralColorscheme};
|
||||||
use crate::screen::logo::build_remote_logo_paragraph;
|
use crate::screen::logo::build_remote_logo_paragraph;
|
||||||
use crate::screen::mode::{mode_color, Mode};
|
use crate::screen::mode::mode_color;
|
||||||
use crate::screen::results::build_results_list;
|
use crate::screen::results::build_results_list;
|
||||||
|
use crate::television::Mode;
|
||||||
use crate::utils::input::Input;
|
use crate::utils::input::Input;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@ -25,7 +24,6 @@ pub fn draw_remote_control(
|
|||||||
use_nerd_font_icons: bool,
|
use_nerd_font_icons: bool,
|
||||||
picker_state: &mut ListState,
|
picker_state: &mut ListState,
|
||||||
input_state: &mut Input,
|
input_state: &mut Input,
|
||||||
icon_color_cache: &mut FxHashMap<String, Color>,
|
|
||||||
mode: &Mode,
|
mode: &Mode,
|
||||||
colorscheme: &Colorscheme,
|
colorscheme: &Colorscheme,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@ -46,7 +44,6 @@ pub fn draw_remote_control(
|
|||||||
entries,
|
entries,
|
||||||
use_nerd_font_icons,
|
use_nerd_font_icons,
|
||||||
picker_state,
|
picker_state,
|
||||||
icon_color_cache,
|
|
||||||
colorscheme,
|
colorscheme,
|
||||||
);
|
);
|
||||||
draw_rc_input(f, layout[1], input_state, colorscheme)?;
|
draw_rc_input(f, layout[1], input_state, colorscheme)?;
|
||||||
@ -65,7 +62,6 @@ fn draw_rc_channels(
|
|||||||
entries: &[Entry],
|
entries: &[Entry],
|
||||||
use_nerd_font_icons: bool,
|
use_nerd_font_icons: bool,
|
||||||
picker_state: &mut ListState,
|
picker_state: &mut ListState,
|
||||||
icon_color_cache: &mut FxHashMap<String, Color>,
|
|
||||||
colorscheme: &Colorscheme,
|
colorscheme: &Colorscheme,
|
||||||
) {
|
) {
|
||||||
let rc_block = Block::default()
|
let rc_block = Block::default()
|
||||||
@ -84,7 +80,6 @@ fn draw_rc_channels(
|
|||||||
None,
|
None,
|
||||||
ListDirection::TopToBottom,
|
ListDirection::TopToBottom,
|
||||||
use_nerd_font_icons,
|
use_nerd_font_icons,
|
||||||
icon_color_cache,
|
|
||||||
&colorscheme.results,
|
&colorscheme.results,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ use ratatui::widgets::{
|
|||||||
Block, BorderType, Borders, List, ListDirection, ListState, Padding,
|
Block, BorderType, Borders, List, ListDirection, ListState, Padding,
|
||||||
};
|
};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::FxHashSet;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
const POINTER_SYMBOL: &str = "> ";
|
const POINTER_SYMBOL: &str = "> ";
|
||||||
@ -26,7 +26,6 @@ pub fn build_results_list<'a, 'b>(
|
|||||||
selected_entries: Option<&FxHashSet<Entry>>,
|
selected_entries: Option<&FxHashSet<Entry>>,
|
||||||
list_direction: ListDirection,
|
list_direction: ListDirection,
|
||||||
use_icons: bool,
|
use_icons: bool,
|
||||||
icon_color_cache: &mut FxHashMap<String, Color>,
|
|
||||||
colorscheme: &ResultsColorscheme,
|
colorscheme: &ResultsColorscheme,
|
||||||
) -> List<'a>
|
) -> List<'a>
|
||||||
where
|
where
|
||||||
@ -50,20 +49,10 @@ where
|
|||||||
// optional icon
|
// optional icon
|
||||||
if let Some(icon) = entry.icon.as_ref() {
|
if let Some(icon) = entry.icon.as_ref() {
|
||||||
if use_icons {
|
if use_icons {
|
||||||
if let Some(icon_color) = icon_color_cache.get(icon.color) {
|
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
icon.to_string(),
|
icon.to_string(),
|
||||||
Style::default().fg(*icon_color),
|
Style::default().fg(Color::from_str(icon.color).unwrap()),
|
||||||
));
|
));
|
||||||
} else {
|
|
||||||
let icon_color = Color::from_str(icon.color).unwrap();
|
|
||||||
icon_color_cache
|
|
||||||
.insert(icon.color.to_string(), icon_color);
|
|
||||||
spans.push(Span::styled(
|
|
||||||
icon.to_string(),
|
|
||||||
Style::default().fg(icon_color),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
spans.push(Span::raw(" "));
|
spans.push(Span::raw(" "));
|
||||||
}
|
}
|
||||||
@ -160,7 +149,6 @@ pub fn draw_results_list(
|
|||||||
relative_picker_state: &mut ListState,
|
relative_picker_state: &mut ListState,
|
||||||
input_bar_position: InputPosition,
|
input_bar_position: InputPosition,
|
||||||
use_nerd_font_icons: bool,
|
use_nerd_font_icons: bool,
|
||||||
icon_color_cache: &mut FxHashMap<String, Color>,
|
|
||||||
colorscheme: &Colorscheme,
|
colorscheme: &Colorscheme,
|
||||||
help_keybinding: &str,
|
help_keybinding: &str,
|
||||||
preview_keybinding: &str,
|
preview_keybinding: &str,
|
||||||
@ -191,7 +179,6 @@ pub fn draw_results_list(
|
|||||||
InputPosition::Top => ListDirection::TopToBottom,
|
InputPosition::Top => ListDirection::TopToBottom,
|
||||||
},
|
},
|
||||||
use_nerd_font_icons,
|
use_nerd_font_icons,
|
||||||
icon_color_cache,
|
|
||||||
&colorscheme.results,
|
&colorscheme.results,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,23 +1,29 @@
|
|||||||
use ratatui::{
|
use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget};
|
||||||
buffer::Buffer, layout::Rect, style::Style, widgets::StatefulWidget,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
const FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||||
|
|
||||||
/// A spinner widget.
|
/// A spinner widget.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
|
||||||
pub struct Spinner {
|
pub struct Spinner {
|
||||||
frames: &'static [&'static str],
|
frames: &'static [&'static str],
|
||||||
|
state: SpinnerState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Spinner {
|
impl Spinner {
|
||||||
pub fn new(frames: &'static [&str]) -> Spinner {
|
pub fn new(frames: &'static [&str]) -> Spinner {
|
||||||
Spinner { frames }
|
Spinner {
|
||||||
|
frames,
|
||||||
|
state: SpinnerState::new(frames.len()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn frame(&self, index: usize) -> &str {
|
pub fn frame(&self, index: usize) -> &str {
|
||||||
self.frames[index]
|
self.frames[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self) {
|
||||||
|
self.state.tick();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Spinner {
|
impl Default for Spinner {
|
||||||
@ -26,7 +32,7 @@ impl Default for Spinner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct SpinnerState {
|
pub struct SpinnerState {
|
||||||
pub current_frame: usize,
|
pub current_frame: usize,
|
||||||
total_frames: usize,
|
total_frames: usize,
|
||||||
@ -51,31 +57,24 @@ impl From<&Spinner> for SpinnerState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatefulWidget for Spinner {
|
impl Widget for Spinner {
|
||||||
type State = SpinnerState;
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
|
||||||
/// Renders the spinner in the given area.
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
|
||||||
buf.set_string(
|
buf.set_string(
|
||||||
area.left(),
|
area.left(),
|
||||||
area.top(),
|
area.top(),
|
||||||
self.frame(state.current_frame),
|
self.frame(self.state.current_frame),
|
||||||
Style::default(),
|
Style::default(),
|
||||||
);
|
);
|
||||||
state.tick();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl StatefulWidget for &Spinner {
|
|
||||||
type State = SpinnerState;
|
|
||||||
|
|
||||||
/// Renders the spinner in the given area.
|
impl Widget for &Spinner {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
buf.set_string(
|
buf.set_string(
|
||||||
area.left(),
|
area.left(),
|
||||||
area.top(),
|
area.top(),
|
||||||
self.frame(state.current_frame),
|
self.frame(self.state.current_frame),
|
||||||
Style::default(),
|
Style::default(),
|
||||||
);
|
);
|
||||||
state.tick();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,63 +1,68 @@
|
|||||||
use crate::action::Action;
|
use crate::action::Action;
|
||||||
|
use crate::cable::load_cable_channels;
|
||||||
use crate::channels::entry::{Entry, PreviewType, ENTRY_PLACEHOLDER};
|
use crate::channels::entry::{Entry, PreviewType, ENTRY_PLACEHOLDER};
|
||||||
use crate::channels::{
|
use crate::channels::{
|
||||||
remote_control::{load_builtin_channels, RemoteControl},
|
remote_control::{load_builtin_channels, RemoteControl},
|
||||||
OnAir, TelevisionChannel, UnitChannel,
|
OnAir, TelevisionChannel, UnitChannel,
|
||||||
};
|
};
|
||||||
use crate::config::{Config, KeyBindings, Theme};
|
use crate::config::{Config, KeyBindings, Theme};
|
||||||
|
use crate::draw::{ChannelState, Ctx, TvState};
|
||||||
use crate::input::convert_action_to_input_request;
|
use crate::input::convert_action_to_input_request;
|
||||||
use crate::picker::Picker;
|
use crate::picker::Picker;
|
||||||
use crate::preview::Previewer;
|
use crate::preview::{PreviewState, Previewer};
|
||||||
use crate::screen::cache::RenderedPreviewCache;
|
|
||||||
use crate::screen::colors::Colorscheme;
|
use crate::screen::colors::Colorscheme;
|
||||||
use crate::screen::help::draw_help_bar;
|
use crate::screen::keybindings::{DisplayableAction, DisplayableKeybindings};
|
||||||
use crate::screen::input::draw_input_box;
|
use crate::screen::layout::InputPosition;
|
||||||
use crate::screen::keybindings::{
|
|
||||||
build_keybindings_table, DisplayableAction, DisplayableKeybindings,
|
|
||||||
};
|
|
||||||
use crate::screen::layout::{Dimensions, InputPosition, Layout};
|
|
||||||
use crate::screen::mode::Mode;
|
|
||||||
use crate::screen::preview::draw_preview_content_block;
|
|
||||||
use crate::screen::remote_control::draw_remote_control;
|
|
||||||
use crate::screen::results::draw_results_list;
|
|
||||||
use crate::screen::spinner::{Spinner, SpinnerState};
|
use crate::screen::spinner::{Spinner, SpinnerState};
|
||||||
use crate::utils::metadata::AppMetadata;
|
use crate::utils::metadata::AppMetadata;
|
||||||
use crate::utils::strings::EMPTY_STRING;
|
use crate::utils::strings::EMPTY_STRING;
|
||||||
use crate::{cable::load_cable_channels, keymap::Keymap};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use copypasta::{ClipboardContext, ClipboardProvider};
|
use copypasta::{ClipboardContext, ClipboardProvider};
|
||||||
use ratatui::{layout::Rect, style::Color, Frame};
|
|
||||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::{Arc, Mutex};
|
use tokio::sync::mpsc::{Receiver, Sender, UnboundedSender};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
|
||||||
|
#[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Mode {
|
||||||
|
Channel,
|
||||||
|
RemoteControl,
|
||||||
|
SendToChannel,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Television {
|
pub struct Television {
|
||||||
action_tx: Option<UnboundedSender<Action>>,
|
action_tx: UnboundedSender<Action>,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub keymap: Keymap,
|
pub channel: TelevisionChannel,
|
||||||
pub(crate) channel: TelevisionChannel,
|
pub remote_control: TelevisionChannel,
|
||||||
pub(crate) remote_control: TelevisionChannel,
|
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
pub current_pattern: String,
|
pub current_pattern: String,
|
||||||
pub(crate) results_picker: Picker,
|
pub results_picker: Picker,
|
||||||
pub(crate) rc_picker: Picker,
|
pub rc_picker: Picker,
|
||||||
results_area_height: u32,
|
results_area_height: u16,
|
||||||
pub previewer: Previewer,
|
pub previewer: Previewer,
|
||||||
pub preview_scroll: Option<u16>,
|
pub preview_state: PreviewState,
|
||||||
pub preview_pane_height: u16,
|
pub spinner: Spinner,
|
||||||
current_preview_total_lines: u16,
|
pub spinner_state: SpinnerState,
|
||||||
pub icon_color_cache: FxHashMap<String, Color>,
|
|
||||||
pub rendered_preview_cache: Arc<Mutex<RenderedPreviewCache<'static>>>,
|
|
||||||
pub(crate) spinner: Spinner,
|
|
||||||
pub(crate) spinner_state: SpinnerState,
|
|
||||||
pub app_metadata: AppMetadata,
|
pub app_metadata: AppMetadata,
|
||||||
pub colorscheme: Colorscheme,
|
pub colorscheme: Colorscheme,
|
||||||
|
pub ticks: u64,
|
||||||
|
// these are really here as a means to communicate between the render thread
|
||||||
|
// and the main thread to update `Television`'s state without needing to pass
|
||||||
|
// a mutable reference to `draw`
|
||||||
|
pub inner_rx: Receiver<Message>,
|
||||||
|
pub inner_tx: Sender<Message>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Message {
|
||||||
|
ResultListHeightChanged(u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Television {
|
impl Television {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
action_tx: UnboundedSender<Action>,
|
||||||
mut channel: TelevisionChannel,
|
mut channel: TelevisionChannel,
|
||||||
config: Config,
|
config: Config,
|
||||||
input: Option<String>,
|
input: Option<String>,
|
||||||
@ -67,7 +72,6 @@ impl Television {
|
|||||||
results_picker = results_picker.inverted();
|
results_picker = results_picker.inverted();
|
||||||
}
|
}
|
||||||
let previewer = Previewer::new(Some(config.previewers.clone().into()));
|
let previewer = Previewer::new(Some(config.previewers.clone().into()));
|
||||||
let keymap = Keymap::from(&config.keybindings);
|
|
||||||
let cable_channels = load_cable_channels().unwrap_or_default();
|
let cable_channels = load_cable_channels().unwrap_or_default();
|
||||||
let builtin_channels = load_builtin_channels(Some(
|
let builtin_channels = load_builtin_channels(Some(
|
||||||
&cable_channels.keys().collect::<Vec<_>>(),
|
&cable_channels.keys().collect::<Vec<_>>(),
|
||||||
@ -84,10 +88,12 @@ impl Television {
|
|||||||
|
|
||||||
channel.find(&input.unwrap_or(EMPTY_STRING.to_string()));
|
channel.find(&input.unwrap_or(EMPTY_STRING.to_string()));
|
||||||
let spinner = Spinner::default();
|
let spinner = Spinner::default();
|
||||||
|
// capacity is quite arbitrary here, we can adjust it later
|
||||||
|
let (inner_tx, inner_rx) = tokio::sync::mpsc::channel(10);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
action_tx: None,
|
action_tx,
|
||||||
config,
|
config,
|
||||||
keymap,
|
|
||||||
channel,
|
channel,
|
||||||
remote_control: TelevisionChannel::RemoteControl(
|
remote_control: TelevisionChannel::RemoteControl(
|
||||||
RemoteControl::new(builtin_channels, Some(cable_channels)),
|
RemoteControl::new(builtin_channels, Some(cable_channels)),
|
||||||
@ -98,17 +104,14 @@ impl Television {
|
|||||||
rc_picker: Picker::default(),
|
rc_picker: Picker::default(),
|
||||||
results_area_height: 0,
|
results_area_height: 0,
|
||||||
previewer,
|
previewer,
|
||||||
preview_scroll: None,
|
preview_state: PreviewState::default(),
|
||||||
preview_pane_height: 0,
|
|
||||||
current_preview_total_lines: 0,
|
|
||||||
icon_color_cache: FxHashMap::default(),
|
|
||||||
rendered_preview_cache: Arc::new(Mutex::new(
|
|
||||||
RenderedPreviewCache::default(),
|
|
||||||
)),
|
|
||||||
spinner,
|
spinner,
|
||||||
spinner_state: SpinnerState::from(&spinner),
|
spinner_state: SpinnerState::from(&spinner),
|
||||||
app_metadata,
|
app_metadata,
|
||||||
colorscheme,
|
colorscheme,
|
||||||
|
ticks: 0,
|
||||||
|
inner_rx,
|
||||||
|
inner_tx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,12 +125,41 @@ impl Television {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dump_context(&self) -> Ctx {
|
||||||
|
let channel_state = ChannelState::new(
|
||||||
|
self.current_channel(),
|
||||||
|
self.channel.selected_entries().clone(),
|
||||||
|
self.channel.total_count(),
|
||||||
|
self.channel.running(),
|
||||||
|
);
|
||||||
|
let tv_state = TvState::new(
|
||||||
|
self.mode,
|
||||||
|
self.get_selected_entry(Some(Mode::Channel)),
|
||||||
|
self.results_area_height,
|
||||||
|
self.results_picker.clone(),
|
||||||
|
self.rc_picker.clone(),
|
||||||
|
channel_state,
|
||||||
|
self.spinner,
|
||||||
|
self.preview_state.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ctx::new(
|
||||||
|
tv_state,
|
||||||
|
self.config.clone(),
|
||||||
|
self.colorscheme.clone(),
|
||||||
|
self.app_metadata.clone(),
|
||||||
|
self.inner_tx.clone(),
|
||||||
|
// now timestamp
|
||||||
|
std::time::Instant::now(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn current_channel(&self) -> UnitChannel {
|
pub fn current_channel(&self) -> UnitChannel {
|
||||||
UnitChannel::from(&self.channel)
|
UnitChannel::from(&self.channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn change_channel(&mut self, channel: TelevisionChannel) {
|
pub fn change_channel(&mut self, channel: TelevisionChannel) {
|
||||||
self.reset_preview_scroll();
|
self.preview_state.reset();
|
||||||
self.reset_picker_selection();
|
self.reset_picker_selection();
|
||||||
self.reset_picker_input();
|
self.reset_picker_input();
|
||||||
self.current_pattern = EMPTY_STRING.to_string();
|
self.current_pattern = EMPTY_STRING.to_string();
|
||||||
@ -147,7 +179,7 @@ impl Television {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_selected_entry(&mut self, mode: Option<Mode>) -> Option<Entry> {
|
pub fn get_selected_entry(&self, mode: Option<Mode>) -> Option<Entry> {
|
||||||
match mode.unwrap_or(self.mode) {
|
match mode.unwrap_or(self.mode) {
|
||||||
Mode::Channel => {
|
Mode::Channel => {
|
||||||
if let Some(i) = self.results_picker.selected() {
|
if let Some(i) = self.results_picker.selected() {
|
||||||
@ -168,7 +200,7 @@ impl Television {
|
|||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_selected_entries(
|
pub fn get_selected_entries(
|
||||||
&mut self,
|
&self,
|
||||||
mode: Option<Mode>,
|
mode: Option<Mode>,
|
||||||
) -> Option<FxHashSet<Entry>> {
|
) -> Option<FxHashSet<Entry>> {
|
||||||
if self.channel.selected_entries().is_empty()
|
if self.channel.selected_entries().is_empty()
|
||||||
@ -221,10 +253,6 @@ impl Television {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_preview_scroll(&mut self) {
|
|
||||||
self.preview_scroll = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset_picker_selection(&mut self) {
|
fn reset_picker_selection(&mut self) {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Channel => self.results_picker.reset_selection(),
|
Mode::Channel => self.results_picker.reset_selection(),
|
||||||
@ -242,46 +270,14 @@ impl Television {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_preview_down(&mut self, offset: u16) {
|
|
||||||
if self.preview_scroll.is_none() {
|
|
||||||
self.preview_scroll = Some(0);
|
|
||||||
}
|
|
||||||
if let Some(scroll) = self.preview_scroll {
|
|
||||||
self.preview_scroll = Some(
|
|
||||||
(scroll + offset).min(
|
|
||||||
self.current_preview_total_lines
|
|
||||||
.saturating_sub(2 * self.preview_pane_height / 3),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_preview_up(&mut self, offset: u16) {
|
const RENDER_EVERY_N_TICKS: u64 = 10;
|
||||||
if let Some(scroll) = self.preview_scroll {
|
|
||||||
self.preview_scroll = Some(scroll.saturating_sub(offset));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Television {
|
impl Television {
|
||||||
/// Register an action handler that can send actions for processing if necessary.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `tx` - An unbounded sender that can send actions.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// * `Result<()>` - An Ok result or an error.
|
|
||||||
pub fn register_action_handler(
|
|
||||||
&mut self,
|
|
||||||
tx: UnboundedSender<Action>,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.action_tx = Some(tx);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_render(&self, action: &Action) -> bool {
|
fn should_render(&self, action: &Action) -> bool {
|
||||||
matches!(
|
self.ticks == RENDER_EVERY_N_TICKS
|
||||||
|
|| matches!(
|
||||||
action,
|
action,
|
||||||
Action::AddInputChar(_)
|
Action::AddInputChar(_)
|
||||||
| Action::DeletePrevChar
|
| Action::DeletePrevChar
|
||||||
@ -307,36 +303,82 @@ impl Television {
|
|||||||
| Action::ToggleHelp
|
| Action::ToggleHelp
|
||||||
| Action::TogglePreview
|
| Action::TogglePreview
|
||||||
| Action::CopyEntryToClipboard
|
| Action::CopyEntryToClipboard
|
||||||
) || self.channel.running()
|
)
|
||||||
|
|| self.channel.running()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unused_async)]
|
pub fn update_preview_state(
|
||||||
/// Update the state of the component based on a received action.
|
&mut self,
|
||||||
///
|
selected_entry: &Entry,
|
||||||
/// # Arguments
|
) -> Result<()> {
|
||||||
/// * `action` - An action that may modify the state of the television.
|
if self.config.ui.show_preview_panel
|
||||||
///
|
&& !matches!(selected_entry.preview_type, PreviewType::None)
|
||||||
/// # Returns
|
{
|
||||||
/// * `Result<Option<Action>>` - An action to be processed or none.
|
// preview content
|
||||||
pub async fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
if let Some(preview) = self.previewer.preview(selected_entry) {
|
||||||
match action {
|
if self.preview_state.preview.title != preview.title {
|
||||||
// handle input actions
|
self.preview_state.update(
|
||||||
Action::AddInputChar(_)
|
preview,
|
||||||
| Action::DeletePrevChar
|
// scroll to center the selected entry
|
||||||
| Action::DeletePrevWord
|
selected_entry
|
||||||
| Action::DeleteNextChar
|
.line_number
|
||||||
| Action::GoToInputEnd
|
.unwrap_or(0)
|
||||||
| Action::GoToInputStart
|
.saturating_sub(
|
||||||
| Action::GoToNextChar
|
(self.results_area_height / 2).into(),
|
||||||
| Action::GoToPrevChar => {
|
)
|
||||||
|
.try_into()?,
|
||||||
|
selected_entry
|
||||||
|
.line_number
|
||||||
|
.and_then(|l| l.try_into().ok()),
|
||||||
|
);
|
||||||
|
self.action_tx.send(Action::Render)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.preview_state.reset();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_results_picker_state(&mut self) {
|
||||||
|
if self.results_picker.selected().is_none()
|
||||||
|
&& self.channel.result_count() > 0
|
||||||
|
{
|
||||||
|
self.results_picker.select(Some(0));
|
||||||
|
self.results_picker.relative_select(Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.results_picker.entries = self.channel.results(
|
||||||
|
self.results_area_height.into(),
|
||||||
|
u32::try_from(self.results_picker.offset()).unwrap(),
|
||||||
|
);
|
||||||
|
self.results_picker.total_items = self.channel.result_count();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_rc_picker_state(&mut self) {
|
||||||
|
if self.rc_picker.selected().is_none()
|
||||||
|
&& self.remote_control.result_count() > 0
|
||||||
|
{
|
||||||
|
self.rc_picker.select(Some(0));
|
||||||
|
self.rc_picker.relative_select(Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rc_picker.entries = self.remote_control.results(
|
||||||
|
// this'll be more than the actual rc height but it's fine
|
||||||
|
self.results_area_height.into(),
|
||||||
|
u32::try_from(self.rc_picker.offset()).unwrap(),
|
||||||
|
);
|
||||||
|
self.rc_picker.total_items = self.remote_control.total_count();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_input_action(&mut self, action: &Action) {
|
||||||
let input = match self.mode {
|
let input = match self.mode {
|
||||||
Mode::Channel => &mut self.results_picker.input,
|
Mode::Channel => &mut self.results_picker.input,
|
||||||
Mode::RemoteControl | Mode::SendToChannel => {
|
Mode::RemoteControl | Mode::SendToChannel => {
|
||||||
&mut self.rc_picker.input
|
&mut self.rc_picker.input
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
input
|
input.handle(convert_action_to_input_request(action).unwrap());
|
||||||
.handle(convert_action_to_input_request(&action).unwrap());
|
|
||||||
match action {
|
match action {
|
||||||
Action::AddInputChar(_)
|
Action::AddInputChar(_)
|
||||||
| Action::DeletePrevChar
|
| Action::DeletePrevChar
|
||||||
@ -347,33 +389,15 @@ impl Television {
|
|||||||
self.current_pattern.clone_from(&new_pattern);
|
self.current_pattern.clone_from(&new_pattern);
|
||||||
self.find(&new_pattern);
|
self.find(&new_pattern);
|
||||||
self.reset_picker_selection();
|
self.reset_picker_selection();
|
||||||
self.reset_preview_scroll();
|
self.preview_state.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::SelectNextEntry => {
|
|
||||||
self.reset_preview_scroll();
|
pub fn handle_toggle_rc(&mut self) {
|
||||||
self.select_next_entry(1);
|
match self.mode {
|
||||||
}
|
|
||||||
Action::SelectPrevEntry => {
|
|
||||||
self.reset_preview_scroll();
|
|
||||||
self.select_prev_entry(1);
|
|
||||||
}
|
|
||||||
Action::SelectNextPage => {
|
|
||||||
self.reset_preview_scroll();
|
|
||||||
self.select_next_entry(self.results_area_height);
|
|
||||||
}
|
|
||||||
Action::SelectPrevPage => {
|
|
||||||
self.reset_preview_scroll();
|
|
||||||
self.select_prev_entry(self.results_area_height);
|
|
||||||
}
|
|
||||||
Action::ScrollPreviewDown => self.scroll_preview_down(1),
|
|
||||||
Action::ScrollPreviewUp => self.scroll_preview_up(1),
|
|
||||||
Action::ScrollPreviewHalfPageDown => self.scroll_preview_down(20),
|
|
||||||
Action::ScrollPreviewHalfPageUp => self.scroll_preview_up(20),
|
|
||||||
Action::ToggleRemoteControl => match self.mode {
|
|
||||||
Mode::Channel => {
|
Mode::Channel => {
|
||||||
self.mode = Mode::RemoteControl;
|
self.mode = Mode::RemoteControl;
|
||||||
self.init_remote_control();
|
self.init_remote_control();
|
||||||
@ -387,8 +411,27 @@ impl Television {
|
|||||||
self.mode = Mode::Channel;
|
self.mode = Mode::Channel;
|
||||||
}
|
}
|
||||||
Mode::SendToChannel => {}
|
Mode::SendToChannel => {}
|
||||||
},
|
}
|
||||||
Action::ToggleSelectionDown | Action::ToggleSelectionUp => {
|
}
|
||||||
|
|
||||||
|
pub fn handle_toggle_send_to_channel(&mut self) {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Channel | Mode::RemoteControl => {
|
||||||
|
self.mode = Mode::SendToChannel;
|
||||||
|
self.remote_control = TelevisionChannel::RemoteControl(
|
||||||
|
RemoteControl::with_transitions_from(&self.channel),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Mode::SendToChannel => {
|
||||||
|
self.reset_picker_input();
|
||||||
|
self.remote_control.find(EMPTY_STRING);
|
||||||
|
self.reset_picker_selection();
|
||||||
|
self.mode = Mode::Channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_toggle_selection(&mut self, action: &Action) {
|
||||||
if matches!(self.mode, Mode::Channel) {
|
if matches!(self.mode, Mode::Channel) {
|
||||||
if let Some(entry) = self.get_selected_entry(None) {
|
if let Some(entry) = self.get_selected_entry(None) {
|
||||||
self.channel.toggle_selection(&entry);
|
self.channel.toggle_selection(&entry);
|
||||||
@ -400,21 +443,16 @@ impl Television {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::ConfirmSelection => {
|
|
||||||
|
pub fn handle_confirm_selection(&mut self) -> Result<()> {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Channel => {
|
Mode::Channel => {
|
||||||
self.action_tx
|
self.action_tx.send(Action::SelectAndExit)?;
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.send(Action::SelectAndExit)?;
|
|
||||||
}
|
}
|
||||||
Mode::RemoteControl => {
|
Mode::RemoteControl => {
|
||||||
if let Some(entry) =
|
if let Some(entry) = self.get_selected_entry(None) {
|
||||||
self.get_selected_entry(Some(Mode::RemoteControl))
|
let new_channel =
|
||||||
{
|
self.remote_control.zap(entry.name.as_str())?;
|
||||||
let new_channel = self
|
|
||||||
.remote_control
|
|
||||||
.zap(entry.name.as_str())?;
|
|
||||||
// this resets the RC picker
|
// this resets the RC picker
|
||||||
self.reset_picker_selection();
|
self.reset_picker_selection();
|
||||||
self.reset_picker_input();
|
self.reset_picker_input();
|
||||||
@ -424,12 +462,10 @@ impl Television {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode::SendToChannel => {
|
Mode::SendToChannel => {
|
||||||
if let Some(entry) =
|
if let Some(entry) = self.get_selected_entry(None) {
|
||||||
self.get_selected_entry(Some(Mode::RemoteControl))
|
let new_channel = self
|
||||||
{
|
.channel
|
||||||
let new_channel = self.channel.transition_to(
|
.transition_to(entry.name.as_str().try_into()?);
|
||||||
entry.name.as_str().try_into().unwrap(),
|
|
||||||
);
|
|
||||||
self.reset_picker_selection();
|
self.reset_picker_selection();
|
||||||
self.reset_picker_input();
|
self.reset_picker_input();
|
||||||
self.remote_control.find(EMPTY_STRING);
|
self.remote_control.find(EMPTY_STRING);
|
||||||
@ -438,8 +474,10 @@ impl Television {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
Action::CopyEntryToClipboard => {
|
|
||||||
|
pub fn handle_copy_entry_to_clipboard(&mut self) {
|
||||||
if self.mode == Mode::Channel {
|
if self.mode == Mode::Channel {
|
||||||
if let Some(entries) = self.get_selected_entries(None) {
|
if let Some(entries) = self.get_selected_entries(None) {
|
||||||
let mut ctx = ClipboardContext::new().unwrap();
|
let mut ctx = ClipboardContext::new().unwrap();
|
||||||
@ -454,20 +492,59 @@ impl Television {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::ToggleSendToChannel => match self.mode {
|
|
||||||
Mode::Channel | Mode::RemoteControl => {
|
pub fn handle_action(&mut self, action: &Action) -> Result<()> {
|
||||||
self.mode = Mode::SendToChannel;
|
// handle actions
|
||||||
self.remote_control = TelevisionChannel::RemoteControl(
|
match action {
|
||||||
RemoteControl::with_transitions_from(&self.channel),
|
Action::AddInputChar(_)
|
||||||
);
|
| Action::DeletePrevChar
|
||||||
|
| Action::DeletePrevWord
|
||||||
|
| Action::DeleteNextChar
|
||||||
|
| Action::GoToInputEnd
|
||||||
|
| Action::GoToInputStart
|
||||||
|
| Action::GoToNextChar
|
||||||
|
| Action::GoToPrevChar => {
|
||||||
|
self.handle_input_action(action);
|
||||||
}
|
}
|
||||||
Mode::SendToChannel => {
|
Action::SelectNextEntry => {
|
||||||
self.reset_picker_input();
|
self.preview_state.reset();
|
||||||
self.remote_control.find(EMPTY_STRING);
|
self.select_next_entry(1);
|
||||||
self.reset_picker_selection();
|
}
|
||||||
self.mode = Mode::Channel;
|
Action::SelectPrevEntry => {
|
||||||
|
self.preview_state.reset();
|
||||||
|
self.select_prev_entry(1);
|
||||||
|
}
|
||||||
|
Action::SelectNextPage => {
|
||||||
|
self.preview_state.reset();
|
||||||
|
self.select_next_entry(self.results_area_height.into());
|
||||||
|
}
|
||||||
|
Action::SelectPrevPage => {
|
||||||
|
self.preview_state.reset();
|
||||||
|
self.select_prev_entry(self.results_area_height.into());
|
||||||
|
}
|
||||||
|
Action::ScrollPreviewDown => self.preview_state.scroll_down(1),
|
||||||
|
Action::ScrollPreviewUp => self.preview_state.scroll_up(1),
|
||||||
|
Action::ScrollPreviewHalfPageDown => {
|
||||||
|
self.preview_state.scroll_down(20);
|
||||||
|
}
|
||||||
|
Action::ScrollPreviewHalfPageUp => {
|
||||||
|
self.preview_state.scroll_up(20);
|
||||||
|
}
|
||||||
|
Action::ToggleRemoteControl => {
|
||||||
|
self.handle_toggle_rc();
|
||||||
|
}
|
||||||
|
Action::ToggleSelectionDown | Action::ToggleSelectionUp => {
|
||||||
|
self.handle_toggle_selection(action);
|
||||||
|
}
|
||||||
|
Action::ConfirmSelection => {
|
||||||
|
self.handle_confirm_selection()?;
|
||||||
|
}
|
||||||
|
Action::CopyEntryToClipboard => {
|
||||||
|
self.handle_copy_entry_to_clipboard();
|
||||||
|
}
|
||||||
|
Action::ToggleSendToChannel => {
|
||||||
|
self.handle_toggle_send_to_channel();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Action::ToggleHelp => {
|
Action::ToggleHelp => {
|
||||||
self.config.ui.show_help_bar = !self.config.ui.show_help_bar;
|
self.config.ui.show_help_bar = !self.config.ui.show_help_bar;
|
||||||
}
|
}
|
||||||
@ -477,180 +554,45 @@ impl Television {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
Ok(if self.should_render(&action) {
|
|
||||||
Some(Action::Render)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the television on the screen.
|
#[allow(clippy::unused_async)]
|
||||||
|
/// Update the television state based on the action provided.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// This function may return an Action that'll be processed by the parent `App`.
|
||||||
/// * `f` - A frame used for rendering.
|
pub fn update(&mut self, action: &Action) -> Result<Option<Action>> {
|
||||||
/// * `area` - The area in which the television should be drawn.
|
if let Ok(Message::ResultListHeightChanged(height)) =
|
||||||
///
|
self.inner_rx.try_recv()
|
||||||
/// # Returns
|
{
|
||||||
/// * `Result<()>` - An Ok result or an error.
|
self.results_area_height = height;
|
||||||
pub fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
|
self.action_tx.send(Action::Render)?;
|
||||||
|
}
|
||||||
|
|
||||||
let selected_entry = self
|
let selected_entry = self
|
||||||
.get_selected_entry(Some(Mode::Channel))
|
.get_selected_entry(Some(Mode::Channel))
|
||||||
.unwrap_or(ENTRY_PLACEHOLDER);
|
.unwrap_or(ENTRY_PLACEHOLDER);
|
||||||
|
|
||||||
let layout = Layout::build(
|
self.update_preview_state(&selected_entry)?;
|
||||||
&Dimensions::from(self.config.ui.ui_scale),
|
|
||||||
area,
|
|
||||||
!matches!(self.mode, Mode::Channel),
|
|
||||||
self.config.ui.show_help_bar,
|
|
||||||
self.config.ui.show_preview_panel
|
|
||||||
&& !matches!(selected_entry.preview_type, PreviewType::None),
|
|
||||||
self.config.ui.input_bar_position,
|
|
||||||
);
|
|
||||||
|
|
||||||
// help bar (metadata, keymaps, logo)
|
self.update_results_picker_state();
|
||||||
draw_help_bar(
|
|
||||||
f,
|
|
||||||
&layout.help_bar,
|
|
||||||
self.current_channel(),
|
|
||||||
build_keybindings_table(
|
|
||||||
&self.config.keybindings.to_displayable(),
|
|
||||||
self.mode,
|
|
||||||
&self.colorscheme,
|
|
||||||
),
|
|
||||||
self.mode,
|
|
||||||
&self.app_metadata,
|
|
||||||
&self.colorscheme,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.results_area_height =
|
self.update_rc_picker_state();
|
||||||
u32::from(layout.results.height.saturating_sub(2)); // 2 for the borders
|
|
||||||
self.preview_pane_height = match layout.preview_window {
|
|
||||||
Some(preview) => preview.height,
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// results list
|
self.handle_action(action)?;
|
||||||
let result_count = self.channel.result_count();
|
|
||||||
if result_count > 0 && self.results_picker.selected().is_none() {
|
self.ticks += 1;
|
||||||
self.results_picker.select(Some(0));
|
|
||||||
self.results_picker.relative_select(Some(0));
|
Ok(if self.should_render(action) {
|
||||||
|
if self.channel.running() {
|
||||||
|
self.spinner.tick();
|
||||||
}
|
}
|
||||||
let entries = self.channel.results(
|
self.ticks = 0;
|
||||||
self.results_area_height,
|
|
||||||
u32::try_from(self.results_picker.offset())?,
|
|
||||||
);
|
|
||||||
draw_results_list(
|
|
||||||
f,
|
|
||||||
layout.results,
|
|
||||||
&entries,
|
|
||||||
self.channel.selected_entries(),
|
|
||||||
&mut self.results_picker.relative_state,
|
|
||||||
self.config.ui.input_bar_position,
|
|
||||||
self.config.ui.use_nerd_font_icons,
|
|
||||||
&mut self.icon_color_cache,
|
|
||||||
&self.colorscheme,
|
|
||||||
&self
|
|
||||||
.config
|
|
||||||
.keybindings
|
|
||||||
.get(&self.mode)
|
|
||||||
.unwrap()
|
|
||||||
.get(&Action::ToggleHelp)
|
|
||||||
// just display the first keybinding
|
|
||||||
.unwrap()
|
|
||||||
.to_string(),
|
|
||||||
&self
|
|
||||||
.config
|
|
||||||
.keybindings
|
|
||||||
.get(&self.mode)
|
|
||||||
.unwrap()
|
|
||||||
.get(&Action::TogglePreview)
|
|
||||||
// just display the first keybinding
|
|
||||||
.unwrap()
|
|
||||||
.to_string(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// input box
|
Some(Action::Render)
|
||||||
draw_input_box(
|
} else {
|
||||||
f,
|
None
|
||||||
layout.input,
|
})
|
||||||
result_count,
|
|
||||||
self.channel.total_count(),
|
|
||||||
&mut self.results_picker.input,
|
|
||||||
&mut self.results_picker.state,
|
|
||||||
self.channel.running(),
|
|
||||||
&self.spinner,
|
|
||||||
&mut self.spinner_state,
|
|
||||||
&self.colorscheme,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if self.config.ui.show_preview_panel
|
|
||||||
&& !matches!(selected_entry.preview_type, PreviewType::None)
|
|
||||||
{
|
|
||||||
// preview content
|
|
||||||
let maybe_preview = self.previewer.preview(&selected_entry);
|
|
||||||
|
|
||||||
let _ = self.previewer.preview(&selected_entry);
|
|
||||||
|
|
||||||
if let Some(preview) = &maybe_preview {
|
|
||||||
self.current_preview_total_lines = preview.total_lines;
|
|
||||||
// initialize preview scroll
|
|
||||||
self.maybe_init_preview_scroll(
|
|
||||||
selected_entry
|
|
||||||
.line_number
|
|
||||||
.map(|l| u16::try_from(l).unwrap_or(0)),
|
|
||||||
layout.preview_window.unwrap().height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
draw_preview_content_block(
|
|
||||||
f,
|
|
||||||
layout.preview_window.unwrap(),
|
|
||||||
&selected_entry,
|
|
||||||
&maybe_preview,
|
|
||||||
&self.rendered_preview_cache,
|
|
||||||
self.preview_scroll.unwrap_or(0),
|
|
||||||
self.config.ui.use_nerd_font_icons,
|
|
||||||
&self.colorscheme,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remote control
|
|
||||||
if matches!(self.mode, Mode::RemoteControl | Mode::SendToChannel) {
|
|
||||||
// NOTE: this should be done in the `update` method
|
|
||||||
let result_count = self.remote_control.result_count();
|
|
||||||
if result_count > 0 && self.rc_picker.selected().is_none() {
|
|
||||||
self.rc_picker.select(Some(0));
|
|
||||||
self.rc_picker.relative_select(Some(0));
|
|
||||||
}
|
|
||||||
let entries = self.remote_control.results(
|
|
||||||
area.height.saturating_sub(2).into(),
|
|
||||||
u32::try_from(self.rc_picker.offset())?,
|
|
||||||
);
|
|
||||||
draw_remote_control(
|
|
||||||
f,
|
|
||||||
layout.remote_control.unwrap(),
|
|
||||||
&entries,
|
|
||||||
self.config.ui.use_nerd_font_icons,
|
|
||||||
&mut self.rc_picker.state,
|
|
||||||
&mut self.rc_picker.input,
|
|
||||||
&mut self.icon_color_cache,
|
|
||||||
&self.mode,
|
|
||||||
&self.colorscheme,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn maybe_init_preview_scroll(
|
|
||||||
&mut self,
|
|
||||||
target_line: Option<u16>,
|
|
||||||
height: u16,
|
|
||||||
) {
|
|
||||||
if self.preview_scroll.is_none() && !self.channel.running() {
|
|
||||||
self.preview_scroll =
|
|
||||||
Some(target_line.unwrap_or(0).saturating_sub(height / 3));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,14 +5,15 @@ use std::{
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor, execute,
|
cursor,
|
||||||
|
event::DisableMouseCapture,
|
||||||
|
execute,
|
||||||
terminal::{
|
terminal::{
|
||||||
disable_raw_mode, enable_raw_mode, is_raw_mode_enabled,
|
disable_raw_mode, enable_raw_mode, is_raw_mode_enabled,
|
||||||
EnterAlternateScreen, LeaveAlternateScreen,
|
EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use ratatui::{backend::CrosstermBackend, layout::Size};
|
use ratatui::{backend::CrosstermBackend, layout::Size};
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@ -20,8 +21,6 @@ pub struct Tui<W>
|
|||||||
where
|
where
|
||||||
W: Write,
|
W: Write,
|
||||||
{
|
{
|
||||||
pub task: JoinHandle<()>,
|
|
||||||
pub frame_rate: f64,
|
|
||||||
pub terminal: ratatui::Terminal<CrosstermBackend<W>>,
|
pub terminal: ratatui::Terminal<CrosstermBackend<W>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,17 +31,10 @@ where
|
|||||||
{
|
{
|
||||||
pub fn new(writer: W) -> Result<Self> {
|
pub fn new(writer: W) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
task: tokio::spawn(async {}),
|
|
||||||
frame_rate: 60.0,
|
|
||||||
terminal: ratatui::Terminal::new(CrosstermBackend::new(writer))?,
|
terminal: ratatui::Terminal::new(CrosstermBackend::new(writer))?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn frame_rate(mut self, frame_rate: f64) -> Self {
|
|
||||||
self.frame_rate = frame_rate;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn size(&self) -> Result<Size> {
|
pub fn size(&self) -> Result<Size> {
|
||||||
Ok(self.terminal.size()?)
|
Ok(self.terminal.size()?)
|
||||||
}
|
}
|
||||||
@ -52,7 +44,7 @@ where
|
|||||||
let mut buffered_stderr = LineWriter::new(stderr());
|
let mut buffered_stderr = LineWriter::new(stderr());
|
||||||
execute!(buffered_stderr, EnterAlternateScreen)?;
|
execute!(buffered_stderr, EnterAlternateScreen)?;
|
||||||
self.terminal.clear()?;
|
self.terminal.clear()?;
|
||||||
execute!(buffered_stderr, cursor::Hide)?;
|
execute!(buffered_stderr, DisableMouseCapture)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||||
use std::collections::{HashSet, VecDeque};
|
use std::collections::{HashSet, VecDeque};
|
||||||
use tracing::debug;
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
/// A ring buffer that also keeps track of the keys it contains to avoid duplicates.
|
/// A ring buffer that also keeps track of the keys it contains to avoid duplicates.
|
||||||
///
|
///
|
||||||
@ -81,7 +81,7 @@ where
|
|||||||
pub fn push(&mut self, item: T) -> Option<T> {
|
pub fn push(&mut self, item: T) -> Option<T> {
|
||||||
// If the key is already in the buffer, do nothing
|
// If the key is already in the buffer, do nothing
|
||||||
if self.contains(&item) {
|
if self.contains(&item) {
|
||||||
debug!("Key already in ring buffer: {:?}", item);
|
trace!("Key already in ring buffer: {:?}", item);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut popped_key = None;
|
let mut popped_key = None;
|
||||||
|
@ -30,7 +30,7 @@ pub struct StateChanged {
|
|||||||
pub type InputResponse = Option<StateChanged>;
|
pub type InputResponse = Option<StateChanged>;
|
||||||
|
|
||||||
/// An input buffer with cursor support.
|
/// An input buffer with cursor support.
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone, PartialEq, Hash)]
|
||||||
pub struct Input {
|
pub struct Input {
|
||||||
value: String,
|
value: String,
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
|
||||||
pub struct AppMetadata {
|
pub struct AppMetadata {
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub current_directory: String,
|
pub current_directory: String,
|
||||||
|
@ -118,7 +118,7 @@ fn set_syntax_set<'a>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||||
pub struct HighlightedLines {
|
pub struct HighlightedLines {
|
||||||
pub lines: Vec<Vec<(Style, String)>>,
|
pub lines: Vec<Vec<(Style, String)>>,
|
||||||
//pub state: Option<HighlightingState>,
|
//pub state: Option<HighlightingState>,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user