From 3a5b5ec0cca14b8f0c5cddec88e038f90b8ef384 Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier <47638216+alexpasmantier@users.noreply.github.com> Date: Wed, 19 Mar 2025 18:55:47 +0100 Subject: [PATCH] refactor(startup): improve overall startup time and remove first frames artifacts (#408) Fixes #406 --- television-derive/src/lib.rs | 10 +++++++ television/channels/alias.rs | 4 +++ television/channels/cable.rs | 4 +++ television/channels/dirs.rs | 4 +++ television/channels/env.rs | 4 +++ television/channels/files.rs | 4 +++ television/channels/git_repos.rs | 4 +++ television/channels/mod.rs | 3 ++ television/channels/remote_control.rs | 4 +++ television/channels/stdin.rs | 4 +++ television/channels/text.rs | 4 +++ television/draw.rs | 40 ++++++++++++++++++++------- television/preview/mod.rs | 3 ++ television/render.rs | 17 ++++++++++++ television/television.rs | 22 ++++++++++----- television/utils/metadata.rs | 1 + 16 files changed, 115 insertions(+), 17 deletions(-) diff --git a/television-derive/src/lib.rs b/television-derive/src/lib.rs index 4d1b7c3..5c008ab 100644 --- a/television-derive/src/lib.rs +++ b/television-derive/src/lib.rs @@ -272,6 +272,16 @@ fn impl_tv_channel(ast: &syn::DeriveInput) -> TokenStream { )* } } + + fn supports_preview(&self) -> bool { + match self { + #( + #enum_name::#variant_names(ref channel) => { + channel.supports_preview() + } + )* + } + } } }; diff --git a/television/channels/alias.rs b/television/channels/alias.rs index 19a8f5e..9a56877 100644 --- a/television/channels/alias.rs +++ b/television/channels/alias.rs @@ -146,6 +146,10 @@ impl OnAir for Channel { } fn shutdown(&self) {} + + fn supports_preview(&self) -> bool { + true + } } #[allow(clippy::unused_async)] diff --git a/television/channels/cable.rs b/television/channels/cable.rs index 519193b..8e1bac8 100644 --- a/television/channels/cable.rs +++ b/television/channels/cable.rs @@ -213,6 +213,10 @@ impl OnAir for Channel { } fn shutdown(&self) {} + + fn supports_preview(&self) -> bool { + self.preview_kind != PreviewKind::None + } } #[derive(Clone, Debug, serde::Deserialize, PartialEq)] diff --git a/television/channels/dirs.rs b/television/channels/dirs.rs index 16bd4ff..f498b4c 100644 --- a/television/channels/dirs.rs +++ b/television/channels/dirs.rs @@ -142,6 +142,10 @@ impl OnAir for Channel { fn shutdown(&self) { self.crawl_handle.abort(); } + + fn supports_preview(&self) -> bool { + true + } } #[allow(clippy::unused_async)] diff --git a/television/channels/env.rs b/television/channels/env.rs index 737c468..f401e1d 100644 --- a/television/channels/env.rs +++ b/television/channels/env.rs @@ -131,4 +131,8 @@ impl OnAir for Channel { } fn shutdown(&self) {} + + fn supports_preview(&self) -> bool { + true + } } diff --git a/television/channels/files.rs b/television/channels/files.rs index 6e7a2ff..22ddb9f 100644 --- a/television/channels/files.rs +++ b/television/channels/files.rs @@ -148,6 +148,10 @@ impl OnAir for Channel { fn shutdown(&self) { self.crawl_handle.abort(); } + + fn supports_preview(&self) -> bool { + true + } } #[allow(clippy::unused_async)] diff --git a/television/channels/git_repos.rs b/television/channels/git_repos.rs index f8cebf5..c7db1e3 100644 --- a/television/channels/git_repos.rs +++ b/television/channels/git_repos.rs @@ -113,6 +113,10 @@ impl OnAir for Channel { debug!("Shutting down git repos channel"); self.crawl_handle.abort(); } + + fn supports_preview(&self) -> bool { + true + } } fn get_ignored_paths() -> Vec { diff --git a/television/channels/mod.rs b/television/channels/mod.rs index 12b21a5..876f3ed 100644 --- a/television/channels/mod.rs +++ b/television/channels/mod.rs @@ -84,6 +84,9 @@ pub trait OnAir: Send { /// Turn off fn shutdown(&self); + + /// Whether this channel supports previewing entries. + fn supports_preview(&self) -> bool; } /// The available television channels. diff --git a/television/channels/remote_control.rs b/television/channels/remote_control.rs index f2530a2..7d1358b 100644 --- a/television/channels/remote_control.rs +++ b/television/channels/remote_control.rs @@ -181,4 +181,8 @@ impl OnAir for RemoteControl { } fn shutdown(&self) {} + + fn supports_preview(&self) -> bool { + false + } } diff --git a/television/channels/stdin.rs b/television/channels/stdin.rs index 94dabef..103d45b 100644 --- a/television/channels/stdin.rs +++ b/television/channels/stdin.rs @@ -120,4 +120,8 @@ impl OnAir for Channel { } fn shutdown(&self) {} + + fn supports_preview(&self) -> bool { + self.preview_type != PreviewType::None + } } diff --git a/television/channels/text.rs b/television/channels/text.rs index f5dbe87..b5c95f7 100644 --- a/television/channels/text.rs +++ b/television/channels/text.rs @@ -245,6 +245,10 @@ impl OnAir for Channel { fn shutdown(&self) { self.crawl_handle.abort(); } + + fn supports_preview(&self) -> bool { + true + } } /// The maximum file size we're willing to search in. diff --git a/television/draw.rs b/television/draw.rs index 82c88ed..0e24e73 100644 --- a/television/draw.rs +++ b/television/draw.rs @@ -6,7 +6,7 @@ use rustc_hash::FxHashSet; use crate::{ action::Action, - channels::entry::{Entry, PreviewType, ENTRY_PLACEHOLDER}, + channels::entry::Entry, config::Config, picker::Picker, preview::PreviewState, @@ -22,6 +22,9 @@ use crate::{ }; #[derive(Debug, Clone, PartialEq)] +/// The state of the current television channel. +/// +/// This struct is passed along to the UI thread as part of the `TvState` struct. pub struct ChannelState { pub current_channel_name: String, pub selected_entries: FxHashSet, @@ -57,6 +60,9 @@ impl Hash for ChannelState { } #[derive(Debug, Clone, PartialEq, Hash)] +/// The state of the main thread `Television` struct. +/// +/// This struct is passed along to the UI thread as part of the `Ctx` struct. pub struct TvState { pub mode: Mode, pub selected_entry: Option, @@ -91,6 +97,11 @@ impl TvState { } #[derive(Debug, Clone)] +/// A drawing context that holds the current state of the application. +/// +/// This is used as a message passing object between the main thread +/// and the UI thread and should contain all the information needed to +/// draw a frame. pub struct Ctx { pub tv_state: TvState, pub config: Config, @@ -152,15 +163,24 @@ impl Ord for Ctx { } } +/// Draw the current UI frame based on the given context. +/// +/// This function is responsible for drawing the entire UI frame based on the given context by +/// ultimately flushing buffers down to the underlying terminal. +/// +/// This function is executed by the UI thread whenever it receives a render message from the main +/// thread. +/// +/// It will draw the help bar, the results list, the input box, the preview content block, and the +/// remote control. +/// +/// # Returns +/// A `Result` containing the layout of the current frame if the drawing was successful. +/// This layout can then be sent back to the main thread to serve for tasks where having that +/// information can be useful or lead to optimizations. 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_preview = + ctx.config.ui.show_preview_panel && ctx.tv_state.preview_state.enabled; let show_remote = !matches!(ctx.tv_state.mode, Mode::Channel); let layout = @@ -219,7 +239,7 @@ pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result { &ctx.colorscheme, )?; - if show_preview { + if layout.preview_window.is_some() { draw_preview_content_block( f, layout.preview_window.unwrap(), diff --git a/television/preview/mod.rs b/television/preview/mod.rs index 1188c7b..949b377 100644 --- a/television/preview/mod.rs +++ b/television/preview/mod.rs @@ -109,6 +109,7 @@ impl Preview { #[derive(Debug, Default, Clone, PartialEq, Hash)] pub struct PreviewState { + pub enabled: bool, pub preview: Arc, pub scroll: u16, pub target_line: Option, @@ -118,11 +119,13 @@ const PREVIEW_MIN_SCROLL_LINES: u16 = 3; impl PreviewState { pub fn new( + enabled: bool, preview: Arc, scroll: u16, target_line: Option, ) -> Self { PreviewState { + enabled, preview, scroll, target_line, diff --git a/television/render.rs b/television/render.rs index fa6dd9f..f307bb9 100644 --- a/television/render.rs +++ b/television/render.rs @@ -37,6 +37,11 @@ impl IoStream { } #[derive(Default)] +/// The state of the UI after rendering. +/// +/// This struct is returned by the UI thread to the main thread after each rendering cycle. +/// It contains information that the main thread might be able to exploit to make certain +/// decisions and optimizations. pub struct UiState { pub layout: Layout, } @@ -47,6 +52,18 @@ impl UiState { } } +/// The main UI rendering task loop. +/// +/// This function is responsible for rendering the UI based on the rendering tasks it receives from +/// the main thread via `render_rx`. +/// +/// This has a handle to the main action queue `action_tx` (for things like self-triggering +/// subsequent rendering instructions) and the UI state queue `ui_state_tx` to send back the layout +/// of the UI after each rendering cycle to the main thread to help make decisions and +/// optimizations. +/// +/// When starting the rendering loop, a choice is made to either render to stdout or stderr based +/// on if the output is believed to be a TTY or not. pub async fn render( mut render_rx: mpsc::UnboundedReceiver, action_tx: mpsc::UnboundedSender, diff --git a/television/television.rs b/television/television.rs index d3b9c3e..9ad9eef 100644 --- a/television/television.rs +++ b/television/television.rs @@ -1,6 +1,6 @@ use crate::action::Action; use crate::cable::load_cable_channels; -use crate::channels::entry::{Entry, PreviewType, ENTRY_PLACEHOLDER}; +use crate::channels::entry::{Entry, ENTRY_PLACEHOLDER}; use crate::channels::{ remote_control::{load_builtin_channels, RemoteControl}, OnAir, TelevisionChannel, UnitChannel, @@ -9,7 +9,7 @@ use crate::config::{Config, Theme}; use crate::draw::{ChannelState, Ctx, TvState}; use crate::input::convert_action_to_input_request; use crate::picker::Picker; -use crate::preview::{PreviewState, Previewer}; +use crate::preview::{Preview, PreviewState, Previewer}; use crate::render::UiState; use crate::screen::colors::Colorscheme; use crate::screen::layout::InputPosition; @@ -21,6 +21,7 @@ use anyhow::Result; use rustc_hash::{FxBuildHasher, FxHashSet}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; +use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; #[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)] @@ -79,6 +80,13 @@ impl Television { channel.find(&input.unwrap_or(EMPTY_STRING.to_string())); let spinner = Spinner::default(); + let preview_state = PreviewState::new( + channel.supports_preview(), + Arc::new(Preview::default()), + 0, + None, + ); + Self { action_tx, config, @@ -91,7 +99,7 @@ impl Television { results_picker, rc_picker: Picker::default(), previewer, - preview_state: PreviewState::default(), + preview_state, spinner, spinner_state: SpinnerState::from(&spinner), app_metadata, @@ -261,11 +269,13 @@ impl Television { } } +const RENDER_FIRST_N_TICKS: u64 = 20; const RENDER_EVERY_N_TICKS: u64 = 10; impl Television { fn should_render(&self, action: &Action) -> bool { - self.ticks == RENDER_EVERY_N_TICKS + self.ticks < RENDER_FIRST_N_TICKS + || self.ticks % RENDER_EVERY_N_TICKS == 0 || matches!( action, Action::AddInputChar(_) @@ -300,8 +310,7 @@ impl Television { &mut self, selected_entry: &Entry, ) -> Result<()> { - if self.config.ui.show_preview_panel - && !matches!(selected_entry.preview_type, PreviewType::None) + if self.config.ui.show_preview_panel && self.channel.supports_preview() { // preview content if let Some(preview) = self @@ -591,7 +600,6 @@ impl Television { if self.channel.running() { self.spinner.tick(); } - self.ticks = 0; Some(Action::Render) } else { diff --git a/television/utils/metadata.rs b/television/utils/metadata.rs index 3afef5d..07fc535 100644 --- a/television/utils/metadata.rs +++ b/television/utils/metadata.rs @@ -1,4 +1,5 @@ #[derive(Debug, Clone, PartialEq, Hash, Eq)] +/// Global application metadata like version and current directory. pub struct AppMetadata { pub version: String, pub current_directory: String,