refactor(startup): improve overall startup time and remove first frames artifacts (#408)

Fixes #406
This commit is contained in:
Alexandre Pasmantier 2025-03-19 18:55:47 +01:00 committed by GitHub
parent 5ee891230c
commit 3a5b5ec0cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 115 additions and 17 deletions

View File

@ -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()
}
)*
}
}
}
};

View File

@ -146,6 +146,10 @@ impl OnAir for Channel {
}
fn shutdown(&self) {}
fn supports_preview(&self) -> bool {
true
}
}
#[allow(clippy::unused_async)]

View File

@ -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)]

View File

@ -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)]

View File

@ -131,4 +131,8 @@ impl OnAir for Channel {
}
fn shutdown(&self) {}
fn supports_preview(&self) -> bool {
true
}
}

View File

@ -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)]

View File

@ -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<PathBuf> {

View File

@ -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.

View File

@ -181,4 +181,8 @@ impl OnAir for RemoteControl {
}
fn shutdown(&self) {}
fn supports_preview(&self) -> bool {
false
}
}

View File

@ -120,4 +120,8 @@ impl OnAir for Channel {
}
fn shutdown(&self) {}
fn supports_preview(&self) -> bool {
self.preview_type != PreviewType::None
}
}

View File

@ -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.

View File

@ -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<Entry>,
@ -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<Entry>,
@ -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<Layout> {
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<Layout> {
&ctx.colorscheme,
)?;
if show_preview {
if layout.preview_window.is_some() {
draw_preview_content_block(
f,
layout.preview_window.unwrap(),

View File

@ -109,6 +109,7 @@ impl Preview {
#[derive(Debug, Default, Clone, PartialEq, Hash)]
pub struct PreviewState {
pub enabled: bool,
pub preview: Arc<Preview>,
pub scroll: u16,
pub target_line: Option<u16>,
@ -118,11 +119,13 @@ const PREVIEW_MIN_SCROLL_LINES: u16 = 3;
impl PreviewState {
pub fn new(
enabled: bool,
preview: Arc<Preview>,
scroll: u16,
target_line: Option<u16>,
) -> Self {
PreviewState {
enabled,
preview,
scroll,
target_line,

View File

@ -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<RenderingTask>,
action_tx: mpsc::UnboundedSender<Action>,

View File

@ -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 {

View File

@ -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,