diff --git a/benches/main/ui.rs b/benches/main/ui.rs index 514d5ae..229f582 100644 --- a/benches/main/ui.rs +++ b/benches/main/ui.rs @@ -31,352 +31,387 @@ pub fn draw_results_list(c: &mut Criterion) { Entry { display: None, raw: "typeshed/LICENSE".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{f016}', color: "#7e8e91", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/README.md".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{f48a}', color: "#dddddd", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/re.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/io.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/gc.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/uu.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/nt.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/dis.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/imp.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/bdb.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/abc.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/cgi.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/bz2.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/grp.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/ast.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/csv.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/pdb.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/pwd.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/ssl.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/tty.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/nis.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/pty.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/cmd.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/tests/utils.py".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/pyproject.toml".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e6b2}', color: "#9c4221", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/MAINTAINERS.md".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{f48a}', color: "#dddddd", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/enum.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/hmac.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/uuid.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/glob.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/_ast.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/_csv.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/code.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/spwd.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, raw: "typeshed/stdlib/_msi.pyi".to_string(), - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), icon: Some(FileIcon { icon: '\u{e606}', color: "#ffbc03", }), line_number: None, + ansi: false, }, Entry { display: None, @@ -387,7 +422,8 @@ pub fn draw_results_list(c: &mut Criterion) { color: "#ffbc03", }), line_number: None, - name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + match_ranges: Some(into_ranges(&[0, 1, 2, 3])), + ansi: false, }, ]; diff --git a/man/tv.1 b/man/tv.1 index ed6b95c..d04cbcc 100644 --- a/man/tv.1 +++ b/man/tv.1 @@ -4,7 +4,7 @@ .SH NAME television \- Cross\-platform, fast and extensible general purpose fuzzy finder TUI. .SH SYNOPSIS -\fBtelevision\fR [\fB\-\-preview\-offset\fR] [\fB\-\-no\-preview\fR] [\fB\-\-hide\-preview\fR] [\fB\-\-show\-preview\fR] [\fB\-\-no\-status\-bar\fR] [\fB\-\-hide\-status\-bar\fR] [\fB\-\-show\-status\-bar\fR] [\fB\-t\fR|\fB\-\-tick\-rate\fR] [\fB\-\-watch\fR] [\fB\-k\fR|\fB\-\-keybindings\fR] [\fB\-i\fR|\fB\-\-input\fR] [\fB\-\-input\-header\fR] [\fB\-\-preview\-header\fR] [\fB\-\-preview\-footer\fR] [\fB\-\-source\-command\fR] [\fB\-\-source\-display\fR] [\fB\-\-source\-output\fR] [\fB\-\-source\-delimiter\fR] [\fB\-p\fR|\fB\-\-preview\-command\fR] [\fB\-\-layout\fR] [\fB\-\-autocomplete\-prompt\fR] [\fB\-\-exact\fR] [\fB\-\-select\-1\fR] [\fB\-\-take\-1\fR] [\fB\-\-take\-1\-fast\fR] [\fB\-\-no\-remote\fR] [\fB\-\-hide\-remote\fR] [\fB\-\-show\-remote\fR] [\fB\-\-no\-help\-panel\fR] [\fB\-\-hide\-help\-panel\fR] [\fB\-\-show\-help\-panel\fR] [\fB\-\-ui\-scale\fR] [\fB\-\-preview\-size\fR] [\fB\-\-config\-file\fR] [\fB\-\-cable\-dir\fR] [\fB\-\-global\-history\fR] [\fB\-\-height\fR] [\fB\-\-width\fR] [\fB\-\-inline\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICHANNEL\fR] [\fIPATH\fR] [\fIsubcommands\fR] +\fBtelevision\fR [\fB\-\-preview\-offset\fR] [\fB\-\-no\-preview\fR] [\fB\-\-hide\-preview\fR] [\fB\-\-show\-preview\fR] [\fB\-\-no\-status\-bar\fR] [\fB\-\-hide\-status\-bar\fR] [\fB\-\-show\-status\-bar\fR] [\fB\-t\fR|\fB\-\-tick\-rate\fR] [\fB\-\-watch\fR] [\fB\-k\fR|\fB\-\-keybindings\fR] [\fB\-i\fR|\fB\-\-input\fR] [\fB\-\-input\-header\fR] [\fB\-\-input\-prompt\fR] [\fB\-\-preview\-header\fR] [\fB\-\-preview\-footer\fR] [\fB\-s\fR|\fB\-\-source\-command\fR] [\fB\-\-ansi\fR] [\fB\-\-source\-display\fR] [\fB\-\-source\-output\fR] [\fB\-\-source\-entry\-delimiter\fR] [\fB\-p\fR|\fB\-\-preview\-command\fR] [\fB\-\-layout\fR] [\fB\-\-autocomplete\-prompt\fR] [\fB\-\-exact\fR] [\fB\-\-select\-1\fR] [\fB\-\-take\-1\fR] [\fB\-\-take\-1\-fast\fR] [\fB\-\-no\-remote\fR] [\fB\-\-hide\-remote\fR] [\fB\-\-show\-remote\fR] [\fB\-\-no\-help\-panel\fR] [\fB\-\-hide\-help\-panel\fR] [\fB\-\-show\-help\-panel\fR] [\fB\-\-ui\-scale\fR] [\fB\-\-preview\-size\fR] [\fB\-\-config\-file\fR] [\fB\-\-cable\-dir\fR] [\fB\-\-global\-history\fR] [\fB\-\-height\fR] [\fB\-\-width\fR] [\fB\-\-inline\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICHANNEL\fR] [\fIPATH\fR] [\fIsubcommands\fR] .SH DESCRIPTION Cross\-platform, fast and extensible general purpose fuzzy finder TUI. .SH OPTIONS @@ -102,6 +102,15 @@ The given value is parsed as a `MultiTemplate`. It is evaluated against the current channel name and the resulting text is shown as the input field title. Defaults to the current channel name when omitted. .TP +\fB\-\-input\-prompt\fR=\fISTRING\fR +Input prompt string + +When a channel is specified: This overrides the prompt defined in the channel prototype. +When no channel is specified: Sets the input prompt for the ad\-hoc channel. + +The given value is used as the prompt string shown before the input field. +Defaults to ">" when omitted. +.TP \fB\-\-preview\-header\fR=\fISTRING\fR Preview header template @@ -120,7 +129,7 @@ When no channel is specified: This flag requires \-\-preview\-command to be set. The given value is parsed as a `MultiTemplate`. It is evaluated for every entry and its result is displayed below the preview panel. .TP -\fB\-\-source\-command\fR=\fISTRING\fR +\fB\-s\fR, \fB\-\-source\-command\fR=\fISTRING\fR Source command to use for the current channel. When a channel is specified: This overrides the command defined in the channel prototype. @@ -128,6 +137,15 @@ When no channel is specified: This creates an ad\-hoc channel with the given com Example: `find . \-name \*(Aq*.rs\*(Aq` .TP +\fB\-\-ansi\fR +Whether tv should extract and parse ANSI style codes from the source command output. + +This is useful when the source command outputs colored text or other ANSI styles and you +want `tv` to preserve them in the UI. It does come with a slight performance cost but +which should go mostly unnoticed for typical human interaction workloads. + +Example: `tv \-\-source\-command="echo \-e \*(Aq\\x1b[31mRed\\x1b[0m\*(Aq" \-\-ansi` +.TP \fB\-\-source\-display\fR=\fISTRING\fR Source display template to use for the current channel. @@ -146,7 +164,7 @@ When no channel is specified: This flag requires \-\-source\-command to be set. The template is used to format the final output when an entry is selected. Example: "{}" (output the full entry) .TP -\fB\-\-source\-delimiter\fR=\fISTRING\fR +\fB\-\-source\-entry\-delimiter\fR=\fISTRING\fR The delimiter byte to use for splitting the source\*(Aqs command output into entries. This can be useful when the source command outputs multiline entries and you want to diff --git a/television/channels/channel.rs b/television/channels/channel.rs index 7807629..45ce513 100644 --- a/television/channels/channel.rs +++ b/television/channels/channel.rs @@ -1,7 +1,7 @@ use crate::{ channels::{ entry::Entry, - prototypes::{ChannelPrototype, SourceSpec}, + prototypes::{ChannelPrototype, SourceSpec, Template}, }, matcher::{Matcher, config::Config, injector::Injector}, utils::command::shell_command, @@ -13,7 +13,7 @@ use std::process::Stdio; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Duration; -use tracing::debug; +use tracing::{debug, trace}; const RELOAD_RENDERING_DELAY: Duration = Duration::from_millis(200); @@ -99,7 +99,8 @@ impl Channel { for item in results { let entry = Entry::new(item.inner) .with_display(item.matched_string) - .with_match_indices(&item.match_indices); + .with_match_indices(&item.match_indices) + .ansi(self.prototype.source.ansi); entries.push(entry); } @@ -204,6 +205,8 @@ async fn load_candidates( .map(|d| *d as u8) .unwrap_or(b'\n'); + let strip_ansi = Template::parse("{strip_ansi}").unwrap(); + loop { buf.clear(); let n = reader.read_until(delimiter, &mut buf).unwrap(); @@ -221,10 +224,17 @@ async fn load_candidates( } if let Ok(l) = std::str::from_utf8(&buf) { - debug!("Read line: {}", l); + trace!("Read line: {}", l); if !l.trim().is_empty() { let () = injector.push(l.to_string(), |e, cols| { - if let Some(display) = &source.display { + if source.ansi { + cols[0] = strip_ansi.format(e).unwrap_or_else(|_| { + panic!( + "Failed to strip ANSI from entry '{}'", + e + ); + }).into(); + } else if let Some(display) = &source.display { let formatted = display.format(e).unwrap_or_else(|_| { panic!( "Failed to format display expression '{}' with entry '{}'", diff --git a/television/channels/entry.rs b/television/channels/entry.rs index 7138465..9c6b274 100644 --- a/television/channels/entry.rs +++ b/television/channels/entry.rs @@ -11,12 +11,14 @@ pub struct Entry { pub raw: String, /// The actual entry string that will be displayed in the UI. pub display: Option, - /// The optional ranges for matching characters in the name. - pub name_match_ranges: Option>, + /// The optional ranges for matching characters (based on `self.display`). + pub match_ranges: Option>, /// The optional icon associated with the entry. pub icon: Option, /// The optional line number associated with the entry. pub line_number: Option, + /// Whether the entry contains ANSI escape sequences. + pub ansi: bool, } impl Hash for Entry { @@ -96,9 +98,10 @@ impl Entry { Self { raw, display: None, - name_match_ranges: None, + match_ranges: None, icon: None, line_number: None, + ansi: false, } } @@ -108,7 +111,7 @@ impl Entry { } pub fn with_match_indices(mut self, indices: &[u32]) -> Self { - self.name_match_ranges = Some(into_ranges(indices)); + self.match_ranges = Some(into_ranges(indices)); self } @@ -137,9 +140,19 @@ impl Entry { } self.raw.clone() } + + /// Sets whether the entry contains ANSI escape sequences. + pub fn ansi(mut self, ansi: bool) -> Self { + self.ansi = ansi; + self + } } impl ResultItem for Entry { + fn raw(&self) -> &str { + &self.raw + } + fn icon(&self) -> Option<&devicons::FileIcon> { self.icon.as_ref() } @@ -149,12 +162,16 @@ impl ResultItem for Entry { } fn match_ranges(&self) -> Option<&[(u32, u32)]> { - self.name_match_ranges.as_deref() + self.match_ranges.as_deref() } fn shortcut(&self) -> Option<&Binding> { None } + + fn ansi(&self) -> bool { + self.ansi + } } #[cfg(test)] @@ -190,9 +207,10 @@ mod tests { let entry = Entry { raw: "test name with spaces".to_string(), display: None, - name_match_ranges: None, + match_ranges: None, icon: None, line_number: None, + ansi: false, }; assert_eq!( entry.output(&Some(Template::parse("{}").unwrap())), diff --git a/television/channels/prototypes.rs b/television/channels/prototypes.rs index 41be5a0..35ebf34 100644 --- a/television/channels/prototypes.rs +++ b/television/channels/prototypes.rs @@ -217,6 +217,7 @@ impl ChannelPrototype { env: FxHashMap::default(), }, entry_delimiter: None, + ansi: false, display: None, output: None, }, @@ -246,6 +247,7 @@ impl ChannelPrototype { interactive: false, env: FxHashMap::default(), }, + ansi: false, entry_delimiter, display: None, output: None, @@ -317,6 +319,8 @@ pub struct SourceSpec { #[serde(deserialize_with = "deserialize_entry_delimiter", default)] pub entry_delimiter: Option, #[serde(default)] + pub ansi: bool, + #[serde(default)] pub display: Option