refactor(ui): communicate ui state to tv using channels

This commit is contained in:
Alexandre Pasmantier 2025-03-04 22:17:44 +01:00
parent e2a0fb2047
commit d8703ce0e6
21 changed files with 591 additions and 532 deletions

1
Cargo.lock generated
View File

@ -1696,7 +1696,6 @@ dependencies = [
"gag", "gag",
"human-panic", "human-panic",
"ignore", "ignore",
"lazy_static",
"nom", "nom",
"nucleo", "nucleo",
"parking_lot", "parking_lot",

View File

@ -34,7 +34,6 @@ anyhow = "1.0"
base64 = "0.22.1" base64 = "0.22.1"
directories = "6.0" directories = "6.0"
devicons = "0.6" devicons = "0.6"
lazy_static = "1.5"
tokio = { version = "1.43", features = ["full"] } tokio = { version = "1.43", features = ["full"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@ -8,6 +8,7 @@ use crate::channels::entry::Entry;
use crate::channels::TelevisionChannel; use crate::channels::TelevisionChannel;
use crate::config::{parse_key, Config}; use crate::config::{parse_key, Config};
use crate::keymap::Keymap; use crate::keymap::Keymap;
use crate::render::UiState;
use crate::television::{Mode, Television}; use crate::television::{Mode, Television};
use crate::{ use crate::{
action::Action, action::Action,
@ -37,6 +38,9 @@ pub struct App {
event_abort_tx: mpsc::UnboundedSender<()>, event_abort_tx: mpsc::UnboundedSender<()>,
/// A sender channel for rendering tasks. /// A sender channel for rendering tasks.
render_tx: mpsc::UnboundedSender<RenderingTask>, render_tx: mpsc::UnboundedSender<RenderingTask>,
/// A channel that listens to UI updates.
ui_state_rx: mpsc::UnboundedReceiver<UiState>,
ui_state_tx: mpsc::UnboundedSender<UiState>,
} }
/// The outcome of an action. /// The outcome of an action.
@ -104,6 +108,7 @@ impl App {
.collect(), .collect(),
)?; )?;
debug!("{:?}", keymap); debug!("{:?}", keymap);
let (ui_state_tx, ui_state_rx) = mpsc::unbounded_channel();
let television = let television =
Television::new(action_tx.clone(), channel, config, input); Television::new(action_tx.clone(), channel, config, input);
@ -118,6 +123,8 @@ impl App {
event_rx, event_rx,
event_abort_tx, event_abort_tx,
render_tx, render_tx,
ui_state_rx,
ui_state_tx,
}) })
} }
@ -145,9 +152,10 @@ impl App {
debug!("Starting rendering loop"); debug!("Starting rendering loop");
let (render_tx, render_rx) = mpsc::unbounded_channel(); let (render_tx, render_rx) = mpsc::unbounded_channel();
self.render_tx = render_tx.clone(); self.render_tx = render_tx.clone();
let ui_state_tx = self.ui_state_tx.clone();
let action_tx_r = self.action_tx.clone(); let action_tx_r = self.action_tx.clone();
let rendering_task = tokio::spawn(async move { 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)?; self.action_tx.send(Action::Render)?;
@ -298,9 +306,14 @@ impl App {
self.render_tx.send(RenderingTask::Resize(w, h))?; self.render_tx.send(RenderingTask::Resize(w, h))?;
} }
Action::Render => { Action::Render => {
// forward to the rendering task
self.render_tx.send(RenderingTask::Render( 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);
}
} }
_ => {} _ => {}
} }

View File

@ -9,7 +9,6 @@ use std::io::{BufRead, BufReader};
use std::process::Stdio; use std::process::Stdio;
use anyhow::Result; use anyhow::Result;
use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use rustc_hash::{FxBuildHasher, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashSet};
use tracing::debug; use tracing::debug;
@ -64,13 +63,10 @@ impl From<CableChannelPrototype> for Channel {
} }
} }
lazy_static! {
static ref BUILTIN_PREVIEW_RE: Regex = Regex::new(r"^:(\w+):$").unwrap();
}
fn parse_preview_kind(command: &PreviewCommand) -> Result<PreviewKind> { fn parse_preview_kind(command: &PreviewCommand) -> Result<PreviewKind> {
debug!("Parsing preview kind for command: {:?}", command); 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])?; let preview_type = PreviewType::try_from(&captures[1])?;
Ok(PreviewKind::Builtin(preview_type)) Ok(PreviewKind::Builtin(preview_type))
} else { } else {

View File

@ -1,7 +1,7 @@
use crate::channels::entry::{Entry, PreviewCommand, PreviewType}; use crate::channels::entry::{Entry, PreviewCommand, PreviewType};
use crate::channels::{OnAir, TelevisionChannel}; use crate::channels::{OnAir, TelevisionChannel};
use crate::matcher::{config::Config, injector::Injector, Matcher}; 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 devicons::FileIcon;
use rustc_hash::{FxBuildHasher, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashSet};
use std::collections::HashSet; use std::collections::HashSet;
@ -151,7 +151,7 @@ async fn load_dirs(paths: Vec<PathBuf>, injector: Injector<String>) {
} }
let current_dir = std::env::current_dir().unwrap(); let current_dir = std::env::current_dir().unwrap();
let mut builder = 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| { paths[1..].iter().for_each(|path| {
builder.add(path); builder.add(path);
}); });

View File

@ -1,7 +1,7 @@
use crate::channels::entry::{Entry, PreviewType}; use crate::channels::entry::{Entry, PreviewType};
use crate::channels::{OnAir, TelevisionChannel}; use crate::channels::{OnAir, TelevisionChannel};
use crate::matcher::{config::Config, injector::Injector, Matcher}; 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 devicons::FileIcon;
use rustc_hash::{FxBuildHasher, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashSet};
use std::collections::HashSet; use std::collections::HashSet;
@ -157,7 +157,7 @@ async fn load_files(paths: Vec<PathBuf>, injector: Injector<String>) {
} }
let current_dir = std::env::current_dir().unwrap(); let current_dir = std::env::current_dir().unwrap();
let mut builder = 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| { paths[1..].iter().for_each(|path| {
builder.add(path); builder.add(path);
}); });

View File

@ -1,7 +1,6 @@
use devicons::FileIcon; use devicons::FileIcon;
use directories::BaseDirs; use directories::BaseDirs;
use ignore::overrides::OverrideBuilder; use ignore::overrides::OverrideBuilder;
use lazy_static::lazy_static;
use rustc_hash::{FxBuildHasher, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashSet};
use std::collections::HashSet; use std::collections::HashSet;
use std::path::PathBuf; use std::path::PathBuf;
@ -11,13 +10,14 @@ use tracing::debug;
use crate::channels::entry::{Entry, PreviewCommand, PreviewType}; use crate::channels::entry::{Entry, PreviewCommand, PreviewType};
use crate::channels::OnAir; use crate::channels::OnAir;
use crate::matcher::{config::Config, injector::Injector, Matcher}; 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 { pub struct Channel {
matcher: Matcher<String>, matcher: Matcher<String>,
icon: FileIcon, icon: FileIcon,
crawl_handle: JoinHandle<()>, crawl_handle: JoinHandle<()>,
selected_entries: FxHashSet<Entry>, selected_entries: FxHashSet<Entry>,
preview_command: PreviewCommand,
} }
impl Channel { impl Channel {
@ -28,11 +28,20 @@ impl Channel {
base_dirs.home_dir().to_path_buf(), base_dirs.home_dir().to_path_buf(),
matcher.injector(), matcher.injector(),
)); ));
let preview_command = PreviewCommand {
command: String::from(
"cd {} && git log -n 200 --pretty=medium --all --graph --color",
),
delimiter: ":".to_string(),
};
Channel { Channel {
matcher, matcher,
icon: FileIcon::from("git"), icon: FileIcon::from("git"),
crawl_handle, crawl_handle,
selected_entries: HashSet::with_hasher(FxBuildHasher), 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 { impl OnAir for Channel {
fn find(&mut self, pattern: &str) { fn find(&mut self, pattern: &str) {
self.matcher.find(pattern); self.matcher.find(pattern);
@ -64,9 +64,12 @@ impl OnAir for Channel {
.into_iter() .into_iter()
.map(|item| { .map(|item| {
let path = item.matched_string; let path = item.matched_string;
Entry::new(path, PreviewType::Command(PREVIEW_COMMAND.clone())) Entry::new(
.with_name_match_ranges(&item.match_indices) path,
.with_icon(self.icon) PreviewType::Command(self.preview_command.clone()),
)
.with_name_match_ranges(&item.match_indices)
.with_icon(self.icon)
}) })
.collect() .collect()
} }
@ -74,8 +77,11 @@ impl OnAir for Channel {
fn get_result(&self, index: u32) -> Option<Entry> { fn get_result(&self, index: u32) -> Option<Entry> {
self.matcher.get_result(index).map(|item| { self.matcher.get_result(index).map(|item| {
let path = item.matched_string; let path = item.matched_string;
Entry::new(path, PreviewType::Command(PREVIEW_COMMAND.clone())) Entry::new(
.with_icon(self.icon) 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<String>) {
walker_overrides_builder.add(".git").unwrap(); walker_overrides_builder.add(".git").unwrap();
let walker = walk_builder( let walker = walk_builder(
&starting_point, &starting_point,
*DEFAULT_NUM_THREADS, get_default_num_threads(),
Some(walker_overrides_builder.build().unwrap()), Some(walker_overrides_builder.build().unwrap()),
Some(get_ignored_paths()), Some(get_ignored_paths()),
) )

View File

@ -1,7 +1,7 @@
use super::{OnAir, TelevisionChannel}; use super::{OnAir, TelevisionChannel};
use crate::channels::entry::{Entry, PreviewType}; use crate::channels::entry::{Entry, PreviewType};
use crate::matcher::{config::Config, injector::Injector, Matcher}; 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::{ use crate::utils::strings::{
proportion_of_printable_ascii_characters, PRINTABLE_ASCII_THRESHOLD, 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 current_dir = std::env::current_dir().unwrap();
let mut walker = 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| { directories[1..].iter().for_each(|path| {
walker.add(path); walker.add(path);
}); });

View File

@ -9,7 +9,6 @@ use anyhow::{Context, Result};
use directories::ProjectDirs; use directories::ProjectDirs;
use keybindings::merge_keybindings; use keybindings::merge_keybindings;
pub use keybindings::{parse_key, Binding, KeyBindings}; pub use keybindings::{parse_key, Binding, KeyBindings};
use lazy_static::lazy_static;
use previewers::PreviewersConfig; use previewers::PreviewersConfig;
use serde::Deserialize; use serde::Deserialize;
use shell_integration::ShellIntegrationConfig; use shell_integration::ShellIntegrationConfig;
@ -70,23 +69,7 @@ pub struct Config {
pub shell_integration: ShellIntegrationConfig, pub shell_integration: ShellIntegrationConfig,
} }
lazy_static! { const PROJECT_NAME: &str = "television";
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<PathBuf> =
// 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<PathBuf> =
// 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 CONFIG_FILE_NAME: &str = "config.toml"; const CONFIG_FILE_NAME: &str = "config.toml";
pub struct ConfigEnv { pub struct ConfigEnv {
@ -184,7 +167,19 @@ impl Config {
} }
pub fn get_data_dir() -> PathBuf { 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); debug!("Using data directory: {:?}", s);
s s
} else if let Some(proj_dirs) = project_directory() { } else if let Some(proj_dirs) = project_directory() {
@ -197,7 +192,18 @@ pub fn get_data_dir() -> PathBuf {
} }
pub fn get_config_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); debug!("Config directory: {:?}", s);
s s
} else if cfg!(unix) { } else if cfg!(unix) {

View File

@ -128,10 +128,10 @@ impl Theme {
pub fn from_builtin( pub fn from_builtin(
name: &str, name: &str,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
let theme_content: &str = builtin::BUILTIN_THEMES.get(name).map_or( let builtin_themes = builtin::builtin_themes();
builtin::BUILTIN_THEMES.get(DEFAULT_THEME).unwrap(), let theme_content: &str = builtin_themes
|t| *t, .get(name)
); .map_or(builtin_themes.get(DEFAULT_THEME).unwrap(), |t| *t);
let theme = toml::from_str(theme_content)?; let theme = toml::from_str(theme_content)?;
Ok(theme) Ok(theme)
} }

View File

@ -1,43 +1,39 @@
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use lazy_static::lazy_static; pub fn builtin_themes() -> FxHashMap<&'static str, &'static str> {
let mut m = FxHashMap::default();
lazy_static! { m.insert("default", include_str!("../../../themes/default.toml"));
pub static ref BUILTIN_THEMES: FxHashMap<&'static str, &'static str> = { m.insert(
let mut m = FxHashMap::default(); "television",
m.insert("default", include_str!("../../../themes/default.toml")); include_str!("../../../themes/television.toml"),
m.insert( );
"television", m.insert(
include_str!("../../../themes/television.toml"), "gruvbox-dark",
); include_str!("../../../themes/gruvbox-dark.toml"),
m.insert( );
"gruvbox-dark", m.insert(
include_str!("../../../themes/gruvbox-dark.toml"), "gruvbox-light",
); include_str!("../../../themes/gruvbox-light.toml"),
m.insert( );
"gruvbox-light", m.insert(
include_str!("../../../themes/gruvbox-light.toml"), "catppuccin",
); include_str!("../../../themes/catppuccin.toml"),
m.insert( );
"catppuccin", m.insert("nord-dark", include_str!("../../../themes/nord-dark.toml"));
include_str!("../../../themes/catppuccin.toml"), m.insert(
); "solarized-dark",
m.insert("nord-dark", include_str!("../../../themes/nord-dark.toml")); include_str!("../../../themes/solarized-dark.toml"),
m.insert( );
"solarized-dark", m.insert(
include_str!("../../../themes/solarized-dark.toml"), "solarized-light",
); include_str!("../../../themes/solarized-light.toml"),
m.insert( );
"solarized-light", m.insert("dracula", include_str!("../../../themes/dracula.toml"));
include_str!("../../../themes/solarized-light.toml"), m.insert("monokai", include_str!("../../../themes/monokai.toml"));
); m.insert("onedark", include_str!("../../../themes/onedark.toml"));
m.insert("dracula", include_str!("../../../themes/dracula.toml")); m.insert(
m.insert("monokai", include_str!("../../../themes/monokai.toml")); "tokyonight",
m.insert("onedark", include_str!("../../../themes/onedark.toml")); include_str!("../../../themes/tokyonight.toml"),
m.insert( );
"tokyonight", m
include_str!("../../../themes/tokyonight.toml"),
);
m
};
} }

View File

@ -3,7 +3,6 @@ use std::{hash::Hash, time::Instant};
use anyhow::Result; use anyhow::Result;
use ratatui::{layout::Rect, Frame}; use ratatui::{layout::Rect, Frame};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use tokio::sync::mpsc::Sender;
use crate::{ use crate::{
action::Action, action::Action,
@ -18,7 +17,7 @@ use crate::{
remote_control::draw_remote_control, results::draw_results_list, remote_control::draw_remote_control, results::draw_results_list,
spinner::Spinner, spinner::Spinner,
}, },
television::{Message, Mode}, television::Mode,
utils::metadata::AppMetadata, utils::metadata::AppMetadata,
}; };
@ -61,7 +60,6 @@ impl Hash for ChannelState {
pub struct TvState { pub struct TvState {
pub mode: Mode, pub mode: Mode,
pub selected_entry: Option<Entry>, pub selected_entry: Option<Entry>,
pub results_area_height: u16,
pub results_picker: Picker, pub results_picker: Picker,
pub rc_picker: Picker, pub rc_picker: Picker,
pub channel_state: ChannelState, pub channel_state: ChannelState,
@ -74,7 +72,6 @@ impl TvState {
pub fn new( pub fn new(
mode: Mode, mode: Mode,
selected_entry: Option<Entry>, selected_entry: Option<Entry>,
results_area_height: u16,
results_picker: Picker, results_picker: Picker,
rc_picker: Picker, rc_picker: Picker,
channel_state: ChannelState, channel_state: ChannelState,
@ -84,7 +81,6 @@ impl TvState {
Self { Self {
mode, mode,
selected_entry, selected_entry,
results_area_height,
results_picker, results_picker,
rc_picker, rc_picker,
channel_state, channel_state,
@ -100,8 +96,8 @@ pub struct Ctx {
pub config: Config, pub config: Config,
pub colorscheme: Colorscheme, pub colorscheme: Colorscheme,
pub app_metadata: AppMetadata, pub app_metadata: AppMetadata,
pub tv_tx_handle: Sender<Message>,
pub instant: Instant, pub instant: Instant,
pub layout: Layout,
} }
impl Ctx { impl Ctx {
@ -110,16 +106,16 @@ impl Ctx {
config: Config, config: Config,
colorscheme: Colorscheme, colorscheme: Colorscheme,
app_metadata: AppMetadata, app_metadata: AppMetadata,
tv_tx_handle: Sender<Message>,
instant: Instant, instant: Instant,
layout: Layout,
) -> Self { ) -> Self {
Self { Self {
tv_state, tv_state,
config, config,
colorscheme, colorscheme,
app_metadata, app_metadata,
tv_tx_handle,
instant, 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<Layout> {
let selected_entry = ctx let selected_entry = ctx
.tv_state .tv_state
.selected_entry .selected_entry
@ -185,14 +181,6 @@ pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result<()> {
&ctx.colorscheme, &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 // results list
draw_results_list( draw_results_list(
f, f,
@ -259,5 +247,5 @@ pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result<()> {
)?; )?;
} }
Ok(()) Ok(layout)
} }

View File

@ -3,14 +3,10 @@ use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use crate::config::get_data_dir; 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<()> { pub fn init() -> Result<()> {
let directory = get_data_dir(); let directory = get_data_dir();
std::fs::create_dir_all(directory.clone())?; 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 log_file = std::fs::File::create(log_path)?;
let file_subscriber = fmt::layer() let file_subscriber = fmt::layer()
.with_file(true) .with_file(true)

View File

@ -2,7 +2,6 @@ use crate::channels::entry::{Entry, PreviewCommand};
use crate::preview::cache::PreviewCache; use crate::preview::cache::PreviewCache;
use crate::preview::{Preview, PreviewContent}; use crate::preview::{Preview, PreviewContent};
use crate::utils::command::shell_command; use crate::utils::command::shell_command;
use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
use regex::Regex; use regex::Regex;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
@ -11,12 +10,19 @@ use std::sync::Arc;
use tracing::debug; use tracing::debug;
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Default)] #[derive(Debug)]
pub struct CommandPreviewer { pub struct CommandPreviewer {
cache: Arc<Mutex<PreviewCache>>, cache: Arc<Mutex<PreviewCache>>,
config: CommandPreviewerConfig, config: CommandPreviewerConfig,
concurrent_preview_tasks: Arc<AtomicU8>, concurrent_preview_tasks: Arc<AtomicU8>,
in_flight_previews: Arc<Mutex<FxHashSet<String>>>, in_flight_previews: Arc<Mutex<FxHashSet<String>>>,
command_re: Regex,
}
impl Default for CommandPreviewer {
fn default() -> Self {
CommandPreviewer::new(None)
}
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -53,6 +59,7 @@ impl CommandPreviewer {
config, config,
concurrent_preview_tasks: Arc::new(AtomicU8::new(0)), concurrent_preview_tasks: Arc::new(AtomicU8::new(0)),
in_flight_previews: Arc::new(Mutex::new(FxHashSet::default())), 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 concurrent_tasks = self.concurrent_preview_tasks.clone();
let command = command.clone(); let command = command.clone();
let in_flight_previews = self.in_flight_previews.clone(); let in_flight_previews = self.in_flight_previews.clone();
let command_re = self.command_re.clone();
tokio::spawn(async move { tokio::spawn(async move {
try_preview( try_preview(
&command, &command,
@ -103,6 +111,7 @@ impl CommandPreviewer {
&cache, &cache,
&concurrent_tasks, &concurrent_tasks,
&in_flight_previews, &in_flight_previews,
&command_re,
); );
}); });
} else { } 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 /// Format the command with the entry name and provided placeholders
/// ///
/// # Example /// # Example
@ -131,11 +135,15 @@ lazy_static! {
/// delimiter: ":".to_string(), /// delimiter: ":".to_string(),
/// }; /// };
/// let entry = Entry::new("a:given:entry:to:preview".to_string(), PreviewType::Command(command.clone())); /// 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, &regex::Regex::new(r"\{(\d+)\}").unwrap());
/// ///
/// assert_eq!(formatted_command, "something 'a:given:entry:to:preview' 'entry' 'a'"); /// 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::<Vec<&str>>(); let parts = entry.name.split(&command.delimiter).collect::<Vec<&str>>();
debug!("Parts: {:?}", parts); debug!("Parts: {:?}", parts);
@ -143,7 +151,7 @@ pub fn format_command(command: &PreviewCommand, entry: &Entry) -> String {
.command .command
.replace("{}", format!("'{}'", entry.name).as_str()); .replace("{}", format!("'{}'", entry.name).as_str());
formatted_command = COMMAND_PLACEHOLDER_REGEX formatted_command = command_re
.replace_all(&formatted_command, |caps: &regex::Captures| { .replace_all(&formatted_command, |caps: &regex::Captures| {
let index = let index =
caps.get(1).unwrap().as_str().parse::<usize>().unwrap(); caps.get(1).unwrap().as_str().parse::<usize>().unwrap();
@ -160,9 +168,10 @@ pub fn try_preview(
cache: &Arc<Mutex<PreviewCache>>, cache: &Arc<Mutex<PreviewCache>>,
concurrent_tasks: &Arc<AtomicU8>, concurrent_tasks: &Arc<AtomicU8>,
in_flight_previews: &Arc<Mutex<FxHashSet<String>>>, in_flight_previews: &Arc<Mutex<FxHashSet<String>>>,
command_re: &Regex,
) { ) {
debug!("Computing preview for {:?}", entry.name); 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); debug!("Formatted preview command: {:?}", command);
let child = shell_command() let child = shell_command()
@ -212,7 +221,11 @@ mod tests {
"an:entry:to:preview".to_string(), "an:entry:to:preview".to_string(),
PreviewType::Command(command.clone()), 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!( assert_eq!(
formatted_command, formatted_command,
@ -230,7 +243,11 @@ mod tests {
"an:entry:to:preview".to_string(), "an:entry:to:preview".to_string(),
PreviewType::Command(command.clone()), 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"); assert_eq!(formatted_command, "something");
} }
@ -245,7 +262,11 @@ mod tests {
"an:entry:to:preview".to_string(), "an:entry:to:preview".to_string(),
PreviewType::Command(command.clone()), 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'"); assert_eq!(formatted_command, "something 'an:entry:to:preview'");
} }
@ -260,7 +281,11 @@ mod tests {
"an:entry:to:preview".to_string(), "an:entry:to:preview".to_string(),
PreviewType::Command(command.clone()), 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'"); assert_eq!(formatted_command, "something 'an' -t 'to'");
} }

View File

@ -8,12 +8,13 @@ use tracing::{debug, warn};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::draw::Ctx; use crate::draw::Ctx;
use crate::screen::layout::Layout;
use crate::{action::Action, draw::draw, tui::Tui}; use crate::{action::Action, draw::draw, tui::Tui};
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)] #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
pub enum RenderingTask { pub enum RenderingTask {
ClearScreen, ClearScreen,
Render(Ctx), Render(Box<Ctx>),
Resize(u16, u16), Resize(u16, u16),
Resume, Resume,
Suspend, 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( pub async fn render(
mut render_rx: mpsc::UnboundedReceiver<RenderingTask>, mut render_rx: mpsc::UnboundedReceiver<RenderingTask>,
action_tx: mpsc::UnboundedSender<Action>, action_tx: mpsc::UnboundedSender<Action>,
ui_state_tx: mpsc::UnboundedSender<UiState>,
is_output_tty: bool, is_output_tty: bool,
) -> Result<()> { ) -> Result<()> {
let stream = if is_output_tty { let stream = if is_output_tty {
@ -73,13 +86,19 @@ pub async fn render(
if size.width.checked_mul(size.height).is_some() { if size.width.checked_mul(size.height).is_some() {
queue!(stderr(), BeginSynchronizedUpdate).ok(); queue!(stderr(), BeginSynchronizedUpdate).ok();
tui.terminal.draw(|frame| { tui.terminal.draw(|frame| {
if let Err(err) = match draw(&context, frame, frame.area()) {
draw(&context, frame, frame.area()) Ok(layout) => {
{ if layout != context.layout {
warn!("Failed to draw: {:?}", err); let _ = ui_state_tx
let _ = action_tx.send(Action::Error( .send(UiState::new(layout));
format!("Failed to draw: {err:?}"), }
)); }
Err(err) => {
warn!("Failed to draw: {:?}", err);
let _ = action_tx.send(Action::Error(
format!("Failed to draw: {err:?}"),
));
}
} }
})?; })?;
execute!(stderr(), EndSynchronizedUpdate).ok(); execute!(stderr(), EndSynchronizedUpdate).ok();

View File

@ -29,7 +29,7 @@ impl Default for Dimensions {
} }
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct HelpBarLayout { pub struct HelpBarLayout {
pub left: Rect, pub left: Rect,
pub middle: Rect, pub middle: Rect,
@ -82,6 +82,7 @@ impl Display for PreviewTitlePosition {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Layout { pub struct Layout {
pub help_bar: Option<HelpBarLayout>, pub help_bar: Option<HelpBarLayout>,
pub results: Rect, pub results: Rect,
@ -90,6 +91,17 @@ pub struct Layout {
pub remote_control: Option<Rect>, pub remote_control: Option<Rect>,
} }
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 { impl Layout {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(

View File

@ -10,6 +10,7 @@ use crate::draw::{ChannelState, Ctx, TvState};
use crate::input::convert_action_to_input_request; use crate::input::convert_action_to_input_request;
use crate::picker::Picker; use crate::picker::Picker;
use crate::preview::{PreviewState, Previewer}; use crate::preview::{PreviewState, Previewer};
use crate::render::UiState;
use crate::screen::colors::Colorscheme; use crate::screen::colors::Colorscheme;
use crate::screen::layout::InputPosition; use crate::screen::layout::InputPosition;
use crate::screen::spinner::{Spinner, SpinnerState}; use crate::screen::spinner::{Spinner, SpinnerState};
@ -20,7 +21,7 @@ use anyhow::Result;
use rustc_hash::{FxBuildHasher, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashSet};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; 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)] #[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
pub enum Mode { pub enum Mode {
@ -38,7 +39,6 @@ pub struct Television {
pub current_pattern: String, pub current_pattern: String,
pub results_picker: Picker, pub results_picker: Picker,
pub rc_picker: Picker, pub rc_picker: Picker,
results_area_height: u16,
pub previewer: Previewer, pub previewer: Previewer,
pub preview_state: PreviewState, pub preview_state: PreviewState,
pub spinner: Spinner, pub spinner: Spinner,
@ -46,16 +46,7 @@ pub struct Television {
pub app_metadata: AppMetadata, pub app_metadata: AppMetadata,
pub colorscheme: Colorscheme, pub colorscheme: Colorscheme,
pub ticks: u64, pub ticks: u64,
// these are really here as a means to communicate between the render thread pub ui_state: UiState,
// and the main thread to update `Television`'s state without needing to pass
// a mutable reference to `draw`
pub inner_rx: Receiver<Message>,
pub inner_tx: Sender<Message>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Message {
ResultListHeightChanged(u16),
} }
impl Television { impl Television {
@ -87,8 +78,6 @@ impl Television {
channel.find(&input.unwrap_or(EMPTY_STRING.to_string())); channel.find(&input.unwrap_or(EMPTY_STRING.to_string()));
let spinner = Spinner::default(); let spinner = Spinner::default();
// capacity is quite arbitrary here, we can adjust it later
let (inner_tx, inner_rx) = tokio::sync::mpsc::channel(10);
Self { Self {
action_tx, action_tx,
@ -101,7 +90,6 @@ impl Television {
current_pattern: EMPTY_STRING.to_string(), current_pattern: EMPTY_STRING.to_string(),
results_picker, results_picker,
rc_picker: Picker::default(), rc_picker: Picker::default(),
results_area_height: 0,
previewer, previewer,
preview_state: PreviewState::default(), preview_state: PreviewState::default(),
spinner, spinner,
@ -109,11 +97,14 @@ impl Television {
app_metadata, app_metadata,
colorscheme, colorscheme,
ticks: 0, ticks: 0,
inner_rx, ui_state: UiState::default(),
inner_tx,
} }
} }
pub fn update_ui_state(&mut self, ui_state: UiState) {
self.ui_state = ui_state;
}
pub fn init_remote_control(&mut self) { pub fn init_remote_control(&mut self) {
let cable_channels = load_cable_channels().unwrap_or_default(); let cable_channels = load_cable_channels().unwrap_or_default();
let builtin_channels = load_builtin_channels(Some( let builtin_channels = load_builtin_channels(Some(
@ -134,7 +125,6 @@ impl Television {
let tv_state = TvState::new( let tv_state = TvState::new(
self.mode, self.mode,
self.get_selected_entry(Some(Mode::Channel)), self.get_selected_entry(Some(Mode::Channel)),
self.results_area_height,
self.results_picker.clone(), self.results_picker.clone(),
self.rc_picker.clone(), self.rc_picker.clone(),
channel_state, channel_state,
@ -147,9 +137,9 @@ impl Television {
self.config.clone(), self.config.clone(),
self.colorscheme.clone(), self.colorscheme.clone(),
self.app_metadata.clone(), self.app_metadata.clone(),
self.inner_tx.clone(),
// now timestamp // now timestamp
std::time::Instant::now(), std::time::Instant::now(),
self.ui_state.layout,
) )
} }
@ -229,7 +219,7 @@ impl Television {
picker.select_prev( picker.select_prev(
step, step,
result_count as usize, 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( picker.select_next(
step, step,
result_count as usize, 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 // preview content
if let Some(preview) = self.previewer.preview(selected_entry) { 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 { if self.preview_state.preview.title != preview.title {
self.preview_state.update( self.preview_state.update(
preview, preview,
@ -323,7 +314,13 @@ impl Television {
.line_number .line_number
.unwrap_or(0) .unwrap_or(0)
.saturating_sub( .saturating_sub(
(self.results_area_height / 2).into(), (self
.ui_state
.layout
.preview_window
.map_or(0, |w| w.height)
/ 2)
.into(),
) )
.try_into()?, .try_into()?,
selected_entry selected_entry
@ -348,7 +345,7 @@ impl Television {
} }
self.results_picker.entries = self.channel.results( 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(), u32::try_from(self.results_picker.offset()).unwrap(),
); );
self.results_picker.total_items = self.channel.result_count(); self.results_picker.total_items = self.channel.result_count();
@ -364,7 +361,7 @@ impl Television {
self.rc_picker.entries = self.remote_control.results( self.rc_picker.entries = self.remote_control.results(
// this'll be more than the actual rc height but it's fine // 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(), u32::try_from(self.rc_picker.offset()).unwrap(),
); );
self.rc_picker.total_items = self.remote_control.total_count(); self.rc_picker.total_items = self.remote_control.total_count();
@ -513,11 +510,25 @@ impl Television {
} }
Action::SelectNextPage => { Action::SelectNextPage => {
self.preview_state.reset(); 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 => { Action::SelectPrevPage => {
self.preview_state.reset(); 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::ScrollPreviewDown => self.preview_state.scroll_down(1),
Action::ScrollPreviewUp => self.preview_state.scroll_up(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`. /// This function may return an Action that'll be processed by the parent `App`.
pub fn update(&mut self, action: &Action) -> Result<Option<Action>> { 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)?;
}
self.handle_action(action)?; self.handle_action(action)?;
self.update_results_picker_state(); self.update_results_picker_state();

View File

@ -6,9 +6,9 @@ use std::io::BufReader;
use std::io::Read; use std::io::Read;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::OnceLock;
use ignore::{overrides::Override, types::TypesBuilder, WalkBuilder}; use ignore::{overrides::Override, types::TypesBuilder, WalkBuilder};
use lazy_static::lazy_static;
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::utils::strings::{ use crate::utils::strings::{
@ -61,8 +61,10 @@ where
} }
} }
lazy_static::lazy_static! { pub static DEFAULT_NUM_THREADS: OnceLock<usize> = OnceLock::new();
pub static ref DEFAULT_NUM_THREADS: usize = default_num_threads().into();
pub fn get_default_num_threads() -> usize {
*DEFAULT_NUM_THREADS.get_or_init(default_num_threads)
} }
pub fn walk_builder( pub fn walk_builder(
@ -143,340 +145,345 @@ where
path.as_ref() path.as_ref()
.extension() .extension()
.and_then(|ext| ext.to_str()) .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! { pub static KNOWN_TEXT_FILE_EXTENSIONS: OnceLock<FxHashSet<&'static str>> =
static ref KNOWN_TEXT_FILE_EXTENSIONS: FxHashSet<&'static str> = [ OnceLock::new();
"ada",
"adb", pub fn get_known_text_file_extensions() -> &'static FxHashSet<&'static str> {
"ads", KNOWN_TEXT_FILE_EXTENSIONS.get_or_init(|| {
"applescript", [
"as", "ada",
"asc", "adb",
"ascii", "ads",
"ascx", "applescript",
"asm", "as",
"asmx", "asc",
"asp", "ascii",
"aspx", "ascx",
"atom", "asm",
"au3", "asmx",
"awk", "asp",
"bas", "aspx",
"bash", "atom",
"bashrc", "au3",
"bat", "awk",
"bbcolors", "bas",
"bcp", "bash",
"bdsgroup", "bashrc",
"bdsproj", "bat",
"bib", "bbcolors",
"bowerrc", "bcp",
"c", "bdsgroup",
"cbl", "bdsproj",
"cc", "bib",
"cfc", "bowerrc",
"cfg", "c",
"cfm", "cbl",
"cfml", "cc",
"cgi", "cfc",
"cjs", "cfg",
"clj", "cfm",
"cljs", "cfml",
"cls", "cgi",
"cmake", "cjs",
"cmd", "clj",
"cnf", "cljs",
"cob", "cls",
"code-snippets", "cmake",
"coffee", "cmd",
"coffeekup", "cnf",
"conf", "cob",
"cp", "code-snippets",
"cpp", "coffee",
"cpt", "coffeekup",
"cpy", "conf",
"crt", "cp",
"cs", "cpp",
"csh", "cpt",
"cson", "cpy",
"csproj", "crt",
"csr", "cs",
"css", "csh",
"csslintrc", "cson",
"csv", "csproj",
"ctl", "csr",
"curlrc", "css",
"cxx", "csslintrc",
"d", "csv",
"dart", "ctl",
"dfm", "curlrc",
"diff", "cxx",
"dof", "d",
"dpk", "dart",
"dpr", "dfm",
"dproj", "diff",
"dtd", "dof",
"eco", "dpk",
"editorconfig", "dpr",
"ejs", "dproj",
"el", "dtd",
"elm", "eco",
"emacs", "editorconfig",
"eml", "ejs",
"ent", "el",
"erb", "elm",
"erl", "emacs",
"eslintignore", "eml",
"eslintrc", "ent",
"ex", "erb",
"exs", "erl",
"f", "eslintignore",
"f03", "eslintrc",
"f77", "ex",
"f90", "exs",
"f95", "f",
"fish", "f03",
"for", "f77",
"fpp", "f90",
"frm", "f95",
"fs", "fish",
"fsproj", "for",
"fsx", "fpp",
"ftn", "frm",
"gemrc", "fs",
"gemspec", "fsproj",
"gitattributes", "fsx",
"gitconfig", "ftn",
"gitignore", "gemrc",
"gitkeep", "gemspec",
"gitmodules", "gitattributes",
"go", "gitconfig",
"gpp", "gitignore",
"gradle", "gitkeep",
"graphql", "gitmodules",
"groovy", "go",
"groupproj", "gpp",
"grunit", "gradle",
"gtmpl", "graphql",
"gvimrc", "groovy",
"h", "groupproj",
"haml", "grunit",
"hbs", "gtmpl",
"hgignore", "gvimrc",
"hh", "h",
"hpp", "haml",
"hrl", "hbs",
"hs", "hgignore",
"hta", "hh",
"htaccess", "hpp",
"htc", "hrl",
"htm", "hs",
"html", "hta",
"htpasswd", "htaccess",
"hxx", "htc",
"iced", "htm",
"iml", "html",
"inc", "htpasswd",
"inf", "hxx",
"info", "iced",
"ini", "iml",
"ino", "inc",
"int", "inf",
"irbrc", "info",
"itcl", "ini",
"itermcolors", "ino",
"itk", "int",
"jade", "irbrc",
"java", "itcl",
"jhtm", "itermcolors",
"jhtml", "itk",
"js", "jade",
"jscsrc", "java",
"jshintignore", "jhtm",
"jshintrc", "jhtml",
"json", "js",
"json5", "jscsrc",
"jsonld", "jshintignore",
"jsp", "jshintrc",
"jspx", "json",
"jsx", "json5",
"ksh", "jsonld",
"less", "jsp",
"lhs", "jspx",
"lisp", "jsx",
"log", "ksh",
"ls", "less",
"lsp", "lhs",
"lua", "lisp",
"m", "log",
"m4", "ls",
"mak", "lsp",
"map", "lua",
"markdown", "m",
"master", "m4",
"md", "mak",
"mdown", "map",
"mdwn", "markdown",
"mdx", "master",
"metadata", "md",
"mht", "mdown",
"mhtml", "mdwn",
"mjs", "mdx",
"mk", "metadata",
"mkd", "mht",
"mkdn", "mhtml",
"mkdown", "mjs",
"ml", "mk",
"mli", "mkd",
"mm", "mkdn",
"mxml", "mkdown",
"nfm", "ml",
"nfo", "mli",
"noon", "mm",
"npmignore", "mxml",
"npmrc", "nfm",
"nuspec", "nfo",
"nvmrc", "noon",
"ops", "npmignore",
"pas", "npmrc",
"pasm", "nuspec",
"patch", "nvmrc",
"pbxproj", "ops",
"pch", "pas",
"pem", "pasm",
"pg", "patch",
"php", "pbxproj",
"php3", "pch",
"php4", "pem",
"php5", "pg",
"phpt", "php",
"phtml", "php3",
"pir", "php4",
"pl", "php5",
"pm", "phpt",
"pmc", "phtml",
"pod", "pir",
"pot", "pl",
"prettierrc", "pm",
"properties", "pmc",
"props", "pod",
"pt", "pot",
"pug", "prettierrc",
"purs", "properties",
"py", "props",
"pyx", "pt",
"r", "pug",
"rake", "purs",
"rb", "py",
"rbw", "pyx",
"rc", "r",
"rdoc", "rake",
"rdoc_options", "rb",
"resx", "rbw",
"rexx", "rc",
"rhtml", "rdoc",
"rjs", "rdoc_options",
"rlib", "resx",
"ron", "rexx",
"rs", "rhtml",
"rss", "rjs",
"rst", "rlib",
"rtf", "ron",
"rvmrc", "rs",
"rxml", "rss",
"s", "rst",
"sass", "rtf",
"scala", "rvmrc",
"scm", "rxml",
"scss", "s",
"seestyle", "sass",
"sh", "scala",
"shtml", "scm",
"sln", "scss",
"sls", "seestyle",
"spec", "sh",
"sql", "shtml",
"sqlite", "sln",
"sqlproj", "sls",
"srt", "spec",
"ss", "sql",
"sss", "sqlite",
"st", "sqlproj",
"strings", "srt",
"sty", "ss",
"styl", "sss",
"stylus", "st",
"sub", "strings",
"sublime-build", "sty",
"sublime-commands", "styl",
"sublime-completions", "stylus",
"sublime-keymap", "sub",
"sublime-macro", "sublime-build",
"sublime-menu", "sublime-commands",
"sublime-project", "sublime-completions",
"sublime-settings", "sublime-keymap",
"sublime-workspace", "sublime-macro",
"sv", "sublime-menu",
"svc", "sublime-project",
"svg", "sublime-settings",
"swift", "sublime-workspace",
"t", "sv",
"tcl", "svc",
"tcsh", "svg",
"terminal", "swift",
"tex", "t",
"text", "tcl",
"textile", "tcsh",
"tg", "terminal",
"tk", "tex",
"tmLanguage", "text",
"tmpl", "textile",
"tmTheme", "tg",
"toml", "tk",
"tpl", "tmLanguage",
"ts", "tmpl",
"tsv", "tmTheme",
"tsx", "toml",
"tt", "tpl",
"tt2", "ts",
"ttml", "tsv",
"twig", "tsx",
"txt", "tt",
"v", "tt2",
"vb", "ttml",
"vbproj", "twig",
"vbs", "txt",
"vcproj", "v",
"vcxproj", "vb",
"vh", "vbproj",
"vhd", "vbs",
"vhdl", "vcproj",
"vim", "vcxproj",
"viminfo", "vh",
"vimrc", "vhd",
"vm", "vhdl",
"vue", "vim",
"webapp", "viminfo",
"webmanifest", "vimrc",
"wsc", "vm",
"x-php", "vue",
"xaml", "webapp",
"xht", "webmanifest",
"xhtml", "wsc",
"xml", "x-php",
"xs", "xaml",
"xsd", "xht",
"xsl", "xhtml",
"xslt", "xml",
"y", "xs",
"yaml", "xsd",
"yml", "xsl",
"zsh", "xslt",
"zshrc", "y",
] "yaml",
.iter() "yml",
.copied() "zsh",
.collect(); "zshrc",
]
.iter()
.copied()
.collect()
})
} }

View File

@ -1,5 +1,3 @@
use lazy_static::lazy_static;
/// Returns the index of the next character boundary in the given string. /// 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. /// 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)) 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 EMPTY_STRING: &str = "";
pub const TAB_WIDTH: usize = 4; 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 TAB_CHARACTER: char = '\t';
const LINE_FEED_CHARACTER: char = '\x0A'; const LINE_FEED_CHARACTER: char = '\x0A';
const DELETE_CHARACTER: char = '\x7F'; const DELETE_CHARACTER: char = '\x7F';
@ -304,7 +299,7 @@ pub fn replace_non_printable(
| BOM_CHARACTER | BOM_CHARACTER
if config.replace_control_characters => if config.replace_control_characters =>
{ {
output.push(*NULL_SYMBOL); output.push(NULL_SYMBOL);
} }
// CJK Unified Ideographs // CJK Unified Ideographs
// ex: 解 // ex: 解
@ -337,13 +332,13 @@ pub fn replace_non_printable(
} }
// Unicode characters above 0x0700 seem unstable with ratatui // Unicode characters above 0x0700 seem unstable with ratatui
c if c > '\u{0700}' => { c if c > '\u{0700}' => {
output.push(*NULL_SYMBOL); output.push(NULL_SYMBOL);
} }
// everything else // everything else
c => output.push(c), c => output.push(c),
} }
} else { } else {
output.push(*NULL_SYMBOL); output.push(NULL_SYMBOL);
idx += 1; idx += 1;
} }
} }

View File

@ -224,7 +224,6 @@ pub fn compute_highlights_for_line<'a>(
// SOFTWARE. // SOFTWARE.
use directories::BaseDirs; use directories::BaseDirs;
use lazy_static::lazy_static;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use std::env; 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 { 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()) .unwrap_or_else(|_| HighlightingAssets::from_binary())
} }

View File

@ -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 /// 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 /// of available threads cannot be determined. It will also never use more than 32 threads to avoid
/// startup overhead. /// 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 // default to 1 thread if we can't determine the number of available threads
let default = NonZeroUsize::MIN; let default = NonZeroUsize::MIN;
// never use more than 32 threads to avoid startup overhead // never use more than 32 threads to avoid startup overhead
@ -14,4 +14,5 @@ pub fn default_num_threads() -> NonZeroUsize {
std::thread::available_parallelism() std::thread::available_parallelism()
.unwrap_or(default) .unwrap_or(default)
.min(limit) .min(limit)
.get()
} }