mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-07 03:55: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
|
||||
# for command history
|
||||
"command_history" = "ctrl-r"
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
pub mod main {
|
||||
pub mod draw;
|
||||
pub mod results_list_benchmark;
|
||||
pub mod draw_results_list;
|
||||
}
|
||||
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::Terminal;
|
||||
use std::path::PathBuf;
|
||||
use television::action::Action;
|
||||
use television::channels::OnAir;
|
||||
use television::channels::{files::Channel, TelevisionChannel};
|
||||
use television::config::Config;
|
||||
@ -22,24 +23,30 @@ fn draw(c: &mut Criterion) {
|
||||
let config = Config::new().unwrap();
|
||||
let backend = TestBackend::new(width, height);
|
||||
let terminal = Terminal::new(backend).unwrap();
|
||||
let (tx, _) = tokio::sync::mpsc::unbounded_channel();
|
||||
let mut channel =
|
||||
TelevisionChannel::Files(Channel::new(vec![
|
||||
PathBuf::from("."),
|
||||
]));
|
||||
channel.find("television");
|
||||
// Wait for the channel to finish loading
|
||||
let mut tv = Television::new(tx, channel, config, None);
|
||||
for _ in 0..5 {
|
||||
// tick the matcher
|
||||
let _ = channel.results(10, 0);
|
||||
let _ = tv.channel.results(10, 0);
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
let mut tv = Television::new(channel, config, None);
|
||||
tv.select_next_entry(10);
|
||||
let _ = tv.update_preview_state(
|
||||
&tv.get_selected_entry(None).unwrap(),
|
||||
);
|
||||
tv.update(&Action::Tick).unwrap();
|
||||
(tv, terminal)
|
||||
},
|
||||
// Measurement
|
||||
|(mut tv, mut terminal)| async move {
|
||||
tv.draw(
|
||||
|(tv, mut terminal)| async move {
|
||||
television::draw::draw(
|
||||
black_box(&tv.dump_context()),
|
||||
black_box(&mut terminal.get_frame()),
|
||||
black_box(Rect::new(0, 0, width, height)),
|
||||
)
|
||||
|
@ -4,14 +4,12 @@ use ratatui::layout::Alignment;
|
||||
use ratatui::prelude::{Line, Style};
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::{Block, BorderType, Borders, ListDirection, Padding};
|
||||
use rustc_hash::FxHashMap;
|
||||
use television::channels::entry::merge_ranges;
|
||||
use television::channels::entry::{Entry, PreviewType};
|
||||
use television::screen::colors::ResultsColorscheme;
|
||||
use television::screen::results::build_results_list;
|
||||
|
||||
pub fn results_list_benchmark(c: &mut Criterion) {
|
||||
let mut icon_color_cache = FxHashMap::default();
|
||||
pub fn draw_results_list(c: &mut Criterion) {
|
||||
// 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
|
||||
// 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,
|
||||
ListDirection::BottomToTop,
|
||||
false,
|
||||
&mut icon_color_cache,
|
||||
&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
|
||||
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")]
|
||||
pub enum UnitChannel {
|
||||
#(
|
||||
|
@ -1,16 +1,14 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::screen::mode::Mode;
|
||||
use anyhow::Result;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tracing::{debug, info};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, info, trace};
|
||||
|
||||
use crate::channels::entry::Entry;
|
||||
use crate::channels::TelevisionChannel;
|
||||
use crate::config::{parse_key, Config};
|
||||
use crate::keymap::Keymap;
|
||||
use crate::television::Television;
|
||||
use crate::television::{Mode, Television};
|
||||
use crate::{
|
||||
action::Action,
|
||||
event::{Event, EventLoop, Key},
|
||||
@ -23,9 +21,8 @@ pub struct App {
|
||||
// maybe move these two into config instead of passing them
|
||||
// via the cli?
|
||||
tick_rate: f64,
|
||||
frame_rate: f64,
|
||||
/// 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.
|
||||
should_quit: bool,
|
||||
/// 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 (_, event_rx) = 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 keymap = Keymap::from(&config.keybindings).with_mode_mappings(
|
||||
Mode::Channel,
|
||||
@ -106,12 +102,11 @@ impl App {
|
||||
)?;
|
||||
debug!("{:?}", keymap);
|
||||
let television =
|
||||
Arc::new(Mutex::new(Television::new(channel, config, input)));
|
||||
Television::new(action_tx.clone(), channel, config, input);
|
||||
|
||||
Ok(Self {
|
||||
keymap,
|
||||
tick_rate,
|
||||
frame_rate,
|
||||
television,
|
||||
should_quit: false,
|
||||
should_suspend: false,
|
||||
@ -148,19 +143,10 @@ impl App {
|
||||
let (render_tx, render_rx) = mpsc::unbounded_channel();
|
||||
self.render_tx = render_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 {
|
||||
render(
|
||||
render_rx,
|
||||
action_tx_r,
|
||||
television_r,
|
||||
frame_rate,
|
||||
is_output_tty,
|
||||
)
|
||||
.await
|
||||
render(render_rx, action_tx_r, is_output_tty).await
|
||||
});
|
||||
render_tx.send(RenderingTask::Render)?;
|
||||
self.action_tx.send(Action::Render)?;
|
||||
|
||||
// event handling loop
|
||||
debug!("Starting event handling loop");
|
||||
@ -168,14 +154,11 @@ impl App {
|
||||
loop {
|
||||
// handle event and convert to action
|
||||
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)?;
|
||||
// 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 {
|
||||
// send a termination signal to the event loop
|
||||
@ -199,7 +182,7 @@ impl App {
|
||||
///
|
||||
/// # Returns
|
||||
/// 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 {
|
||||
Event::Input(keycode) => {
|
||||
info!("{:?}", keycode);
|
||||
@ -219,7 +202,7 @@ impl App {
|
||||
}
|
||||
// get action based on keybindings
|
||||
self.keymap
|
||||
.get(&self.television.lock().await.mode)
|
||||
.get(&self.television.mode)
|
||||
.and_then(|keymap| keymap.get(&keycode).cloned())
|
||||
.unwrap_or(if let Key::Char(c) = keycode {
|
||||
Action::AddInputChar(c)
|
||||
@ -246,10 +229,10 @@ impl App {
|
||||
///
|
||||
/// # Errors
|
||||
/// 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() {
|
||||
if action != Action::Tick && action != Action::Render {
|
||||
debug!("{action:?}");
|
||||
if action != Action::Tick {
|
||||
trace!("{action:?}");
|
||||
}
|
||||
match action {
|
||||
Action::Quit => {
|
||||
@ -269,15 +252,13 @@ impl App {
|
||||
self.render_tx.send(RenderingTask::Quit)?;
|
||||
if let Some(entries) = self
|
||||
.television
|
||||
.lock()
|
||||
.await
|
||||
.get_selected_entries(Some(Mode::Channel))
|
||||
{
|
||||
return Ok(ActionOutcome::Entries(entries));
|
||||
}
|
||||
|
||||
return Ok(ActionOutcome::Input(
|
||||
self.television.lock().await.current_pattern.clone(),
|
||||
self.television.current_pattern.clone(),
|
||||
));
|
||||
}
|
||||
Action::SelectPassthrough(passthrough) => {
|
||||
@ -285,8 +266,6 @@ impl App {
|
||||
self.render_tx.send(RenderingTask::Quit)?;
|
||||
if let Some(entries) = self
|
||||
.television
|
||||
.lock()
|
||||
.await
|
||||
.get_selected_entries(Some(Mode::Channel))
|
||||
{
|
||||
return Ok(ActionOutcome::Passthrough(
|
||||
@ -303,14 +282,14 @@ impl App {
|
||||
self.render_tx.send(RenderingTask::Resize(w, h))?;
|
||||
}
|
||||
Action::Render => {
|
||||
self.render_tx.send(RenderingTask::Render)?;
|
||||
self.render_tx.send(RenderingTask::Render(
|
||||
self.television.dump_context(),
|
||||
))?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// forward action to the television handler
|
||||
if let Some(action) =
|
||||
self.television.lock().await.update(action.clone()).await?
|
||||
{
|
||||
if let Some(action) = self.television.update(&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.
|
||||
///
|
||||
/// 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.
|
||||
const MAX_LINES_IN_MEM: usize = 5_000_000;
|
||||
/// so this should take around 100 x `10_000_000` = 1GB of memory.
|
||||
const MAX_LINES_IN_MEM: usize = 10_000_000;
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn crawl_for_candidates(
|
||||
|
@ -1,13 +1,14 @@
|
||||
use crate::action::Action;
|
||||
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 rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Hash)]
|
||||
pub enum Binding {
|
||||
SingleKey(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>>);
|
||||
|
||||
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 {
|
||||
type Target = FxHashMap<Mode, FxHashMap<Action, Binding>>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
@ -1,5 +1,5 @@
|
||||
#![allow(clippy::module_name_repetitions, clippy::ref_option)]
|
||||
use std::{env, path::PathBuf};
|
||||
use std::{env, hash::Hash, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use directories::ProjectDirs;
|
||||
@ -11,7 +11,7 @@ use serde::Deserialize;
|
||||
use shell_integration::ShellIntegrationConfig;
|
||||
pub use themes::Theme;
|
||||
use tracing::{debug, warn};
|
||||
use ui::UiConfig;
|
||||
pub use ui::UiConfig;
|
||||
|
||||
mod keybindings;
|
||||
mod previewers;
|
||||
@ -22,7 +22,7 @@ mod ui;
|
||||
const DEFAULT_CONFIG: &str = include_str!("../../.config/config.toml");
|
||||
|
||||
#[allow(dead_code, clippy::module_name_repetitions)]
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AppConfig {
|
||||
#[serde(default = "get_data_dir")]
|
||||
@ -35,8 +35,17 @@ pub struct AppConfig {
|
||||
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)]
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, PartialEq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
/// General application configuration
|
||||
@ -77,7 +86,6 @@ lazy_static! {
|
||||
const CONFIG_FILE_NAME: &str = "config.toml";
|
||||
|
||||
impl Config {
|
||||
// FIXME: default management is a bit of a mess right now
|
||||
#[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]
|
||||
pub fn new() -> Result<Self> {
|
||||
// Load the default_config values as base defaults
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::preview::{previewers, PreviewerConfig};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, PartialEq, Hash)]
|
||||
pub struct PreviewersConfig {
|
||||
#[serde(default)]
|
||||
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 {}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Hash)]
|
||||
#[serde(default)]
|
||||
pub struct FilePreviewerConfig {
|
||||
//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 {}
|
||||
|
@ -1,14 +1,24 @@
|
||||
use std::hash::Hash;
|
||||
|
||||
use crate::config::parse_key;
|
||||
use crate::event::Key;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct ShellIntegrationConfig {
|
||||
pub commands: 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 COMMAND_HISTORY_CONFIGURATION_KEY: &str = "command_history";
|
||||
const DEFAULT_SHELL_AUTOCOMPLETE_KEY: char = 'T';
|
||||
|
@ -6,7 +6,7 @@ use super::themes::DEFAULT_THEME;
|
||||
|
||||
const DEFAULT_UI_SCALE: u16 = 100;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Hash)]
|
||||
#[serde(default)]
|
||||
pub struct UiConfig {
|
||||
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"));
|
||||
},
|
||||
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 {
|
||||
debug!("Raw event: {:?}", event);
|
||||
if event.kind == KeyEventKind::Release {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::screen::mode::Mode;
|
||||
use crate::television::Mode;
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::action::Action;
|
||||
|
@ -4,6 +4,7 @@ pub mod cable;
|
||||
pub mod channels;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod draw;
|
||||
pub mod errors;
|
||||
pub mod event;
|
||||
pub mod input;
|
||||
|
@ -57,8 +57,6 @@ async fn main() -> Result<()> {
|
||||
|
||||
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 {
|
||||
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;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub struct Picker {
|
||||
pub(crate) state: ListState,
|
||||
pub(crate) relative_state: ListState,
|
||||
inverted: bool,
|
||||
pub(crate) input: Input,
|
||||
pub entries: Vec<Entry>,
|
||||
pub total_items: u32,
|
||||
}
|
||||
|
||||
impl Default for Picker {
|
||||
@ -22,6 +27,8 @@ impl Picker {
|
||||
relative_state: ListState::default(),
|
||||
inverted: false,
|
||||
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 relative_selected = self.relative_selected().unwrap_or(0);
|
||||
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 {
|
||||
self.relative_select(Some(0));
|
||||
}
|
||||
@ -111,7 +118,7 @@ impl Picker {
|
||||
self.select(Some((selected + (total_items - 1)) % total_items));
|
||||
self.relative_select(Some(relative_selected.saturating_sub(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();
|
||||
picker.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.relative_selected(), Some(1), "relative_selected");
|
||||
}
|
||||
@ -143,7 +150,7 @@ mod tests {
|
||||
let mut picker = Picker::default();
|
||||
picker.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.relative_selected(), Some(2), "relative_selected");
|
||||
}
|
||||
@ -157,7 +164,7 @@ mod tests {
|
||||
let mut picker = Picker::default();
|
||||
picker.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.relative_selected(), Some(2), "relative_selected");
|
||||
}
|
||||
@ -171,7 +178,7 @@ mod tests {
|
||||
let mut picker = Picker::default();
|
||||
picker.select(Some(3));
|
||||
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.relative_selected(), Some(0), "relative_selected");
|
||||
}
|
||||
@ -185,7 +192,7 @@ mod tests {
|
||||
let mut picker = Picker::default();
|
||||
picker.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.relative_selected(), Some(0), "relative_selected");
|
||||
}
|
||||
@ -199,7 +206,7 @@ mod tests {
|
||||
let mut picker = Picker::default();
|
||||
picker.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.relative_selected(), Some(0), "relative_selected");
|
||||
}
|
||||
@ -213,7 +220,7 @@ mod tests {
|
||||
let mut picker = Picker::default();
|
||||
picker.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.relative_selected(), Some(2), "relative_selected");
|
||||
}
|
||||
@ -227,7 +234,7 @@ mod tests {
|
||||
let mut picker = Picker::default();
|
||||
picker.select(Some(3));
|
||||
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.relative_selected(), Some(1), "relative_selected");
|
||||
}
|
||||
@ -241,7 +248,7 @@ mod tests {
|
||||
let mut picker = Picker::default();
|
||||
picker.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.relative_selected(), Some(1), "relative_selected");
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ pub use previewers::env::EnvVarPreviewerConfig;
|
||||
pub use previewers::files::FilePreviewer;
|
||||
pub use previewers::files::FilePreviewerConfig;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq, Hash)]
|
||||
pub enum PreviewContent {
|
||||
Empty,
|
||||
FileTooLarge,
|
||||
@ -60,7 +60,7 @@ pub const TIMEOUT_MSG: &str = "Preview timed out";
|
||||
/// # Fields
|
||||
/// - `title`: The title of the preview.
|
||||
/// - `content`: The content of the preview.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq, Hash)]
|
||||
pub struct Preview {
|
||||
pub title: String,
|
||||
pub content: PreviewContent,
|
||||
@ -99,19 +99,58 @@ impl Preview {
|
||||
total_lines,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total_lines(&self) -> u16 {
|
||||
match &self.content {
|
||||
PreviewContent::SyntectHighlightedText(hl_lines) => {
|
||||
hl_lines.lines.len().try_into().unwrap_or(u16::MAX)
|
||||
#[derive(Debug, Default, Clone, PartialEq, Hash)]
|
||||
pub struct PreviewState {
|
||||
pub preview: Arc<Preview>,
|
||||
pub scroll: u16,
|
||||
pub target_line: Option<u16>,
|
||||
}
|
||||
|
||||
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::PlainText(lines) => {
|
||||
lines.len().try_into().unwrap_or(u16::MAX)
|
||||
}
|
||||
PreviewContent::AnsiText(text) => {
|
||||
text.lines().count().try_into().unwrap_or(u16::MAX)
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
_ => 0,
|
||||
|
||||
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 {
|
||||
pub fn new(config: Option<PreviewerConfig>) -> Self {
|
||||
@ -174,15 +213,10 @@ impl Previewer {
|
||||
}
|
||||
}
|
||||
|
||||
fn cached(&self, entry: &Entry) -> Option<Arc<Preview>> {
|
||||
match &entry.preview_type {
|
||||
PreviewType::Files => self.file.cached(entry),
|
||||
PreviewType::Command(_) => self.command.cached(entry),
|
||||
PreviewType::Basic | PreviewType::EnvVar => None,
|
||||
PreviewType::None => Some(Arc::new(Preview::default())),
|
||||
}
|
||||
}
|
||||
|
||||
// we could use a target scroll here to make the previewer
|
||||
// faster, but since it's already running in the background and quite
|
||||
// fast for most standard file sizes, plus we're caching the previews,
|
||||
// I'm not sure the extra complexity is worth it.
|
||||
pub fn preview(&mut self, entry: &Entry) -> Option<Arc<Preview>> {
|
||||
// if we haven't acknowledged the request yet, acknowledge it
|
||||
self.requests.push(entry.clone());
|
||||
@ -190,9 +224,10 @@ impl Previewer {
|
||||
if let Some(preview) = self.dispatch_request(entry) {
|
||||
return Some(preview);
|
||||
}
|
||||
|
||||
// lookup request stack and return the most recent preview available
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ use std::sync::{
|
||||
};
|
||||
|
||||
use syntect::{highlighting::Theme, parsing::SyntaxSet};
|
||||
use tracing::{debug, warn};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use crate::channels::entry;
|
||||
use crate::preview::cache::PreviewCache;
|
||||
@ -80,7 +80,7 @@ impl FilePreviewer {
|
||||
|
||||
pub fn preview(&mut self, entry: &entry::Entry) -> Option<Arc<Preview>> {
|
||||
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() {
|
||||
// preview is partial, spawn a task to compute the next chunk
|
||||
// and return the partial preview
|
||||
@ -90,7 +90,7 @@ impl FilePreviewer {
|
||||
Some(preview)
|
||||
} else {
|
||||
// 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);
|
||||
None
|
||||
}
|
||||
@ -102,7 +102,7 @@ impl FilePreviewer {
|
||||
partial_preview: Option<Arc<Preview>>,
|
||||
) {
|
||||
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)
|
||||
@ -139,7 +139,7 @@ impl FilePreviewer {
|
||||
|
||||
/// The size of the buffer used to read the file in bytes.
|
||||
/// 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(
|
||||
entry: &entry::Entry,
|
||||
|
@ -1,20 +1,19 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate};
|
||||
use crossterm::{execute, queue};
|
||||
use ratatui::layout::Rect;
|
||||
use std::{
|
||||
io::{stderr, stdout, LineWriter},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::io::{stderr, stdout, LineWriter};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::television::Television;
|
||||
use crate::{action::Action, tui::Tui};
|
||||
use crate::draw::Ctx;
|
||||
use crate::{action::Action, draw::draw, tui::Tui};
|
||||
|
||||
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
|
||||
pub enum RenderingTask {
|
||||
ClearScreen,
|
||||
Render,
|
||||
Render(Ctx),
|
||||
Resize(u16, u16),
|
||||
Resume,
|
||||
Suspend,
|
||||
@ -39,8 +38,6 @@ impl IoStream {
|
||||
pub async fn render(
|
||||
mut render_rx: mpsc::UnboundedReceiver<RenderingTask>,
|
||||
action_tx: mpsc::UnboundedSender<Action>,
|
||||
television: Arc<Mutex<Television>>,
|
||||
frame_rate: f64,
|
||||
is_output_tty: bool,
|
||||
) -> Result<()> {
|
||||
let stream = if is_output_tty {
|
||||
@ -50,21 +47,15 @@ pub async fn render(
|
||||
debug!("Rendering to stderr");
|
||||
IoStream::BufferedStderr.to_stream()
|
||||
};
|
||||
let mut tui = Tui::new(stream)?.frame_rate(frame_rate);
|
||||
let mut tui = Tui::new(stream)?;
|
||||
|
||||
debug!("Entering tui");
|
||||
tui.enter()?;
|
||||
|
||||
debug!("Registering action handler");
|
||||
television
|
||||
.lock()
|
||||
.await
|
||||
.register_action_handler(action_tx.clone())?;
|
||||
|
||||
let mut buffer = Vec::with_capacity(128);
|
||||
let mut buffer = Vec::with_capacity(256);
|
||||
|
||||
// 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
|
||||
buffer.sort_unstable();
|
||||
buffer.dedup();
|
||||
@ -73,17 +64,17 @@ pub async fn render(
|
||||
RenderingTask::ClearScreen => {
|
||||
tui.terminal.clear()?;
|
||||
}
|
||||
RenderingTask::Render => {
|
||||
RenderingTask::Render(context) => {
|
||||
if let Ok(size) = tui.size() {
|
||||
// Ratatui uses `u16`s to encode terminal dimensions and its
|
||||
// content for each terminal cell is stored linearly in a
|
||||
// buffer with a `u16` index which means we can't support
|
||||
// terminal areas larger than `u16::MAX`.
|
||||
if size.width.checked_mul(size.height).is_some() {
|
||||
let mut television = television.lock().await;
|
||||
queue!(stderr(), BeginSynchronizedUpdate).ok();
|
||||
tui.terminal.draw(|frame| {
|
||||
if let Err(err) =
|
||||
television.draw(frame, frame.area())
|
||||
draw(&context, frame, frame.area())
|
||||
{
|
||||
warn!("Failed to draw: {:?}", err);
|
||||
let _ = action_tx.send(Action::Error(
|
||||
@ -91,6 +82,7 @@ pub async fn render(
|
||||
));
|
||||
}
|
||||
})?;
|
||||
execute!(stderr(), EndSynchronizedUpdate).ok();
|
||||
} else {
|
||||
warn!("Terminal area too large");
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use ratatui::style::Color;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Colorscheme {
|
||||
pub general: GeneralColorscheme,
|
||||
pub help: HelpColorscheme,
|
||||
@ -10,19 +10,19 @@ pub struct Colorscheme {
|
||||
pub mode: ModeColorscheme,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct GeneralColorscheme {
|
||||
pub border_fg: Color,
|
||||
pub background: Option<Color>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct HelpColorscheme {
|
||||
pub metadata_field_name_fg: Color,
|
||||
pub metadata_field_value_fg: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ResultsColorscheme {
|
||||
pub result_name_fg: Color,
|
||||
pub result_preview_fg: Color,
|
||||
@ -32,7 +32,7 @@ pub struct ResultsColorscheme {
|
||||
pub match_foreground_color: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct PreviewColorscheme {
|
||||
pub title_fg: Color,
|
||||
pub highlight_bg: Color,
|
||||
@ -41,13 +41,13 @@ pub struct PreviewColorscheme {
|
||||
pub gutter_selected_fg: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct InputColorscheme {
|
||||
pub input_fg: Color,
|
||||
pub results_count_fg: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ModeColorscheme {
|
||||
pub channel: Color,
|
||||
pub remote_control: Color,
|
||||
|
@ -3,7 +3,8 @@ use crate::channels::UnitChannel;
|
||||
use crate::screen::colors::{Colorscheme, GeneralColorscheme};
|
||||
use crate::screen::logo::build_logo_paragraph;
|
||||
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 ratatui::layout::Rect;
|
||||
use ratatui::prelude::{Color, Style};
|
||||
|
@ -10,10 +10,7 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::screen::{
|
||||
colors::Colorscheme,
|
||||
spinner::{Spinner, SpinnerState},
|
||||
};
|
||||
use crate::screen::{colors::Colorscheme, spinner::Spinner};
|
||||
|
||||
// TODO: refactor arguments (e.g. use a struct for the spinner+state, same
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@ -22,11 +19,10 @@ pub fn draw_input_box(
|
||||
rect: Rect,
|
||||
results_count: u32,
|
||||
total_count: u32,
|
||||
input_state: &mut Input,
|
||||
results_picker_state: &mut ListState,
|
||||
input_state: &Input,
|
||||
results_picker_state: &ListState,
|
||||
matcher_running: bool,
|
||||
spinner: &Spinner,
|
||||
spinner_state: &mut SpinnerState,
|
||||
colorscheme: &Colorscheme,
|
||||
) -> Result<()> {
|
||||
let input_block = Block::default()
|
||||
@ -89,11 +85,7 @@ pub fn draw_input_box(
|
||||
f.render_widget(input, inner_input_chunks[1]);
|
||||
|
||||
if matcher_running {
|
||||
f.render_stateful_widget(
|
||||
spinner,
|
||||
inner_input_chunks[3],
|
||||
spinner_state,
|
||||
);
|
||||
f.render_widget(spinner, inner_input_chunks[3]);
|
||||
}
|
||||
|
||||
let result_count_block = Block::default();
|
||||
@ -119,8 +111,9 @@ pub fn draw_input_box(
|
||||
// specified coordinates after rendering
|
||||
f.set_cursor_position((
|
||||
// Put cursor past the end of the input text
|
||||
inner_input_chunks[1].x
|
||||
+ u16::try_from(input_state.visual_cursor().max(scroll) - scroll)?,
|
||||
inner_input_chunks[1].x.saturating_add(u16::try_from(
|
||||
input_state.visual_cursor().max(scroll) - scroll,
|
||||
)?),
|
||||
// Move one line down, from the border to the input line
|
||||
inner_input_chunks[1].y,
|
||||
));
|
||||
|
@ -1,7 +1,8 @@
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::screen::{colors::Colorscheme, mode::Mode};
|
||||
use crate::screen::colors::Colorscheme;
|
||||
use crate::television::Mode;
|
||||
use ratatui::{
|
||||
layout::Constraint,
|
||||
style::{Color, Style},
|
||||
|
@ -4,6 +4,8 @@ use ratatui::layout;
|
||||
use ratatui::layout::{Constraint, Direction, Rect};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::UiConfig;
|
||||
|
||||
pub struct Dimensions {
|
||||
pub x: 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 {
|
||||
#[serde(rename = "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 {
|
||||
#[serde(rename = "top")]
|
||||
#[default]
|
||||
@ -107,19 +109,20 @@ impl Layout {
|
||||
}
|
||||
|
||||
pub fn build(
|
||||
dimensions: &Dimensions,
|
||||
area: Rect,
|
||||
with_remote: bool,
|
||||
with_help_bar: bool,
|
||||
with_preview: bool,
|
||||
input_position: InputPosition,
|
||||
ui_config: &UiConfig,
|
||||
show_remote: bool,
|
||||
show_preview: bool,
|
||||
//
|
||||
) -> 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);
|
||||
// split the main block into two vertical chunks (help bar + rest)
|
||||
let main_rect: Rect;
|
||||
let help_bar_layout: Option<HelpBarLayout>;
|
||||
|
||||
if with_help_bar {
|
||||
if ui_config.show_help_bar {
|
||||
let hz_chunks = layout::Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Max(9), Constraint::Fill(1)])
|
||||
@ -152,10 +155,10 @@ impl Layout {
|
||||
// split the main block into 1, 2, or 3 vertical chunks
|
||||
// (results + preview + remote)
|
||||
let mut constraints = vec![Constraint::Fill(1)];
|
||||
if with_preview {
|
||||
if show_preview {
|
||||
constraints.push(Constraint::Fill(1));
|
||||
}
|
||||
if with_remote {
|
||||
if show_remote {
|
||||
// in order to fit with the help bar logo
|
||||
constraints.push(Constraint::Length(24));
|
||||
}
|
||||
@ -170,28 +173,28 @@ impl Layout {
|
||||
|
||||
let left_chunks = layout::Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(match input_position {
|
||||
.constraints(match ui_config.input_bar_position {
|
||||
InputPosition::Top => {
|
||||
results_constraints.into_iter().rev().collect()
|
||||
}
|
||||
InputPosition::Bottom => results_constraints,
|
||||
})
|
||||
.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::Top => (left_chunks[0], left_chunks[1]),
|
||||
};
|
||||
|
||||
// right block: preview title + preview
|
||||
let mut remote_idx = 1;
|
||||
let preview_window = if with_preview {
|
||||
let preview_window = if show_preview {
|
||||
remote_idx += 1;
|
||||
Some(vt_chunks[1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let remote_control = if with_remote {
|
||||
let remote_control = if show_remote {
|
||||
Some(vt_chunks[remote_idx])
|
||||
} else {
|
||||
None
|
||||
|
@ -1,10 +1,8 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::channels::UnitChannel;
|
||||
use crate::screen::{
|
||||
colors::Colorscheme,
|
||||
mode::{mode_color, Mode},
|
||||
};
|
||||
use crate::screen::{colors::Colorscheme, mode::mode_color};
|
||||
use crate::television::Mode;
|
||||
use crate::utils::metadata::AppMetadata;
|
||||
use ratatui::{
|
||||
layout::Constraint,
|
||||
|
@ -1,7 +1,6 @@
|
||||
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 {
|
||||
match mode {
|
||||
@ -10,11 +9,3 @@ pub fn mode_color(mode: Mode, colorscheme: &ModeColorscheme) -> Color {
|
||||
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::{
|
||||
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,
|
||||
};
|
||||
use crate::screen::{
|
||||
cache::RenderedPreviewCache,
|
||||
colors::{Colorscheme, PreviewColorscheme},
|
||||
};
|
||||
use crate::screen::colors::{Colorscheme, PreviewColorscheme};
|
||||
use crate::utils::strings::{
|
||||
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
||||
EMPTY_STRING,
|
||||
@ -20,19 +17,46 @@ use ratatui::{
|
||||
prelude::{Color, Line, Modifier, Span, Style, Stylize, Text},
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[allow(dead_code)]
|
||||
const FILL_CHAR_SLANTED: 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>(
|
||||
inner: Rect,
|
||||
preview_content: PreviewContent,
|
||||
preview_content: &'a PreviewContent,
|
||||
target_line: Option<u16>,
|
||||
preview_scroll: u16,
|
||||
colorscheme: Colorscheme,
|
||||
colorscheme: &'a Colorscheme,
|
||||
) -> Paragraph<'a> {
|
||||
let preview_block =
|
||||
Block::default().style(Style::default()).padding(Padding {
|
||||
@ -58,14 +82,16 @@ pub fn build_preview_paragraph<'a>(
|
||||
preview_block,
|
||||
colorscheme.preview,
|
||||
)
|
||||
.scroll((preview_scroll, 0))
|
||||
}
|
||||
PreviewContent::SyntectHighlightedText(highlighted_lines) => {
|
||||
build_syntect_highlighted_paragraph(
|
||||
highlighted_lines.lines,
|
||||
&highlighted_lines.lines,
|
||||
preview_block,
|
||||
target_line,
|
||||
preview_scroll,
|
||||
colorscheme.preview,
|
||||
inner.height,
|
||||
)
|
||||
}
|
||||
// meta
|
||||
@ -104,12 +130,11 @@ pub fn build_preview_paragraph<'a>(
|
||||
const ANSI_BEFORE_CONTEXT_SIZE: u16 = 10;
|
||||
const ANSI_CONTEXT_SIZE: usize = 150;
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn build_ansi_text_paragraph(
|
||||
text: String,
|
||||
preview_block: Block,
|
||||
fn build_ansi_text_paragraph<'a>(
|
||||
text: &'a str,
|
||||
preview_block: Block<'a>,
|
||||
preview_scroll: u16,
|
||||
) -> Paragraph {
|
||||
) -> Paragraph<'a> {
|
||||
let lines = text.lines();
|
||||
let skip =
|
||||
preview_scroll.saturating_sub(ANSI_BEFORE_CONTEXT_SIZE) as usize;
|
||||
@ -137,14 +162,13 @@ fn build_ansi_text_paragraph(
|
||||
.scroll((preview_scroll, 0))
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn build_plain_text_paragraph(
|
||||
text: Vec<String>,
|
||||
preview_block: Block<'_>,
|
||||
fn build_plain_text_paragraph<'a>(
|
||||
text: &'a [String],
|
||||
preview_block: Block<'a>,
|
||||
target_line: Option<u16>,
|
||||
preview_scroll: u16,
|
||||
colorscheme: PreviewColorscheme,
|
||||
) -> Paragraph<'_> {
|
||||
) -> Paragraph<'a> {
|
||||
let mut lines = Vec::new();
|
||||
for (i, line) in text.iter().enumerate() {
|
||||
lines.push(Line::from(vec![
|
||||
@ -179,12 +203,11 @@ fn build_plain_text_paragraph(
|
||||
.scroll((preview_scroll, 0))
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn build_plain_text_wrapped_paragraph(
|
||||
text: String,
|
||||
preview_block: Block<'_>,
|
||||
fn build_plain_text_wrapped_paragraph<'a>(
|
||||
text: &'a str,
|
||||
preview_block: Block<'a>,
|
||||
colorscheme: PreviewColorscheme,
|
||||
) -> Paragraph<'_> {
|
||||
) -> Paragraph<'a> {
|
||||
let mut lines = Vec::new();
|
||||
for line in text.lines() {
|
||||
lines.push(Line::styled(
|
||||
@ -198,22 +221,24 @@ fn build_plain_text_wrapped_paragraph(
|
||||
.wrap(Wrap { trim: true })
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn build_syntect_highlighted_paragraph(
|
||||
highlighted_lines: Vec<Vec<(syntect::highlighting::Style, String)>>,
|
||||
preview_block: Block,
|
||||
fn build_syntect_highlighted_paragraph<'a>(
|
||||
highlighted_lines: &'a [Vec<(syntect::highlighting::Style, String)>],
|
||||
preview_block: Block<'a>,
|
||||
target_line: Option<u16>,
|
||||
preview_scroll: u16,
|
||||
colorscheme: PreviewColorscheme,
|
||||
) -> Paragraph {
|
||||
height: u16,
|
||||
) -> Paragraph<'a> {
|
||||
compute_paragraph_from_highlighted_lines(
|
||||
&highlighted_lines,
|
||||
highlighted_lines,
|
||||
target_line.map(|l| l as usize),
|
||||
preview_scroll,
|
||||
colorscheme,
|
||||
height,
|
||||
)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.scroll((preview_scroll, 0))
|
||||
//.scroll((preview_scroll, 0))
|
||||
}
|
||||
|
||||
pub fn build_meta_preview_paragraph<'a>(
|
||||
@ -325,109 +350,6 @@ fn draw_content_outer_block(
|
||||
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> {
|
||||
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(
|
||||
highlighted_lines: &[Vec<(syntect::highlighting::Style, String)>],
|
||||
line_specifier: Option<usize>,
|
||||
preview_scroll: u16,
|
||||
colorscheme: PreviewColorscheme,
|
||||
height: u16,
|
||||
) -> Paragraph<'static> {
|
||||
let preview_lines: Vec<Line> = highlighted_lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(preview_scroll.saturating_sub(1).into())
|
||||
.take(height.into())
|
||||
.map(|(i, l)| {
|
||||
let line_number =
|
||||
build_line_number_span(i + 1).style(Style::default().fg(
|
||||
@ -501,11 +427,3 @@ fn convert_syn_color_to_ratatui_color(
|
||||
) -> Color {
|
||||
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::screen::colors::{Colorscheme, GeneralColorscheme};
|
||||
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::television::Mode;
|
||||
use crate::utils::input::Input;
|
||||
|
||||
use anyhow::Result;
|
||||
@ -25,7 +24,6 @@ pub fn draw_remote_control(
|
||||
use_nerd_font_icons: bool,
|
||||
picker_state: &mut ListState,
|
||||
input_state: &mut Input,
|
||||
icon_color_cache: &mut FxHashMap<String, Color>,
|
||||
mode: &Mode,
|
||||
colorscheme: &Colorscheme,
|
||||
) -> Result<()> {
|
||||
@ -46,7 +44,6 @@ pub fn draw_remote_control(
|
||||
entries,
|
||||
use_nerd_font_icons,
|
||||
picker_state,
|
||||
icon_color_cache,
|
||||
colorscheme,
|
||||
);
|
||||
draw_rc_input(f, layout[1], input_state, colorscheme)?;
|
||||
@ -65,7 +62,6 @@ fn draw_rc_channels(
|
||||
entries: &[Entry],
|
||||
use_nerd_font_icons: bool,
|
||||
picker_state: &mut ListState,
|
||||
icon_color_cache: &mut FxHashMap<String, Color>,
|
||||
colorscheme: &Colorscheme,
|
||||
) {
|
||||
let rc_block = Block::default()
|
||||
@ -84,7 +80,6 @@ fn draw_rc_channels(
|
||||
None,
|
||||
ListDirection::TopToBottom,
|
||||
use_nerd_font_icons,
|
||||
icon_color_cache,
|
||||
&colorscheme.results,
|
||||
);
|
||||
|
||||
|
@ -13,7 +13,7 @@ use ratatui::widgets::{
|
||||
Block, BorderType, Borders, List, ListDirection, ListState, Padding,
|
||||
};
|
||||
use ratatui::Frame;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
const POINTER_SYMBOL: &str = "> ";
|
||||
@ -26,7 +26,6 @@ pub fn build_results_list<'a, 'b>(
|
||||
selected_entries: Option<&FxHashSet<Entry>>,
|
||||
list_direction: ListDirection,
|
||||
use_icons: bool,
|
||||
icon_color_cache: &mut FxHashMap<String, Color>,
|
||||
colorscheme: &ResultsColorscheme,
|
||||
) -> List<'a>
|
||||
where
|
||||
@ -50,20 +49,10 @@ where
|
||||
// optional icon
|
||||
if let Some(icon) = entry.icon.as_ref() {
|
||||
if use_icons {
|
||||
if let Some(icon_color) = icon_color_cache.get(icon.color) {
|
||||
spans.push(Span::styled(
|
||||
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(" "));
|
||||
}
|
||||
@ -160,7 +149,6 @@ pub fn draw_results_list(
|
||||
relative_picker_state: &mut ListState,
|
||||
input_bar_position: InputPosition,
|
||||
use_nerd_font_icons: bool,
|
||||
icon_color_cache: &mut FxHashMap<String, Color>,
|
||||
colorscheme: &Colorscheme,
|
||||
help_keybinding: &str,
|
||||
preview_keybinding: &str,
|
||||
@ -191,7 +179,6 @@ pub fn draw_results_list(
|
||||
InputPosition::Top => ListDirection::TopToBottom,
|
||||
},
|
||||
use_nerd_font_icons,
|
||||
icon_color_cache,
|
||||
&colorscheme.results,
|
||||
);
|
||||
|
||||
|
@ -1,23 +1,29 @@
|
||||
use ratatui::{
|
||||
buffer::Buffer, layout::Rect, style::Style, widgets::StatefulWidget,
|
||||
};
|
||||
use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget};
|
||||
|
||||
const FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
|
||||
/// A spinner widget.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
|
||||
pub struct Spinner {
|
||||
frames: &'static [&'static str],
|
||||
state: SpinnerState,
|
||||
}
|
||||
|
||||
impl Spinner {
|
||||
pub fn new(frames: &'static [&str]) -> Spinner {
|
||||
Spinner { frames }
|
||||
Spinner {
|
||||
frames,
|
||||
state: SpinnerState::new(frames.len()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn frame(&self, index: usize) -> &str {
|
||||
self.frames[index]
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
self.state.tick();
|
||||
}
|
||||
}
|
||||
|
||||
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 current_frame: usize,
|
||||
total_frames: usize,
|
||||
@ -51,31 +57,24 @@ impl From<&Spinner> for SpinnerState {
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulWidget for Spinner {
|
||||
type State = SpinnerState;
|
||||
|
||||
/// Renders the spinner in the given area.
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
impl Widget for Spinner {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(
|
||||
area.left(),
|
||||
area.top(),
|
||||
self.frame(state.current_frame),
|
||||
self.frame(self.state.current_frame),
|
||||
Style::default(),
|
||||
);
|
||||
state.tick();
|
||||
}
|
||||
}
|
||||
impl StatefulWidget for &Spinner {
|
||||
type State = SpinnerState;
|
||||
|
||||
/// Renders the spinner in the given area.
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
impl Widget for &Spinner {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(
|
||||
area.left(),
|
||||
area.top(),
|
||||
self.frame(state.current_frame),
|
||||
self.frame(self.state.current_frame),
|
||||
Style::default(),
|
||||
);
|
||||
state.tick();
|
||||
}
|
||||
}
|
||||
|
@ -1,63 +1,68 @@
|
||||
use crate::action::Action;
|
||||
use crate::cable::load_cable_channels;
|
||||
use crate::channels::entry::{Entry, PreviewType, ENTRY_PLACEHOLDER};
|
||||
use crate::channels::{
|
||||
remote_control::{load_builtin_channels, RemoteControl},
|
||||
OnAir, TelevisionChannel, UnitChannel,
|
||||
};
|
||||
use crate::config::{Config, KeyBindings, Theme};
|
||||
use crate::draw::{ChannelState, Ctx, TvState};
|
||||
use crate::input::convert_action_to_input_request;
|
||||
use crate::picker::Picker;
|
||||
use crate::preview::Previewer;
|
||||
use crate::screen::cache::RenderedPreviewCache;
|
||||
use crate::preview::{PreviewState, Previewer};
|
||||
use crate::screen::colors::Colorscheme;
|
||||
use crate::screen::help::draw_help_bar;
|
||||
use crate::screen::input::draw_input_box;
|
||||
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::keybindings::{DisplayableAction, DisplayableKeybindings};
|
||||
use crate::screen::layout::InputPosition;
|
||||
use crate::screen::spinner::{Spinner, SpinnerState};
|
||||
use crate::utils::metadata::AppMetadata;
|
||||
use crate::utils::strings::EMPTY_STRING;
|
||||
use crate::{cable::load_cable_channels, keymap::Keymap};
|
||||
use anyhow::Result;
|
||||
use copypasta::{ClipboardContext, ClipboardProvider};
|
||||
use ratatui::{layout::Rect, style::Color, Frame};
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio::sync::mpsc::{Receiver, Sender, UnboundedSender};
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
|
||||
pub enum Mode {
|
||||
Channel,
|
||||
RemoteControl,
|
||||
SendToChannel,
|
||||
}
|
||||
|
||||
pub struct Television {
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
action_tx: UnboundedSender<Action>,
|
||||
pub config: Config,
|
||||
pub keymap: Keymap,
|
||||
pub(crate) channel: TelevisionChannel,
|
||||
pub(crate) remote_control: TelevisionChannel,
|
||||
pub channel: TelevisionChannel,
|
||||
pub remote_control: TelevisionChannel,
|
||||
pub mode: Mode,
|
||||
pub current_pattern: String,
|
||||
pub(crate) results_picker: Picker,
|
||||
pub(crate) rc_picker: Picker,
|
||||
results_area_height: u32,
|
||||
pub results_picker: Picker,
|
||||
pub rc_picker: Picker,
|
||||
results_area_height: u16,
|
||||
pub previewer: Previewer,
|
||||
pub preview_scroll: Option<u16>,
|
||||
pub preview_pane_height: u16,
|
||||
current_preview_total_lines: u16,
|
||||
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 preview_state: PreviewState,
|
||||
pub spinner: Spinner,
|
||||
pub spinner_state: SpinnerState,
|
||||
pub app_metadata: AppMetadata,
|
||||
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 {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
action_tx: UnboundedSender<Action>,
|
||||
mut channel: TelevisionChannel,
|
||||
config: Config,
|
||||
input: Option<String>,
|
||||
@ -67,7 +72,6 @@ impl Television {
|
||||
results_picker = results_picker.inverted();
|
||||
}
|
||||
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 builtin_channels = load_builtin_channels(Some(
|
||||
&cable_channels.keys().collect::<Vec<_>>(),
|
||||
@ -84,10 +88,12 @@ impl Television {
|
||||
|
||||
channel.find(&input.unwrap_or(EMPTY_STRING.to_string()));
|
||||
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 {
|
||||
action_tx: None,
|
||||
action_tx,
|
||||
config,
|
||||
keymap,
|
||||
channel,
|
||||
remote_control: TelevisionChannel::RemoteControl(
|
||||
RemoteControl::new(builtin_channels, Some(cable_channels)),
|
||||
@ -98,17 +104,14 @@ impl Television {
|
||||
rc_picker: Picker::default(),
|
||||
results_area_height: 0,
|
||||
previewer,
|
||||
preview_scroll: None,
|
||||
preview_pane_height: 0,
|
||||
current_preview_total_lines: 0,
|
||||
icon_color_cache: FxHashMap::default(),
|
||||
rendered_preview_cache: Arc::new(Mutex::new(
|
||||
RenderedPreviewCache::default(),
|
||||
)),
|
||||
preview_state: PreviewState::default(),
|
||||
spinner,
|
||||
spinner_state: SpinnerState::from(&spinner),
|
||||
app_metadata,
|
||||
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 {
|
||||
UnitChannel::from(&self.channel)
|
||||
}
|
||||
|
||||
pub fn change_channel(&mut self, channel: TelevisionChannel) {
|
||||
self.reset_preview_scroll();
|
||||
self.preview_state.reset();
|
||||
self.reset_picker_selection();
|
||||
self.reset_picker_input();
|
||||
self.current_pattern = EMPTY_STRING.to_string();
|
||||
@ -147,7 +179,7 @@ impl Television {
|
||||
}
|
||||
|
||||
#[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) {
|
||||
Mode::Channel => {
|
||||
if let Some(i) = self.results_picker.selected() {
|
||||
@ -168,7 +200,7 @@ impl Television {
|
||||
|
||||
#[must_use]
|
||||
pub fn get_selected_entries(
|
||||
&mut self,
|
||||
&self,
|
||||
mode: Option<Mode>,
|
||||
) -> Option<FxHashSet<Entry>> {
|
||||
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) {
|
||||
match self.mode {
|
||||
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) {
|
||||
if let Some(scroll) = self.preview_scroll {
|
||||
self.preview_scroll = Some(scroll.saturating_sub(offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
const RENDER_EVERY_N_TICKS: u64 = 10;
|
||||
|
||||
impl Television {
|
||||
fn should_render(&self, action: &Action) -> bool {
|
||||
matches!(
|
||||
self.ticks == RENDER_EVERY_N_TICKS
|
||||
|| matches!(
|
||||
action,
|
||||
Action::AddInputChar(_)
|
||||
| Action::DeletePrevChar
|
||||
@ -307,36 +303,82 @@ impl Television {
|
||||
| Action::ToggleHelp
|
||||
| Action::TogglePreview
|
||||
| Action::CopyEntryToClipboard
|
||||
) || self.channel.running()
|
||||
)
|
||||
|| self.channel.running()
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
/// Update the state of the component based on a received action.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `action` - An action that may modify the state of the television.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<Option<Action>>` - An action to be processed or none.
|
||||
pub async fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
// handle input actions
|
||||
Action::AddInputChar(_)
|
||||
| Action::DeletePrevChar
|
||||
| Action::DeletePrevWord
|
||||
| Action::DeleteNextChar
|
||||
| Action::GoToInputEnd
|
||||
| Action::GoToInputStart
|
||||
| Action::GoToNextChar
|
||||
| Action::GoToPrevChar => {
|
||||
pub fn update_preview_state(
|
||||
&mut self,
|
||||
selected_entry: &Entry,
|
||||
) -> Result<()> {
|
||||
if self.config.ui.show_preview_panel
|
||||
&& !matches!(selected_entry.preview_type, PreviewType::None)
|
||||
{
|
||||
// preview content
|
||||
if let Some(preview) = self.previewer.preview(selected_entry) {
|
||||
if self.preview_state.preview.title != preview.title {
|
||||
self.preview_state.update(
|
||||
preview,
|
||||
// scroll to center the selected entry
|
||||
selected_entry
|
||||
.line_number
|
||||
.unwrap_or(0)
|
||||
.saturating_sub(
|
||||
(self.results_area_height / 2).into(),
|
||||
)
|
||||
.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 {
|
||||
Mode::Channel => &mut self.results_picker.input,
|
||||
Mode::RemoteControl | Mode::SendToChannel => {
|
||||
&mut self.rc_picker.input
|
||||
}
|
||||
};
|
||||
input
|
||||
.handle(convert_action_to_input_request(&action).unwrap());
|
||||
input.handle(convert_action_to_input_request(action).unwrap());
|
||||
match action {
|
||||
Action::AddInputChar(_)
|
||||
| Action::DeletePrevChar
|
||||
@ -347,33 +389,15 @@ impl Television {
|
||||
self.current_pattern.clone_from(&new_pattern);
|
||||
self.find(&new_pattern);
|
||||
self.reset_picker_selection();
|
||||
self.reset_preview_scroll();
|
||||
self.preview_state.reset();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Action::SelectNextEntry => {
|
||||
self.reset_preview_scroll();
|
||||
self.select_next_entry(1);
|
||||
}
|
||||
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 {
|
||||
|
||||
pub fn handle_toggle_rc(&mut self) {
|
||||
match self.mode {
|
||||
Mode::Channel => {
|
||||
self.mode = Mode::RemoteControl;
|
||||
self.init_remote_control();
|
||||
@ -387,8 +411,27 @@ impl Television {
|
||||
self.mode = Mode::Channel;
|
||||
}
|
||||
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 let Some(entry) = self.get_selected_entry(None) {
|
||||
self.channel.toggle_selection(&entry);
|
||||
@ -400,21 +443,16 @@ impl Television {
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::ConfirmSelection => {
|
||||
|
||||
pub fn handle_confirm_selection(&mut self) -> Result<()> {
|
||||
match self.mode {
|
||||
Mode::Channel => {
|
||||
self.action_tx
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send(Action::SelectAndExit)?;
|
||||
self.action_tx.send(Action::SelectAndExit)?;
|
||||
}
|
||||
Mode::RemoteControl => {
|
||||
if let Some(entry) =
|
||||
self.get_selected_entry(Some(Mode::RemoteControl))
|
||||
{
|
||||
let new_channel = self
|
||||
.remote_control
|
||||
.zap(entry.name.as_str())?;
|
||||
if let Some(entry) = self.get_selected_entry(None) {
|
||||
let new_channel =
|
||||
self.remote_control.zap(entry.name.as_str())?;
|
||||
// this resets the RC picker
|
||||
self.reset_picker_selection();
|
||||
self.reset_picker_input();
|
||||
@ -424,12 +462,10 @@ impl Television {
|
||||
}
|
||||
}
|
||||
Mode::SendToChannel => {
|
||||
if let Some(entry) =
|
||||
self.get_selected_entry(Some(Mode::RemoteControl))
|
||||
{
|
||||
let new_channel = self.channel.transition_to(
|
||||
entry.name.as_str().try_into().unwrap(),
|
||||
);
|
||||
if let Some(entry) = self.get_selected_entry(None) {
|
||||
let new_channel = self
|
||||
.channel
|
||||
.transition_to(entry.name.as_str().try_into()?);
|
||||
self.reset_picker_selection();
|
||||
self.reset_picker_input();
|
||||
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 let Some(entries) = self.get_selected_entries(None) {
|
||||
let mut ctx = ClipboardContext::new().unwrap();
|
||||
@ -454,20 +492,59 @@ impl Television {
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::ToggleSendToChannel => match self.mode {
|
||||
Mode::Channel | Mode::RemoteControl => {
|
||||
self.mode = Mode::SendToChannel;
|
||||
self.remote_control = TelevisionChannel::RemoteControl(
|
||||
RemoteControl::with_transitions_from(&self.channel),
|
||||
);
|
||||
|
||||
pub fn handle_action(&mut self, action: &Action) -> Result<()> {
|
||||
// handle actions
|
||||
match action {
|
||||
Action::AddInputChar(_)
|
||||
| Action::DeletePrevChar
|
||||
| Action::DeletePrevWord
|
||||
| Action::DeleteNextChar
|
||||
| Action::GoToInputEnd
|
||||
| Action::GoToInputStart
|
||||
| Action::GoToNextChar
|
||||
| Action::GoToPrevChar => {
|
||||
self.handle_input_action(action);
|
||||
}
|
||||
Mode::SendToChannel => {
|
||||
self.reset_picker_input();
|
||||
self.remote_control.find(EMPTY_STRING);
|
||||
self.reset_picker_selection();
|
||||
self.mode = Mode::Channel;
|
||||
Action::SelectNextEntry => {
|
||||
self.preview_state.reset();
|
||||
self.select_next_entry(1);
|
||||
}
|
||||
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 => {
|
||||
self.config.ui.show_help_bar = !self.config.ui.show_help_bar;
|
||||
}
|
||||
@ -477,180 +554,45 @@ impl Television {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(if self.should_render(&action) {
|
||||
Some(Action::Render)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render the television on the screen.
|
||||
#[allow(clippy::unused_async)]
|
||||
/// Update the television state based on the action provided.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `f` - A frame used for rendering.
|
||||
/// * `area` - The area in which the television should be drawn.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<()>` - An Ok result or an error.
|
||||
pub fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
|
||||
/// This function may return an Action that'll be processed by the parent `App`.
|
||||
pub fn update(&mut self, action: &Action) -> Result<Option<Action>> {
|
||||
if let Ok(Message::ResultListHeightChanged(height)) =
|
||||
self.inner_rx.try_recv()
|
||||
{
|
||||
self.results_area_height = height;
|
||||
self.action_tx.send(Action::Render)?;
|
||||
}
|
||||
|
||||
let selected_entry = self
|
||||
.get_selected_entry(Some(Mode::Channel))
|
||||
.unwrap_or(ENTRY_PLACEHOLDER);
|
||||
|
||||
let layout = Layout::build(
|
||||
&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,
|
||||
);
|
||||
self.update_preview_state(&selected_entry)?;
|
||||
|
||||
// help bar (metadata, keymaps, logo)
|
||||
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.update_results_picker_state();
|
||||
|
||||
self.results_area_height =
|
||||
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,
|
||||
};
|
||||
self.update_rc_picker_state();
|
||||
|
||||
// results list
|
||||
let result_count = self.channel.result_count();
|
||||
if result_count > 0 && self.results_picker.selected().is_none() {
|
||||
self.results_picker.select(Some(0));
|
||||
self.results_picker.relative_select(Some(0));
|
||||
self.handle_action(action)?;
|
||||
|
||||
self.ticks += 1;
|
||||
|
||||
Ok(if self.should_render(action) {
|
||||
if self.channel.running() {
|
||||
self.spinner.tick();
|
||||
}
|
||||
let entries = self.channel.results(
|
||||
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(),
|
||||
)?;
|
||||
self.ticks = 0;
|
||||
|
||||
// input box
|
||||
draw_input_box(
|
||||
f,
|
||||
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));
|
||||
}
|
||||
Some(Action::Render)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,14 +5,15 @@ use std::{
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
cursor, execute,
|
||||
cursor,
|
||||
event::DisableMouseCapture,
|
||||
execute,
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, is_raw_mode_enabled,
|
||||
EnterAlternateScreen, LeaveAlternateScreen,
|
||||
},
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, layout::Size};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::debug;
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -20,8 +21,6 @@ pub struct Tui<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
pub task: JoinHandle<()>,
|
||||
pub frame_rate: f64,
|
||||
pub terminal: ratatui::Terminal<CrosstermBackend<W>>,
|
||||
}
|
||||
|
||||
@ -32,17 +31,10 @@ where
|
||||
{
|
||||
pub fn new(writer: W) -> Result<Self> {
|
||||
Ok(Self {
|
||||
task: tokio::spawn(async {}),
|
||||
frame_rate: 60.0,
|
||||
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> {
|
||||
Ok(self.terminal.size()?)
|
||||
}
|
||||
@ -52,7 +44,7 @@ where
|
||||
let mut buffered_stderr = LineWriter::new(stderr());
|
||||
execute!(buffered_stderr, EnterAlternateScreen)?;
|
||||
self.terminal.clear()?;
|
||||
execute!(buffered_stderr, cursor::Hide)?;
|
||||
execute!(buffered_stderr, DisableMouseCapture)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
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.
|
||||
///
|
||||
@ -81,7 +81,7 @@ where
|
||||
pub fn push(&mut self, item: T) -> Option<T> {
|
||||
// If the key is already in the buffer, do nothing
|
||||
if self.contains(&item) {
|
||||
debug!("Key already in ring buffer: {:?}", item);
|
||||
trace!("Key already in ring buffer: {:?}", item);
|
||||
return None;
|
||||
}
|
||||
let mut popped_key = None;
|
||||
|
@ -30,7 +30,7 @@ pub struct StateChanged {
|
||||
pub type InputResponse = Option<StateChanged>;
|
||||
|
||||
/// An input buffer with cursor support.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Hash)]
|
||||
pub struct Input {
|
||||
value: String,
|
||||
cursor: usize,
|
||||
|
@ -1,3 +1,4 @@
|
||||
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
|
||||
pub struct AppMetadata {
|
||||
pub version: 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 lines: Vec<Vec<(Style, String)>>,
|
||||
//pub state: Option<HighlightingState>,
|
||||
|
Loading…
x
Reference in New Issue
Block a user