From d8703ce0e66111464e754f8235404ab3b285024a Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier Date: Tue, 4 Mar 2025 22:17:44 +0100 Subject: [PATCH] refactor(ui): communicate ui state to tv using channels --- Cargo.lock | 1 - Cargo.toml | 1 - television/app.rs | 17 +- television/channels/cable.rs | 8 +- television/channels/dirs.rs | 4 +- television/channels/files.rs | 4 +- television/channels/git_repos.rs | 40 +- television/channels/text.rs | 4 +- television/config/mod.rs | 46 +- television/config/themes.rs | 8 +- television/config/themes/builtin.rs | 76 ++- television/draw.rs | 24 +- television/logging.rs | 6 +- television/preview/previewers/command.rs | 55 +- television/render.rs | 35 +- television/screen/layout.rs | 14 +- television/television.rs | 70 +-- television/utils/files.rs | 681 ++++++++++++----------- television/utils/strings.rs | 15 +- television/utils/syntax.rs | 11 +- television/utils/threads.rs | 3 +- 21 files changed, 591 insertions(+), 532 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d6b777..89d0adb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1696,7 +1696,6 @@ dependencies = [ "gag", "human-panic", "ignore", - "lazy_static", "nom", "nucleo", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index c31c645..fe3112a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ anyhow = "1.0" base64 = "0.22.1" directories = "6.0" devicons = "0.6" -lazy_static = "1.5" tokio = { version = "1.43", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/television/app.rs b/television/app.rs index 65ae958..f1a7c16 100644 --- a/television/app.rs +++ b/television/app.rs @@ -8,6 +8,7 @@ use crate::channels::entry::Entry; use crate::channels::TelevisionChannel; use crate::config::{parse_key, Config}; use crate::keymap::Keymap; +use crate::render::UiState; use crate::television::{Mode, Television}; use crate::{ action::Action, @@ -37,6 +38,9 @@ pub struct App { event_abort_tx: mpsc::UnboundedSender<()>, /// A sender channel for rendering tasks. render_tx: mpsc::UnboundedSender, + /// A channel that listens to UI updates. + ui_state_rx: mpsc::UnboundedReceiver, + ui_state_tx: mpsc::UnboundedSender, } /// The outcome of an action. @@ -104,6 +108,7 @@ impl App { .collect(), )?; debug!("{:?}", keymap); + let (ui_state_tx, ui_state_rx) = mpsc::unbounded_channel(); let television = Television::new(action_tx.clone(), channel, config, input); @@ -118,6 +123,8 @@ impl App { event_rx, event_abort_tx, render_tx, + ui_state_rx, + ui_state_tx, }) } @@ -145,9 +152,10 @@ impl App { debug!("Starting rendering loop"); let (render_tx, render_rx) = mpsc::unbounded_channel(); self.render_tx = render_tx.clone(); + let ui_state_tx = self.ui_state_tx.clone(); let action_tx_r = self.action_tx.clone(); let rendering_task = tokio::spawn(async move { - render(render_rx, action_tx_r, is_output_tty).await + render(render_rx, action_tx_r, ui_state_tx, is_output_tty).await }); self.action_tx.send(Action::Render)?; @@ -298,9 +306,14 @@ impl App { self.render_tx.send(RenderingTask::Resize(w, h))?; } Action::Render => { + // forward to the rendering task self.render_tx.send(RenderingTask::Render( - self.television.dump_context(), + Box::new(self.television.dump_context()), ))?; + // update the television UI state with the previous frame + if let Ok(ui_state) = self.ui_state_rx.try_recv() { + self.television.update_ui_state(ui_state); + } } _ => {} } diff --git a/television/channels/cable.rs b/television/channels/cable.rs index 14c7162..4546c9d 100644 --- a/television/channels/cable.rs +++ b/television/channels/cable.rs @@ -9,7 +9,6 @@ use std::io::{BufRead, BufReader}; use std::process::Stdio; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use rustc_hash::{FxBuildHasher, FxHashSet}; use tracing::debug; @@ -64,13 +63,10 @@ impl From for Channel { } } -lazy_static! { - static ref BUILTIN_PREVIEW_RE: Regex = Regex::new(r"^:(\w+):$").unwrap(); -} - fn parse_preview_kind(command: &PreviewCommand) -> Result { debug!("Parsing preview kind for command: {:?}", command); - if let Some(captures) = BUILTIN_PREVIEW_RE.captures(&command.command) { + let re = Regex::new(r"^\:(\w+)\:$").unwrap(); + if let Some(captures) = re.captures(&command.command) { let preview_type = PreviewType::try_from(&captures[1])?; Ok(PreviewKind::Builtin(preview_type)) } else { diff --git a/television/channels/dirs.rs b/television/channels/dirs.rs index 26b62dd..16bd4ff 100644 --- a/television/channels/dirs.rs +++ b/television/channels/dirs.rs @@ -1,7 +1,7 @@ use crate::channels::entry::{Entry, PreviewCommand, PreviewType}; use crate::channels::{OnAir, TelevisionChannel}; use crate::matcher::{config::Config, injector::Injector, Matcher}; -use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; +use crate::utils::files::{get_default_num_threads, walk_builder}; use devicons::FileIcon; use rustc_hash::{FxBuildHasher, FxHashSet}; use std::collections::HashSet; @@ -151,7 +151,7 @@ async fn load_dirs(paths: Vec, injector: Injector) { } let current_dir = std::env::current_dir().unwrap(); let mut builder = - walk_builder(&paths[0], *DEFAULT_NUM_THREADS, None, None); + walk_builder(&paths[0], get_default_num_threads(), None, None); paths[1..].iter().for_each(|path| { builder.add(path); }); diff --git a/television/channels/files.rs b/television/channels/files.rs index ef09b07..6e7a2ff 100644 --- a/television/channels/files.rs +++ b/television/channels/files.rs @@ -1,7 +1,7 @@ use crate::channels::entry::{Entry, PreviewType}; use crate::channels::{OnAir, TelevisionChannel}; use crate::matcher::{config::Config, injector::Injector, Matcher}; -use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; +use crate::utils::files::{get_default_num_threads, walk_builder}; use devicons::FileIcon; use rustc_hash::{FxBuildHasher, FxHashSet}; use std::collections::HashSet; @@ -157,7 +157,7 @@ async fn load_files(paths: Vec, injector: Injector) { } let current_dir = std::env::current_dir().unwrap(); let mut builder = - walk_builder(&paths[0], *DEFAULT_NUM_THREADS, None, None); + walk_builder(&paths[0], get_default_num_threads(), None, None); paths[1..].iter().for_each(|path| { builder.add(path); }); diff --git a/television/channels/git_repos.rs b/television/channels/git_repos.rs index fec62f0..f8cebf5 100644 --- a/television/channels/git_repos.rs +++ b/television/channels/git_repos.rs @@ -1,7 +1,6 @@ use devicons::FileIcon; use directories::BaseDirs; use ignore::overrides::OverrideBuilder; -use lazy_static::lazy_static; use rustc_hash::{FxBuildHasher, FxHashSet}; use std::collections::HashSet; use std::path::PathBuf; @@ -11,13 +10,14 @@ use tracing::debug; use crate::channels::entry::{Entry, PreviewCommand, PreviewType}; use crate::channels::OnAir; use crate::matcher::{config::Config, injector::Injector, Matcher}; -use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; +use crate::utils::files::{get_default_num_threads, walk_builder}; pub struct Channel { matcher: Matcher, icon: FileIcon, crawl_handle: JoinHandle<()>, selected_entries: FxHashSet, + preview_command: PreviewCommand, } impl Channel { @@ -28,11 +28,20 @@ impl Channel { base_dirs.home_dir().to_path_buf(), matcher.injector(), )); + + let preview_command = PreviewCommand { + command: String::from( + "cd {} && git log -n 200 --pretty=medium --all --graph --color", + ), + delimiter: ":".to_string(), + }; + Channel { matcher, icon: FileIcon::from("git"), crawl_handle, selected_entries: HashSet::with_hasher(FxBuildHasher), + preview_command, } } } @@ -43,15 +52,6 @@ impl Default for Channel { } } -lazy_static! { - static ref PREVIEW_COMMAND: PreviewCommand = PreviewCommand { - command: String::from( - "cd {} && git log -n 200 --pretty=medium --all --graph --color", - ), - delimiter: ":".to_string(), - }; -} - impl OnAir for Channel { fn find(&mut self, pattern: &str) { self.matcher.find(pattern); @@ -64,9 +64,12 @@ impl OnAir for Channel { .into_iter() .map(|item| { let path = item.matched_string; - Entry::new(path, PreviewType::Command(PREVIEW_COMMAND.clone())) - .with_name_match_ranges(&item.match_indices) - .with_icon(self.icon) + Entry::new( + path, + PreviewType::Command(self.preview_command.clone()), + ) + .with_name_match_ranges(&item.match_indices) + .with_icon(self.icon) }) .collect() } @@ -74,8 +77,11 @@ impl OnAir for Channel { fn get_result(&self, index: u32) -> Option { self.matcher.get_result(index).map(|item| { let path = item.matched_string; - Entry::new(path, PreviewType::Command(PREVIEW_COMMAND.clone())) - .with_icon(self.icon) + Entry::new( + path, + PreviewType::Command(self.preview_command.clone()), + ) + .with_icon(self.icon) }) } @@ -162,7 +168,7 @@ async fn crawl_for_repos(starting_point: PathBuf, injector: Injector) { walker_overrides_builder.add(".git").unwrap(); let walker = walk_builder( &starting_point, - *DEFAULT_NUM_THREADS, + get_default_num_threads(), Some(walker_overrides_builder.build().unwrap()), Some(get_ignored_paths()), ) diff --git a/television/channels/text.rs b/television/channels/text.rs index 97024df..f5dbe87 100644 --- a/television/channels/text.rs +++ b/television/channels/text.rs @@ -1,7 +1,7 @@ use super::{OnAir, TelevisionChannel}; use crate::channels::entry::{Entry, PreviewType}; use crate::matcher::{config::Config, injector::Injector, Matcher}; -use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; +use crate::utils::files::{get_default_num_threads, walk_builder}; use crate::utils::strings::{ proportion_of_printable_ascii_characters, PRINTABLE_ASCII_THRESHOLD, }; @@ -277,7 +277,7 @@ async fn crawl_for_candidates( } let current_dir = std::env::current_dir().unwrap(); let mut walker = - walk_builder(&directories[0], *DEFAULT_NUM_THREADS, None, None); + walk_builder(&directories[0], get_default_num_threads(), None, None); directories[1..].iter().for_each(|path| { walker.add(path); }); diff --git a/television/config/mod.rs b/television/config/mod.rs index f3bdeac..451695d 100644 --- a/television/config/mod.rs +++ b/television/config/mod.rs @@ -9,7 +9,6 @@ use anyhow::{Context, Result}; use directories::ProjectDirs; use keybindings::merge_keybindings; pub use keybindings::{parse_key, Binding, KeyBindings}; -use lazy_static::lazy_static; use previewers::PreviewersConfig; use serde::Deserialize; use shell_integration::ShellIntegrationConfig; @@ -70,23 +69,7 @@ pub struct Config { pub shell_integration: ShellIntegrationConfig, } -lazy_static! { - pub static ref PROJECT_NAME: String = String::from("television"); - pub static ref PROJECT_NAME_UPPER: String = PROJECT_NAME.to_uppercase(); - pub static ref DATA_FOLDER: Option = - // if `TELEVISION_DATA` is set, use that as the data directory - env::var_os(format!("{}_DATA", PROJECT_NAME_UPPER.clone())).map(PathBuf::from).or_else(|| { - // otherwise, use the XDG data directory - env::var_os("XDG_DATA_HOME").map(PathBuf::from).map(|p| p.join(PROJECT_NAME.as_str())).filter(|p| p.is_absolute()) - }); - pub static ref CONFIG_FOLDER: Option = - // if `TELEVISION_CONFIG` is set, use that as the television config directory - env::var_os(format!("{}_CONFIG", PROJECT_NAME_UPPER.clone())).map(PathBuf::from).or_else(|| { - // otherwise, use the XDG config directory + 'television' - env::var_os("XDG_CONFIG_HOME").map(PathBuf::from).map(|p| p.join(PROJECT_NAME.as_str())).filter(|p| p.is_absolute()) - }); -} - +const PROJECT_NAME: &str = "television"; const CONFIG_FILE_NAME: &str = "config.toml"; pub struct ConfigEnv { @@ -184,7 +167,19 @@ impl Config { } pub fn get_data_dir() -> PathBuf { - let directory = if let Some(s) = DATA_FOLDER.clone() { + // if `TELEVISION_DATA` is set, use that as the data directory + let data_folder = + env::var_os(format!("{}_DATA", PROJECT_NAME.to_uppercase())) + .map(PathBuf::from) + .or_else(|| { + // otherwise, use the XDG data directory + env::var_os("XDG_DATA_HOME") + .map(PathBuf::from) + .map(|p| p.join(PROJECT_NAME)) + .filter(|p| p.is_absolute()) + }); + + let directory = if let Some(s) = data_folder { debug!("Using data directory: {:?}", s); s } else if let Some(proj_dirs) = project_directory() { @@ -197,7 +192,18 @@ pub fn get_data_dir() -> PathBuf { } pub fn get_config_dir() -> PathBuf { - let directory = if let Some(s) = CONFIG_FOLDER.clone() { + // if `TELEVISION_CONFIG` is set, use that as the television config directory + let config_dir = + env::var_os(format!("{}_CONFIG", PROJECT_NAME.to_uppercase())) + .map(PathBuf::from) + .or_else(|| { + // otherwise, use the XDG config directory + 'television' + env::var_os("XDG_CONFIG_HOME") + .map(PathBuf::from) + .map(|p| p.join(PROJECT_NAME)) + .filter(|p| p.is_absolute()) + }); + let directory = if let Some(s) = config_dir { debug!("Config directory: {:?}", s); s } else if cfg!(unix) { diff --git a/television/config/themes.rs b/television/config/themes.rs index 2708278..8f64558 100644 --- a/television/config/themes.rs +++ b/television/config/themes.rs @@ -128,10 +128,10 @@ impl Theme { pub fn from_builtin( name: &str, ) -> Result> { - let theme_content: &str = builtin::BUILTIN_THEMES.get(name).map_or( - builtin::BUILTIN_THEMES.get(DEFAULT_THEME).unwrap(), - |t| *t, - ); + let builtin_themes = builtin::builtin_themes(); + let theme_content: &str = builtin_themes + .get(name) + .map_or(builtin_themes.get(DEFAULT_THEME).unwrap(), |t| *t); let theme = toml::from_str(theme_content)?; Ok(theme) } diff --git a/television/config/themes/builtin.rs b/television/config/themes/builtin.rs index bb6826d..081d481 100644 --- a/television/config/themes/builtin.rs +++ b/television/config/themes/builtin.rs @@ -1,43 +1,39 @@ use rustc_hash::FxHashMap; -use lazy_static::lazy_static; - -lazy_static! { - pub static ref BUILTIN_THEMES: FxHashMap<&'static str, &'static str> = { - let mut m = FxHashMap::default(); - m.insert("default", include_str!("../../../themes/default.toml")); - m.insert( - "television", - include_str!("../../../themes/television.toml"), - ); - m.insert( - "gruvbox-dark", - include_str!("../../../themes/gruvbox-dark.toml"), - ); - m.insert( - "gruvbox-light", - include_str!("../../../themes/gruvbox-light.toml"), - ); - m.insert( - "catppuccin", - include_str!("../../../themes/catppuccin.toml"), - ); - m.insert("nord-dark", include_str!("../../../themes/nord-dark.toml")); - m.insert( - "solarized-dark", - include_str!("../../../themes/solarized-dark.toml"), - ); - m.insert( - "solarized-light", - include_str!("../../../themes/solarized-light.toml"), - ); - m.insert("dracula", include_str!("../../../themes/dracula.toml")); - m.insert("monokai", include_str!("../../../themes/monokai.toml")); - m.insert("onedark", include_str!("../../../themes/onedark.toml")); - m.insert( - "tokyonight", - include_str!("../../../themes/tokyonight.toml"), - ); - m - }; +pub fn builtin_themes() -> FxHashMap<&'static str, &'static str> { + let mut m = FxHashMap::default(); + m.insert("default", include_str!("../../../themes/default.toml")); + m.insert( + "television", + include_str!("../../../themes/television.toml"), + ); + m.insert( + "gruvbox-dark", + include_str!("../../../themes/gruvbox-dark.toml"), + ); + m.insert( + "gruvbox-light", + include_str!("../../../themes/gruvbox-light.toml"), + ); + m.insert( + "catppuccin", + include_str!("../../../themes/catppuccin.toml"), + ); + m.insert("nord-dark", include_str!("../../../themes/nord-dark.toml")); + m.insert( + "solarized-dark", + include_str!("../../../themes/solarized-dark.toml"), + ); + m.insert( + "solarized-light", + include_str!("../../../themes/solarized-light.toml"), + ); + m.insert("dracula", include_str!("../../../themes/dracula.toml")); + m.insert("monokai", include_str!("../../../themes/monokai.toml")); + m.insert("onedark", include_str!("../../../themes/onedark.toml")); + m.insert( + "tokyonight", + include_str!("../../../themes/tokyonight.toml"), + ); + m } diff --git a/television/draw.rs b/television/draw.rs index 3846791..5c93690 100644 --- a/television/draw.rs +++ b/television/draw.rs @@ -3,7 +3,6 @@ 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, @@ -18,7 +17,7 @@ use crate::{ remote_control::draw_remote_control, results::draw_results_list, spinner::Spinner, }, - television::{Message, Mode}, + television::Mode, utils::metadata::AppMetadata, }; @@ -61,7 +60,6 @@ impl Hash for ChannelState { pub struct TvState { pub mode: Mode, pub selected_entry: Option, - pub results_area_height: u16, pub results_picker: Picker, pub rc_picker: Picker, pub channel_state: ChannelState, @@ -74,7 +72,6 @@ impl TvState { pub fn new( mode: Mode, selected_entry: Option, - results_area_height: u16, results_picker: Picker, rc_picker: Picker, channel_state: ChannelState, @@ -84,7 +81,6 @@ impl TvState { Self { mode, selected_entry, - results_area_height, results_picker, rc_picker, channel_state, @@ -100,8 +96,8 @@ pub struct Ctx { pub config: Config, pub colorscheme: Colorscheme, pub app_metadata: AppMetadata, - pub tv_tx_handle: Sender, pub instant: Instant, + pub layout: Layout, } impl Ctx { @@ -110,16 +106,16 @@ impl Ctx { config: Config, colorscheme: Colorscheme, app_metadata: AppMetadata, - tv_tx_handle: Sender, instant: Instant, + layout: Layout, ) -> Self { Self { tv_state, config, colorscheme, app_metadata, - tv_tx_handle, instant, + layout, } } } @@ -156,7 +152,7 @@ impl Ord for Ctx { } } -pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result<()> { +pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result { let selected_entry = ctx .tv_state .selected_entry @@ -185,14 +181,6 @@ pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result<()> { &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, @@ -259,5 +247,5 @@ pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result<()> { )?; } - Ok(()) + Ok(layout) } diff --git a/television/logging.rs b/television/logging.rs index 8913fcc..1ed37fe 100644 --- a/television/logging.rs +++ b/television/logging.rs @@ -3,14 +3,10 @@ use tracing_subscriber::{fmt, prelude::*, EnvFilter}; use crate::config::get_data_dir; -lazy_static::lazy_static! { - pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); -} - pub fn init() -> Result<()> { let directory = get_data_dir(); std::fs::create_dir_all(directory.clone())?; - let log_path = directory.join(LOG_FILE.clone()); + let log_path = directory.join(format!("{}.log", env!("CARGO_PKG_NAME"))); let log_file = std::fs::File::create(log_path)?; let file_subscriber = fmt::layer() .with_file(true) diff --git a/television/preview/previewers/command.rs b/television/preview/previewers/command.rs index 122c08a..b239fa7 100644 --- a/television/preview/previewers/command.rs +++ b/television/preview/previewers/command.rs @@ -2,7 +2,6 @@ use crate::channels::entry::{Entry, PreviewCommand}; use crate::preview::cache::PreviewCache; use crate::preview::{Preview, PreviewContent}; use crate::utils::command::shell_command; -use lazy_static::lazy_static; use parking_lot::Mutex; use regex::Regex; use rustc_hash::FxHashSet; @@ -11,12 +10,19 @@ use std::sync::Arc; use tracing::debug; #[allow(dead_code)] -#[derive(Debug, Default)] +#[derive(Debug)] pub struct CommandPreviewer { cache: Arc>, config: CommandPreviewerConfig, concurrent_preview_tasks: Arc, in_flight_previews: Arc>>, + command_re: Regex, +} + +impl Default for CommandPreviewer { + fn default() -> Self { + CommandPreviewer::new(None) + } } #[allow(dead_code)] @@ -53,6 +59,7 @@ impl CommandPreviewer { config, concurrent_preview_tasks: Arc::new(AtomicU8::new(0)), in_flight_previews: Arc::new(Mutex::new(FxHashSet::default())), + command_re: Regex::new(r"\{(\d+)\}").unwrap(), } } @@ -96,6 +103,7 @@ impl CommandPreviewer { let concurrent_tasks = self.concurrent_preview_tasks.clone(); let command = command.clone(); let in_flight_previews = self.in_flight_previews.clone(); + let command_re = self.command_re.clone(); tokio::spawn(async move { try_preview( &command, @@ -103,6 +111,7 @@ impl CommandPreviewer { &cache, &concurrent_tasks, &in_flight_previews, + &command_re, ); }); } else { @@ -114,11 +123,6 @@ impl CommandPreviewer { } } -lazy_static! { - static ref COMMAND_PLACEHOLDER_REGEX: Regex = - Regex::new(r"\{(\d+)\}").unwrap(); -} - /// Format the command with the entry name and provided placeholders /// /// # Example @@ -131,11 +135,15 @@ lazy_static! { /// delimiter: ":".to_string(), /// }; /// let entry = Entry::new("a:given:entry:to:preview".to_string(), PreviewType::Command(command.clone())); -/// let formatted_command = format_command(&command, &entry); +/// let formatted_command = format_command(&command, &entry, ®ex::Regex::new(r"\{(\d+)\}").unwrap()); /// /// assert_eq!(formatted_command, "something 'a:given:entry:to:preview' 'entry' 'a'"); /// ``` -pub fn format_command(command: &PreviewCommand, entry: &Entry) -> String { +pub fn format_command( + command: &PreviewCommand, + entry: &Entry, + command_re: &Regex, +) -> String { let parts = entry.name.split(&command.delimiter).collect::>(); debug!("Parts: {:?}", parts); @@ -143,7 +151,7 @@ pub fn format_command(command: &PreviewCommand, entry: &Entry) -> String { .command .replace("{}", format!("'{}'", entry.name).as_str()); - formatted_command = COMMAND_PLACEHOLDER_REGEX + formatted_command = command_re .replace_all(&formatted_command, |caps: ®ex::Captures| { let index = caps.get(1).unwrap().as_str().parse::().unwrap(); @@ -160,9 +168,10 @@ pub fn try_preview( cache: &Arc>, concurrent_tasks: &Arc, in_flight_previews: &Arc>>, + command_re: &Regex, ) { debug!("Computing preview for {:?}", entry.name); - let command = format_command(command, entry); + let command = format_command(command, entry, command_re); debug!("Formatted preview command: {:?}", command); let child = shell_command() @@ -212,7 +221,11 @@ mod tests { "an:entry:to:preview".to_string(), PreviewType::Command(command.clone()), ); - let formatted_command = format_command(&command, &entry); + let formatted_command = format_command( + &command, + &entry, + &Regex::new(r"\{(\d+)\}").unwrap(), + ); assert_eq!( formatted_command, @@ -230,7 +243,11 @@ mod tests { "an:entry:to:preview".to_string(), PreviewType::Command(command.clone()), ); - let formatted_command = format_command(&command, &entry); + let formatted_command = format_command( + &command, + &entry, + &Regex::new(r"\{(\d+)\}").unwrap(), + ); assert_eq!(formatted_command, "something"); } @@ -245,7 +262,11 @@ mod tests { "an:entry:to:preview".to_string(), PreviewType::Command(command.clone()), ); - let formatted_command = format_command(&command, &entry); + let formatted_command = format_command( + &command, + &entry, + &Regex::new(r"\{(\d+)\}").unwrap(), + ); assert_eq!(formatted_command, "something 'an:entry:to:preview'"); } @@ -260,7 +281,11 @@ mod tests { "an:entry:to:preview".to_string(), PreviewType::Command(command.clone()), ); - let formatted_command = format_command(&command, &entry); + let formatted_command = format_command( + &command, + &entry, + &Regex::new(r"\{(\d+)\}").unwrap(), + ); assert_eq!(formatted_command, "something 'an' -t 'to'"); } diff --git a/television/render.rs b/television/render.rs index 036175e..fa6dd9f 100644 --- a/television/render.rs +++ b/television/render.rs @@ -8,12 +8,13 @@ use tracing::{debug, warn}; use tokio::sync::mpsc; use crate::draw::Ctx; +use crate::screen::layout::Layout; use crate::{action::Action, draw::draw, tui::Tui}; #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)] pub enum RenderingTask { ClearScreen, - Render(Ctx), + Render(Box), Resize(u16, u16), Resume, Suspend, @@ -35,9 +36,21 @@ impl IoStream { } } +#[derive(Default)] +pub struct UiState { + pub layout: Layout, +} + +impl UiState { + pub fn new(layout: Layout) -> Self { + Self { layout } + } +} + pub async fn render( mut render_rx: mpsc::UnboundedReceiver, action_tx: mpsc::UnboundedSender, + ui_state_tx: mpsc::UnboundedSender, is_output_tty: bool, ) -> Result<()> { let stream = if is_output_tty { @@ -73,13 +86,19 @@ pub async fn render( if size.width.checked_mul(size.height).is_some() { queue!(stderr(), BeginSynchronizedUpdate).ok(); tui.terminal.draw(|frame| { - if let Err(err) = - draw(&context, frame, frame.area()) - { - warn!("Failed to draw: {:?}", err); - let _ = action_tx.send(Action::Error( - format!("Failed to draw: {err:?}"), - )); + match draw(&context, frame, frame.area()) { + Ok(layout) => { + if layout != context.layout { + let _ = ui_state_tx + .send(UiState::new(layout)); + } + } + Err(err) => { + warn!("Failed to draw: {:?}", err); + let _ = action_tx.send(Action::Error( + format!("Failed to draw: {err:?}"), + )); + } } })?; execute!(stderr(), EndSynchronizedUpdate).ok(); diff --git a/television/screen/layout.rs b/television/screen/layout.rs index 7049317..9197df0 100644 --- a/television/screen/layout.rs +++ b/television/screen/layout.rs @@ -29,7 +29,7 @@ impl Default for Dimensions { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct HelpBarLayout { pub left: Rect, pub middle: Rect, @@ -82,6 +82,7 @@ impl Display for PreviewTitlePosition { } } +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Layout { pub help_bar: Option, pub results: Rect, @@ -90,6 +91,17 @@ pub struct Layout { pub remote_control: Option, } +impl Default for Layout { + /// Having a default layout with a non-zero height for the results area + /// is important for the initial rendering of the application. For the first + /// frame, this avoids not rendering any results at all since the picker's contents + /// depend on the height of the results area which is not known until the first + /// frame is rendered. + fn default() -> Self { + Self::new(None, Rect::new(0, 0, 0, 100), Rect::default(), None, None) + } +} + impl Layout { #[allow(clippy::too_many_arguments)] pub fn new( diff --git a/television/television.rs b/television/television.rs index 980d33b..07161e6 100644 --- a/television/television.rs +++ b/television/television.rs @@ -10,6 +10,7 @@ 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::render::UiState; use crate::screen::colors::Colorscheme; use crate::screen::layout::InputPosition; use crate::screen::spinner::{Spinner, SpinnerState}; @@ -20,7 +21,7 @@ use anyhow::Result; use rustc_hash::{FxBuildHasher, FxHashSet}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; -use tokio::sync::mpsc::{Receiver, Sender, UnboundedSender}; +use tokio::sync::mpsc::UnboundedSender; #[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)] pub enum Mode { @@ -38,7 +39,6 @@ pub struct Television { pub current_pattern: String, pub results_picker: Picker, pub rc_picker: Picker, - results_area_height: u16, pub previewer: Previewer, pub preview_state: PreviewState, pub spinner: Spinner, @@ -46,16 +46,7 @@ pub struct Television { 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, - pub inner_tx: Sender, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Message { - ResultListHeightChanged(u16), + pub ui_state: UiState, } impl Television { @@ -87,8 +78,6 @@ 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, @@ -101,7 +90,6 @@ impl Television { current_pattern: EMPTY_STRING.to_string(), results_picker, rc_picker: Picker::default(), - results_area_height: 0, previewer, preview_state: PreviewState::default(), spinner, @@ -109,11 +97,14 @@ impl Television { app_metadata, colorscheme, ticks: 0, - inner_rx, - inner_tx, + ui_state: UiState::default(), } } + pub fn update_ui_state(&mut self, ui_state: UiState) { + self.ui_state = ui_state; + } + pub fn init_remote_control(&mut self) { let cable_channels = load_cable_channels().unwrap_or_default(); let builtin_channels = load_builtin_channels(Some( @@ -134,7 +125,6 @@ impl Television { 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, @@ -147,9 +137,9 @@ impl Television { self.config.clone(), self.colorscheme.clone(), self.app_metadata.clone(), - self.inner_tx.clone(), // now timestamp std::time::Instant::now(), + self.ui_state.layout, ) } @@ -229,7 +219,7 @@ impl Television { picker.select_prev( step, result_count as usize, - self.results_area_height as usize, + self.ui_state.layout.results.height.saturating_sub(2) as usize, ); } @@ -248,7 +238,7 @@ impl Television { picker.select_next( step, result_count as usize, - self.results_area_height as usize, + self.ui_state.layout.results.height.saturating_sub(2) as usize, ); } @@ -315,6 +305,7 @@ impl Television { { // preview content if let Some(preview) = self.previewer.preview(selected_entry) { + // only update if the preview content has changed if self.preview_state.preview.title != preview.title { self.preview_state.update( preview, @@ -323,7 +314,13 @@ impl Television { .line_number .unwrap_or(0) .saturating_sub( - (self.results_area_height / 2).into(), + (self + .ui_state + .layout + .preview_window + .map_or(0, |w| w.height) + / 2) + .into(), ) .try_into()?, selected_entry @@ -348,7 +345,7 @@ impl Television { } self.results_picker.entries = self.channel.results( - self.results_area_height.into(), + self.ui_state.layout.results.height.into(), u32::try_from(self.results_picker.offset()).unwrap(), ); self.results_picker.total_items = self.channel.result_count(); @@ -364,7 +361,7 @@ impl Television { 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(), + self.ui_state.layout.results.height.into(), u32::try_from(self.rc_picker.offset()).unwrap(), ); self.rc_picker.total_items = self.remote_control.total_count(); @@ -513,11 +510,25 @@ impl Television { } Action::SelectNextPage => { self.preview_state.reset(); - self.select_next_entry(self.results_area_height.into()); + self.select_next_entry( + self.ui_state + .layout + .results + .height + .saturating_sub(2) + .into(), + ); } Action::SelectPrevPage => { self.preview_state.reset(); - self.select_prev_entry(self.results_area_height.into()); + self.select_prev_entry( + self.ui_state + .layout + .results + .height + .saturating_sub(2) + .into(), + ); } Action::ScrollPreviewDown => self.preview_state.scroll_down(1), Action::ScrollPreviewUp => self.preview_state.scroll_up(1), @@ -559,13 +570,6 @@ impl Television { /// /// This function may return an Action that'll be processed by the parent `App`. pub fn update(&mut self, action: &Action) -> Result> { - if let Ok(Message::ResultListHeightChanged(height)) = - self.inner_rx.try_recv() - { - self.results_area_height = height; - self.action_tx.send(Action::Render)?; - } - self.handle_action(action)?; self.update_results_picker_state(); diff --git a/television/utils/files.rs b/television/utils/files.rs index fb81fc4..f21d613 100644 --- a/television/utils/files.rs +++ b/television/utils/files.rs @@ -6,9 +6,9 @@ use std::io::BufReader; use std::io::Read; use std::path::Path; use std::path::PathBuf; +use std::sync::OnceLock; use ignore::{overrides::Override, types::TypesBuilder, WalkBuilder}; -use lazy_static::lazy_static; use tracing::{debug, warn}; use crate::utils::strings::{ @@ -61,8 +61,10 @@ where } } -lazy_static::lazy_static! { - pub static ref DEFAULT_NUM_THREADS: usize = default_num_threads().into(); +pub static DEFAULT_NUM_THREADS: OnceLock = OnceLock::new(); + +pub fn get_default_num_threads() -> usize { + *DEFAULT_NUM_THREADS.get_or_init(default_num_threads) } pub fn walk_builder( @@ -143,340 +145,345 @@ where path.as_ref() .extension() .and_then(|ext| ext.to_str()) - .is_some_and(|ext| KNOWN_TEXT_FILE_EXTENSIONS.contains(ext)) + .is_some_and(|ext| get_known_text_file_extensions().contains(ext)) } -lazy_static! { - static ref KNOWN_TEXT_FILE_EXTENSIONS: FxHashSet<&'static str> = [ - "ada", - "adb", - "ads", - "applescript", - "as", - "asc", - "ascii", - "ascx", - "asm", - "asmx", - "asp", - "aspx", - "atom", - "au3", - "awk", - "bas", - "bash", - "bashrc", - "bat", - "bbcolors", - "bcp", - "bdsgroup", - "bdsproj", - "bib", - "bowerrc", - "c", - "cbl", - "cc", - "cfc", - "cfg", - "cfm", - "cfml", - "cgi", - "cjs", - "clj", - "cljs", - "cls", - "cmake", - "cmd", - "cnf", - "cob", - "code-snippets", - "coffee", - "coffeekup", - "conf", - "cp", - "cpp", - "cpt", - "cpy", - "crt", - "cs", - "csh", - "cson", - "csproj", - "csr", - "css", - "csslintrc", - "csv", - "ctl", - "curlrc", - "cxx", - "d", - "dart", - "dfm", - "diff", - "dof", - "dpk", - "dpr", - "dproj", - "dtd", - "eco", - "editorconfig", - "ejs", - "el", - "elm", - "emacs", - "eml", - "ent", - "erb", - "erl", - "eslintignore", - "eslintrc", - "ex", - "exs", - "f", - "f03", - "f77", - "f90", - "f95", - "fish", - "for", - "fpp", - "frm", - "fs", - "fsproj", - "fsx", - "ftn", - "gemrc", - "gemspec", - "gitattributes", - "gitconfig", - "gitignore", - "gitkeep", - "gitmodules", - "go", - "gpp", - "gradle", - "graphql", - "groovy", - "groupproj", - "grunit", - "gtmpl", - "gvimrc", - "h", - "haml", - "hbs", - "hgignore", - "hh", - "hpp", - "hrl", - "hs", - "hta", - "htaccess", - "htc", - "htm", - "html", - "htpasswd", - "hxx", - "iced", - "iml", - "inc", - "inf", - "info", - "ini", - "ino", - "int", - "irbrc", - "itcl", - "itermcolors", - "itk", - "jade", - "java", - "jhtm", - "jhtml", - "js", - "jscsrc", - "jshintignore", - "jshintrc", - "json", - "json5", - "jsonld", - "jsp", - "jspx", - "jsx", - "ksh", - "less", - "lhs", - "lisp", - "log", - "ls", - "lsp", - "lua", - "m", - "m4", - "mak", - "map", - "markdown", - "master", - "md", - "mdown", - "mdwn", - "mdx", - "metadata", - "mht", - "mhtml", - "mjs", - "mk", - "mkd", - "mkdn", - "mkdown", - "ml", - "mli", - "mm", - "mxml", - "nfm", - "nfo", - "noon", - "npmignore", - "npmrc", - "nuspec", - "nvmrc", - "ops", - "pas", - "pasm", - "patch", - "pbxproj", - "pch", - "pem", - "pg", - "php", - "php3", - "php4", - "php5", - "phpt", - "phtml", - "pir", - "pl", - "pm", - "pmc", - "pod", - "pot", - "prettierrc", - "properties", - "props", - "pt", - "pug", - "purs", - "py", - "pyx", - "r", - "rake", - "rb", - "rbw", - "rc", - "rdoc", - "rdoc_options", - "resx", - "rexx", - "rhtml", - "rjs", - "rlib", - "ron", - "rs", - "rss", - "rst", - "rtf", - "rvmrc", - "rxml", - "s", - "sass", - "scala", - "scm", - "scss", - "seestyle", - "sh", - "shtml", - "sln", - "sls", - "spec", - "sql", - "sqlite", - "sqlproj", - "srt", - "ss", - "sss", - "st", - "strings", - "sty", - "styl", - "stylus", - "sub", - "sublime-build", - "sublime-commands", - "sublime-completions", - "sublime-keymap", - "sublime-macro", - "sublime-menu", - "sublime-project", - "sublime-settings", - "sublime-workspace", - "sv", - "svc", - "svg", - "swift", - "t", - "tcl", - "tcsh", - "terminal", - "tex", - "text", - "textile", - "tg", - "tk", - "tmLanguage", - "tmpl", - "tmTheme", - "toml", - "tpl", - "ts", - "tsv", - "tsx", - "tt", - "tt2", - "ttml", - "twig", - "txt", - "v", - "vb", - "vbproj", - "vbs", - "vcproj", - "vcxproj", - "vh", - "vhd", - "vhdl", - "vim", - "viminfo", - "vimrc", - "vm", - "vue", - "webapp", - "webmanifest", - "wsc", - "x-php", - "xaml", - "xht", - "xhtml", - "xml", - "xs", - "xsd", - "xsl", - "xslt", - "y", - "yaml", - "yml", - "zsh", - "zshrc", - ] - .iter() - .copied() - .collect(); +pub static KNOWN_TEXT_FILE_EXTENSIONS: OnceLock> = + OnceLock::new(); + +pub fn get_known_text_file_extensions() -> &'static FxHashSet<&'static str> { + KNOWN_TEXT_FILE_EXTENSIONS.get_or_init(|| { + [ + "ada", + "adb", + "ads", + "applescript", + "as", + "asc", + "ascii", + "ascx", + "asm", + "asmx", + "asp", + "aspx", + "atom", + "au3", + "awk", + "bas", + "bash", + "bashrc", + "bat", + "bbcolors", + "bcp", + "bdsgroup", + "bdsproj", + "bib", + "bowerrc", + "c", + "cbl", + "cc", + "cfc", + "cfg", + "cfm", + "cfml", + "cgi", + "cjs", + "clj", + "cljs", + "cls", + "cmake", + "cmd", + "cnf", + "cob", + "code-snippets", + "coffee", + "coffeekup", + "conf", + "cp", + "cpp", + "cpt", + "cpy", + "crt", + "cs", + "csh", + "cson", + "csproj", + "csr", + "css", + "csslintrc", + "csv", + "ctl", + "curlrc", + "cxx", + "d", + "dart", + "dfm", + "diff", + "dof", + "dpk", + "dpr", + "dproj", + "dtd", + "eco", + "editorconfig", + "ejs", + "el", + "elm", + "emacs", + "eml", + "ent", + "erb", + "erl", + "eslintignore", + "eslintrc", + "ex", + "exs", + "f", + "f03", + "f77", + "f90", + "f95", + "fish", + "for", + "fpp", + "frm", + "fs", + "fsproj", + "fsx", + "ftn", + "gemrc", + "gemspec", + "gitattributes", + "gitconfig", + "gitignore", + "gitkeep", + "gitmodules", + "go", + "gpp", + "gradle", + "graphql", + "groovy", + "groupproj", + "grunit", + "gtmpl", + "gvimrc", + "h", + "haml", + "hbs", + "hgignore", + "hh", + "hpp", + "hrl", + "hs", + "hta", + "htaccess", + "htc", + "htm", + "html", + "htpasswd", + "hxx", + "iced", + "iml", + "inc", + "inf", + "info", + "ini", + "ino", + "int", + "irbrc", + "itcl", + "itermcolors", + "itk", + "jade", + "java", + "jhtm", + "jhtml", + "js", + "jscsrc", + "jshintignore", + "jshintrc", + "json", + "json5", + "jsonld", + "jsp", + "jspx", + "jsx", + "ksh", + "less", + "lhs", + "lisp", + "log", + "ls", + "lsp", + "lua", + "m", + "m4", + "mak", + "map", + "markdown", + "master", + "md", + "mdown", + "mdwn", + "mdx", + "metadata", + "mht", + "mhtml", + "mjs", + "mk", + "mkd", + "mkdn", + "mkdown", + "ml", + "mli", + "mm", + "mxml", + "nfm", + "nfo", + "noon", + "npmignore", + "npmrc", + "nuspec", + "nvmrc", + "ops", + "pas", + "pasm", + "patch", + "pbxproj", + "pch", + "pem", + "pg", + "php", + "php3", + "php4", + "php5", + "phpt", + "phtml", + "pir", + "pl", + "pm", + "pmc", + "pod", + "pot", + "prettierrc", + "properties", + "props", + "pt", + "pug", + "purs", + "py", + "pyx", + "r", + "rake", + "rb", + "rbw", + "rc", + "rdoc", + "rdoc_options", + "resx", + "rexx", + "rhtml", + "rjs", + "rlib", + "ron", + "rs", + "rss", + "rst", + "rtf", + "rvmrc", + "rxml", + "s", + "sass", + "scala", + "scm", + "scss", + "seestyle", + "sh", + "shtml", + "sln", + "sls", + "spec", + "sql", + "sqlite", + "sqlproj", + "srt", + "ss", + "sss", + "st", + "strings", + "sty", + "styl", + "stylus", + "sub", + "sublime-build", + "sublime-commands", + "sublime-completions", + "sublime-keymap", + "sublime-macro", + "sublime-menu", + "sublime-project", + "sublime-settings", + "sublime-workspace", + "sv", + "svc", + "svg", + "swift", + "t", + "tcl", + "tcsh", + "terminal", + "tex", + "text", + "textile", + "tg", + "tk", + "tmLanguage", + "tmpl", + "tmTheme", + "toml", + "tpl", + "ts", + "tsv", + "tsx", + "tt", + "tt2", + "ttml", + "twig", + "txt", + "v", + "vb", + "vbproj", + "vbs", + "vcproj", + "vcxproj", + "vh", + "vhd", + "vhdl", + "vim", + "viminfo", + "vimrc", + "vm", + "vue", + "webapp", + "webmanifest", + "wsc", + "x-php", + "xaml", + "xht", + "xhtml", + "xml", + "xs", + "xsd", + "xsl", + "xslt", + "y", + "yaml", + "yml", + "zsh", + "zshrc", + ] + .iter() + .copied() + .collect() + }) } diff --git a/television/utils/strings.rs b/television/utils/strings.rs index be63aa2..1610f1e 100644 --- a/television/utils/strings.rs +++ b/television/utils/strings.rs @@ -1,5 +1,3 @@ -use lazy_static::lazy_static; - /// Returns the index of the next character boundary in the given string. /// /// If the given index is already a character boundary, it is returned as is. @@ -151,14 +149,11 @@ pub fn try_parse_utf8_char(input: &[u8]) -> Option<(char, usize)> { decoded.map(|(seq, n)| (seq.chars().next().unwrap(), n)) } -lazy_static! { - /// The Unicode symbol to use for non-printable characters. - static ref NULL_SYMBOL: char = char::from_u32(0x2400).unwrap(); -} - pub const EMPTY_STRING: &str = ""; pub const TAB_WIDTH: usize = 4; +/// The Unicode symbol to use for non-printable characters. +const NULL_SYMBOL: char = '\u{2400}'; const TAB_CHARACTER: char = '\t'; const LINE_FEED_CHARACTER: char = '\x0A'; const DELETE_CHARACTER: char = '\x7F'; @@ -304,7 +299,7 @@ pub fn replace_non_printable( | BOM_CHARACTER if config.replace_control_characters => { - output.push(*NULL_SYMBOL); + output.push(NULL_SYMBOL); } // CJK Unified Ideographs // ex: 解 @@ -337,13 +332,13 @@ pub fn replace_non_printable( } // Unicode characters above 0x0700 seem unstable with ratatui c if c > '\u{0700}' => { - output.push(*NULL_SYMBOL); + output.push(NULL_SYMBOL); } // everything else c => output.push(c), } } else { - output.push(*NULL_SYMBOL); + output.push(NULL_SYMBOL); idx += 1; } } diff --git a/television/utils/syntax.rs b/television/utils/syntax.rs index 5ca69f5..6b2f5c1 100644 --- a/television/utils/syntax.rs +++ b/television/utils/syntax.rs @@ -224,7 +224,6 @@ pub fn compute_highlights_for_line<'a>( // SOFTWARE. use directories::BaseDirs; -use lazy_static::lazy_static; #[cfg(target_os = "macos")] use std::env; @@ -258,13 +257,11 @@ impl BatProjectDirs { } } -lazy_static! { - pub static ref PROJECT_DIRS: BatProjectDirs = BatProjectDirs::new() - .unwrap_or_else(|| panic!("Could not get home directory")); -} - pub fn load_highlighting_assets() -> HighlightingAssets { - HighlightingAssets::from_cache(PROJECT_DIRS.cache_dir()) + let project_dirs = BatProjectDirs::new() + .unwrap_or_else(|| panic!("Could not get home directory")); + + HighlightingAssets::from_cache(project_dirs.cache_dir()) .unwrap_or_else(|_| HighlightingAssets::from_binary()) } diff --git a/television/utils/threads.rs b/television/utils/threads.rs index 64d2c09..160b0da 100644 --- a/television/utils/threads.rs +++ b/television/utils/threads.rs @@ -5,7 +5,7 @@ use std::num::NonZeroUsize; /// This will use the number of available threads if possible, but will default to 1 if the number /// of available threads cannot be determined. It will also never use more than 32 threads to avoid /// startup overhead. -pub fn default_num_threads() -> NonZeroUsize { +pub fn default_num_threads() -> usize { // default to 1 thread if we can't determine the number of available threads let default = NonZeroUsize::MIN; // never use more than 32 threads to avoid startup overhead @@ -14,4 +14,5 @@ pub fn default_num_threads() -> NonZeroUsize { std::thread::available_parallelism() .unwrap_or(default) .min(limit) + .get() }