refactoring

This commit is contained in:
Alexandre Pasmantier 2024-10-16 00:29:30 +02:00
parent 44b8a8580b
commit e36f0d450f
38 changed files with 462 additions and 208 deletions

26
Cargo.lock generated
View File

@ -626,9 +626,9 @@ dependencies = [
[[package]]
name = "devicons"
version = "0.6.8"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f85ba42bea802686a1532ab64b4b885b2df6e0f931d523b3e64a31ce68f4888d"
checksum = "f44d7af4053366d3bdc831abed4fdbf3adcd8e8f6401b52177c1fd2b79100083"
dependencies = [
"lazy_static",
]
@ -1896,9 +1896,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
version = "2.7.13"
version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9"
checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442"
dependencies = [
"memchr",
"thiserror",
@ -1907,9 +1907,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.7.13"
version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0"
checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd"
dependencies = [
"pest",
"pest_generator",
@ -1917,9 +1917,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.7.13"
version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e"
checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e"
dependencies = [
"pest",
"pest_meta",
@ -1930,9 +1930,9 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.7.13"
version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f"
checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d"
dependencies = [
"once_cell",
"pest",
@ -2168,9 +2168,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
[[package]]
name = "ryu"
@ -2419,7 +2419,7 @@ dependencies = [
[[package]]
name = "television"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"anyhow",
"better-panic",

View File

@ -1,6 +1,6 @@
[package]
name = "television"
version = "0.1.3"
version = "0.1.4"
edition = "2021"
description = "The revolution will be televised."
license = "MIT"
@ -75,6 +75,12 @@ anyhow = "1.0.86"
vergen-gix = { version = "1.0.0", features = ["build", "cargo"] }
[profile.staging]
inherits = "dev"
opt-level = 3
debug = true
lto = false
[profile.release]
opt-level = 3
@ -82,7 +88,7 @@ debug = "none"
strip = "symbols"
debug-assertions = false
overflow-checks = false
lto = "fat"
lto = "thin"
panic = "abort"
[target.'cfg(target_os = "macos")'.dependencies]

34
TODO.md
View File

@ -1,10 +1,17 @@
# bugs
- [x] index out of bounds when resizing the terminal to a very small size
- [x] meta previews in cache are not terminal size aware
# tasks
- [x] preview navigation
- [ ] add a way to open the selected file in the default editor (or maybe that should be achieved using pipes?)
- [ ] add a way to open the selected file in the default editor (or maybe that
should be achieved using pipes?)
- [x] maybe filter out image types etc. for now
- [x] return selected entry on exit
- [x] piping output to another command
- [x] piping custom entries from stdin (e.g. `ls | tv`, what bout choosing previewers in that case? Some AUTO mode?)
- [x] piping custom entries from stdin (e.g. `ls | tv`, what bout choosing
previewers in that case? Some AUTO mode?)
- [x] documentation
## improvements
- [x] async finder initialization
@ -12,29 +19,36 @@
- [x] use nucleo for env
- [ ] better keymaps
- [ ] mutualize placeholder previews in cache (really not a priority)
- [x] better abstractions for channels / separation / isolation so that others can contribute new ones easily
- [x] better abstractions for channels / separation / isolation so that others
can contribute new ones easily
- [ ] channel selection in the UI (separate menu or top panel or something)
- [x] only render highlighted lines that are visible
- [x] only ever read a portion of the file for the temp preview
- [ ] make layout an attribute of the channel?
- [x] I feel like the finder abstraction is a superfluous layer, maybe just use the channel directly?
- [x] support for images is implemented but do we really want that in the core? it's quite heavy
- [ ] use an icon for the prompt
- [ ] profile using dyn Traits instead of an enum for channels (might degrade performance by storing on the heap)
- [x] I feel like the finder abstraction is a superfluous layer, maybe just use
the channel directly?
- [x] support for images is implemented but do we really want that in the core?
it's quite heavy
- [x] shrink entry names that are too long (from the middle)
## feature ideas
- [ ] some sort of iterative fuzzy file explorer (preview contents of folders on the right, enter to go in etc.) maybe
with mixed previews of files and folders
- [ ] some sort of iterative fuzzy file explorer (preview contents of folders
on the right, enter to go in etc.) maybe with mixed previews of files and
folders
- [x] environment variables
- [x] aliases
- [ ] shell history
- [x] text
- [ ] text in documents (pdfs, archives, ...) (rga, adapters) https://github.com/jrmuizel/pdf-extract
- [ ] text in documents (pdfs, archives, ...) (rga, adapters)
https://github.com/jrmuizel/pdf-extract
- [x] fd
- [ ] recent directories
- [ ] git (commits, branches, status, diff, ...)
- [ ] makefile commands
- [ ] remote files (s3, ...)
- [ ] custom actions as part of a channel (mappable)
- [ ] from one set of entries to another? (fuzzy-refine)
- [ ] from one set of entries to another? (fuzzy-refine) maybe piping
tv with itself?
- [ ] add a way of copying the selected entry name/value to the clipboard

View File

@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use strum::Display;
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
pub enum Action {
pub(crate) enum Action {
// input actions
AddInputChar(char),
DeletePrevChar,

View File

@ -67,7 +67,7 @@ use crate::{
render::{render, RenderingTask},
};
pub struct App {
pub(crate) struct App {
config: Config,
// maybe move these two into config instead of passing them
// via the cli?
@ -87,7 +87,7 @@ pub struct App {
#[derive(
Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize,
)]
pub enum Mode {
pub(crate) enum Mode {
#[default]
Help,
Input,
@ -96,7 +96,7 @@ pub enum Mode {
}
impl App {
pub fn new(
pub(crate) fn new(
channel: CliTvChannel,
tick_rate: f64,
frame_rate: f64,

View File

@ -84,7 +84,7 @@ pub trait TelevisionChannel: Send {
///
#[allow(dead_code, clippy::module_name_repetitions)]
#[derive(CliChannel)]
pub enum AvailableChannels {
pub(crate) enum AvailableChannels {
Env(env::Channel),
Files(files::Channel),
Text(text::Channel),

View File

@ -16,7 +16,7 @@ struct Alias {
value: String,
}
pub struct Channel {
pub(crate) struct Channel {
matcher: Nucleo<Alias>,
last_pattern: String,
file_icon: FileIcon,
@ -66,7 +66,7 @@ fn get_raw_aliases(shell: &str) -> Vec<String> {
}
impl Channel {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
let raw_shell = get_current_shell().unwrap_or("bash".to_string());
let shell = raw_shell.split('/').last().unwrap();
debug!("Current shell: {}", shell);

View File

@ -17,7 +17,7 @@ struct EnvVar {
}
#[allow(clippy::module_name_repetitions)]
pub struct Channel {
pub(crate) struct Channel {
matcher: Nucleo<EnvVar>,
last_pattern: String,
file_icon: FileIcon,
@ -29,7 +29,7 @@ const NUM_THREADS: usize = 1;
const FILE_ICON_STR: &str = "config";
impl Channel {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
let matcher = Nucleo::new(
Config::DEFAULT,
Arc::new(|| {}),

View File

@ -13,7 +13,7 @@ use crate::fuzzy::MATCHER;
use crate::previewers::PreviewType;
use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS};
pub struct Channel {
pub(crate) struct Channel {
matcher: Nucleo<DirEntry>,
last_pattern: String,
result_count: u32,
@ -21,7 +21,7 @@ pub struct Channel {
}
impl Channel {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
let matcher = Nucleo::new(
Config::DEFAULT.match_paths(),
Arc::new(|| {}),

View File

@ -1,3 +1,4 @@
use std::path::Path;
use std::{io::BufRead, sync::Arc};
use devicons::FileIcon;
@ -10,7 +11,7 @@ use crate::previewers::PreviewType;
use super::TelevisionChannel;
pub struct Channel {
pub(crate) struct Channel {
matcher: Nucleo<String>,
last_pattern: String,
result_count: u32,
@ -21,7 +22,7 @@ pub struct Channel {
const NUM_THREADS: usize = 2;
impl Channel {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
let mut lines = Vec::new();
for line in std::io::stdin().lock().lines().map_while(Result::ok) {
debug!("Read line: {:?}", line);
@ -95,6 +96,12 @@ impl TelevisionChannel for Channel {
let indices = indices.drain(..);
let content = item.matcher_columns[0].to_string();
let path = Path::new(&content);
let icon = if path.try_exists().unwrap_or(false) {
FileIcon::from(path)
} else {
icon
};
Entry::new(content.clone(), PreviewType::Basic)
.with_name_match_ranges(
indices.map(|i| (i, i + 1)).collect(),
@ -108,8 +115,19 @@ impl TelevisionChannel for Channel {
let snapshot = self.matcher.snapshot();
snapshot.get_matched_item(index).map(|item| {
let content = item.matcher_columns[0].to_string();
Entry::new(content.clone(), PreviewType::Basic)
.with_icon(self.icon)
// if we recognize a file path, use a file icon
// and set the preview type to "Files"
let path = Path::new(&content);
if path.is_file() {
Entry::new(content.clone(), PreviewType::Files)
.with_icon(FileIcon::from(path))
} else if path.is_dir() {
Entry::new(content.clone(), PreviewType::Directory)
.with_icon(FileIcon::from(path))
} else {
Entry::new(content.clone(), PreviewType::Basic)
.with_icon(self.icon)
}
})
}

View File

@ -39,7 +39,7 @@ impl CandidateLine {
}
#[allow(clippy::module_name_repetitions)]
pub struct Channel {
pub(crate) struct Channel {
matcher: Nucleo<CandidateLine>,
last_pattern: String,
result_count: u32,
@ -47,7 +47,7 @@ pub struct Channel {
}
impl Channel {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
let matcher = Nucleo::new(Config::DEFAULT, Arc::new(|| {}), None, 1);
// start loading files in the background
tokio::spawn(load_candidates(
@ -139,6 +139,7 @@ impl TelevisionChannel for Channel {
+ ":"
+ &item.data.line_number.to_string(),
)
.with_icon(FileIcon::from(item.data.path.as_path()))
.with_line_number(item.data.line_number)
})
}

View File

@ -5,7 +5,7 @@ use crate::config::{get_config_dir, get_data_dir};
#[derive(Parser, Debug)]
#[command(author, version = version(), about)]
pub struct Cli {
pub(crate) struct Cli {
/// Which channel shall we watch?
#[arg(value_enum, default_value = "files")]
pub channel: CliTvChannel,
@ -28,7 +28,7 @@ const VERSION_MESSAGE: &str = concat!(
")"
);
pub fn version() -> String {
pub(crate) fn version() -> String {
let author = clap::crate_authors!();
// let current_exe_path = PathBuf::from(clap::crate_name!()).display().to_string();

View File

@ -20,7 +20,7 @@ const CONFIG: &str = include_str!("../../.config/config.toml");
#[allow(dead_code, clippy::module_name_repetitions)]
#[derive(Clone, Debug, Deserialize, Default)]
pub struct AppConfig {
pub(crate) struct AppConfig {
#[serde(default)]
pub data_dir: PathBuf,
#[serde(default)]
@ -28,7 +28,7 @@ pub struct AppConfig {
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct Config {
pub(crate) struct Config {
#[allow(clippy::struct_field_names)]
#[serde(default, flatten)]
pub config: AppConfig,
@ -52,7 +52,7 @@ lazy_static! {
}
impl Config {
pub fn new() -> Result<Self, config::ConfigError> {
pub(crate) fn new() -> Result<Self, config::ConfigError> {
//let default_config: Config = json5::from_str(CONFIG).unwrap();
let default_config: Config = toml::from_str(CONFIG).unwrap();
let data_dir = get_data_dir();
@ -101,7 +101,7 @@ impl Config {
}
}
pub fn get_data_dir() -> PathBuf {
pub(crate) fn get_data_dir() -> PathBuf {
let directory = if let Some(s) = DATA_FOLDER.clone() {
s
} else if let Some(proj_dirs) = project_directory() {
@ -112,7 +112,7 @@ pub fn get_data_dir() -> PathBuf {
directory
}
pub fn get_config_dir() -> PathBuf {
pub(crate) fn get_config_dir() -> PathBuf {
let directory = if let Some(s) = CONFIG_FOLDER.clone() {
s
} else if let Some(proj_dirs) = project_directory() {
@ -128,7 +128,7 @@ fn project_directory() -> Option<ProjectDirs> {
}
#[derive(Clone, Debug, Default, Deref, DerefMut)]
pub struct KeyBindings(pub HashMap<Mode, HashMap<Key, Action>>);
pub(crate) struct KeyBindings(pub HashMap<Mode, HashMap<Key, Action>>);
impl<'de> Deserialize<'de> for KeyBindings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
@ -236,7 +236,7 @@ fn parse_key_code_with_modifiers(
}
#[allow(dead_code)]
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
pub(crate) fn key_event_to_string(key_event: &KeyEvent) -> String {
let char;
let key_code = match key_event.code {
KeyCode::Backspace => "backspace",
@ -299,7 +299,7 @@ pub fn key_event_to_string(key_event: &KeyEvent) -> String {
key
}
pub fn parse_key(raw: &str) -> Result<Key, String> {
pub(crate) fn parse_key(raw: &str) -> Result<Key, String> {
if raw.chars().filter(|c| *c == '>').count()
!= raw.chars().filter(|c| *c == '<').count()
{
@ -316,7 +316,7 @@ pub fn parse_key(raw: &str) -> Result<Key, String> {
Ok(convert_raw_event_to_key(key_event))
}
pub fn default_num_threads() -> NonZeroUsize {
pub(crate) fn default_num_threads() -> NonZeroUsize {
// 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
@ -328,7 +328,7 @@ pub fn default_num_threads() -> NonZeroUsize {
}
#[derive(Clone, Debug, Default, Deref, DerefMut)]
pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
pub(crate) struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
impl<'de> Deserialize<'de> for Styles {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
@ -355,7 +355,7 @@ impl<'de> Deserialize<'de> for Styles {
}
}
pub fn parse_style(line: &str) -> Style {
pub(crate) fn parse_style(line: &str) -> Style {
let (foreground, background) =
line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
let foreground = process_color_string(foreground);

View File

@ -3,7 +3,7 @@ use devicons::FileIcon;
use crate::previewers::PreviewType;
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Entry {
pub(crate) struct Entry {
pub name: String,
display_name: Option<String>,
pub value: Option<String>,
@ -15,7 +15,7 @@ pub struct Entry {
}
impl Entry {
pub fn new(name: String, preview_type: PreviewType) -> Self {
pub(crate) fn new(name: String, preview_type: PreviewType) -> Self {
Self {
name,
display_name: None,
@ -28,17 +28,17 @@ impl Entry {
}
}
pub fn with_display_name(mut self, display_name: String) -> Self {
pub(crate) fn with_display_name(mut self, display_name: String) -> Self {
self.display_name = Some(display_name);
self
}
pub fn with_value(mut self, value: String) -> Self {
pub(crate) fn with_value(mut self, value: String) -> Self {
self.value = Some(value);
self
}
pub fn with_name_match_ranges(
pub(crate) fn with_name_match_ranges(
mut self,
name_match_ranges: Vec<(u32, u32)>,
) -> Self {
@ -46,7 +46,7 @@ impl Entry {
self
}
pub fn with_value_match_ranges(
pub(crate) fn with_value_match_ranges(
mut self,
value_match_ranges: Vec<(u32, u32)>,
) -> Self {
@ -54,21 +54,21 @@ impl Entry {
self
}
pub fn with_icon(mut self, icon: FileIcon) -> Self {
pub(crate) fn with_icon(mut self, icon: FileIcon) -> Self {
self.icon = Some(icon);
self
}
pub fn with_line_number(mut self, line_number: usize) -> Self {
pub(crate) fn with_line_number(mut self, line_number: usize) -> Self {
self.line_number = Some(line_number);
self
}
pub fn display_name(&self) -> &str {
pub(crate) fn display_name(&self) -> &str {
self.display_name.as_ref().unwrap_or(&self.name)
}
pub fn stdout_repr(&self) -> String {
pub(crate) fn stdout_repr(&self) -> String {
let mut repr = self.name.clone();
if let Some(line_number) = self.line_number {
repr.push_str(&format!(":{line_number}"));

View File

@ -3,7 +3,7 @@ use std::env;
use color_eyre::Result;
use tracing::error;
pub fn init() -> Result<()> {
pub(crate) fn init() -> Result<()> {
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
.panic_section(format!(
"This is a bug. Consider reporting it at {}",

View File

@ -17,7 +17,7 @@ use tokio::sync::mpsc;
use tracing::warn;
#[derive(Debug, Clone, Copy)]
pub enum Event<I> {
pub(crate) enum Event<I> {
Closed,
Input(I),
FocusLost,
@ -29,7 +29,7 @@ pub enum Event<I> {
#[derive(
Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Hash,
)]
pub enum Key {
pub(crate) enum Key {
Backspace,
Enter,
Left,
@ -62,7 +62,7 @@ pub enum Key {
}
#[allow(clippy::module_name_repetitions)]
pub struct EventLoop {
pub(crate) struct EventLoop {
pub rx: mpsc::UnboundedReceiver<Event<Key>>,
//tx: mpsc::UnboundedSender<Event<Key>>,
pub abort_tx: mpsc::UnboundedSender<()>,
@ -99,7 +99,7 @@ async fn poll_event(timeout: Duration) -> bool {
}
impl EventLoop {
pub fn new(tick_rate: f64, init: bool) -> Self {
pub(crate) fn new(tick_rate: f64, init: bool) -> Self {
let (tx, rx) = mpsc::unbounded_channel();
let tx_c = tx.clone();
let tick_interval =
@ -162,7 +162,7 @@ impl EventLoop {
}
}
pub fn convert_raw_event_to_key(event: KeyEvent) -> Key {
pub(crate) fn convert_raw_event_to_key(event: KeyEvent) -> Key {
match event.code {
Backspace => match event.modifiers {
KeyModifiers::CONTROL => Key::CtrlBackspace,

View File

@ -1,7 +1,7 @@
use parking_lot::Mutex;
use std::ops::DerefMut;
pub struct LazyMutex<T> {
pub(crate) struct LazyMutex<T> {
inner: Mutex<Option<T>>,
init: fn() -> T,
}
@ -14,7 +14,7 @@ impl<T> LazyMutex<T> {
}
}
pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
pub(crate) fn lock(&self) -> impl DerefMut<Target = T> + '_ {
parking_lot::MutexGuard::map(self.inner.lock(), |val| {
val.get_or_insert_with(self.init)
})

View File

@ -8,7 +8,7 @@ lazy_static::lazy_static! {
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME"));
}
pub fn init() -> Result<()> {
pub(crate) fn init() -> Result<()> {
let directory = config::get_data_dir();
std::fs::create_dir_all(directory.clone())?;
let log_path = directory.join(LOG_FILE.clone());

View File

@ -55,7 +55,7 @@ async fn main() -> Result<()> {
Ok(())
}
pub fn is_readable_stdin() -> bool {
pub(crate) fn is_readable_stdin() -> bool {
use std::io::IsTerminal;
#[cfg(unix)]

View File

@ -4,26 +4,29 @@ use crate::entry::Entry;
mod basic;
mod cache;
mod directory;
mod env;
mod files;
// previewer types
pub use basic::BasicPreviewer;
pub use env::EnvVarPreviewer;
pub use files::FilePreviewer;
pub(crate) use basic::BasicPreviewer;
pub(crate) use directory::DirectoryPreviewer;
pub(crate) use env::EnvVarPreviewer;
pub(crate) use files::FilePreviewer;
//use ratatui_image::protocol::StatefulProtocol;
use syntect::highlighting::Style;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub enum PreviewType {
pub(crate) enum PreviewType {
#[default]
Basic,
Directory,
EnvVar,
Files,
}
#[derive(Clone)]
pub enum PreviewContent {
pub(crate) enum PreviewContent {
Empty,
FileTooLarge,
HighlightedText(Vec<Vec<(Style, String)>>),
@ -44,7 +47,7 @@ pub const FILE_TOO_LARGE_MSG: &str = "File too large";
/// - `title`: The title of the preview.
/// - `content`: The content of the preview.
#[derive(Clone)]
pub struct Preview {
pub(crate) struct Preview {
pub title: String,
pub content: PreviewContent,
}
@ -59,11 +62,11 @@ impl Default for Preview {
}
impl Preview {
pub fn new(title: String, content: PreviewContent) -> Self {
pub(crate) fn new(title: String, content: PreviewContent) -> Self {
Preview { title, content }
}
pub fn total_lines(&self) -> u16 {
pub(crate) fn total_lines(&self) -> u16 {
match &self.content {
PreviewContent::HighlightedText(lines) => lines.len() as u16,
_ => 0,
@ -71,16 +74,18 @@ impl Preview {
}
}
pub struct Previewer {
pub(crate) struct Previewer {
basic: BasicPreviewer,
directory: DirectoryPreviewer,
file: FilePreviewer,
env_var: EnvVarPreviewer,
}
impl Previewer {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
Previewer {
basic: BasicPreviewer::new(),
directory: DirectoryPreviewer::new(),
file: FilePreviewer::new(),
env_var: EnvVarPreviewer::new(),
}
@ -89,6 +94,7 @@ impl Previewer {
pub async fn preview(&mut self, entry: &Entry) -> Arc<Preview> {
match entry.preview_type {
PreviewType::Basic => self.basic.preview(entry),
PreviewType::Directory => self.directory.preview(entry),
PreviewType::EnvVar => self.env_var.preview(entry),
PreviewType::Files => self.file.preview(entry).await,
}

View File

@ -3,14 +3,14 @@ use std::sync::Arc;
use crate::entry::Entry;
use crate::previewers::{Preview, PreviewContent};
pub struct BasicPreviewer {}
pub(crate) struct BasicPreviewer {}
impl BasicPreviewer {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
BasicPreviewer {}
}
pub fn preview(&self, entry: &Entry) -> Arc<Preview> {
pub(crate) fn preview(&self, entry: &Entry) -> Arc<Preview> {
Arc::new(Preview {
title: entry.name.clone(),
content: PreviewContent::PlainTextWrapped(entry.name.clone()),

View File

@ -7,13 +7,43 @@ use tracing::debug;
use crate::previewers::Preview;
/// TODO: add unit tests
/// A ring buffer that also keeps track of the keys it contains to avoid duplicates.
///
/// I'm planning on using this as a backend LRU-cache for the preview cache.
/// This serves as a backend for the preview cache.
/// Basic idea:
/// - When a new key is pushed, if it's already in the buffer, do nothing.
/// - If the buffer is full, remove the oldest key and push the new key.
///
/// # Example
/// ```rust
/// let mut ring_set = RingSet::with_capacity(3);
/// // push 3 values into the ringset
/// assert_eq!(ring_set.push(1), None);
/// assert_eq!(ring_set.push(2), None);
/// assert_eq!(ring_set.push(3), None);
///
/// // check that the values are in the buffer
/// assert!(ring_set.contains(&1));
/// assert!(ring_set.contains(&2));
/// assert!(ring_set.contains(&3));
///
/// // push an existing value (should do nothing)
/// assert_eq!(ring_set.push(1), None);
///
/// // entries should still be there
/// assert!(ring_set.contains(&1));
/// assert!(ring_set.contains(&2));
/// assert!(ring_set.contains(&3));
///
/// // push a new value, should remove the oldest value (1)
/// assert_eq!(ring_set.push(4), Some(1));
///
/// // 1 is no longer there but 2 and 3 remain
/// assert!(!ring_set.contains(&1));
/// assert!(ring_set.contains(&2));
/// assert!(ring_set.contains(&3));
/// assert!(ring_set.contains(&4));
/// ```
struct RingSet<T> {
ring_buffer: VecDeque<T>,
known_keys: HashSet<T>,
@ -24,7 +54,8 @@ impl<T> RingSet<T>
where
T: Eq + std::hash::Hash + Clone + std::fmt::Debug,
{
pub fn with_capacity(capacity: usize) -> Self {
/// Create a new `RingSet` with the given capacity.
pub(crate) fn with_capacity(capacity: usize) -> Self {
RingSet {
ring_buffer: VecDeque::with_capacity(capacity),
known_keys: HashSet::with_capacity(capacity),
@ -35,7 +66,7 @@ where
/// Push a new item to the back of the buffer, removing the oldest item if the buffer is full.
/// Returns the item that was removed, if any.
/// If the item is already in the buffer, do nothing and return None.
pub fn push(&mut self, item: T) -> Option<T> {
pub(crate) fn push(&mut self, item: T) -> Option<T> {
// If the key is already in the buffer, do nothing
if self.contains(&item) {
debug!("Key already in ring buffer: {:?}", item);
@ -67,34 +98,37 @@ where
}
}
/// Default size of the preview cache.
/// Default size of the preview cache: 100 entries.
///
/// This does seem kind of arbitrary for now, will need to play around with it.
/// At the moment, files over 4 MB are not previewed, so the cache size
/// shouldn't exceed 400 MB.
const DEFAULT_PREVIEW_CACHE_SIZE: usize = 100;
/// A cache for previews.
/// The cache is implemented as an LRU cache with a fixed size.
pub struct PreviewCache {
pub(crate) struct PreviewCache {
entries: HashMap<String, Arc<Preview>>,
ring_set: RingSet<String>,
}
impl PreviewCache {
/// Create a new preview cache with the given capacity.
pub fn new(capacity: usize) -> Self {
pub(crate) fn new(capacity: usize) -> Self {
PreviewCache {
entries: HashMap::new(),
ring_set: RingSet::with_capacity(capacity),
}
}
pub fn get(&self, key: &str) -> Option<Arc<Preview>> {
pub(crate) fn get(&self, key: &str) -> Option<Arc<Preview>> {
self.entries.get(key).cloned()
}
/// Insert a new preview into the cache.
/// If the cache is full, the oldest entry will be removed.
/// If the key is already in the cache, the preview will be updated.
pub fn insert(&mut self, key: String, preview: Arc<Preview>) {
pub(crate) fn insert(&mut self, key: String, preview: Arc<Preview>) {
debug!("Inserting preview into cache: {}", key);
self.entries.insert(key.clone(), preview.clone());
if let Some(oldest_key) = self.ring_set.push(key) {
@ -102,6 +136,24 @@ impl PreviewCache {
self.entries.remove(&oldest_key);
}
}
/// Get the preview for the given key, or insert a new preview if it doesn't exist.
pub(crate) fn get_or_insert<F>(
&mut self,
key: String,
f: F,
) -> Arc<Preview>
where
F: FnOnce() -> Preview,
{
if let Some(preview) = self.get(&key) {
preview
} else {
let preview = Arc::new(f());
self.insert(key, preview.clone());
preview
}
}
}
impl Default for PreviewCache {
@ -109,3 +161,50 @@ impl Default for PreviewCache {
PreviewCache::new(DEFAULT_PREVIEW_CACHE_SIZE)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ring_set() {
let mut ring_set = RingSet::with_capacity(3);
// push 3 values into the ringset
assert_eq!(ring_set.push(1), None);
assert_eq!(ring_set.push(2), None);
assert_eq!(ring_set.push(3), None);
// check that the values are in the buffer
assert!(ring_set.contains(&1));
assert!(ring_set.contains(&2));
assert!(ring_set.contains(&3));
// push an existing value (should do nothing)
assert_eq!(ring_set.push(1), None);
// entries should still be there
assert!(ring_set.contains(&1));
assert!(ring_set.contains(&2));
assert!(ring_set.contains(&3));
// push a new value, should remove the oldest value (1)
assert_eq!(ring_set.push(4), Some(1));
// 1 is no longer there but 2 and 3 remain
assert!(!ring_set.contains(&1));
assert!(ring_set.contains(&2));
assert!(ring_set.contains(&3));
assert!(ring_set.contains(&4));
// push two new values, should remove 2 and 3
assert_eq!(ring_set.push(5), Some(2));
assert_eq!(ring_set.push(6), Some(3));
// 2 and 3 are no longer there but 4, 5 and 6 remain
assert!(!ring_set.contains(&2));
assert!(!ring_set.contains(&3));
assert!(ring_set.contains(&4));
assert!(ring_set.contains(&5));
assert!(ring_set.contains(&6));
}
}

View File

@ -0,0 +1,52 @@
use std::path::Path;
use std::sync::Arc;
use devicons::FileIcon;
use crate::entry::Entry;
use crate::previewers::cache::PreviewCache;
use crate::previewers::{Preview, PreviewContent};
pub(crate) struct DirectoryPreviewer {
cache: PreviewCache,
}
impl DirectoryPreviewer {
pub(crate) fn new() -> Self {
DirectoryPreviewer {
cache: PreviewCache::default(),
}
}
pub(crate) fn preview(&mut self, entry: &Entry) -> Arc<Preview> {
if let Some(preview) = self.cache.get(&entry.name) {
return preview;
}
let preview = Arc::new(build_preview(entry));
self.cache.insert(entry.name.clone(), preview.clone());
preview
}
}
fn build_preview(entry: &Entry) -> Preview {
let dir_path = Path::new(&entry.name);
// get the list of files in the directory
let mut lines = vec![];
if let Ok(entries) = std::fs::read_dir(dir_path) {
for entry in entries.flatten() {
if let Ok(file_name) = entry.file_name().into_string() {
lines.push(format!(
"{} {}",
FileIcon::from(&file_name),
&file_name
));
}
}
}
Preview {
title: entry.name.clone(),
content: PreviewContent::PlainText(lines),
}
}

View File

@ -4,18 +4,18 @@ use std::sync::Arc;
use crate::entry;
use crate::previewers::{Preview, PreviewContent};
pub struct EnvVarPreviewer {
pub(crate) struct EnvVarPreviewer {
cache: HashMap<entry::Entry, Arc<Preview>>,
}
impl EnvVarPreviewer {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
EnvVarPreviewer {
cache: HashMap::new(),
}
}
pub fn preview(&mut self, entry: &entry::Entry) -> Arc<Preview> {
pub(crate) fn preview(&mut self, entry: &entry::Entry) -> Arc<Preview> {
// check if we have that preview in the cache
if let Some(preview) = self.cache.get(entry) {
return preview.clone();

View File

@ -24,7 +24,7 @@ use crate::utils::strings::preprocess_line;
use super::cache::PreviewCache;
pub struct FilePreviewer {
pub(crate) struct FilePreviewer {
cache: Arc<Mutex<PreviewCache>>,
syntax_set: Arc<SyntaxSet>,
syntax_theme: Arc<Theme>,
@ -32,7 +32,7 @@ pub struct FilePreviewer {
}
impl FilePreviewer {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
let syntax_set = SyntaxSet::load_defaults_nonewlines();
let theme_set = ThemeSet::load_defaults();
//info!("getting image picker");

View File

@ -15,7 +15,7 @@ use crate::television::Television;
use crate::{action::Action, config::Config, tui::Tui};
#[derive(Debug)]
pub enum RenderingTask {
pub(crate) enum RenderingTask {
ClearScreen,
Render,
Resize(u16, u16),
@ -72,7 +72,7 @@ pub async fn render(
// Rendering loop
loop {
select! {
_ = tokio::time::sleep(tokio::time::Duration::from_secs_f64(1.0 / frame_rate)) => {
() = tokio::time::sleep(tokio::time::Duration::from_secs_f64(1.0 / frame_rate)) => {
action_tx.send(Action::Render)?;
}
maybe_task = render_rx.recv() => {
@ -83,13 +83,24 @@ pub async fn render(
}
RenderingTask::Render => {
let mut television = television.lock().await;
tui.terminal.draw(|frame| {
if let Err(err) = television.draw(frame, frame.area()) {
warn!("Failed to draw: {:?}", err);
let _ = action_tx
.send(Action::Error(format!("Failed to draw: {err:?}")));
if let Ok(size) = tui.size() {
// Ratatui uses u16s to encode terminal dimensions and its
// content for each terminal cell is stored linearly in a
// buffer with a u16 index which means we can't support
// terminal areas larger than u16::MAX.
if size.width.checked_mul(size.height).is_some() {
tui.terminal.draw(|frame| {
if let Err(err) = television.draw(frame, frame.area()) {
warn!("Failed to draw: {:?}", err);
let _ = action_tx
.send(Action::Error(format!("Failed to draw: {err:?}")));
}
})?;
} else {
warn!("Terminal area too large");
}
})?;
}
}
RenderingTask::Resize(w, h) => {
tui.resize(Rect::new(0, 0, w, h))?;

View File

@ -4,7 +4,7 @@ use ratatui::{
layout::{
Alignment, Constraint, Direction, Layout as RatatuiLayout, Rect,
},
style::{Color, Style},
style::{Color, Style, Stylize},
text::{Line, Span},
widgets::{
block::{Position, Title},
@ -15,7 +15,6 @@ use ratatui::{
use std::{collections::HashMap, str::FromStr};
use tokio::sync::mpsc::UnboundedSender;
use crate::channels::{CliTvChannel, TelevisionChannel};
use crate::entry::{Entry, ENTRY_PLACEHOLDER};
use crate::previewers::Previewer;
use crate::ui::get_border_style;
@ -26,6 +25,10 @@ use crate::ui::preview::DEFAULT_PREVIEW_TITLE_FG;
use crate::ui::results::build_results_list;
use crate::utils::strings::EMPTY_STRING;
use crate::{action::Action, config::Config};
use crate::{
channels::{CliTvChannel, TelevisionChannel},
utils::strings::shrink_with_ellipsis,
};
#[derive(PartialEq, Copy, Clone)]
enum Pane {
@ -36,7 +39,7 @@ enum Pane {
static PANES: [Pane; 3] = [Pane::Input, Pane::Results, Pane::Preview];
pub struct Television {
pub(crate) struct Television {
action_tx: Option<UnboundedSender<Action>>,
config: Config,
channel: Box<dyn TelevisionChannel>,
@ -48,15 +51,22 @@ pub struct Television {
picker_view_offset: usize,
results_area_height: u32,
previewer: Previewer,
pub preview_scroll: Option<u16>,
pub(crate) preview_scroll: Option<u16>,
pub(crate) preview_pane_height: u16,
current_preview_total_lines: u16,
pub(crate) meta_paragraph_cache: HashMap<String, Paragraph<'static>>,
/// A cache for meta paragraphs (i.e. previews like "Not Supported", etc.).
///
/// The key is a tuple of the preview name and the dimensions of the
/// preview pane. This is a little extra security to ensure meta previews
/// are rendered correctly even when resizing the terminal while still
/// benefiting from a cache mechanism.
pub(crate) meta_paragraph_cache:
HashMap<(String, u16, u16), Paragraph<'static>>,
}
impl Television {
#[must_use]
pub fn new(cli_channel: CliTvChannel) -> Self {
pub(crate) fn new(cli_channel: CliTvChannel) -> Self {
let mut tv_channel = cli_channel.to_channel();
tv_channel.find(EMPTY_STRING);
@ -86,13 +96,13 @@ impl Television {
#[must_use]
/// # Panics
/// This method will panic if the index doesn't fit into an u32.
pub fn get_selected_entry(&self) -> Option<Entry> {
pub(crate) fn get_selected_entry(&self) -> Option<Entry> {
self.picker_state
.selected()
.and_then(|i| self.channel.get_result(u32::try_from(i).unwrap()))
}
pub fn select_prev_entry(&mut self) {
pub(crate) fn select_prev_entry(&mut self) {
if self.channel.result_count() == 0 {
return;
}
@ -122,7 +132,7 @@ impl Television {
}
}
pub fn select_next_entry(&mut self) {
pub(crate) fn select_next_entry(&mut self) {
if self.channel.result_count() == 0 {
return;
}
@ -155,7 +165,7 @@ impl Television {
self.preview_scroll = None;
}
pub fn scroll_preview_down(&mut self, offset: u16) {
pub(crate) fn scroll_preview_down(&mut self, offset: u16) {
if self.preview_scroll.is_none() {
self.preview_scroll = Some(0);
}
@ -169,7 +179,7 @@ impl Television {
}
}
pub fn scroll_preview_up(&mut self, offset: u16) {
pub(crate) fn scroll_preview_up(&mut self, offset: u16) {
if let Some(scroll) = self.preview_scroll {
self.preview_scroll = Some(scroll.saturating_sub(offset));
}
@ -182,13 +192,13 @@ impl Television {
.unwrap()
}
pub fn next_pane(&mut self) {
pub(crate) fn next_pane(&mut self) {
let current_index = self.get_current_pane_index();
let next_index = (current_index + 1) % PANES.len();
self.current_pane = PANES[next_index];
}
pub fn previous_pane(&mut self) {
pub(crate) fn previous_pane(&mut self) {
let current_index = self.get_current_pane_index();
let previous_index = if current_index == 0 {
PANES.len() - 1
@ -207,7 +217,7 @@ impl Television {
/// ┌───────────────────┐│ │
/// │ Search x ││ │
/// └───────────────────┘└─────────────┘
pub fn move_to_pane_on_top(&mut self) {
pub(crate) fn move_to_pane_on_top(&mut self) {
if self.current_pane == Pane::Input {
self.current_pane = Pane::Results;
}
@ -222,7 +232,7 @@ impl Television {
/// ┌───────────────────┐│ │
/// │ Search ││ │
/// └───────────────────┘└─────────────┘
pub fn move_to_pane_below(&mut self) {
pub(crate) fn move_to_pane_below(&mut self) {
if self.current_pane == Pane::Results {
self.current_pane = Pane::Input;
}
@ -237,7 +247,7 @@ impl Television {
/// ┌───────────────────┐│ │
/// │ Search x ││ │
/// └───────────────────┘└─────────────┘
pub fn move_to_pane_right(&mut self) {
pub(crate) fn move_to_pane_right(&mut self) {
match self.current_pane {
Pane::Results | Pane::Input => {
self.current_pane = Pane::Preview;
@ -255,22 +265,22 @@ impl Television {
/// ┌───────────────────┐│ │
/// │ Search ││ │
/// └───────────────────┘└─────────────┘
pub fn move_to_pane_left(&mut self) {
pub(crate) fn move_to_pane_left(&mut self) {
if self.current_pane == Pane::Preview {
self.current_pane = Pane::Results;
}
}
#[must_use]
pub fn is_input_focused(&self) -> bool {
pub(crate) fn is_input_focused(&self) -> bool {
Pane::Input == self.current_pane
}
}
// Styles
// input
const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200);
const DEFAULT_RESULTS_COUNT_FG: Color = Color::Rgb(150, 150, 150);
const DEFAULT_INPUT_FG: Color = Color::LightRed;
const DEFAULT_RESULTS_COUNT_FG: Color = Color::LightRed;
impl Television {
/// Register an action handler that can send actions for processing if necessary.
@ -282,7 +292,7 @@ impl Television {
/// # Returns
///
/// * `Result<()>` - An Ok result or an error.
pub fn register_action_handler(
pub(crate) fn register_action_handler(
&mut self,
tx: UnboundedSender<Action>,
) -> Result<()> {
@ -299,7 +309,10 @@ impl Television {
/// # Returns
///
/// * `Result<()>` - An Ok result or an error.
pub fn register_config_handler(&mut self, config: Config) -> Result<()> {
pub(crate) fn register_config_handler(
&mut self,
config: Config,
) -> Result<()> {
self.config = config;
Ok(())
}
@ -388,7 +401,11 @@ impl Television {
/// # Returns
///
/// * `Result<()>` - An Ok result or an error.
pub fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
pub(crate) fn draw(
&mut self,
frame: &mut Frame,
area: Rect,
) -> Result<()> {
let layout = Layout::all_panes_centered(Dimensions::default(), area);
//let layout =
// Layout::results_only_centered(Dimensions::new(40, 60), area);
@ -446,11 +463,15 @@ impl Television {
frame.render_widget(input_block, layout.input);
// split input block into 3 parts: prompt symbol, input, result count
let inner_input_chunks = RatatuiLayout::default()
.direction(Direction::Horizontal)
.constraints([
// prompt symbol
Constraint::Length(2),
// input field
Constraint::Fill(1),
// result count
Constraint::Length(
3 * ((self.channel.total_count() as f32).log10().ceil()
as u16
@ -461,8 +482,11 @@ impl Television {
.split(input_block_inner);
let arrow_block = Block::default();
let arrow = Paragraph::new(Span::styled("> ", Style::default()))
.block(arrow_block);
let arrow = Paragraph::new(Span::styled(
"> ",
Style::default().fg(DEFAULT_INPUT_FG).bold(),
))
.block(arrow_block);
frame.render_widget(arrow, inner_input_chunks[0]);
let interactive_input_block = Block::default();
@ -472,7 +496,7 @@ impl Television {
let input = Paragraph::new(self.input.value())
.scroll((0, u16::try_from(scroll)?))
.block(interactive_input_block)
.style(Style::default().fg(DEFAULT_INPUT_FG))
.style(Style::default().fg(DEFAULT_INPUT_FG).bold().italic())
.alignment(Alignment::Left);
frame.render_widget(input, inner_input_chunks[1]);
@ -487,7 +511,7 @@ impl Television {
},
self.channel.result_count(),
),
Style::default().fg(DEFAULT_RESULTS_COUNT_FG),
Style::default().fg(DEFAULT_RESULTS_COUNT_FG).italic(),
))
.block(result_count_block)
.alignment(Alignment::Right);
@ -519,14 +543,21 @@ impl Television {
let mut preview_title_spans = Vec::new();
if let Some(icon) = &selected_entry.icon {
preview_title_spans.push(Span::styled(
icon.to_string(),
{
let mut icon_str = String::from(" ");
icon_str.push(icon.icon);
icon_str.push(' ');
icon_str
},
Style::default().fg(Color::from_str(icon.color)?),
));
preview_title_spans.push(Span::raw(" "));
}
preview_title_spans.push(Span::styled(
preview.title.clone(),
Style::default().fg(DEFAULT_PREVIEW_TITLE_FG),
shrink_with_ellipsis(
&preview.title,
(preview_title_area.width - 4) as usize,
),
Style::default().fg(DEFAULT_PREVIEW_TITLE_FG).bold(),
));
let preview_title =
Paragraph::new(Line::from(preview_title_spans))
@ -581,8 +612,7 @@ impl Television {
&preview,
selected_entry
.line_number
// FIXME: this actually might panic in some edge cases
.map(|l| u16::try_from(l).unwrap()),
.map(|l| u16::try_from(l).unwrap_or(0)),
);
frame.render_widget(preview_block, inner);
//}

View File

@ -16,7 +16,7 @@ use tokio::task::JoinHandle;
use tracing::debug;
#[allow(dead_code)]
pub struct Tui<W>
pub(crate) struct Tui<W>
where
W: Write,
{
@ -30,7 +30,7 @@ impl<W> Tui<W>
where
W: Write,
{
pub fn new(writer: W) -> Result<Self> {
pub(crate) fn new(writer: W) -> Result<Self> {
Ok(Self {
task: tokio::spawn(async {}),
frame_rate: 60.0,
@ -38,16 +38,16 @@ where
})
}
pub fn frame_rate(mut self, frame_rate: f64) -> Self {
pub(crate) fn frame_rate(mut self, frame_rate: f64) -> Self {
self.frame_rate = frame_rate;
self
}
pub fn size(&self) -> Result<Size> {
pub(crate) fn size(&self) -> Result<Size> {
Ok(self.terminal.size()?)
}
pub fn enter(&mut self) -> Result<()> {
pub(crate) fn enter(&mut self) -> Result<()> {
enable_raw_mode()?;
let mut buffered_stderr = LineWriter::new(stderr());
execute!(buffered_stderr, EnterAlternateScreen)?;
@ -56,7 +56,7 @@ where
Ok(())
}
pub fn exit(&mut self) -> Result<()> {
pub(crate) fn exit(&mut self) -> Result<()> {
if is_raw_mode_enabled()? {
debug!("Exiting terminal");
@ -69,14 +69,14 @@ where
Ok(())
}
pub fn suspend(&mut self) -> Result<()> {
pub(crate) fn suspend(&mut self) -> Result<()> {
self.exit()?;
#[cfg(not(windows))]
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?;
Ok(())
}
pub fn resume(&mut self) -> Result<()> {
pub(crate) fn resume(&mut self) -> Result<()> {
self.enter()?;
Ok(())
}

View File

@ -15,11 +15,14 @@ pub mod results;
//const DEFAULT_PREVIEW_GUTTER_FG: Color = Color::Rgb(70, 70, 70);
//const DEFAULT_PREVIEW_GUTTER_SELECTED_FG: Color = Color::Rgb(255, 150, 150);
pub fn get_border_style(focused: bool) -> Style {
if focused {
Style::default().fg(Color::Green)
} else {
// TODO: make this depend on self.config
Style::default().fg(Color::Rgb(90, 90, 110)).dim()
}
pub(crate) fn get_border_style(focused: bool) -> Style {
Style::default().fg(Color::Blue)
// NOTE: do we want to change the border color based on focus? Are we
// keeping the focus feature at all?
// if focused {
// Style::default().fg(Color::Green)
// } else {
// Style::default().fg(Color::Blue)
// }
}

View File

@ -6,7 +6,7 @@ pub mod backend;
/// Different backends can be used to convert events into requests.
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub enum InputRequest {
pub(crate) enum InputRequest {
SetCursor(usize),
InsertChar(char),
GoToPrevChar,
@ -24,7 +24,7 @@ pub enum InputRequest {
}
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub struct StateChanged {
pub(crate) struct StateChanged {
pub value: bool,
pub cursor: bool,
}
@ -45,7 +45,7 @@ pub type InputResponse = Option<StateChanged>;
/// assert_eq!(input.to_string(), "Hello World");
/// ```
#[derive(Default, Debug, Clone)]
pub struct Input {
pub(crate) struct Input {
value: String,
cursor: usize,
}
@ -53,14 +53,14 @@ pub struct Input {
impl Input {
/// Initialize a new instance with a given value
/// Cursor will be set to the given value's length.
pub fn new(value: String) -> Self {
pub(crate) fn new(value: String) -> Self {
let len = value.chars().count();
Self { value, cursor: len }
}
/// Set the value manually.
/// Cursor will be set to the given value's length.
pub fn with_value(mut self, value: String) -> Self {
pub(crate) fn with_value(mut self, value: String) -> Self {
self.cursor = value.chars().count();
self.value = value;
self
@ -68,20 +68,20 @@ impl Input {
/// Set the cursor manually.
/// If the input is larger than the value length, it'll be auto adjusted.
pub fn with_cursor(mut self, cursor: usize) -> Self {
pub(crate) fn with_cursor(mut self, cursor: usize) -> Self {
self.cursor = cursor.min(self.value.chars().count());
self
}
// Reset the cursor and value to default
pub fn reset(&mut self) {
pub(crate) fn reset(&mut self) {
self.cursor = Default::default();
self.value = String::default();
}
/// Handle request and emit response.
#[allow(clippy::too_many_lines)]
pub fn handle(&mut self, req: InputRequest) -> InputResponse {
pub(crate) fn handle(&mut self, req: InputRequest) -> InputResponse {
use InputRequest::{
DeleteLine, DeleteNextChar, DeleteNextWord, DeletePrevChar,
DeletePrevWord, DeleteTillEnd, GoToEnd, GoToNextChar,
@ -328,17 +328,17 @@ impl Input {
}
/// Get a reference to the current value.
pub fn value(&self) -> &str {
pub(crate) fn value(&self) -> &str {
self.value.as_str()
}
/// Get the currect cursor placement.
pub fn cursor(&self) -> usize {
pub(crate) fn cursor(&self) -> usize {
self.cursor
}
/// Get the current cursor position with account for multispace characters.
pub fn visual_cursor(&self) -> usize {
pub(crate) fn visual_cursor(&self) -> usize {
if self.cursor == 0 {
return 0;
}
@ -356,7 +356,7 @@ impl Input {
}
/// Get the scroll position with account for multispace characters.
pub fn visual_scroll(&self, width: usize) -> usize {
pub(crate) fn visual_scroll(&self, width: usize) -> usize {
let scroll = self.visual_cursor().max(width) - width;
let mut uscroll = 0;
let mut chars = self.value().chars();

View File

@ -5,7 +5,7 @@ use ratatui::crossterm::event::{
/// Converts crossterm event into input requests.
/// TODO: make these keybindings configurable.
pub fn to_input_request(evt: &CrosstermEvent) -> Option<InputRequest> {
pub(crate) fn to_input_request(evt: &CrosstermEvent) -> Option<InputRequest> {
use InputRequest::*;
use KeyCode::*;
match evt {

View File

@ -1,13 +1,13 @@
use ratatui::layout;
use ratatui::layout::{Constraint, Direction, Rect};
pub struct Dimensions {
pub(crate) struct Dimensions {
pub x: u16,
pub y: u16,
}
impl Dimensions {
pub fn new(x: u16, y: u16) -> Self {
pub(crate) fn new(x: u16, y: u16) -> Self {
Self { x, y }
}
}
@ -18,7 +18,7 @@ impl Default for Dimensions {
}
}
pub struct Layout {
pub(crate) struct Layout {
pub results: Rect,
pub input: Rect,
pub preview_title: Option<Rect>,
@ -26,7 +26,7 @@ pub struct Layout {
}
impl Layout {
pub fn new(
pub(crate) fn new(
results: Rect,
input: Rect,
preview_title: Option<Rect>,
@ -42,7 +42,7 @@ impl Layout {
/// TODO: add diagram
#[allow(dead_code)]
pub fn all_panes_centered(dimensions: Dimensions, area: Rect) -> Self {
pub(crate) fn all_panes_centered(dimensions: Dimensions, area: Rect) -> Self {
let main_block = centered_rect(dimensions.x, dimensions.y, area);
// split the main block into two vertical chunks
let chunks = layout::Layout::default()
@ -75,7 +75,7 @@ impl Layout {
/// TODO: add diagram
#[allow(dead_code)]
pub fn results_only_centered(dimensions: Dimensions, area: Rect) -> Self {
pub(crate) fn results_only_centered(dimensions: Dimensions, area: Rect) -> Self {
let main_block = centered_rect(dimensions.x, dimensions.y, area);
// split the main block into two vertical chunks
let chunks = layout::Layout::default()

View File

@ -20,7 +20,7 @@ impl Television {
const FILL_CHAR_SLANTED: char = '';
const FILL_CHAR_EMPTY: char = ' ';
pub fn build_preview_paragraph<'b>(
pub(crate) fn build_preview_paragraph<'b>(
&'b mut self,
preview_block: Block<'b>,
inner: Rect,
@ -34,10 +34,9 @@ impl Television {
for (i, line) in content.iter().enumerate() {
lines.push(Line::from(vec![
build_line_number_span(i + 1).style(Style::default().fg(
// FIXME: this actually might panic in some edge cases
if matches!(
target_line,
Some(l) if l == u16::try_from(i).unwrap() + 1
Some(l) if l == u16::try_from(i).unwrap_or(0) + 1
)
{
DEFAULT_PREVIEW_GUTTER_SELECTED_FG
@ -120,7 +119,7 @@ impl Television {
}
}
pub fn maybe_init_preview_scroll(
pub(crate) fn maybe_init_preview_scroll(
&mut self,
target_line: Option<u16>,
height: u16,
@ -131,13 +130,17 @@ impl Television {
}
}
pub fn build_meta_preview_paragraph<'a>(
pub(crate) fn build_meta_preview_paragraph<'a>(
&mut self,
inner: Rect,
message: &str,
fill_char: char,
) -> Paragraph<'a> {
if let Some(paragraph) = self.meta_paragraph_cache.get(message) {
if let Some(paragraph) = self.meta_paragraph_cache.get(&(
message.to_string(),
inner.width,
inner.height,
)) {
return paragraph.clone();
}
let message_len = message.len();
@ -187,8 +190,10 @@ impl Television {
// Create a paragraph with the generated content
let p = Paragraph::new(Text::from(lines));
self.meta_paragraph_cache
.insert(message.to_string(), p.clone());
self.meta_paragraph_cache.insert(
(message.to_string(), inner.width, inner.height),
p.clone(),
);
p
}
}

View File

@ -9,7 +9,7 @@ const DEFAULT_RESULT_NAME_FG: Color = Color::Blue;
const DEFAULT_RESULT_PREVIEW_FG: Color = Color::Rgb(150, 150, 150);
const DEFAULT_RESULT_LINE_NUMBER_FG: Color = Color::Yellow;
pub fn build_results_list<'a, 'b>(
pub(crate) fn build_results_list<'a, 'b>(
results_block: Block<'b>,
entries: &'a [Entry],
) -> List<'a>
@ -39,25 +39,22 @@ where
last_match_end,
start,
),
Style::default()
.fg(DEFAULT_RESULT_NAME_FG)
.bold()
.italic(),
Style::default().fg(DEFAULT_RESULT_NAME_FG),
));
spans.push(Span::styled(
slice_at_char_boundaries(&entry.name, start, end),
Style::default().fg(Color::Red).bold().italic(),
Style::default().fg(Color::Red),
));
last_match_end = end;
}
spans.push(Span::styled(
&entry.name[next_char_boundary(&entry.name, last_match_end)..],
Style::default().fg(DEFAULT_RESULT_NAME_FG).bold().italic(),
Style::default().fg(DEFAULT_RESULT_NAME_FG),
));
} else {
spans.push(Span::styled(
entry.display_name(),
Style::default().fg(DEFAULT_RESULT_NAME_FG).bold().italic(),
Style::default().fg(DEFAULT_RESULT_NAME_FG),
));
}
// optional line number

View File

@ -11,7 +11,7 @@ lazy_static::lazy_static! {
pub static ref DEFAULT_NUM_THREADS: usize = default_num_threads().into();
}
pub fn walk_builder(path: &Path, n_threads: usize) -> WalkBuilder {
pub(crate) fn walk_builder(path: &Path, n_threads: usize) -> WalkBuilder {
let mut builder = WalkBuilder::new(path);
// ft-based filtering
@ -23,19 +23,19 @@ pub fn walk_builder(path: &Path, n_threads: usize) -> WalkBuilder {
builder
}
pub fn get_file_size(path: &Path) -> Option<u64> {
pub(crate) fn get_file_size(path: &Path) -> Option<u64> {
std::fs::metadata(path).ok().map(|m| m.len())
}
#[derive(Debug)]
pub enum FileType {
pub(crate) enum FileType {
Text,
Image,
Other,
Unknown,
}
pub fn is_not_text(bytes: &[u8]) -> Option<bool> {
pub(crate) fn is_not_text(bytes: &[u8]) -> Option<bool> {
let infer = Infer::new();
match infer.get(bytes) {
Some(t) => {
@ -56,11 +56,11 @@ pub fn is_not_text(bytes: &[u8]) -> Option<bool> {
}
}
pub fn is_valid_utf8(bytes: &[u8]) -> bool {
pub(crate) fn is_valid_utf8(bytes: &[u8]) -> bool {
std::str::from_utf8(bytes).is_ok()
}
pub fn is_known_text_extension(path: &Path) -> bool {
pub(crate) fn is_known_text_extension(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| KNOWN_TEXT_FILE_EXTENSIONS.contains(ext))

View File

@ -1,4 +1,4 @@
pub fn sep_name_and_value_indices(
pub(crate) fn sep_name_and_value_indices(
indices: &mut Vec<u32>,
name_len: u32,
) -> (Vec<u32>, Vec<u32>, bool, bool) {

View File

@ -1,7 +1,7 @@
use lazy_static::lazy_static;
use std::fmt::Write;
pub fn next_char_boundary(s: &str, start: usize) -> usize {
pub(crate) fn next_char_boundary(s: &str, start: usize) -> usize {
let mut i = start;
while !s.is_char_boundary(i) {
i += 1;
@ -9,7 +9,7 @@ pub fn next_char_boundary(s: &str, start: usize) -> usize {
i
}
pub fn prev_char_boundary(s: &str, start: usize) -> usize {
pub(crate) fn prev_char_boundary(s: &str, start: usize) -> usize {
let mut i = start;
while !s.is_char_boundary(i) {
i -= 1;
@ -17,7 +17,7 @@ pub fn prev_char_boundary(s: &str, start: usize) -> usize {
i
}
pub fn slice_at_char_boundaries(
pub(crate) fn slice_at_char_boundaries(
s: &str,
start_byte_index: usize,
end_byte_index: usize,
@ -26,7 +26,7 @@ pub fn slice_at_char_boundaries(
..next_char_boundary(s, end_byte_index)]
}
pub fn slice_up_to_char_boundary(s: &str, byte_index: usize) -> &str {
pub(crate) fn slice_up_to_char_boundary(s: &str, byte_index: usize) -> &str {
let mut char_index = byte_index;
while !s.is_char_boundary(char_index) {
char_index -= 1;
@ -64,7 +64,7 @@ const NULL_CHARACTER: char = '\x00';
const UNIT_SEPARATOR_CHARACTER: char = '\u{001F}';
const APPLICATION_PROGRAM_COMMAND_CHARACTER: char = '\u{009F}';
pub fn replace_nonprintable(input: &[u8], tab_width: usize) -> String {
pub(crate) fn replace_nonprintable(input: &[u8], tab_width: usize) -> String {
let mut output = String::new();
let mut idx = 0;
@ -110,7 +110,7 @@ pub fn replace_nonprintable(input: &[u8], tab_width: usize) -> String {
const MAX_LINE_LENGTH: usize = 500;
pub fn preprocess_line(line: &str) -> String {
pub(crate) fn preprocess_line(line: &str) -> String {
replace_nonprintable(
{
if line.len() > MAX_LINE_LENGTH {
@ -124,3 +124,15 @@ pub fn preprocess_line(line: &str) -> String {
2,
)
}
pub(crate) fn shrink_with_ellipsis(s: &str, max_length: usize) -> String {
if s.len() <= max_length {
return s.to_string();
}
let half_max_length = (max_length / 2) - 2;
let first_half = slice_up_to_char_boundary(s, half_max_length);
let second_half =
slice_at_char_boundaries(s, s.len() - half_max_length, s.len());
format!("{first_half}{second_half}")
}

View File

@ -36,7 +36,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
use clap::ValueEnum;
#[derive(Debug, Clone, ValueEnum, Default, Copy)]
pub enum CliTvChannel {
pub(crate) enum CliTvChannel {
#[default]
#(#cli_enum_variants),*
}
@ -67,7 +67,7 @@ fn impl_cli_channel(ast: &syn::DeriveInput) -> TokenStream {
#cli_enum
impl CliTvChannel {
pub fn to_channel(self) -> Box<dyn TelevisionChannel> {
pub(crate) fn to_channel(self) -> Box<dyn TelevisionChannel> {
match self {
#(#arms),*
}