mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-23 02:20:03 +00:00
refactoring
This commit is contained in:
parent
44b8a8580b
commit
e36f0d450f
26
Cargo.lock
generated
26
Cargo.lock
generated
@ -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",
|
||||
|
10
Cargo.toml
10
Cargo.toml
@ -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
34
TODO.md
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
|
@ -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(|| {}),
|
||||
|
@ -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(|| {}),
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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}"));
|
||||
|
@ -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 {}",
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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());
|
||||
|
@ -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)]
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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()),
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
52
crates/television/previewers/directory.rs
Normal file
52
crates/television/previewers/directory.rs
Normal 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),
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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");
|
||||
|
@ -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))?;
|
||||
|
@ -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);
|
||||
//}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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)
|
||||
// }
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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) {
|
||||
|
@ -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}")
|
||||
}
|
||||
|
@ -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),*
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user