mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-29 06:11:37 +00:00
feat: ansi-styled results with --ansi
or corresponding channel option (#655)
# Examples: ## ripgrep ```toml [metadata] name = "text" description = "A channel to find and select text from files" requirements = ["rg", "bat"] [source] command = "rg . --no-heading --line-number --colors 'match:fg:white' --colors 'path:fg:blue' --color=always" ansi = true output = "{strip_ansi|split:\\::..2}" [preview] command = "bat -n --color=always '{strip_ansi|split:\\::0}'" env = { BAT_THEME = "ansi" } offset = '{strip_ansi|split:\::1}' [ui] preview_panel = { header = '{strip_ansi|split:\::..2}' } ``` <img width="2880" height="1620" alt="Screenshot From 2025-07-19 20-56-53" src="https://github.com/user-attachments/assets/67055fd1-d03e-4c4b-ad82-dbc3ba8c80d4" /> ## git-log ```toml [metadata] name = "git-log" description = "A channel to select from git log entries" requirements = ["git", "delta"] [source] command = "git log --graph --pretty=format:'%C(yellow)%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --color=always" output = "{strip_ansi|split: :1}" ansi = true [preview] command = "git show -p --stat --pretty=fuller --color=always '{strip_ansi|split: :1}' | delta" ``` <img width="2880" height="1620" alt="Screenshot From 2025-07-19 19-17-15" src="https://github.com/user-attachments/assets/be97ff35-8a35-4d26-ad14-afd5ece3fed6" />
This commit is contained in:
parent
c789802d86
commit
098f3f4fe4
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
24
man/tv.1
24
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
|
||||
|
@ -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 '{}'",
|
||||
|
@ -11,12 +11,14 @@ pub struct Entry {
|
||||
pub raw: String,
|
||||
/// The actual entry string that will be displayed in the UI.
|
||||
pub display: Option<String>,
|
||||
/// The optional ranges for matching characters in the name.
|
||||
pub name_match_ranges: Option<Vec<(u32, u32)>>,
|
||||
/// The optional ranges for matching characters (based on `self.display`).
|
||||
pub match_ranges: Option<Vec<(u32, u32)>>,
|
||||
/// The optional icon associated with the entry.
|
||||
pub icon: Option<FileIcon>,
|
||||
/// The optional line number associated with the entry.
|
||||
pub line_number: Option<usize>,
|
||||
/// 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())),
|
||||
|
@ -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<char>,
|
||||
#[serde(default)]
|
||||
pub ansi: bool,
|
||||
#[serde(default)]
|
||||
pub display: Option<Template>,
|
||||
#[serde(default)]
|
||||
pub output: Option<Template>,
|
||||
|
@ -50,6 +50,10 @@ impl CableEntry {
|
||||
}
|
||||
|
||||
impl ResultItem for CableEntry {
|
||||
fn raw(&self) -> &str {
|
||||
&self.channel_name
|
||||
}
|
||||
|
||||
fn icon(&self) -> Option<&devicons::FileIcon> {
|
||||
// Remote control entries always share the same popcorn icon
|
||||
Some(&CABLE_ICON)
|
||||
|
@ -138,7 +138,7 @@ pub struct Cli {
|
||||
/// 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.
|
||||
#[arg(long = "input-header", value_name = "STRING", verbatim_doc_comment)]
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub input_header: Option<String>,
|
||||
|
||||
/// Input prompt string
|
||||
@ -148,7 +148,7 @@ pub struct Cli {
|
||||
///
|
||||
/// The given value is used as the prompt string shown before the input field.
|
||||
/// Defaults to ">" when omitted.
|
||||
#[arg(long = "input-prompt", value_name = "STRING", verbatim_doc_comment)]
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub input_prompt: Option<String>,
|
||||
|
||||
/// Preview header template
|
||||
@ -159,7 +159,7 @@ pub struct Cli {
|
||||
/// The given value is parsed as a `MultiTemplate`. It is evaluated for every
|
||||
/// entry and its result is displayed above the preview panel.
|
||||
#[arg(
|
||||
long = "preview-header",
|
||||
long,
|
||||
value_name = "STRING",
|
||||
verbatim_doc_comment,
|
||||
conflicts_with = "no_preview"
|
||||
@ -174,7 +174,7 @@ pub struct Cli {
|
||||
/// The given value is parsed as a `MultiTemplate`. It is evaluated for every
|
||||
/// entry and its result is displayed below the preview panel.
|
||||
#[arg(
|
||||
long = "preview-footer",
|
||||
long,
|
||||
value_name = "STRING",
|
||||
verbatim_doc_comment,
|
||||
conflicts_with = "no_preview"
|
||||
@ -187,13 +187,19 @@ pub struct Cli {
|
||||
/// When no channel is specified: This creates an ad-hoc channel with the given command.
|
||||
///
|
||||
/// Example: `find . -name '*.rs'`
|
||||
#[arg(
|
||||
long = "source-command",
|
||||
value_name = "STRING",
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
#[arg(short, long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub source_command: Option<String>,
|
||||
|
||||
/// 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 '\x1b[31mRed\x1b[0m'" --ansi`
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
pub ansi: bool,
|
||||
|
||||
/// Source display template to use for the current channel.
|
||||
///
|
||||
/// When a channel is specified: This overrides the display template defined in the channel prototype.
|
||||
@ -201,11 +207,7 @@ pub struct Cli {
|
||||
///
|
||||
/// The template is used to format each entry in the results list.
|
||||
/// Example: `{split:/:-1}` (show only the last path segment)
|
||||
#[arg(
|
||||
long = "source-display",
|
||||
value_name = "STRING",
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub source_display: Option<String>,
|
||||
|
||||
/// Source output template to use for the current channel.
|
||||
@ -215,22 +217,14 @@ pub struct Cli {
|
||||
///
|
||||
/// The template is used to format the final output when an entry is selected.
|
||||
/// Example: "{}" (output the full entry)
|
||||
#[arg(
|
||||
long = "source-output",
|
||||
value_name = "STRING",
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub source_output: Option<String>,
|
||||
|
||||
/// The delimiter byte to use for splitting the source's command output into entries.
|
||||
///
|
||||
/// This can be useful when the source command outputs multiline entries and you want to
|
||||
/// rely on another delimiter to split the entries such a null byte or a custom character.
|
||||
#[arg(
|
||||
long = "source-delimiter",
|
||||
value_name = "STRING",
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub source_entry_delimiter: Option<String>,
|
||||
|
||||
/// Preview command to use for the current channel.
|
||||
@ -246,7 +240,7 @@ pub struct Cli {
|
||||
/// the first two fields to the command.
|
||||
#[arg(
|
||||
short,
|
||||
long = "preview-command",
|
||||
long,
|
||||
value_name = "STRING",
|
||||
verbatim_doc_comment,
|
||||
conflicts_with = "no_preview"
|
||||
@ -259,7 +253,7 @@ pub struct Cli {
|
||||
/// When no channel is specified: Sets the layout orientation for the ad-hoc channel.
|
||||
///
|
||||
/// Options are "landscape" or "portrait".
|
||||
#[arg(long = "layout", value_enum, verbatim_doc_comment)]
|
||||
#[arg(long, value_enum, verbatim_doc_comment)]
|
||||
pub layout: Option<LayoutOrientation>,
|
||||
|
||||
/// The working directory to start the application in.
|
||||
|
@ -51,6 +51,7 @@ pub struct PostProcessedCli {
|
||||
pub source_entry_delimiter: Option<char>,
|
||||
pub working_directory: Option<PathBuf>,
|
||||
pub autocomplete_prompt: Option<String>,
|
||||
pub ansi: bool,
|
||||
|
||||
// Preview configuration
|
||||
pub preview_command_override: Option<Template>,
|
||||
@ -122,6 +123,7 @@ impl Default for PostProcessedCli {
|
||||
source_entry_delimiter: None,
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
ansi: false,
|
||||
|
||||
// Preview configuration
|
||||
preview_command_override: None,
|
||||
@ -313,6 +315,7 @@ pub fn post_process(cli: Cli, readable_stdin: bool) -> PostProcessedCli {
|
||||
source_entry_delimiter,
|
||||
working_directory,
|
||||
autocomplete_prompt: cli.autocomplete_prompt,
|
||||
ansi: cli.ansi,
|
||||
|
||||
// Preview configuration
|
||||
preview_command_override,
|
||||
|
@ -338,6 +338,9 @@ fn apply_source_overrides(
|
||||
if let Some(source_output) = &args.source_output_override {
|
||||
prototype.source.output = Some(source_output.clone());
|
||||
}
|
||||
if args.ansi {
|
||||
prototype.source.ansi = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies preview-related CLI overrides to the channel prototype
|
||||
|
@ -263,7 +263,13 @@ pub fn try_preview(
|
||||
|
||||
let total_lines = u16::try_from(text.lines.len()).unwrap_or(0);
|
||||
|
||||
Preview::new(&entry.raw, text, None, total_lines, String::new())
|
||||
Preview::new(
|
||||
entry.display(),
|
||||
text,
|
||||
None,
|
||||
total_lines,
|
||||
String::new(),
|
||||
)
|
||||
} else {
|
||||
let mut text = child
|
||||
.stderr
|
||||
@ -284,7 +290,13 @@ pub fn try_preview(
|
||||
|
||||
let total_lines = u16::try_from(text.lines.len()).unwrap_or(0);
|
||||
|
||||
Preview::new(&entry.raw, text, None, total_lines, String::new())
|
||||
Preview::new(
|
||||
entry.display(),
|
||||
text,
|
||||
None,
|
||||
total_lines,
|
||||
String::new(),
|
||||
)
|
||||
}
|
||||
};
|
||||
results_handle
|
||||
|
@ -6,9 +6,10 @@ use crate::{
|
||||
},
|
||||
utils::{
|
||||
indices::truncate_highlighted_string,
|
||||
strings::make_matched_string_printable,
|
||||
strings::make_result_item_printable,
|
||||
},
|
||||
};
|
||||
use ansi_to_tui::IntoText;
|
||||
use devicons::FileIcon;
|
||||
use ratatui::{
|
||||
prelude::{Color, Line, Span, Style},
|
||||
@ -20,6 +21,9 @@ use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// Trait implemented by any item that can be displayed in the results or remote-control list.
|
||||
pub trait ResultItem {
|
||||
/// Returns the raw string representation of the item.
|
||||
fn raw(&self) -> &str;
|
||||
|
||||
/// Returns an optional icon to display in front of the item.
|
||||
fn icon(&self) -> Option<&FileIcon> {
|
||||
None
|
||||
@ -29,6 +33,8 @@ pub trait ResultItem {
|
||||
fn display(&self) -> &str;
|
||||
|
||||
/// Highlight match ranges (char based indices) within `display()`.
|
||||
///
|
||||
/// These are contiguous ranges of character indices that should be highlighted.
|
||||
fn match_ranges(&self) -> Option<&[(u32, u32)]> {
|
||||
None
|
||||
}
|
||||
@ -37,13 +43,16 @@ pub trait ResultItem {
|
||||
fn shortcut(&self) -> Option<&Binding> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether the item uses ANSI escape codes for styling.
|
||||
fn ansi(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a single `Line` for a [`ResultItem`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
// TODO: pass the right colors directly as arguments and make the
|
||||
// calling function responsible for the colors used for each line.
|
||||
pub fn build_result_line<'a, T: ResultItem + ?Sized>(
|
||||
item: &'a T,
|
||||
use_icons: bool,
|
||||
@ -82,7 +91,7 @@ pub fn build_result_line<'a, T: ResultItem + ?Sized>(
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let name_max_width = area_width
|
||||
let item_max_width = area_width
|
||||
.saturating_sub(2) // pointer + space (kept for caller)
|
||||
.saturating_sub(2) // borders
|
||||
.saturating_sub(2 * u16::from(use_icons))
|
||||
@ -100,66 +109,20 @@ pub fn build_result_line<'a, T: ResultItem + ?Sized>(
|
||||
}
|
||||
}
|
||||
|
||||
let (mut entry_name, mut match_ranges) =
|
||||
make_matched_string_printable(item.display(), item.match_ranges());
|
||||
|
||||
// Truncate if too long.
|
||||
if UnicodeWidthStr::width(entry_name.as_str()) > name_max_width as usize {
|
||||
let (name, ranges) = truncate_highlighted_string(
|
||||
&entry_name,
|
||||
&match_ranges,
|
||||
name_max_width,
|
||||
);
|
||||
entry_name = name;
|
||||
match_ranges = ranges;
|
||||
}
|
||||
|
||||
// PERF: Early return for empty match ranges - common case optimization
|
||||
if match_ranges.is_empty() {
|
||||
spans.push(Span::styled(entry_name, Style::default().fg(result_fg)));
|
||||
if item.ansi() {
|
||||
spans.extend(build_entry_spans_ansi(
|
||||
item,
|
||||
item_max_width,
|
||||
result_fg,
|
||||
match_fg,
|
||||
));
|
||||
} else {
|
||||
// PERF: Collect chars once to avoid repeated Unicode parsing
|
||||
let chars: Vec<char> = entry_name.chars().collect();
|
||||
let name_len = chars.len();
|
||||
let mut last_end = 0;
|
||||
|
||||
for (start, end) in
|
||||
match_ranges.iter().map(|(s, e)| (*s as usize, *e as usize))
|
||||
{
|
||||
// Add unhighlighted text before match
|
||||
if start > last_end && start <= name_len {
|
||||
let text: String =
|
||||
chars[last_end..start.min(name_len)].iter().collect();
|
||||
if !text.is_empty() {
|
||||
spans.push(Span::styled(
|
||||
text,
|
||||
Style::default().fg(result_fg),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Add highlighted match
|
||||
if end > start && start < name_len {
|
||||
let text: String =
|
||||
chars[start..end.min(name_len)].iter().collect();
|
||||
if !text.is_empty() {
|
||||
spans.push(Span::styled(
|
||||
text,
|
||||
Style::default().fg(match_fg),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
last_end = end;
|
||||
}
|
||||
|
||||
// Add remaining unhighlighted text
|
||||
if last_end < name_len {
|
||||
let text: String = chars[last_end..].iter().collect();
|
||||
if !text.is_empty() {
|
||||
spans.push(Span::styled(text, Style::default().fg(result_fg)));
|
||||
}
|
||||
}
|
||||
spans.extend(build_entry_spans(
|
||||
item,
|
||||
item_max_width,
|
||||
result_fg,
|
||||
match_fg,
|
||||
));
|
||||
}
|
||||
|
||||
// Show shortcut if present.
|
||||
@ -187,6 +150,169 @@ pub fn build_result_line<'a, T: ResultItem + ?Sized>(
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
fn build_entry_spans<T: ResultItem + ?Sized>(
|
||||
item: &T,
|
||||
max_width: u16,
|
||||
result_fg: Color,
|
||||
match_fg: Color,
|
||||
) -> Vec<Span> {
|
||||
let mut spans = Vec::with_capacity(16);
|
||||
|
||||
let (mut entry_name, mut match_ranges) = make_result_item_printable(item);
|
||||
|
||||
// Truncate if too long.
|
||||
if UnicodeWidthStr::width(entry_name.as_str()) > max_width as usize {
|
||||
let (name, ranges) =
|
||||
truncate_highlighted_string(&entry_name, &match_ranges, max_width);
|
||||
entry_name = name;
|
||||
match_ranges = ranges;
|
||||
}
|
||||
|
||||
if match_ranges.is_empty() {
|
||||
spans.push(Span::styled(entry_name, Style::default().fg(result_fg)));
|
||||
return spans;
|
||||
}
|
||||
|
||||
let chars: Vec<char> = entry_name.chars().collect();
|
||||
let mut idx = 0;
|
||||
for &(start, end) in &match_ranges {
|
||||
let start = start as usize;
|
||||
let end = end as usize;
|
||||
if idx < start {
|
||||
let text: String = chars[idx..start].iter().collect();
|
||||
if !text.is_empty() {
|
||||
spans.push(Span::styled(text, Style::default().fg(result_fg)));
|
||||
}
|
||||
}
|
||||
if start < end {
|
||||
let text: String = chars[start..end].iter().collect();
|
||||
if !text.is_empty() {
|
||||
spans.push(Span::styled(text, Style::default().fg(match_fg)));
|
||||
}
|
||||
}
|
||||
idx = end;
|
||||
}
|
||||
if idx < chars.len() {
|
||||
let text: String = chars[idx..].iter().collect();
|
||||
if !text.is_empty() {
|
||||
spans.push(Span::styled(text, Style::default().fg(result_fg)));
|
||||
}
|
||||
}
|
||||
spans
|
||||
}
|
||||
|
||||
/// Builds a vector of [`Span`]s for a [`ResultItem`] that may contain ANSI escape codes.
|
||||
///
|
||||
/// # Algorithm
|
||||
///
|
||||
/// - 1/ The function parses the raw string of the item into styled spans using `ansi_to_tui`.
|
||||
///
|
||||
/// - 2/ If the parsed result contains only a single span with the default style (i.e., no ANSI codes),
|
||||
/// it falls back to the simpler `build_entry_spans` function.
|
||||
///
|
||||
/// - 3/ Otherwise, it iterates over the parsed spans and overlays the match highlight ranges onto them.
|
||||
/// - It tracks the current character position across all spans.
|
||||
/// - For each span, it walks through its characters, checking if the current position falls within
|
||||
/// any of the match highlight ranges.
|
||||
/// - If a highlight range is encountered, it splits the span into sub-spans:
|
||||
/// - Unhighlighted text before the match uses the original style.
|
||||
/// - Highlighted text within the match uses the original style but overrides the foreground color with `match_fg`.
|
||||
/// - Remaining text after the match continues with the original style.
|
||||
/// - The algorithm advances through both the spans and the highlight ranges, ensuring that highlights
|
||||
/// are applied correctly even if they cross span boundaries.
|
||||
/// - 4/ The result is a vector of spans that preserves the original ANSI styling, but overlays match highlights
|
||||
/// as specified by the input ranges.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `item`: The result item to render.
|
||||
/// - `max_width`: The maximum width for the rendered line (currently not used for truncation in this function).
|
||||
/// - `result_fg`: The default foreground color for non-highlighted text.
|
||||
/// - `match_fg`: The foreground color to use for highlighted (matched) text.
|
||||
///
|
||||
/// # Returns
|
||||
/// A vector of [`Span`]s, each with appropriate styling and highlighting.
|
||||
///
|
||||
/// # Notes
|
||||
/// - This function is designed to work with items that use ANSI escape codes for styling.
|
||||
/// - It ensures that match highlights do not disrupt the underlying ANSI styles, except for the foreground color.
|
||||
/// - If no ANSI codes are present, it delegates to the simpler span builder for efficiency.
|
||||
fn build_entry_spans_ansi<T: ResultItem + ?Sized>(
|
||||
item: &T,
|
||||
max_width: u16,
|
||||
result_fg: Color,
|
||||
match_fg: Color,
|
||||
) -> Vec<Span> {
|
||||
let text = item.raw();
|
||||
let match_ranges = item.match_ranges().unwrap_or(&[]);
|
||||
let parsed = text.into_text().unwrap();
|
||||
let spans = &parsed.lines[0].spans;
|
||||
|
||||
// If there are no ANSI codes, fall back to the simple span builder
|
||||
if spans.len() == 1 && spans[0].style == Style::default() {
|
||||
return build_entry_spans(item, max_width, result_fg, match_fg);
|
||||
}
|
||||
|
||||
// hypothesis: ~ 2 to 3 highlighted clusters + in the worst case scenario
|
||||
// each cluster splits its containing span into 3 parts -> + 6 spans so we
|
||||
// should be fine pre-allocating `spans.len() + 8`
|
||||
let mut highlighted_spans: Vec<Span> = Vec::with_capacity(spans.len() + 8);
|
||||
let mut hl_ranges = match_ranges
|
||||
.iter()
|
||||
.map(|(start, end)| (*start as usize, *end as usize))
|
||||
.peekable();
|
||||
let mut char_pos = 0;
|
||||
|
||||
for span in spans {
|
||||
let span_len = span.content.chars().count();
|
||||
let mut cursor = 0;
|
||||
while cursor < span_len {
|
||||
if let Some(&(hl_start, hl_end)) = hl_ranges.peek() {
|
||||
if char_pos + cursor >= hl_end {
|
||||
hl_ranges.next();
|
||||
continue;
|
||||
}
|
||||
let highlight_start = hl_start.saturating_sub(char_pos);
|
||||
let highlight_end = hl_end.saturating_sub(char_pos);
|
||||
|
||||
// note that the following will also just push the entire span if
|
||||
// the highlight range comes anywhere after the current span
|
||||
if cursor < highlight_start {
|
||||
let s: String = span
|
||||
.content
|
||||
.chars()
|
||||
.skip(cursor)
|
||||
.take(highlight_start - cursor)
|
||||
.collect();
|
||||
if !s.is_empty() {
|
||||
highlighted_spans.push(Span::styled(s, span.style));
|
||||
}
|
||||
cursor = highlight_start;
|
||||
} else {
|
||||
let s: String = span
|
||||
.content
|
||||
.chars()
|
||||
.skip(cursor)
|
||||
.take(highlight_end - cursor)
|
||||
.collect();
|
||||
if !s.is_empty() {
|
||||
highlighted_spans
|
||||
.push(Span::styled(s, span.style.fg(match_fg)));
|
||||
}
|
||||
cursor = highlight_end;
|
||||
}
|
||||
} else {
|
||||
let s: String = span.content.chars().skip(cursor).collect();
|
||||
if !s.is_empty() {
|
||||
highlighted_spans.push(Span::styled(s, span.style));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
char_pos += span_len;
|
||||
}
|
||||
highlighted_spans
|
||||
}
|
||||
|
||||
/// Build a `List` widget from a slice of [`ResultItem`]s.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn build_results_list<'a, 'b, T, F>(
|
||||
@ -281,4 +407,131 @@ mod tests {
|
||||
line.spans.iter().map(|s| s.content.clone()).collect();
|
||||
assert!(rendered.contains('…'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_entry_spans_ansi_no_ansi() {
|
||||
let entry = Entry::new("A simple string".to_string())
|
||||
.with_match_indices(&[3, 4, 5]);
|
||||
let spans =
|
||||
build_entry_spans_ansi(&entry, 200, Color::Blue, Color::Red);
|
||||
let blue_fg = Style::default().fg(Color::Blue);
|
||||
|
||||
assert_eq!(spans.len(), 3);
|
||||
assert_eq!(spans[0], Span::styled("A s", blue_fg));
|
||||
assert_eq!(
|
||||
spans[1],
|
||||
Span::styled("imp", Style::default().fg(Color::Red))
|
||||
);
|
||||
assert_eq!(spans[2], Span::styled("le string", blue_fg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_entry_spans_ansi_no_ansi_corner_cases() {
|
||||
let entry = Entry::new("A".to_string()).with_match_indices(&[0]);
|
||||
let spans =
|
||||
build_entry_spans_ansi(&entry, 200, Color::Reset, Color::Red);
|
||||
|
||||
assert_eq!(spans.len(), 1);
|
||||
assert_eq!(
|
||||
spans[0],
|
||||
Span::styled("A", Style::default().fg(Color::Red))
|
||||
);
|
||||
|
||||
let entry = Entry::new(String::new()).with_match_indices(&[]);
|
||||
let spans =
|
||||
build_entry_spans_ansi(&entry, 200, Color::Reset, Color::Red);
|
||||
|
||||
assert!(spans.is_empty());
|
||||
|
||||
let entry = Entry::new("A".to_string()).with_match_indices(&[]);
|
||||
let spans =
|
||||
build_entry_spans_ansi(&entry, 200, Color::Reset, Color::Red);
|
||||
|
||||
assert_eq!(spans.len(), 1);
|
||||
assert_eq!(
|
||||
spans[0],
|
||||
Span::styled("A", Style::default().fg(Color::Reset))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_entry_spans_ansi_with_ansi() {
|
||||
let entry = Entry::new(
|
||||
"\x1b[31mRed\x1b[0m and \x1b[32mGreen\x1b[0m".to_string(),
|
||||
)
|
||||
.with_match_indices(&[1, 4, 5]);
|
||||
let spans =
|
||||
build_entry_spans_ansi(&entry, 200, Color::Blue, Color::Yellow);
|
||||
|
||||
assert_eq!(
|
||||
spans.len(),
|
||||
7,
|
||||
"Expected 7 spans but got {:?}",
|
||||
spans
|
||||
.iter()
|
||||
.map(|s| (s.content.clone(), s.style.fg))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(spans[0], Span::raw("R").fg(Color::Red));
|
||||
assert_eq!(spans[1], Span::raw("e").fg(Color::Yellow));
|
||||
assert_eq!(spans[2], Span::raw("d").fg(Color::Red));
|
||||
assert_eq!(spans[3].content, Span::raw(" ").content);
|
||||
assert_eq!(spans[3].style, Style::reset());
|
||||
assert_eq!(spans[4], Span::raw("an").reset().fg(Color::Yellow));
|
||||
assert_eq!(spans[5], Span::raw("d ").reset());
|
||||
assert_eq!(spans[6], Span::raw("Green").reset().fg(Color::Green));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_entry_spans_full_string_highlight() {
|
||||
let entry = Entry::new("highlight me".to_string())
|
||||
.with_match_indices(&(0..12).collect::<Vec<_>>());
|
||||
let spans = build_entry_spans(&entry, 200, Color::Blue, Color::Red);
|
||||
|
||||
// All chars should be highlighted
|
||||
assert_eq!(spans.len(), 1);
|
||||
assert_eq!(
|
||||
spans[0],
|
||||
Span::styled("highlight me", Style::default().fg(Color::Red))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_entry_spans_match_at_boundaries() {
|
||||
let entry =
|
||||
Entry::new("boundary".to_string()).with_match_indices(&[0, 7]);
|
||||
let spans = build_entry_spans(&entry, 200, Color::Blue, Color::Red);
|
||||
|
||||
assert_eq!(
|
||||
spans[0],
|
||||
Span::styled("b", Style::default().fg(Color::Red))
|
||||
);
|
||||
assert_eq!(
|
||||
spans[1],
|
||||
Span::styled("oundar", Style::default().fg(Color::Blue))
|
||||
);
|
||||
assert_eq!(
|
||||
spans[2],
|
||||
Span::styled("y", Style::default().fg(Color::Red))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_entry_spans_unicode_boundaries() {
|
||||
let entry = Entry::new("a😀b".to_string()).with_match_indices(&[1]); // highlight the emoji only
|
||||
let spans = build_entry_spans(&entry, 200, Color::Blue, Color::Red);
|
||||
|
||||
assert_eq!(
|
||||
spans[0],
|
||||
Span::styled("a", Style::default().fg(Color::Blue))
|
||||
);
|
||||
assert_eq!(
|
||||
spans[1],
|
||||
Span::styled("😀", Style::default().fg(Color::Red))
|
||||
);
|
||||
assert_eq!(
|
||||
spans[2],
|
||||
Span::styled("b", Style::default().fg(Color::Blue))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -566,8 +566,6 @@ impl Television {
|
||||
preview.title = template
|
||||
.format(&entry.raw)
|
||||
.unwrap_or_else(|_| entry.raw.clone());
|
||||
} else {
|
||||
preview.title.clone_from(&entry.raw);
|
||||
}
|
||||
|
||||
if let Some(template) = &self.config.ui.preview_panel.footer {
|
||||
|
@ -1,5 +1,7 @@
|
||||
use lazy_regex::{Lazy, Regex, regex};
|
||||
|
||||
use crate::screen::result_item::ResultItem;
|
||||
|
||||
/// Returns the index of the next character boundary in the given string.
|
||||
///
|
||||
/// If the given index is already a character boundary, it is returned as is.
|
||||
@ -456,60 +458,47 @@ pub fn preprocess_line(line: &str) -> (String, Vec<i16>) {
|
||||
/// the match ranges adjusted to the new string.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use television::utils::strings::make_matched_string_printable;
|
||||
/// ```ignore
|
||||
/// use television::channels::entry::Entry;
|
||||
/// use television::utils::strings::make_result_item_printable;
|
||||
///
|
||||
/// let matched_string = "Hello, World!";
|
||||
/// let match_ranges = vec![(0, 1), (7, 8)];
|
||||
/// let match_ranges = Some(match_ranges.as_slice());
|
||||
/// let (printable, match_indices) = make_matched_string_printable(matched_string, match_ranges);
|
||||
/// let entry = Entry::new("Hello, World!".to_string()).with_match_indices(&[0, 7]);
|
||||
/// let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
/// assert_eq!(printable, "Hello, World!");
|
||||
/// assert_eq!(match_indices, vec![(0, 1), (7, 8)]);
|
||||
///
|
||||
/// let matched_string = "Hello,\tWorld!";
|
||||
/// let match_ranges = vec![(0, 1), (7, 8)];
|
||||
/// let match_ranges = Some(match_ranges.as_slice());
|
||||
/// let (printable, match_indices) = make_matched_string_printable(matched_string, match_ranges);
|
||||
/// let entry = Entry::new("Hello,\tWorld!".to_string()).with_match_indices(&[0, 10]);
|
||||
/// let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
/// assert_eq!(printable, "Hello, World!");
|
||||
/// assert_eq!(match_indices, vec![(0, 1), (10, 11)]);
|
||||
///
|
||||
/// let matched_string = "Hello,\nWorld!";
|
||||
/// let match_ranges = vec![(0, 1), (7, 8)];
|
||||
/// let match_ranges = Some(match_ranges.as_slice());
|
||||
/// let (printable, match_indices) = make_matched_string_printable(matched_string, match_ranges);
|
||||
/// let entry = Entry::new("Hello,\nWorld!".to_string()).with_match_indices(&[0, 6]);
|
||||
/// let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
/// assert_eq!(printable, "Hello,World!");
|
||||
/// assert_eq!(match_indices, vec![(0, 1), (6, 7)]);
|
||||
///
|
||||
/// let matched_string = "Hello, World!";
|
||||
/// let (printable, match_indices) = make_matched_string_printable(matched_string, None);
|
||||
/// let entry = Entry::new("Hello, World!".to_string());
|
||||
/// let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
/// assert_eq!(printable, "Hello, World!");
|
||||
/// assert_eq!(match_indices, vec![]);
|
||||
///
|
||||
/// let matched_string = "build.rs";
|
||||
/// let match_ranges = vec![(0, 1), (7, 8)];
|
||||
/// let match_ranges = Some(match_ranges.as_slice());
|
||||
/// let (printable, match_indices) = make_matched_string_printable(matched_string, match_ranges);
|
||||
/// let entry = Entry::new("build.rs".to_string()).with_match_indices(&[0, 7]);
|
||||
/// let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
/// assert_eq!(printable, "build.rs");
|
||||
/// assert_eq!(match_indices, vec![(0, 1), (7, 8)]);
|
||||
///
|
||||
/// let matched_string = "a\tb";
|
||||
/// let match_ranges = vec![(0, 1), (2, 3)];
|
||||
/// let match_ranges = Some(match_ranges.as_slice());
|
||||
/// let (printable, match_indices) = make_matched_string_printable(matched_string, match_ranges);
|
||||
/// let entry = Entry::new("a\tb".to_string()).with_match_indices(&[0, 5]);
|
||||
/// let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
/// assert_eq!(printable, "a b");
|
||||
/// assert_eq!(match_indices, vec![(0, 1), (5, 6)]);
|
||||
///
|
||||
/// let matched_string = "a\tbcd".repeat(65);
|
||||
/// let match_ranges = vec![(0, 1), (310, 311)];
|
||||
/// let match_ranges = Some(match_ranges.as_slice());
|
||||
/// let (printable, match_indices) = make_matched_string_printable(&matched_string, match_ranges);
|
||||
/// let entry = Entry::new("a\tbcd".repeat(65)).with_match_indices(&[0, 330]);
|
||||
/// let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
/// assert_eq!(printable.len(), 480);
|
||||
/// assert_eq!(match_indices, vec![(0, 1)]);
|
||||
///
|
||||
/// let matched_string = "ジェ abc";
|
||||
/// let match_ranges = vec![(0, 1), (2, 3)];
|
||||
/// let match_ranges = Some(match_ranges.as_slice());
|
||||
/// let (printable, match_indices) = make_matched_string_printable(matched_string, match_ranges);
|
||||
/// let entry = Entry::new("ジェ abc".to_string()).with_match_indices(&[0, 2]);
|
||||
/// let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
/// assert_eq!(printable, "ジェ abc");
|
||||
/// assert_eq!(match_indices, vec![(0, 1), (2, 3)]);
|
||||
/// ```
|
||||
@ -517,33 +506,38 @@ pub fn preprocess_line(line: &str) -> (String, Vec<i16>) {
|
||||
/// # Panics
|
||||
/// This will panic if the length of the printable string or the match indices don't fit into a
|
||||
/// `u32`.
|
||||
pub fn make_matched_string_printable(
|
||||
matched_string: &str,
|
||||
match_ranges: Option<&[(u32, u32)]>,
|
||||
pub fn make_result_item_printable(
|
||||
result_item: &(impl ResultItem + ?Sized),
|
||||
) -> (String, Vec<(u32, u32)>) {
|
||||
// PERF: Fast path for ASCII strings without match ranges
|
||||
if matched_string.is_ascii() && match_ranges.is_none() {
|
||||
return (matched_string.to_string(), Vec::new());
|
||||
}
|
||||
|
||||
// PERF: If ASCII with ranges, check if we need preprocessing (tabs, newlines, etc.)
|
||||
if let Some(ranges) = match_ranges {
|
||||
if matched_string.is_ascii() {
|
||||
// Only use fast path if no characters need preprocessing
|
||||
if !matched_string
|
||||
.chars()
|
||||
.any(|c| c == '\t' || c == '\n' || c.is_control())
|
||||
{
|
||||
return (matched_string.to_string(), ranges.to_vec());
|
||||
// PERF: fast path for ascii
|
||||
if result_item.display().is_ascii() {
|
||||
match result_item.match_ranges() {
|
||||
// If there are no match ranges, we can return the display string directly
|
||||
None => {
|
||||
return (result_item.display().to_string(), Vec::new());
|
||||
}
|
||||
// Otherwise, check if we can return the display string without further processing
|
||||
Some(ranges) => {
|
||||
if !result_item
|
||||
.display()
|
||||
.chars()
|
||||
.any(|c| c == '\t' || c == '\n' || c.is_control())
|
||||
{
|
||||
return (
|
||||
result_item.display().to_string(),
|
||||
ranges.to_vec(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full processing for non-ASCII strings or strings that need preprocessing
|
||||
let (printable, transformation_offsets) = preprocess_line(matched_string);
|
||||
let (printable, transformation_offsets) =
|
||||
preprocess_line(result_item.display());
|
||||
let mut match_indices = Vec::new();
|
||||
|
||||
if let Some(ranges) = match_ranges {
|
||||
if let Some(ranges) = result_item.match_ranges() {
|
||||
// PERF: Pre-allocate with known capacity
|
||||
match_indices.reserve(ranges.len());
|
||||
|
||||
@ -631,6 +625,7 @@ pub fn format_string(template: &str, source: &str, delimiter: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::channels::entry::Entry;
|
||||
|
||||
fn test_next_char_boundary(input: &str, start: usize, expected: usize) {
|
||||
let actual = next_char_boundary(input, start);
|
||||
@ -889,4 +884,48 @@ mod tests {
|
||||
test_preprocess_line("Hello, World!\u{FEFF}", "Hello, World!␀");
|
||||
test_preprocess_line(&"a".repeat(400), &"a".repeat(300));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_match_string_printable() {
|
||||
let entry = Entry::new("Hello, World!".to_string())
|
||||
.with_match_indices(&[0, 7]);
|
||||
let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
assert_eq!(printable, "Hello, World!");
|
||||
assert_eq!(match_indices, vec![(0, 1), (7, 8)]);
|
||||
|
||||
let entry = Entry::new("Hello,\tWorld!".to_string())
|
||||
.with_match_indices(&[0, 10]);
|
||||
let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
assert_eq!(printable, "Hello, World!");
|
||||
assert_eq!(match_indices, vec![(0, 1), (13, 14)]);
|
||||
|
||||
let entry = Entry::new("Hello,\nWorld!".to_string())
|
||||
.with_match_indices(&[0, 6]);
|
||||
let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
assert_eq!(printable, "Hello,World!");
|
||||
assert_eq!(match_indices, vec![(0, 1), (6, 6)]);
|
||||
|
||||
let entry = Entry::new("Hello, World!".to_string());
|
||||
let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
assert_eq!(printable, "Hello, World!");
|
||||
assert_eq!(match_indices, vec![]);
|
||||
|
||||
let entry =
|
||||
Entry::new("build.rs".to_string()).with_match_indices(&[0, 7]);
|
||||
let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
assert_eq!(printable, "build.rs");
|
||||
assert_eq!(match_indices, vec![(0, 1), (7, 8)]);
|
||||
|
||||
let entry =
|
||||
Entry::new("a\tbcd".repeat(65)).with_match_indices(&[0, 330]);
|
||||
let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
assert_eq!(printable.len(), 480);
|
||||
assert_eq!(match_indices, vec![(0, 1)]);
|
||||
|
||||
let entry =
|
||||
Entry::new("ジェ abc".to_string()).with_match_indices(&[0, 2]);
|
||||
let (printable, match_indices) = make_result_item_printable(&entry);
|
||||
assert_eq!(printable, "ジェ abc");
|
||||
assert_eq!(match_indices, vec![(0, 1), (2, 3)]);
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ pub const DEFAULT_PTY_SIZE: PtySize = PtySize {
|
||||
pixel_height: 0,
|
||||
};
|
||||
|
||||
pub const DEFAULT_DELAY: Duration = Duration::from_millis(300);
|
||||
pub const DEFAULT_DELAY: Duration = Duration::from_millis(100);
|
||||
|
||||
/// A helper to test terminal user interfaces (TUIs) using a pseudo-terminal (pty).
|
||||
///
|
||||
@ -214,7 +214,7 @@ impl PtyTester {
|
||||
}
|
||||
|
||||
/// How long to wait for the TUI to stabilize before asserting its output.
|
||||
const FRAME_STABILITY_TIMEOUT: Duration = Duration::from_millis(5000);
|
||||
const FRAME_STABILITY_TIMEOUT: Duration = Duration::from_millis(3000);
|
||||
|
||||
/// Gets the current TUI frame, ensuring it has stabilized.
|
||||
///
|
||||
@ -249,9 +249,9 @@ impl PtyTester {
|
||||
// wait for the UI to stabilize with a timeout
|
||||
let mut frame = String::new();
|
||||
let start_time = std::time::Instant::now();
|
||||
// wait till we get 3 consecutive frames that are the same
|
||||
// wait till we get 2 consecutive frames that are the same
|
||||
let mut counter = 0;
|
||||
while counter < 3 {
|
||||
while counter < 2 {
|
||||
let new_frame = self.read_tui_output();
|
||||
if new_frame == frame {
|
||||
counter += 1;
|
||||
@ -266,7 +266,7 @@ impl PtyTester {
|
||||
frame
|
||||
);
|
||||
// Sleep briefly to allow the UI to update
|
||||
sleep(Duration::from_millis(50));
|
||||
sleep(Duration::from_millis(20));
|
||||
}
|
||||
frame
|
||||
}
|
||||
|
34
tests/ui.rs
34
tests/ui.rs
@ -1,34 +0,0 @@
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
|
||||
#[test]
|
||||
// FIXME: was lazy, this should be more robust
|
||||
fn toggle_preview() {
|
||||
let mut tester = PtyTester::new();
|
||||
let mut child =
|
||||
tester.spawn_command_tui(tv_local_config_and_cable_with_args(&[
|
||||
"files",
|
||||
"-p",
|
||||
"cat -n {}",
|
||||
]));
|
||||
|
||||
let with_preview =
|
||||
"╭───────────────────────── files ──────────────────────────╮";
|
||||
tester.assert_tui_frame_contains(with_preview);
|
||||
|
||||
// Toggle preview
|
||||
tester.send(&ctrl('o'));
|
||||
|
||||
let without_preview = "╭─────────────────────────────────────────────────────── files ────────────────────────────────────────────────────────╮";
|
||||
tester.assert_tui_frame_contains(without_preview);
|
||||
|
||||
// Toggle preview
|
||||
tester.send(&ctrl('o'));
|
||||
|
||||
tester.assert_tui_frame_contains(with_preview);
|
||||
|
||||
// Exit the application
|
||||
tester.send(&ctrl('c'));
|
||||
PtyTester::assert_exit_ok(&mut child, DEFAULT_DELAY);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user