mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 11:35:25 +00:00
wip: compiles and works
TODO: we seem to be sending quite a lot of preview requests (see logs)
This commit is contained in:
parent
11db510eb3
commit
5afb3c5e7e
@ -61,15 +61,6 @@ orientation = "landscape"
|
|||||||
# directory in your configuration directory (see the `config.toml` location above).
|
# directory in your configuration directory (see the `config.toml` location above).
|
||||||
theme = "default"
|
theme = "default"
|
||||||
|
|
||||||
# Previewers settings
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
[previewers.file]
|
|
||||||
# The theme to use for syntax highlighting.
|
|
||||||
# Bulitin syntax highlighting uses the same syntax highlighting engine as bat.
|
|
||||||
# To get a list of your currently available themes, run `bat --list-themes`
|
|
||||||
# Note that setting the BAT_THEME environment variable will override this setting.
|
|
||||||
theme = "TwoDark"
|
|
||||||
|
|
||||||
# Keybindings
|
# Keybindings
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
|
17
Cargo.lock
generated
17
Cargo.lock
generated
@ -38,6 +38,19 @@ version = "0.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ansi-to-tui"
|
||||||
|
version = "7.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
"ratatui",
|
||||||
|
"simdutf8",
|
||||||
|
"smallvec",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.18"
|
version = "0.6.18"
|
||||||
@ -1392,6 +1405,7 @@ dependencies = [
|
|||||||
name = "television"
|
name = "television"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ansi-to-tui",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
"better-panic",
|
"better-panic",
|
||||||
@ -1404,15 +1418,12 @@ dependencies = [
|
|||||||
"directories",
|
"directories",
|
||||||
"human-panic",
|
"human-panic",
|
||||||
"lazy-regex",
|
"lazy-regex",
|
||||||
"nom",
|
|
||||||
"nucleo",
|
"nucleo",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"simdutf8",
|
|
||||||
"smallvec",
|
|
||||||
"strum",
|
"strum",
|
||||||
"television-derive",
|
"television-derive",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
@ -51,15 +51,13 @@ human-panic = "2.0"
|
|||||||
# FIXME: we probably don't need strum anymore
|
# FIXME: we probably don't need strum anymore
|
||||||
strum = { version = "0.26", features = ["derive"] }
|
strum = { version = "0.26", features = ["derive"] }
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
nom = "7.1"
|
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
simdutf8 = { version = "0.1", optional = true }
|
|
||||||
smallvec = { version = "1.13", features = ["const_generics"] }
|
|
||||||
nucleo = "0.5"
|
nucleo = "0.5"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
lazy-regex = { version = "3.4.1", features = [
|
lazy-regex = { version = "3.4.1", features = [
|
||||||
"lite",
|
"lite",
|
||||||
], default-features = false }
|
], default-features = false }
|
||||||
|
ansi-to-tui = "7.0.0"
|
||||||
|
|
||||||
|
|
||||||
# target specific dependencies
|
# target specific dependencies
|
||||||
@ -78,11 +76,6 @@ clipboard-win = "5.4.0"
|
|||||||
criterion = { version = "0.5", features = ["async_tokio"] }
|
criterion = { version = "0.5", features = ["async_tokio"] }
|
||||||
tempfile = "3.16.0"
|
tempfile = "3.16.0"
|
||||||
|
|
||||||
[features]
|
|
||||||
simd = ["dep:simdutf8"]
|
|
||||||
zero-copy = []
|
|
||||||
default = ["zero-copy", "simd"]
|
|
||||||
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
clap = { version = "4.5", features = ["derive", "cargo"] }
|
clap = { version = "4.5", features = ["derive", "cargo"] }
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
[[cable_channel]]
|
[[cable_channel]]
|
||||||
name = "files"
|
name = "files"
|
||||||
source_command = "fd -t f"
|
source_command = "fd -t f"
|
||||||
preview_command = ":files:"
|
preview_command = "bat -n --color=always {0}"
|
||||||
|
|
||||||
# Text
|
# Text
|
||||||
[[cable_channel]]
|
[[cable_channel]]
|
||||||
@ -72,7 +72,7 @@ preview_command = "aws s3 ls s3://{0}"
|
|||||||
[[cable_channel]]
|
[[cable_channel]]
|
||||||
name = "my-dotfiles"
|
name = "my-dotfiles"
|
||||||
source_command = "fd -t f . $HOME/.config"
|
source_command = "fd -t f . $HOME/.config"
|
||||||
preview_command = ":files:"
|
preview_command = "bat -n --color=always {0}"
|
||||||
|
|
||||||
# Shell history
|
# Shell history
|
||||||
[[cable_channel]]
|
[[cable_channel]]
|
||||||
|
@ -4,7 +4,9 @@ use std::{
|
|||||||
ops::Deref,
|
ops::Deref,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::cable::SerializedChannelPrototypes;
|
use crate::{
|
||||||
|
cable::SerializedChannelPrototypes, channels::preview::PreviewCommand,
|
||||||
|
};
|
||||||
|
|
||||||
/// A prototype for a cable channel.
|
/// A prototype for a cable channel.
|
||||||
///
|
///
|
||||||
@ -49,6 +51,9 @@ pub struct CableChannelPrototype {
|
|||||||
pub preview_offset: Option<String>,
|
pub preview_offset: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STDIN_CHANNEL_NAME: &str = "stdin";
|
||||||
|
const STDIN_SOURCE_COMMAND: &str = "cat";
|
||||||
|
|
||||||
impl CableChannelPrototype {
|
impl CableChannelPrototype {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
name: &str,
|
name: &str,
|
||||||
@ -67,6 +72,31 @@ impl CableChannelPrototype {
|
|||||||
preview_offset,
|
preview_offset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn stdin(preview: Option<PreviewCommand>) -> Self {
|
||||||
|
match preview {
|
||||||
|
Some(PreviewCommand {
|
||||||
|
command,
|
||||||
|
delimiter,
|
||||||
|
offset_expr,
|
||||||
|
}) => Self {
|
||||||
|
name: STDIN_CHANNEL_NAME.to_string(),
|
||||||
|
source_command: STDIN_SOURCE_COMMAND.to_string(),
|
||||||
|
interactive: false,
|
||||||
|
preview_command: Some(command),
|
||||||
|
preview_delimiter: Some(delimiter),
|
||||||
|
preview_offset: offset_expr,
|
||||||
|
},
|
||||||
|
None => Self {
|
||||||
|
name: STDIN_CHANNEL_NAME.to_string(),
|
||||||
|
source_command: STDIN_SOURCE_COMMAND.to_string(),
|
||||||
|
interactive: false,
|
||||||
|
preview_command: None,
|
||||||
|
preview_delimiter: None,
|
||||||
|
preview_offset: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PROTOTYPE_NAME: &str = "files";
|
const DEFAULT_PROTOTYPE_NAME: &str = "files";
|
||||||
|
@ -143,15 +143,6 @@ impl Entry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ENTRY_PLACEHOLDER: Entry = Entry {
|
|
||||||
name: String::new(),
|
|
||||||
value: None,
|
|
||||||
name_match_ranges: None,
|
|
||||||
value_match_ranges: None,
|
|
||||||
icon: None,
|
|
||||||
line_number: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -3,6 +3,7 @@ use std::fmt::Display;
|
|||||||
use super::{cable::prototypes::DEFAULT_DELIMITER, entry::Entry};
|
use super::{cable::prototypes::DEFAULT_DELIMITER, entry::Entry};
|
||||||
use crate::channels::cable::prototypes::CableChannelPrototype;
|
use crate::channels::cable::prototypes::CableChannelPrototype;
|
||||||
use lazy_regex::{regex, Lazy, Regex};
|
use lazy_regex::{regex, Lazy, Regex};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
static CMD_RE: &Lazy<Regex> = regex!(r"\{(\d+)\}");
|
static CMD_RE: &Lazy<Regex> = regex!(r"\{(\d+)\}");
|
||||||
|
|
||||||
@ -49,6 +50,8 @@ impl PreviewCommand {
|
|||||||
let mut formatted_command = self
|
let mut formatted_command = self
|
||||||
.command
|
.command
|
||||||
.replace("{}", format!("'{}'", entry.name).as_str());
|
.replace("{}", format!("'{}'", entry.name).as_str());
|
||||||
|
debug!("FORMATTED_COMMAND: {formatted_command}");
|
||||||
|
debug!("PARTS: {parts:?}");
|
||||||
|
|
||||||
formatted_command = CMD_RE
|
formatted_command = CMD_RE
|
||||||
.replace_all(&formatted_command, |caps: ®ex::Captures| {
|
.replace_all(&formatted_command, |caps: ®ex::Captures| {
|
||||||
|
@ -1,156 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::HashSet,
|
|
||||||
io::{stdin, BufRead},
|
|
||||||
thread::spawn,
|
|
||||||
};
|
|
||||||
|
|
||||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use super::{preview::PreviewCommand, OnAir};
|
|
||||||
use crate::channels::entry::Entry;
|
|
||||||
use crate::matcher::{config::Config, injector::Injector, Matcher};
|
|
||||||
|
|
||||||
pub struct Channel {
|
|
||||||
matcher: Matcher<String>,
|
|
||||||
pub preview_command: Option<PreviewCommand>,
|
|
||||||
selected_entries: FxHashSet<Entry>,
|
|
||||||
instream_handle: std::thread::JoinHandle<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Channel {
|
|
||||||
pub fn new(preview_command: Option<PreviewCommand>) -> Self {
|
|
||||||
let matcher = Matcher::new(Config::default());
|
|
||||||
let injector = matcher.injector();
|
|
||||||
|
|
||||||
let instream_handle = spawn(move || stream_from_stdin(&injector));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
matcher,
|
|
||||||
preview_command,
|
|
||||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
|
||||||
instream_handle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Channel {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> From<E> for Channel
|
|
||||||
where
|
|
||||||
E: AsRef<Vec<String>>,
|
|
||||||
{
|
|
||||||
fn from(entries: E) -> Self {
|
|
||||||
let matcher = Matcher::new(Config::default());
|
|
||||||
let injector = matcher.injector();
|
|
||||||
|
|
||||||
let entries = entries.as_ref().clone();
|
|
||||||
|
|
||||||
let instream_handle = spawn(move || {
|
|
||||||
for entry in entries {
|
|
||||||
injector.push(entry.clone(), |e, cols| {
|
|
||||||
cols[0] = e.to_string().into();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
matcher,
|
|
||||||
preview_command: None,
|
|
||||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
|
||||||
instream_handle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
|
||||||
|
|
||||||
fn stream_from_stdin(injector: &Injector<String>) {
|
|
||||||
let mut stdin = stdin().lock();
|
|
||||||
let mut buffer = String::new();
|
|
||||||
|
|
||||||
let instant = std::time::Instant::now();
|
|
||||||
loop {
|
|
||||||
match stdin.read_line(&mut buffer) {
|
|
||||||
Ok(c) if c > 0 => {
|
|
||||||
let trimmed = buffer.trim();
|
|
||||||
if !trimmed.is_empty() {
|
|
||||||
injector.push(trimmed.to_owned(), |e, cols| {
|
|
||||||
cols[0] = e.clone().into();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
buffer.clear();
|
|
||||||
}
|
|
||||||
Ok(0) => {
|
|
||||||
debug!("EOF");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
debug!("Error reading from stdin");
|
|
||||||
if instant.elapsed() > TIMEOUT {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OnAir for Channel {
|
|
||||||
fn find(&mut self, pattern: &str) {
|
|
||||||
self.matcher.find(pattern);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
|
||||||
self.matcher.tick();
|
|
||||||
self.matcher
|
|
||||||
.results(num_entries, offset)
|
|
||||||
.into_iter()
|
|
||||||
.map(|item| {
|
|
||||||
// NOTE: we're passing `PreviewType::Basic` here just as a placeholder
|
|
||||||
// to avoid storing the preview command multiple times for each item.
|
|
||||||
Entry::new(item.matched_string)
|
|
||||||
.with_name_match_indices(&item.match_indices)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
|
||||||
self.matcher
|
|
||||||
.get_result(index)
|
|
||||||
.map(|item| Entry::new(item.matched_string))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_entries(&self) -> &FxHashSet<Entry> {
|
|
||||||
&self.selected_entries
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_selection(&mut self, entry: &Entry) {
|
|
||||||
if self.selected_entries.contains(entry) {
|
|
||||||
self.selected_entries.remove(entry);
|
|
||||||
} else {
|
|
||||||
self.selected_entries.insert(entry.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn result_count(&self) -> u32 {
|
|
||||||
self.matcher.matched_item_count
|
|
||||||
}
|
|
||||||
|
|
||||||
fn total_count(&self) -> u32 {
|
|
||||||
self.matcher.total_item_count
|
|
||||||
}
|
|
||||||
|
|
||||||
fn running(&self) -> bool {
|
|
||||||
self.matcher.status.running || !self.instream_handle.is_finished()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shutdown(&self) {}
|
|
||||||
|
|
||||||
fn supports_preview(&self) -> bool {
|
|
||||||
self.preview_command.is_some()
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,7 +12,7 @@ pub struct Cli {
|
|||||||
#[arg(value_enum, index = 1, verbatim_doc_comment)]
|
#[arg(value_enum, index = 1, verbatim_doc_comment)]
|
||||||
pub channel: Option<String>,
|
pub channel: Option<String>,
|
||||||
|
|
||||||
/// A preview command to use with the stdin channel.
|
/// A preview command to use with the current channel.
|
||||||
///
|
///
|
||||||
/// If provided, the preview command will be executed and formatted using
|
/// If provided, the preview command will be executed and formatted using
|
||||||
/// the entry.
|
/// the entry.
|
||||||
|
@ -5,7 +5,6 @@ use std::process::exit;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use crossterm::terminal::enable_raw_mode;
|
|
||||||
use television::cable;
|
use television::cable;
|
||||||
use television::channels::cable::prototypes::{
|
use television::channels::cable::prototypes::{
|
||||||
CableChannelPrototype, CableChannelPrototypes,
|
CableChannelPrototype, CableChannelPrototypes,
|
||||||
@ -52,8 +51,6 @@ async fn main() -> Result<()> {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|x| handle_subcommands(x, &config));
|
.map(|x| handle_subcommands(x, &config));
|
||||||
|
|
||||||
enable_raw_mode()?;
|
|
||||||
|
|
||||||
// optionally change the working directory
|
// optionally change the working directory
|
||||||
args.working_directory.as_ref().map(set_current_dir);
|
args.working_directory.as_ref().map(set_current_dir);
|
||||||
|
|
||||||
@ -161,9 +158,7 @@ pub fn determine_channel(
|
|||||||
) -> Result<CableChannelPrototype> {
|
) -> Result<CableChannelPrototype> {
|
||||||
if readable_stdin {
|
if readable_stdin {
|
||||||
debug!("Using stdin channel");
|
debug!("Using stdin channel");
|
||||||
Ok(CableChannelPrototype::new(
|
Ok(CableChannelPrototype::stdin(args.preview_command))
|
||||||
"STDIN", "cat", false, None, None, None,
|
|
||||||
))
|
|
||||||
} else if let Some(prompt) = args.autocomplete_prompt {
|
} else if let Some(prompt) = args.autocomplete_prompt {
|
||||||
debug!("Using autocomplete prompt: {:?}", prompt);
|
debug!("Using autocomplete prompt: {:?}", prompt);
|
||||||
let channel_prototype = guess_channel_from_prompt(
|
let channel_prototype = guess_channel_from_prompt(
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
#![allow(unused_imports)]
|
|
||||||
//! This module provides a way to parse ansi escape codes and convert them to ratatui objects.
|
|
||||||
//!
|
|
||||||
//! This code is a modified version of [ansi_to_tui](https://github.com/ratatui/ansi-to-tui).
|
|
||||||
|
|
||||||
// mod ansi;
|
|
||||||
pub mod code;
|
|
||||||
pub mod error;
|
|
||||||
pub mod parser;
|
|
||||||
pub use error::Error;
|
|
||||||
use ratatui::text::Text;
|
|
||||||
|
|
||||||
/// `IntoText` will convert any type that has a `AsRef<[u8]>` to a Text.
|
|
||||||
pub trait IntoText {
|
|
||||||
/// Convert the type to a Text.
|
|
||||||
#[allow(clippy::wrong_self_convention)]
|
|
||||||
fn into_text(&self) -> Result<Text<'static>, Error>;
|
|
||||||
/// Convert the type to a Text while trying to copy as less as possible
|
|
||||||
#[cfg(feature = "zero-copy")]
|
|
||||||
fn to_text(&self) -> Result<Text<'_>, Error>;
|
|
||||||
}
|
|
||||||
impl<T> IntoText for T
|
|
||||||
where
|
|
||||||
T: AsRef<[u8]>,
|
|
||||||
{
|
|
||||||
fn into_text(&self) -> Result<Text<'static>, Error> {
|
|
||||||
Ok(parser::text(self.as_ref())?.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "zero-copy")]
|
|
||||||
fn to_text(&self) -> Result<Text<'_>, Error> {
|
|
||||||
Ok(parser::text_fast(self.as_ref())?.1)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
Copyright 2021 Uttarayan Mondal
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -1,140 +0,0 @@
|
|||||||
use ratatui::style::Color;
|
|
||||||
|
|
||||||
/// This enum stores most types of ansi escape sequences
|
|
||||||
///
|
|
||||||
/// You can turn an escape sequence to this enum variant using
|
|
||||||
/// `AnsiCode::from(code: u8)`
|
|
||||||
/// This doesn't support all of them but does support most of them.
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum AnsiCode {
|
|
||||||
/// Reset the terminal
|
|
||||||
Reset,
|
|
||||||
/// Set font to bold
|
|
||||||
Bold,
|
|
||||||
/// Set font to faint
|
|
||||||
Faint,
|
|
||||||
/// Set font to italic
|
|
||||||
Italic,
|
|
||||||
/// Set font to underline
|
|
||||||
Underline,
|
|
||||||
/// Set cursor to slowblink
|
|
||||||
SlowBlink,
|
|
||||||
/// Set cursor to rapidblink
|
|
||||||
RapidBlink,
|
|
||||||
/// Invert the colors
|
|
||||||
Reverse,
|
|
||||||
/// Conceal text
|
|
||||||
Conceal,
|
|
||||||
/// Display crossed out text
|
|
||||||
CrossedOut,
|
|
||||||
/// Choose primary font
|
|
||||||
PrimaryFont,
|
|
||||||
/// Choose alternate font
|
|
||||||
AlternateFont,
|
|
||||||
/// Choose alternate fonts 1-9
|
|
||||||
#[allow(dead_code)]
|
|
||||||
AlternateFonts(u8), // = 11..19, // from 11 to 19
|
|
||||||
/// Fraktur ? No clue
|
|
||||||
Fraktur,
|
|
||||||
/// Turn off bold
|
|
||||||
BoldOff,
|
|
||||||
/// Set text to normal
|
|
||||||
Normal,
|
|
||||||
/// Turn off Italic
|
|
||||||
NotItalic,
|
|
||||||
/// Turn off underline
|
|
||||||
UnderlineOff,
|
|
||||||
/// Turn off blinking
|
|
||||||
BlinkOff,
|
|
||||||
// 26 ?
|
|
||||||
/// Don't invert colors
|
|
||||||
InvertOff,
|
|
||||||
/// Reveal text
|
|
||||||
Reveal,
|
|
||||||
/// Turn off Crossedout text
|
|
||||||
CrossedOutOff,
|
|
||||||
/// Set foreground color (4-bit)
|
|
||||||
ForegroundColor(Color), //, 31..37//Issue 60553 https://github.com/rust-lang/rust/issues/60553
|
|
||||||
/// Set foreground color (8-bit and 24-bit)
|
|
||||||
SetForegroundColor,
|
|
||||||
/// Default foreground color
|
|
||||||
DefaultForegroundColor,
|
|
||||||
/// Set background color (4-bit)
|
|
||||||
BackgroundColor(Color), // 41..47
|
|
||||||
/// Set background color (8-bit and 24-bit)
|
|
||||||
SetBackgroundColor,
|
|
||||||
/// Default background color
|
|
||||||
DefaultBackgroundColor, // 49
|
|
||||||
/// Other / non supported escape codes
|
|
||||||
Code(Vec<u8>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u8> for AnsiCode {
|
|
||||||
fn from(code: u8) -> Self {
|
|
||||||
match code {
|
|
||||||
0 => AnsiCode::Reset,
|
|
||||||
1 => AnsiCode::Bold,
|
|
||||||
2 => AnsiCode::Faint,
|
|
||||||
3 => AnsiCode::Italic,
|
|
||||||
4 => AnsiCode::Underline,
|
|
||||||
5 => AnsiCode::SlowBlink,
|
|
||||||
6 => AnsiCode::RapidBlink,
|
|
||||||
7 => AnsiCode::Reverse,
|
|
||||||
8 => AnsiCode::Conceal,
|
|
||||||
9 => AnsiCode::CrossedOut,
|
|
||||||
10 => AnsiCode::PrimaryFont,
|
|
||||||
11 => AnsiCode::AlternateFont,
|
|
||||||
// AnsiCode::// AlternateFont = 11..19, // from 11 to 19
|
|
||||||
20 => AnsiCode::Fraktur,
|
|
||||||
21 => AnsiCode::BoldOff,
|
|
||||||
22 => AnsiCode::Normal,
|
|
||||||
23 => AnsiCode::NotItalic,
|
|
||||||
24 => AnsiCode::UnderlineOff,
|
|
||||||
25 => AnsiCode::BlinkOff,
|
|
||||||
// 26 ?
|
|
||||||
27 => AnsiCode::InvertOff,
|
|
||||||
28 => AnsiCode::Reveal,
|
|
||||||
29 => AnsiCode::CrossedOutOff,
|
|
||||||
30 => AnsiCode::ForegroundColor(Color::Black),
|
|
||||||
31 => AnsiCode::ForegroundColor(Color::Red),
|
|
||||||
32 => AnsiCode::ForegroundColor(Color::Green),
|
|
||||||
33 => AnsiCode::ForegroundColor(Color::Yellow),
|
|
||||||
34 => AnsiCode::ForegroundColor(Color::Blue),
|
|
||||||
35 => AnsiCode::ForegroundColor(Color::Magenta),
|
|
||||||
36 => AnsiCode::ForegroundColor(Color::Cyan),
|
|
||||||
37 => AnsiCode::ForegroundColor(Color::Gray),
|
|
||||||
38 => AnsiCode::SetForegroundColor,
|
|
||||||
39 => AnsiCode::DefaultForegroundColor,
|
|
||||||
40 => AnsiCode::BackgroundColor(Color::Black),
|
|
||||||
41 => AnsiCode::BackgroundColor(Color::Red),
|
|
||||||
42 => AnsiCode::BackgroundColor(Color::Green),
|
|
||||||
43 => AnsiCode::BackgroundColor(Color::Yellow),
|
|
||||||
44 => AnsiCode::BackgroundColor(Color::Blue),
|
|
||||||
45 => AnsiCode::BackgroundColor(Color::Magenta),
|
|
||||||
46 => AnsiCode::BackgroundColor(Color::Cyan),
|
|
||||||
47 => AnsiCode::BackgroundColor(Color::Gray),
|
|
||||||
48 => AnsiCode::SetBackgroundColor,
|
|
||||||
49 => AnsiCode::DefaultBackgroundColor,
|
|
||||||
90 => AnsiCode::ForegroundColor(Color::DarkGray),
|
|
||||||
91 => AnsiCode::ForegroundColor(Color::LightRed),
|
|
||||||
92 => AnsiCode::ForegroundColor(Color::LightGreen),
|
|
||||||
93 => AnsiCode::ForegroundColor(Color::LightYellow),
|
|
||||||
94 => AnsiCode::ForegroundColor(Color::LightBlue),
|
|
||||||
95 => AnsiCode::ForegroundColor(Color::LightMagenta),
|
|
||||||
96 => AnsiCode::ForegroundColor(Color::LightCyan),
|
|
||||||
#[allow(clippy::match_same_arms)]
|
|
||||||
97 => AnsiCode::ForegroundColor(Color::White),
|
|
||||||
100 => AnsiCode::BackgroundColor(Color::DarkGray),
|
|
||||||
101 => AnsiCode::BackgroundColor(Color::LightRed),
|
|
||||||
102 => AnsiCode::BackgroundColor(Color::LightGreen),
|
|
||||||
103 => AnsiCode::BackgroundColor(Color::LightYellow),
|
|
||||||
104 => AnsiCode::BackgroundColor(Color::LightBlue),
|
|
||||||
105 => AnsiCode::BackgroundColor(Color::LightMagenta),
|
|
||||||
106 => AnsiCode::BackgroundColor(Color::LightCyan),
|
|
||||||
107 => AnsiCode::ForegroundColor(Color::White),
|
|
||||||
code => AnsiCode::Code(vec![code]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
/// This enum stores the error types
|
|
||||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
|
||||||
pub enum Error {
|
|
||||||
/// Stack is empty (should never happen)
|
|
||||||
#[error("Internal error: stack is empty")]
|
|
||||||
NomError(String),
|
|
||||||
|
|
||||||
/// Error parsing the input as utf-8
|
|
||||||
#[cfg(feature = "simd")]
|
|
||||||
/// Cannot determine the foreground or background
|
|
||||||
#[error("{0:?}")]
|
|
||||||
Utf8Error(#[from] simdutf8::basic::Utf8Error),
|
|
||||||
|
|
||||||
#[cfg(not(feature = "simd"))]
|
|
||||||
/// Cannot determine the foreground or background
|
|
||||||
#[error("{0:?}")]
|
|
||||||
Utf8Error(#[from] std::string::FromUtf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<nom::Err<nom::error::Error<&[u8]>>> for Error {
|
|
||||||
fn from(e: nom::Err<nom::error::Error<&[u8]>>) -> Self {
|
|
||||||
Self::NomError(format!("{:?}", e))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,461 +0,0 @@
|
|||||||
use crate::preview::ansi::code::AnsiCode;
|
|
||||||
use nom::{
|
|
||||||
branch::alt,
|
|
||||||
bytes::complete::{tag, take, take_till, take_while},
|
|
||||||
character::{
|
|
||||||
complete::{char, i64, not_line_ending, u8},
|
|
||||||
is_alphabetic,
|
|
||||||
},
|
|
||||||
combinator::{map_res, opt},
|
|
||||||
multi::fold_many0,
|
|
||||||
sequence::{delimited, preceded, tuple},
|
|
||||||
IResult, Parser,
|
|
||||||
};
|
|
||||||
use ratatui::{
|
|
||||||
style::{Color, Modifier, Style, Stylize},
|
|
||||||
text::{Line, Span, Text},
|
|
||||||
};
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
||||||
enum ColorType {
|
|
||||||
/// Eight Bit color
|
|
||||||
EightBit,
|
|
||||||
/// 24-bit color or true color
|
|
||||||
TrueColor,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
struct AnsiItem {
|
|
||||||
code: AnsiCode,
|
|
||||||
color: Option<Color>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
struct AnsiStates {
|
|
||||||
pub items: SmallVec<[AnsiItem; 2]>,
|
|
||||||
pub style: Style,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<AnsiStates> for Style {
|
|
||||||
fn from(states: AnsiStates) -> Self {
|
|
||||||
let mut style = states.style;
|
|
||||||
for item in states.items {
|
|
||||||
match item.code {
|
|
||||||
AnsiCode::Bold => style = style.add_modifier(Modifier::BOLD),
|
|
||||||
AnsiCode::Faint => style = style.add_modifier(Modifier::DIM),
|
|
||||||
AnsiCode::Normal => {
|
|
||||||
style = style
|
|
||||||
.remove_modifier(Modifier::BOLD)
|
|
||||||
.remove_modifier(Modifier::DIM);
|
|
||||||
}
|
|
||||||
AnsiCode::Italic => {
|
|
||||||
style = style.add_modifier(Modifier::ITALIC);
|
|
||||||
}
|
|
||||||
AnsiCode::Underline => {
|
|
||||||
style = style.add_modifier(Modifier::UNDERLINED);
|
|
||||||
}
|
|
||||||
AnsiCode::SlowBlink => {
|
|
||||||
style = style.add_modifier(Modifier::SLOW_BLINK);
|
|
||||||
}
|
|
||||||
AnsiCode::RapidBlink => {
|
|
||||||
style = style.add_modifier(Modifier::RAPID_BLINK);
|
|
||||||
}
|
|
||||||
AnsiCode::Reverse => {
|
|
||||||
style = style.add_modifier(Modifier::REVERSED);
|
|
||||||
}
|
|
||||||
AnsiCode::Conceal => {
|
|
||||||
style = style.add_modifier(Modifier::HIDDEN);
|
|
||||||
}
|
|
||||||
AnsiCode::CrossedOut => {
|
|
||||||
style = style.add_modifier(Modifier::CROSSED_OUT);
|
|
||||||
}
|
|
||||||
AnsiCode::DefaultForegroundColor => {
|
|
||||||
style = style.fg(Color::Reset);
|
|
||||||
}
|
|
||||||
AnsiCode::SetForegroundColor => {
|
|
||||||
if let Some(color) = item.color {
|
|
||||||
style = style.fg(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AnsiCode::ForegroundColor(color) => style = style.fg(color),
|
|
||||||
AnsiCode::Reset => style = style.fg(Color::Reset),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
style
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
|
||||||
pub(crate) fn text(mut s: &[u8]) -> IResult<&[u8], Text<'static>> {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
let mut last_style = Style::new();
|
|
||||||
while let Ok((remaining, (line, style))) = line(last_style)(s) {
|
|
||||||
lines.push(line);
|
|
||||||
last_style = style;
|
|
||||||
s = remaining;
|
|
||||||
if s.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok((s, Text::from(lines)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "zero-copy")]
|
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
|
||||||
pub(crate) fn text_fast(mut s: &[u8]) -> IResult<&[u8], Text<'_>> {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
let mut last = Style::new();
|
|
||||||
while let Ok((c, (line, style))) = line_fast(last)(s) {
|
|
||||||
lines.push(line);
|
|
||||||
last = style;
|
|
||||||
s = c;
|
|
||||||
if s.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok((s, Text::from(lines)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn line(
|
|
||||||
style: Style,
|
|
||||||
) -> impl Fn(&[u8]) -> IResult<&[u8], (Line<'static>, Style)> {
|
|
||||||
// let style_: Style = Default::default();
|
|
||||||
move |s: &[u8]| -> IResult<&[u8], (Line<'static>, Style)> {
|
|
||||||
// consume s until a line ending is found
|
|
||||||
let (s, mut text) = not_line_ending(s)?;
|
|
||||||
// discard the line ending
|
|
||||||
let (s, _) = opt(alt((tag("\r\n"), tag("\n"))))(s)?;
|
|
||||||
let mut spans = Vec::new();
|
|
||||||
// carry over the style from the previous line (passed in as an argument)
|
|
||||||
let mut last_style = style;
|
|
||||||
// parse spans from the given text
|
|
||||||
while let Ok((remaining, span)) = span(last_style)(text) {
|
|
||||||
// Since reset now tracks separately we can skip the reset check
|
|
||||||
last_style = last_style.patch(span.style);
|
|
||||||
|
|
||||||
if !span.content.is_empty() {
|
|
||||||
spans.push(span);
|
|
||||||
}
|
|
||||||
text = remaining;
|
|
||||||
if text.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: what is last_style here
|
|
||||||
Ok((s, (Line::from(spans), last_style)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "zero-copy")]
|
|
||||||
fn line_fast(
|
|
||||||
style: Style,
|
|
||||||
) -> impl Fn(&[u8]) -> IResult<&[u8], (Line<'_>, Style)> {
|
|
||||||
// let style_: Style = Default::default();
|
|
||||||
move |s: &[u8]| -> IResult<&[u8], (Line<'_>, Style)> {
|
|
||||||
let (s, mut text) = not_line_ending(s)?;
|
|
||||||
let (s, _) = opt(alt((tag("\r\n"), tag("\n"))))(s)?;
|
|
||||||
let mut spans = Vec::new();
|
|
||||||
let mut last = style;
|
|
||||||
while let Ok((s, span)) = span_fast(last)(text) {
|
|
||||||
last = last.patch(span.style);
|
|
||||||
// If the spans is empty then it might be possible that the style changes
|
|
||||||
// but there is no text change
|
|
||||||
if !span.content.is_empty() {
|
|
||||||
spans.push(span);
|
|
||||||
}
|
|
||||||
text = s;
|
|
||||||
if text.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((s, (Line::from(spans), last)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fn span(s: &[u8]) -> IResult<&[u8], tui::text::Span> {
|
|
||||||
fn span(
|
|
||||||
last: Style,
|
|
||||||
) -> impl Fn(&[u8]) -> IResult<&[u8], Span<'static>, nom::error::Error<&[u8]>>
|
|
||||||
{
|
|
||||||
move |s: &[u8]| -> IResult<&[u8], Span<'static>> {
|
|
||||||
let mut last_style = last;
|
|
||||||
// optionally consume a style
|
|
||||||
let (s, maybe_style) = opt(style(last_style))(s)?;
|
|
||||||
|
|
||||||
// consume until an escape sequence is found
|
|
||||||
#[cfg(feature = "simd")]
|
|
||||||
let (s, text) = map_res(take_while(|c| c != b'\x1b'), |t| {
|
|
||||||
simdutf8::basic::from_utf8(t)
|
|
||||||
})(s)?;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "simd"))]
|
|
||||||
let (s, text) =
|
|
||||||
map_res(take_while(|c| c != b'\x1b'), |t| std::str::from_utf8(t))(
|
|
||||||
s,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// if a style was found, patch the last style with it
|
|
||||||
if let Some(st) = maybe_style.flatten() {
|
|
||||||
last_style = last_style.patch(st);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((s, Span::styled(text.to_owned(), last_style)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "zero-copy")]
|
|
||||||
fn span_fast(
|
|
||||||
last: Style,
|
|
||||||
) -> impl Fn(&[u8]) -> IResult<&[u8], Span<'_>, nom::error::Error<&[u8]>> {
|
|
||||||
move |s: &[u8]| -> IResult<&[u8], Span<'_>> {
|
|
||||||
let mut last = last;
|
|
||||||
let (s, style) = opt(style(last))(s)?;
|
|
||||||
|
|
||||||
#[cfg(feature = "simd")]
|
|
||||||
let (s, text) = map_res(take_while(|c| c != b'\x1b'), |t| {
|
|
||||||
simdutf8::basic::from_utf8(t)
|
|
||||||
})(s)?;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "simd"))]
|
|
||||||
let (s, text) =
|
|
||||||
map_res(take_while(|c| c != b'\x1b'), |t| std::str::from_utf8(t))(
|
|
||||||
s,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if let Some(style) = style.flatten() {
|
|
||||||
last = last.patch(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((s, Span::styled(text, last)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
fn style(
|
|
||||||
style: Style,
|
|
||||||
) -> impl Fn(&[u8]) -> IResult<&[u8], Option<Style>, nom::error::Error<&[u8]>>
|
|
||||||
{
|
|
||||||
move |s: &[u8]| -> IResult<&[u8], Option<Style>> {
|
|
||||||
let (s, r) = match opt(ansi_sgr_code)(s)? {
|
|
||||||
(s, Some(r)) => {
|
|
||||||
// This would correspond to an implicit reset code (\x1b[m)
|
|
||||||
if r.is_empty() {
|
|
||||||
let mut sv = SmallVec::<[AnsiItem; 2]>::new();
|
|
||||||
sv.push(AnsiItem {
|
|
||||||
code: AnsiCode::Reset,
|
|
||||||
color: None,
|
|
||||||
});
|
|
||||||
(s, Some(sv))
|
|
||||||
} else {
|
|
||||||
(s, Some(r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(s, None) => {
|
|
||||||
let (s, _) = any_escape_sequence(s)?;
|
|
||||||
(s, None)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok((s, r.map(|r| Style::from(AnsiStates { style, items: r }))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A complete ANSI SGR code
|
|
||||||
fn ansi_sgr_code(
|
|
||||||
s: &[u8],
|
|
||||||
) -> IResult<&[u8], smallvec::SmallVec<[AnsiItem; 2]>, nom::error::Error<&[u8]>>
|
|
||||||
{
|
|
||||||
delimited(
|
|
||||||
tag("\x1b["),
|
|
||||||
fold_many0(
|
|
||||||
ansi_sgr_item,
|
|
||||||
smallvec::SmallVec::new,
|
|
||||||
|mut items, item| {
|
|
||||||
items.push(item);
|
|
||||||
items
|
|
||||||
},
|
|
||||||
),
|
|
||||||
char('m'),
|
|
||||||
)(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn any_escape_sequence(s: &[u8]) -> IResult<&[u8], Option<&[u8]>> {
|
|
||||||
// Attempt to consume most escape codes, including a single escape char.
|
|
||||||
//
|
|
||||||
// Most escape codes begin with ESC[ and are terminated by an alphabetic character,
|
|
||||||
// but OSC codes begin with ESC] and are terminated by an ascii bell (\x07)
|
|
||||||
// and a truncated/invalid code may just be a standalone ESC or not be terminated.
|
|
||||||
//
|
|
||||||
// We should try to consume as much of it as possible to match behavior of most terminals;
|
|
||||||
// where we fail at that we should at least consume the escape char to avoid infinitely looping
|
|
||||||
|
|
||||||
let (input, garbage) = preceded(
|
|
||||||
char('\x1b'),
|
|
||||||
opt(alt((
|
|
||||||
delimited(char('['), take_till(is_alphabetic), opt(take(1u8))),
|
|
||||||
delimited(char(']'), take_till(|c| c == b'\x07'), opt(take(1u8))),
|
|
||||||
))),
|
|
||||||
)(s)?;
|
|
||||||
Ok((input, garbage))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An ANSI SGR attribute
|
|
||||||
fn ansi_sgr_item(s: &[u8]) -> IResult<&[u8], AnsiItem> {
|
|
||||||
let (s, c) = u8(s)?;
|
|
||||||
let code = AnsiCode::from(c);
|
|
||||||
let (s, color) = match code {
|
|
||||||
AnsiCode::SetForegroundColor | AnsiCode::SetBackgroundColor => {
|
|
||||||
let (s, _) = opt(tag(";"))(s)?;
|
|
||||||
let (s, color) = color(s)?;
|
|
||||||
(s, Some(color))
|
|
||||||
}
|
|
||||||
_ => (s, None),
|
|
||||||
};
|
|
||||||
let (s, _) = opt(tag(";"))(s)?;
|
|
||||||
Ok((s, AnsiItem { code, color }))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color(s: &[u8]) -> IResult<&[u8], Color> {
|
|
||||||
let (s, c_type) = color_type(s)?;
|
|
||||||
let (s, _) = opt(tag(";"))(s)?;
|
|
||||||
match c_type {
|
|
||||||
ColorType::TrueColor => {
|
|
||||||
let (s, (r, _, g, _, b)) =
|
|
||||||
tuple((u8, tag(";"), u8, tag(";"), u8))(s)?;
|
|
||||||
Ok((s, Color::Rgb(r, g, b)))
|
|
||||||
}
|
|
||||||
ColorType::EightBit => {
|
|
||||||
let (s, index) = u8(s)?;
|
|
||||||
Ok((s, Color::Indexed(index)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color_type(s: &[u8]) -> IResult<&[u8], ColorType> {
|
|
||||||
let (s, t) = i64(s)?;
|
|
||||||
// NOTE: This isn't opt because a color type must always be followed by a color
|
|
||||||
// let (s, _) = opt(tag(";"))(s)?;
|
|
||||||
let (s, _) = tag(";")(s)?;
|
|
||||||
match t {
|
|
||||||
2 => Ok((s, ColorType::TrueColor)),
|
|
||||||
5 => Ok((s, ColorType::EightBit)),
|
|
||||||
_ => Err(nom::Err::Error(nom::error::Error::new(
|
|
||||||
s,
|
|
||||||
nom::error::ErrorKind::Alt,
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn color_test() {
|
|
||||||
let c = color(b"2;255;255;255").unwrap();
|
|
||||||
assert_eq!(c.1, Color::Rgb(255, 255, 255));
|
|
||||||
let c = color(b"5;255").unwrap();
|
|
||||||
assert_eq!(c.1, Color::Indexed(255));
|
|
||||||
let err = color(b"10;255");
|
|
||||||
assert_ne!(err, Ok(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_color_reset() {
|
|
||||||
let t = text(b"\x1b[33msome arbitrary text\x1b[0m\nmore text")
|
|
||||||
.unwrap()
|
|
||||||
.1;
|
|
||||||
assert_eq!(
|
|
||||||
t,
|
|
||||||
Text::from(vec![
|
|
||||||
Line::from(vec![Span::styled(
|
|
||||||
"some arbitrary text",
|
|
||||||
Style::default().fg(Color::Yellow)
|
|
||||||
),]),
|
|
||||||
Line::from(Span::from("more text").fg(Color::Reset)),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_color_reset_implicit_escape() {
|
|
||||||
let t = text(b"\x1b[33msome arbitrary text\x1b[m\nmore text")
|
|
||||||
.unwrap()
|
|
||||||
.1;
|
|
||||||
assert_eq!(
|
|
||||||
t,
|
|
||||||
Text::from(vec![
|
|
||||||
Line::from(vec![Span::styled(
|
|
||||||
"some arbitrary text",
|
|
||||||
Style::default().fg(Color::Yellow)
|
|
||||||
),]),
|
|
||||||
Line::from(Span::from("more text").fg(Color::Reset)),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ansi_items_test() {
|
|
||||||
let sc = Style::default();
|
|
||||||
let t = style(sc)(b"\x1b[38;2;3;3;3m").unwrap().1.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
t,
|
|
||||||
Style::from(AnsiStates {
|
|
||||||
style: sc,
|
|
||||||
items: vec![AnsiItem {
|
|
||||||
code: AnsiCode::SetForegroundColor,
|
|
||||||
color: Some(Color::Rgb(3, 3, 3))
|
|
||||||
}]
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
style(sc)(b"\x1b[38;5;3m").unwrap().1.unwrap(),
|
|
||||||
Style::from(AnsiStates {
|
|
||||||
style: sc,
|
|
||||||
items: vec![AnsiItem {
|
|
||||||
code: AnsiCode::SetForegroundColor,
|
|
||||||
color: Some(Color::Indexed(3))
|
|
||||||
}]
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
style(sc)(b"\x1b[38;5;3;48;5;3m").unwrap().1.unwrap(),
|
|
||||||
Style::from(AnsiStates {
|
|
||||||
style: sc,
|
|
||||||
items: vec![
|
|
||||||
AnsiItem {
|
|
||||||
code: AnsiCode::SetForegroundColor,
|
|
||||||
color: Some(Color::Indexed(3))
|
|
||||||
},
|
|
||||||
AnsiItem {
|
|
||||||
code: AnsiCode::SetBackgroundColor,
|
|
||||||
color: Some(Color::Indexed(3))
|
|
||||||
}
|
|
||||||
]
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
style(sc)(b"\x1b[38;5;3;48;5;3;1m").unwrap().1.unwrap(),
|
|
||||||
Style::from(AnsiStates {
|
|
||||||
style: sc,
|
|
||||||
items: vec![
|
|
||||||
AnsiItem {
|
|
||||||
code: AnsiCode::SetForegroundColor,
|
|
||||||
color: Some(Color::Indexed(3))
|
|
||||||
},
|
|
||||||
AnsiItem {
|
|
||||||
code: AnsiCode::SetBackgroundColor,
|
|
||||||
color: Some(Color::Indexed(3))
|
|
||||||
},
|
|
||||||
AnsiItem {
|
|
||||||
code: AnsiCode::Bold,
|
|
||||||
color: None
|
|
||||||
}
|
|
||||||
]
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use devicons::FileIcon;
|
use devicons::FileIcon;
|
||||||
|
|
||||||
pub mod ansi;
|
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod meta;
|
pub mod meta;
|
||||||
pub mod previewer;
|
pub mod previewer;
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
use crate::preview::PreviewState;
|
use crate::preview::PreviewState;
|
||||||
use crate::preview::{
|
use crate::preview::{PreviewContent, LOADING_MSG, TIMEOUT_MSG};
|
||||||
ansi::IntoText, PreviewContent, LOADING_MSG, TIMEOUT_MSG,
|
use crate::screen::colors::Colorscheme;
|
||||||
};
|
|
||||||
use crate::screen::colors::{Colorscheme, PreviewColorscheme};
|
|
||||||
use crate::utils::strings::{
|
use crate::utils::strings::{
|
||||||
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
||||||
EMPTY_STRING,
|
EMPTY_STRING,
|
||||||
};
|
};
|
||||||
|
use ansi_to_tui::IntoText;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use devicons::FileIcon;
|
use devicons::FileIcon;
|
||||||
use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap};
|
use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
@ -49,12 +48,12 @@ pub fn draw_preview_content_block(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_preview_paragraph<'a>(
|
pub fn build_preview_paragraph(
|
||||||
inner: Rect,
|
inner: Rect,
|
||||||
preview_content: &'a PreviewContent,
|
preview_content: &PreviewContent,
|
||||||
target_line: Option<u16>,
|
target_line: Option<u16>,
|
||||||
preview_scroll: u16,
|
preview_scroll: u16,
|
||||||
) -> Paragraph<'a> {
|
) -> Paragraph<'_> {
|
||||||
let preview_block =
|
let preview_block =
|
||||||
Block::default().style(Style::default()).padding(Padding {
|
Block::default().style(Style::default()).padding(Padding {
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -119,65 +118,6 @@ fn build_ansi_text_paragraph<'a>(
|
|||||||
.scroll((preview_scroll, 0))
|
.scroll((preview_scroll, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_plain_text_paragraph<'a>(
|
|
||||||
text: &'a [String],
|
|
||||||
preview_block: Block<'a>,
|
|
||||||
target_line: Option<u16>,
|
|
||||||
preview_scroll: u16,
|
|
||||||
colorscheme: PreviewColorscheme,
|
|
||||||
) -> Paragraph<'a> {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
for (i, line) in text.iter().enumerate() {
|
|
||||||
lines.push(Line::from(vec![
|
|
||||||
build_line_number_span(i + 1).style(Style::default().fg(
|
|
||||||
if matches!(
|
|
||||||
target_line,
|
|
||||||
Some(l) if l == u16::try_from(i).unwrap_or(0) + 1
|
|
||||||
)
|
|
||||||
{
|
|
||||||
colorscheme.gutter_selected_fg
|
|
||||||
} else {
|
|
||||||
colorscheme.gutter_fg
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
Span::styled(" │ ",
|
|
||||||
Style::default().fg(colorscheme.gutter_fg).dim()),
|
|
||||||
Span::styled(
|
|
||||||
line.to_string(),
|
|
||||||
Style::default().fg(colorscheme.content_fg).bg(
|
|
||||||
if matches!(target_line, Some(l) if l == u16::try_from(i).unwrap() + 1) {
|
|
||||||
colorscheme.highlight_bg
|
|
||||||
} else {
|
|
||||||
Color::Reset
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
let text = Text::from(lines);
|
|
||||||
Paragraph::new(text)
|
|
||||||
.block(preview_block)
|
|
||||||
.scroll((preview_scroll, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_plain_text_wrapped_paragraph<'a>(
|
|
||||||
text: &'a str,
|
|
||||||
preview_block: Block<'a>,
|
|
||||||
colorscheme: PreviewColorscheme,
|
|
||||||
) -> Paragraph<'a> {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
for line in text.lines() {
|
|
||||||
lines.push(Line::styled(
|
|
||||||
line.to_string(),
|
|
||||||
Style::default().fg(colorscheme.content_fg),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let text = Text::from(lines);
|
|
||||||
Paragraph::new(text)
|
|
||||||
.block(preview_block)
|
|
||||||
.wrap(Wrap { trim: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_meta_preview_paragraph<'a>(
|
pub fn build_meta_preview_paragraph<'a>(
|
||||||
inner: Rect,
|
inner: Rect,
|
||||||
message: &str,
|
message: &str,
|
||||||
@ -286,7 +226,3 @@ fn draw_content_outer_block(
|
|||||||
f.render_widget(preview_outer_block, rect);
|
f.render_widget(preview_outer_block, rect);
|
||||||
Ok(inner)
|
Ok(inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_line_number_span<'a>(line_number: usize) -> Span<'a> {
|
|
||||||
Span::from(format!("{line_number:5} "))
|
|
||||||
}
|
|
||||||
|
@ -6,7 +6,7 @@ use crate::{
|
|||||||
prototypes::{CableChannelPrototype, CableChannelPrototypes},
|
prototypes::{CableChannelPrototype, CableChannelPrototypes},
|
||||||
Channel as CableChannel,
|
Channel as CableChannel,
|
||||||
},
|
},
|
||||||
entry::{Entry, ENTRY_PLACEHOLDER},
|
entry::Entry,
|
||||||
remote_control::RemoteControl,
|
remote_control::RemoteControl,
|
||||||
// stdin::Channel as StdinChannel,
|
// stdin::Channel as StdinChannel,
|
||||||
OnAir,
|
OnAir,
|
||||||
@ -201,6 +201,7 @@ impl Television {
|
|||||||
self.reset_picker_input();
|
self.reset_picker_input();
|
||||||
self.current_pattern = EMPTY_STRING.to_string();
|
self.current_pattern = EMPTY_STRING.to_string();
|
||||||
self.channel.shutdown();
|
self.channel.shutdown();
|
||||||
|
self.previewer = (&channel_prototype).into();
|
||||||
self.channel = channel_prototype.into();
|
self.channel = channel_prototype.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,11 +388,15 @@ impl Television {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if self.config.ui.show_preview_panel
|
if self.config.ui.show_preview_panel
|
||||||
&& self.channel.supports_preview()
|
&& self.channel.supports_preview()
|
||||||
|
// FIXME: this is probably redundant with the channel supporting previews
|
||||||
&& self.previewer.is_some()
|
&& self.previewer.is_some()
|
||||||
{
|
{
|
||||||
// preview content
|
// preview content
|
||||||
if let Some(preview) =
|
if let Some(preview) = self
|
||||||
self.previewer.as_mut().unwrap().preview(selected_entry)
|
.previewer
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.handle_request(selected_entry)
|
||||||
{
|
{
|
||||||
// only update if the preview content has changed
|
// only update if the preview content has changed
|
||||||
if self.preview_state.preview.title != preview.title {
|
if self.preview_state.preview.title != preview.title {
|
||||||
@ -648,11 +653,11 @@ impl Television {
|
|||||||
self.update_rc_picker_state();
|
self.update_rc_picker_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
let selected_entry = self
|
if let Some(selected_entry) =
|
||||||
.get_selected_entry(Some(Mode::Channel))
|
self.get_selected_entry(Some(Mode::Channel))
|
||||||
.unwrap_or(ENTRY_PLACEHOLDER);
|
{
|
||||||
|
self.update_preview_state(&selected_entry)?;
|
||||||
self.update_preview_state(&selected_entry)?;
|
}
|
||||||
|
|
||||||
self.ticks += 1;
|
self.ticks += 1;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user