feat: add --hide-nth to hide fields from display but keep them searchable (#1122)

* Add --hide-nth flag to hide fields while keeping them searchable

Introduce a `--hide-nth <fieldspec>` option that takes the same
comma-separated field index expressions as `--nth`/`--with-nth`. The
listed fields are removed from the displayed line but remain part of the
text used for matching, so a query can still match them. Characters in
the hidden fields are ignored for match highlighting and horizontal
scrolling.

Implementation:
- Resolve the fieldspec to byte ranges in the same coordinate space as
  the matching/display text and store them as `hidden_ranges` in
  DefaultSkimItem metadata, exposed via a new `SkimItem::hidden_ranges()`
  trait method. text()/output() keep the full text so hidden fields stay
  searchable and are preserved on output.
- DefaultSkimItem::display() removes hidden characters and remaps match
  highlight positions into visible coordinates (project_visible_text /
  project_match_indices); this path takes precedence over ANSI styling.
- ItemRenderer::render_item applies the same projection to derive the
  visible sub-line text and hscroll match range, so hidden characters are
  ignored for horizontal scrolling.

Add unit tests for range normalization/projection and item behavior,
plus insta snapshot tests covering display removal, searchability, and
hscroll. Update ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCunXeAFMYAFduTc8SNjSM

* Preserve ANSI colors for surviving text under --hide-nth

Previously the hidden-field rendering path was applied ahead of the ANSI
display branch and rebuilt the line from the ANSI-stripped text, so
combining --hide-nth with --ansi dropped the colors of the visible
fields.

Integrate hidden-field removal into the ANSI branch instead: after
parsing the styled spans, drop the hidden characters while preserving
each span's style (retain_visible_spans) and remap the match positions
into the resulting visible coordinate space, then run the normal
highlighting. The plain (non-ANSI) branch keeps its project-and-to_line
handling. Surviving characters now keep their ANSI colors while hidden
fields stay searchable.

Add unit tests for ANSI color preservation and remapped highlighting,
plus ANSI color-snapshot integration tests. Update ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCunXeAFMYAFduTc8SNjSM

* Set hidden fields via builder instead of DefaultSkimItem::new param

Remove the `hidden_fields` parameter from `DefaultSkimItem::new` and set
the hidden fields through a `hidden_fields(&[FieldRange], &Regex)`
builder method instead. The builder resolves the fields against the
item's own `text()` (the same coordinate space `new` would have used),
so the result is identical while keeping `new`'s signature unchanged for
its many existing call sites.

The reader chains `.hidden_fields(&opt.hidden_fields, &opt.delimiter)`
onto construction. Revert the extra `&[]` argument at the other call
sites (selector, fuzz target, tests) and update the hide-nth tests to
use the builder. Update ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCunXeAFMYAFduTc8SNjSM

* chore: generate files

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
LoricAndre 2026-07-18 15:13:43 +02:00 committed by GitHub
parent 55aa7dacd5
commit dce26d622a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 854 additions and 13 deletions

View file

@ -102,7 +102,7 @@ skim/ ← workspace root
│ ├── binds.rs ← KeyMap, parse_key, parse_action_chain
│ ├── theme.rs ← ColorTheme, named palettes
│ ├── thread_pool.rs ← ThreadPool + parallel_work_queue
│ ├── field.rs ← field range parsing (--nth / --with-nth)
│ ├── field.rs ← field range parsing (--nth / --with-nth / --hide-nth)
│ ├── spinlock.rs ← lightweight SpinLock<T>
│ ├── util.rs ← printf helper, misc utilities
│ ├── popup/ ← tmux & zellij popup integration
@ -428,7 +428,8 @@ Source (stdin bytes or child process stdout)
│ assigns monotonic sequence numbers, sends to MPMC channel
├─ Thread N: workers — receive chunks, validate UTF-8,
│ create DefaultSkimItem::new(line, ansi, trans_fields, matching_fields, delimiter)
│ (handles ANSI stripping, --nth / --with-nth transforms inline),
│ .hidden_fields(hidden_fields, delimiter)
│ (handles ANSI stripping, --nth / --with-nth / --hide-nth inline),
│ send (seq, items) pairs
├─ Thread 1: reorder — collects (seq, items), emits in order through SkimItemReceiver;
│ drops tx_pipeline_done on exit (signals killer thread)
@ -458,6 +459,24 @@ SkimItemReceiver channel
Fields `/0` bytes are stripped from `text` (used for display/matching) but preserved in `orig_text` (used for output).
**`--hide-nth`** is orthogonal to the matrix above and applied through the builder method
`DefaultSkimItem::hidden_fields(hidden_fields, delimiter)` after construction (rather than a `new`
parameter). The requested fields are resolved to byte ranges (in the same coordinate space as
`text()` — the stripped text under `--ansi`, otherwise the `text` field) and stored as
`hidden_ranges` in the item metadata, exposed via the `SkimItem::hidden_ranges()` trait method. The
hidden fields **remain part of `text()`**, so they stay searchable and still participate in matching.
They only affect rendering:
- `DefaultSkimItem::display()` removes the hidden characters and remaps the match highlight
positions into the visible coordinate space (`project_visible_text` / `project_match_indices` in
`src/helper/item.rs`). This is integrated into **both** display branches: the plain branch projects
the text through `to_line`, and the ANSI branch drops the hidden characters from the already-parsed
styled spans (`retain_visible_spans`) so surviving characters **keep their ANSI colors**, then runs
the normal highlighting on the remapped visible-coordinate matches.
- `ItemRenderer::render_item` applies the same projection to derive the visible sub-line text and the
match range used for horizontal scrolling, so hidden characters are ignored for hscroll and never
highlighted.
---
## The Matching Subsystem
@ -1216,10 +1235,10 @@ The global allocator is `mimalloc` (v3), chosen for its low-latency multi-thread
| `merge_worker_results` | `src/matcher.rs:28` | Merge k sorted runs → ProcessedItems |
| `ItemPool::append` | `src/item.rs:469` | Add items, notify matcher |
| `ItemPool::take` | `src/item.rs:502` | Take un-matched items for matcher |
| `DefaultSkimItem::new` | `src/helper/item.rs:58` | ANSI strip, field transform, ranges, disable pattern |
| `SkimItemReader::parallel_bufread` | `src/helper/item_reader.rs:263` | Unified parallel pipeline (all inputs) |
| `spawn_io_reader` | `src/helper/item_reader.rs:354` | I/O reader thread: chunk reads + line splitting |
| `spawn_reorder_thread` | `src/helper/item_reader.rs:458` | Reorder thread: ordered output + pipeline-done signal |
| `DefaultSkimItem::new` | `src/helper/item.rs:64` | ANSI strip, field transform, matching ranges (hidden ranges set later via `hidden_fields` builder) |
| `SkimItemReader::parallel_bufread` | `src/helper/item_reader.rs:287` | Unified parallel pipeline (all inputs) |
| `spawn_io_reader` | `src/helper/item_reader.rs:378` | I/O reader thread: chunk reads + line splitting |
| `spawn_reorder_thread` | `src/helper/item_reader.rs:483` | Reorder thread: ordered output + pipeline-done signal |
| `Preview::spawn` | `src/tui/preview.rs:319` | Start image, PTY, or plain preview worker |
| `Tui::new_with_height_and_backend` | `src/tui/backend.rs:77` | Terminal init + viewport sizing |
| `Tui::enter` | `src/tui/backend.rs:126` | Enable raw mode + terminal setup |

View file

@ -46,6 +46,7 @@
gnuplot
llvm
cargo-bloat
cargo-public-api
];
gungraun = with pkgs; [
valgrind

View file

@ -8,7 +8,7 @@ sk \- Fuzzy Finder in rust!
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.SH SYNOPSIS
\fBsk\fR [\fB\-\-tac\fR] [\fB\-\-min\-query\-length\fR] [\fB\-\-no\-sort\fR] [\fB\-t\fR|\fB\-\-tiebreak\fR] [\fB\-n\fR|\fB\-\-nth\fR] [\fB\-\-with\-nth\fR] [\fB\-d\fR|\fB\-\-delimiter\fR] [\fB\-e\fR|\fB\-\-exact\fR] [\fB\-\-regex\fR] [\fB\-\-algo\fR] [\fB\-\-case\fR] [\fB\-\-typos\fR] [\fB\-\-no\-typos\fR] [\fB\-\-normalize\fR] [\fB\-\-split\-match\fR] [\fB\-\-last\-match\fR] [\fB\-\-scheme\fR] [\fB\-b\fR|\fB\-\-bind\fR] [\fB\-m\fR|\fB\-\-multi\fR] [\fB\-\-no\-multi\fR] [\fB\-\-no\-mouse\fR] [\fB\-c\fR|\fB\-\-cmd\fR] [\fB\-i\fR|\fB\-\-interactive\fR] [\fB\-I \fR] [\fB\-\-color\fR] [\fB\-\-highlight\-line\fR] [\fB\-\-no\-hscroll\fR] [\fB\-\-keep\-right\fR] [\fB\-\-skip\-to\-pattern\fR] [\fB\-\-no\-clear\-if\-empty\fR] [\fB\-\-no\-clear\-start\fR] [\fB\-\-no\-clear\fR] [\fB\-\-show\-cmd\-error\fR] [\fB\-\-cycle\fR] [\fB\-\-disabled\fR] [\fB\-\-disable\-pattern\fR] [\fB\-\-layout\fR] [\fB\-\-reverse\fR] [\fB\-\-height\fR] [\fB\-\-no\-height\fR] [\fB\-\-min\-height\fR] [\fB\-\-margin\fR] [\fB\-p\fR|\fB\-\-prompt\fR] [\fB\-\-cmd\-prompt\fR] [\fB\-\-selector\fR] [\fB\-\-multi\-selector\fR] [\fB\-\-ansi\fR] [\fB\-\-tabstop\fR] [\fB\-\-info\fR] [\fB\-\-no\-info\fR] [\fB\-\-inline\-info\fR] [\fB\-\-header\fR] [\fB\-\-header\-lines\fR] [\fB\-\-border\fR] [\fB\-\-border\-no\-collapse\fR] [\fB\-\-no\-border\fR] [\fB\-\-wrap\fR] [\fB\-\-multiline\fR] [\fB\-\-scrollbar\fR] [\fB\-\-no\-scrollbar\fR] [\fB\-\-history\fR] [\fB\-\-history\-size\fR] [\fB\-\-cmd\-history\fR] [\fB\-\-cmd\-history\-size\fR] [\fB\-\-preview\fR] [\fB\-\-preview\-window\fR] [\fB\-\-image\fR] [\fB\-q\fR|\fB\-\-query\fR] [\fB\-\-cmd\-query\fR] [\fB\-\-read0\fR] [\fB\-\-print0\fR] [\fB\-\-print\-query\fR] [\fB\-\-print\-cmd\fR] [\fB\-\-print\-score\fR] [\fB\-\-print\-header\fR] [\fB\-\-print\-current\fR] [\fB\-\-output\-format\fR] [\fB\-\-no\-strip\-ansi\fR] [\fB\-1\fR|\fB\-\-select\-1\fR] [\fB\-0\fR|\fB\-\-exit\-0\fR] [\fB\-\-sync\fR] [\fB\-\-pre\-select\-n\fR] [\fB\-\-pre\-select\-pat\fR] [\fB\-\-pre\-select\-items\fR] [\fB\-\-pre\-select\-file\fR] [\fB\-f\fR|\fB\-\-filter\fR] [\fB\-\-shell\fR] [\fB\-\-shell\-bindings\fR] [\fB\-\-man\fR] [\fB\-\-listen\fR] [\fB\-\-remote\fR] [\fB\-\-popup\fR] [\fB\-\-log\-level\fR] [\fB\-\-log\-file\fR] [\fB\-\-expect\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR]
\fBsk\fR [\fB\-\-tac\fR] [\fB\-\-min\-query\-length\fR] [\fB\-\-no\-sort\fR] [\fB\-t\fR|\fB\-\-tiebreak\fR] [\fB\-n\fR|\fB\-\-nth\fR] [\fB\-\-with\-nth\fR] [\fB\-\-hide\-nth\fR] [\fB\-d\fR|\fB\-\-delimiter\fR] [\fB\-e\fR|\fB\-\-exact\fR] [\fB\-\-regex\fR] [\fB\-\-algo\fR] [\fB\-\-case\fR] [\fB\-\-typos\fR] [\fB\-\-no\-typos\fR] [\fB\-\-normalize\fR] [\fB\-\-split\-match\fR] [\fB\-\-last\-match\fR] [\fB\-\-scheme\fR] [\fB\-b\fR|\fB\-\-bind\fR] [\fB\-m\fR|\fB\-\-multi\fR] [\fB\-\-no\-multi\fR] [\fB\-\-no\-mouse\fR] [\fB\-c\fR|\fB\-\-cmd\fR] [\fB\-i\fR|\fB\-\-interactive\fR] [\fB\-I \fR] [\fB\-\-color\fR] [\fB\-\-highlight\-line\fR] [\fB\-\-no\-hscroll\fR] [\fB\-\-keep\-right\fR] [\fB\-\-skip\-to\-pattern\fR] [\fB\-\-no\-clear\-if\-empty\fR] [\fB\-\-no\-clear\-start\fR] [\fB\-\-no\-clear\fR] [\fB\-\-show\-cmd\-error\fR] [\fB\-\-cycle\fR] [\fB\-\-disabled\fR] [\fB\-\-disable\-pattern\fR] [\fB\-\-layout\fR] [\fB\-\-reverse\fR] [\fB\-\-height\fR] [\fB\-\-no\-height\fR] [\fB\-\-min\-height\fR] [\fB\-\-margin\fR] [\fB\-p\fR|\fB\-\-prompt\fR] [\fB\-\-cmd\-prompt\fR] [\fB\-\-selector\fR] [\fB\-\-multi\-selector\fR] [\fB\-\-ansi\fR] [\fB\-\-tabstop\fR] [\fB\-\-info\fR] [\fB\-\-no\-info\fR] [\fB\-\-inline\-info\fR] [\fB\-\-header\fR] [\fB\-\-header\-lines\fR] [\fB\-\-border\fR] [\fB\-\-border\-no\-collapse\fR] [\fB\-\-no\-border\fR] [\fB\-\-wrap\fR] [\fB\-\-multiline\fR] [\fB\-\-scrollbar\fR] [\fB\-\-no\-scrollbar\fR] [\fB\-\-history\fR] [\fB\-\-history\-size\fR] [\fB\-\-cmd\-history\fR] [\fB\-\-cmd\-history\-size\fR] [\fB\-\-preview\fR] [\fB\-\-preview\-window\fR] [\fB\-\-image\fR] [\fB\-q\fR|\fB\-\-query\fR] [\fB\-\-cmd\-query\fR] [\fB\-\-read0\fR] [\fB\-\-print0\fR] [\fB\-\-print\-query\fR] [\fB\-\-print\-cmd\fR] [\fB\-\-print\-score\fR] [\fB\-\-print\-header\fR] [\fB\-\-print\-current\fR] [\fB\-\-output\-format\fR] [\fB\-\-no\-strip\-ansi\fR] [\fB\-1\fR|\fB\-\-select\-1\fR] [\fB\-0\fR|\fB\-\-exit\-0\fR] [\fB\-\-sync\fR] [\fB\-\-pre\-select\-n\fR] [\fB\-\-pre\-select\-pat\fR] [\fB\-\-pre\-select\-items\fR] [\fB\-\-pre\-select\-file\fR] [\fB\-f\fR|\fB\-\-filter\fR] [\fB\-\-shell\fR] [\fB\-\-shell\-bindings\fR] [\fB\-\-man\fR] [\fB\-\-listen\fR] [\fB\-\-remote\fR] [\fB\-\-popup\fR] [\fB\-\-log\-level\fR] [\fB\-\-log\-file\fR] [\fB\-\-expect\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR]
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.SH OPTIONS
@ -94,6 +94,16 @@ Fields to be transformed
See nth for the details
.TP
\fB\-\-hide\-nth\fR \fI<HIDE_NTH>\fR [default: ]
Fields to hide from display while keeping them searchable
Takes the same comma\-separated field index expressions as **nth**. The listed
fields are removed from the displayed line but remain part of the text used for
matching, so a query can still match them. Characters in the hidden fields are
ignored for match highlighting and horizontal scrolling.
See **nth** for the field index expression syntax.
.TP
\fB\-d\fR, \fB\-\-delimiter\fR \fI<DELIMITER>\fR [default: [\\t\\n ]+]
Delimiter between fields

View file

@ -23,7 +23,7 @@ _sk() {
case "${cmd}" in
sk)
opts="-t -n -d -e -b -m -c -i -I -p -q -1 -0 -f -x -h -V --tac --min-query-length --no-sort --tiebreak --nth --with-nth --delimiter --exact --regex --algo --case --typos --no-typos --normalize --split-match --last-match --scheme --bind --multi --no-multi --no-mouse --cmd --interactive --color --highlight-line --no-hscroll --keep-right --skip-to-pattern --no-clear-if-empty --no-clear-start --no-clear --show-cmd-error --cycle --disabled --disable-pattern --layout --reverse --height --no-height --min-height --margin --prompt --cmd-prompt --selector --multi-selector --ansi --tabstop --ellipsis --info --no-info --inline-info --header --header-lines --border --border-no-collapse --no-border --wrap --multiline --scrollbar --no-scrollbar --history --history-size --cmd-history --cmd-history-size --preview --preview-window --image --query --cmd-query --read0 --print0 --print-query --print-cmd --print-score --print-header --print-current --output-format --no-strip-ansi --select-1 --exit-0 --sync --pre-select-n --pre-select-pat --pre-select-items --pre-select-file --filter --shell --shell-bindings --man --listen --remote --popup --log-level --log-file --flags --extended --literal --hscroll-off --filepath-word --jump-labels --no-bold --phony --tail --style --no-color --padding --border-label --border-label-pos --wrap-sign --no-multi-line --raw --track --gap --gap-line --freeze-left --freeze-right --scroll-off --gutter --gutter-raw --marker-multi-line --list-border --list-label --list-label-pos --no-input --info-command --separator --no-separator --ghost --input-border --input-label --input-label-pos --preview-label --preview-label-pos --header-first --header-border --header-lines-border --footer --footer-border --footer-label --footer-label-pos --with-shell --expect --help --version"
opts="-t -n -d -e -b -m -c -i -I -p -q -1 -0 -f -x -h -V --tac --min-query-length --no-sort --tiebreak --nth --with-nth --hide-nth --delimiter --exact --regex --algo --case --typos --no-typos --normalize --split-match --last-match --scheme --bind --multi --no-multi --no-mouse --cmd --interactive --color --highlight-line --no-hscroll --keep-right --skip-to-pattern --no-clear-if-empty --no-clear-start --no-clear --show-cmd-error --cycle --disabled --disable-pattern --layout --reverse --height --no-height --min-height --margin --prompt --cmd-prompt --selector --multi-selector --ansi --tabstop --ellipsis --info --no-info --inline-info --header --header-lines --border --border-no-collapse --no-border --wrap --multiline --scrollbar --no-scrollbar --history --history-size --cmd-history --cmd-history-size --preview --preview-window --image --query --cmd-query --read0 --print0 --print-query --print-cmd --print-score --print-header --print-current --output-format --no-strip-ansi --select-1 --exit-0 --sync --pre-select-n --pre-select-pat --pre-select-items --pre-select-file --filter --shell --shell-bindings --man --listen --remote --popup --log-level --log-file --flags --extended --literal --hscroll-off --filepath-word --jump-labels --no-bold --phony --tail --style --no-color --padding --border-label --border-label-pos --wrap-sign --no-multi-line --raw --track --gap --gap-line --freeze-left --freeze-right --scroll-off --gutter --gutter-raw --marker-multi-line --list-border --list-label --list-label-pos --no-input --info-command --separator --no-separator --ghost --input-border --input-label --input-label-pos --preview-label --preview-label-pos --header-first --header-border --header-lines-border --footer --footer-border --footer-label --footer-label-pos --with-shell --expect --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
@ -53,6 +53,10 @@ _sk() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--hide-nth)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--delimiter)
COMPREPLY=($(compgen -f "${cur}"))
return 0

View file

@ -13,6 +13,7 @@ pathname\t''
-pathname\t''"
complete -c sk -s n -l nth -d 'Fields to be matched' -r
complete -c sk -l with-nth -d 'Fields to be transformed' -r
complete -c sk -l hide-nth -d 'Fields to hide from display while keeping them searchable' -r
complete -c sk -s d -l delimiter -d 'Delimiter between fields' -r
complete -c sk -l algo -d 'Fuzzy matching algorithm' -r -f -a "arinae\t'Arinae: typo-resistant & natural algorithm, default'
clangd\t'Clangd fuzzy matching algorithm'

View file

@ -44,6 +44,7 @@ module completions {
--tiebreak(-t): string@"nu-complete sk tiebreak" # Comma-separated list of sort criteria to apply when the scores are tied.
--nth(-n): string # Fields to be matched
--with-nth: string # Fields to be transformed
--hide-nth: string # Fields to hide from display while keeping them searchable
--delimiter(-d): string # Delimiter between fields
--exact(-e) # Run in exact mode
--regex # Start in regex mode instead of fuzzy-match

View file

@ -21,6 +21,7 @@ _sk() {
'*-n+[Fields to be matched]:NTH:_default' \
'*--nth=[Fields to be matched]:NTH:_default' \
'*--with-nth=[Fields to be transformed]:WITH_NTH:_default' \
'*--hide-nth=[Fields to hide from display while keeping them searchable]:HIDE_NTH:_default' \
'-d+[Delimiter between fields]:DELIMITER:_default' \
'--delimiter=[Delimiter between fields]:DELIMITER:_default' \
'--algo=[Fuzzy matching algorithm]:ALGORITHM:((arinae\:"Arinae\: typo-resistant & natural algorithm, default"

View file

@ -2,7 +2,7 @@
//! Including the `DefaultSkimItem`
use crate::field::{FieldRange, parse_matching_fields, parse_transform_fields};
use crate::tui::util::merge_styles;
use crate::{DisplayContext, SkimItem};
use crate::{DisplayContext, Matches, SkimItem};
use ansi_to_tui::IntoText;
use ratatui::text::{Line, Span};
use regex::Regex;
@ -48,6 +48,12 @@ pub struct DefaultSkimItemMetadata {
/// The ranges on which to perform matching
matching_ranges: Option<Vec<(usize, usize)>>,
/// Byte ranges (in the display/matching text) of fields hidden via `--hide-nth`.
/// Characters inside these ranges are removed from the rendered line and ignored
/// for match highlighting and horizontal scrolling, but remain part of the text
/// used for matching so they stay searchable.
hidden_ranges: Option<Vec<(usize, usize)>>,
/// Whether the item should be disabled or not
disabled: bool,
}
@ -162,6 +168,7 @@ impl DefaultSkimItem {
stripped_text: stripped_text.map(std::string::String::into_boxed_str),
ansi_info,
matching_ranges,
hidden_ranges: None,
disabled: false,
}))
} else {
@ -174,6 +181,32 @@ impl DefaultSkimItem {
}
}
/// Builder-style setter for the fields hidden from display (via `--hide-nth`).
///
/// The fields are resolved against the item's display/matching text — which is
/// exactly what [`text()`](Self::text) returns (the ANSI-stripped text under
/// `--ansi`, otherwise the raw text) — so this must be called after construction.
/// The requested fields stay part of `text()` (and therefore searchable); they are
/// only removed from the rendered line and ignored for highlighting and hscroll.
///
/// A no-op when `hidden_fields` is empty or resolves to no ranges.
#[must_use]
pub fn hidden_fields(mut self, hidden_fields: &[FieldRange], delimiter: &Regex) -> Self {
if hidden_fields.is_empty() {
return self;
}
// Resolve the ranges before touching `self.metadata`; the `text()` borrow must
// end before the mutable borrow below.
let ranges = {
let text = self.text();
normalize_ranges(&parse_matching_fields(delimiter, text.as_ref(), hidden_fields))
};
if !ranges.is_empty() {
self.metadata.get_or_insert_default().hidden_ranges = Some(ranges);
}
self
}
fn contains_ansi_escape(s: &str) -> bool {
memchr::memchr(b'\x1b', s.as_bytes()).is_some()
}
@ -228,6 +261,16 @@ impl DefaultSkimItem {
None
}
}
/// Getter for `hidden_ranges` stored in metadata
#[must_use]
pub fn hidden_ranges(&self) -> Option<&[(usize, usize)]> {
if let Some(meta) = &self.metadata {
meta.hidden_ranges.as_ref().map(|v| v.as_ref() as &[(usize, usize)])
} else {
None
}
}
}
impl DefaultSkimItem {
@ -273,6 +316,10 @@ impl SkimItem for DefaultSkimItem {
self.matching_ranges()
}
fn hidden_ranges(&self) -> Option<&[(usize, usize)]> {
self.hidden_ranges()
}
// The display function handles ANSI stripping, field highlighting, and match
// rendering in a single pass; splitting it would require duplicating context handling.
#[allow(clippy::too_many_lines)]
@ -289,9 +336,23 @@ impl SkimItem for DefaultSkimItem {
// Extract all spans from the parsed text (should be a single line)
let all_spans: Vec<Span> = parsed_text.lines.into_iter().flat_map(|line| line.spans).collect();
// When fields are hidden (--hide-nth), drop the hidden characters from the
// parsed spans while preserving their ANSI styling, and remap the match
// positions into the resulting visible coordinate space. The remaining
// highlighting logic then runs unchanged on visible-coordinate CharIndices.
let (all_spans, matches) = if let Some(hidden) = self.hidden_ranges() {
let stripped = self.text();
let (_, map) = project_visible_text(stripped.as_ref(), hidden);
let visible_spans = retain_visible_spans(all_spans, &map);
let indices = project_match_indices(stripped.as_ref(), &context.matches, &map);
(visible_spans, Matches::CharIndices(indices))
} else {
(all_spans, context.matches.clone())
};
// Now apply highlighting based on matched positions
// We need to map match positions from stripped text to original text
match context.matches {
match matches {
crate::Matches::CharIndices(ref indices) => {
// Indices are already in stripped text coordinates (same as parsed ANSI text)
// No need to remap since both matching and ANSI parsing strip the codes
@ -446,6 +507,19 @@ impl SkimItem for DefaultSkimItem {
}
crate::Matches::None => Line::from(all_spans),
}
} else if let Some(hidden) = self.hidden_ranges() {
// Non-ANSI hidden path: remove the hidden characters and remap the match
// highlight positions into the visible coordinate space.
let (visible, map) = project_visible_text(&self.text, hidden);
let indices = project_match_indices(&self.text, &context.matches, &map);
DisplayContext {
score: context.score,
matches: Matches::CharIndices(indices),
container_width: context.container_width,
base_style: context.base_style,
matched_style: context.matched_style,
}
.to_line(Cow::Owned(visible))
} else {
// No ANSI mapping needed, use text as-is
context.to_line(Cow::Borrowed(&self.text))
@ -551,6 +625,110 @@ fn escape_ansi(raw: &str) -> String {
unsafe { String::from_utf8_unchecked(raw.bytes().map(|b| if b == 27 { b'?' } else { b }).collect()) }
}
/// Sort and merge a list of byte ranges into a canonical, non-overlapping form.
///
/// Empty ranges are dropped. Overlapping or touching ranges are merged so callers
/// can iterate the result assuming disjoint, ascending ranges.
#[must_use]
pub(crate) fn normalize_ranges(ranges: &[(usize, usize)]) -> Vec<(usize, usize)> {
let mut sorted: Vec<(usize, usize)> = ranges.iter().copied().filter(|(s, e)| e > s).collect();
sorted.sort_unstable();
let mut merged: Vec<(usize, usize)> = Vec::with_capacity(sorted.len());
for (start, end) in sorted {
if let Some(last) = merged.last_mut()
&& start <= last.1
{
last.1 = last.1.max(end);
} else {
merged.push((start, end));
}
}
merged
}
/// Remove the hidden byte ranges from `text`, returning the visible string and a
/// map from each original char index to its `Some(visible char index)`, or `None`
/// when that char falls inside a hidden range.
///
/// `hidden` must be normalized (see [`normalize_ranges`]): sorted, disjoint byte ranges.
#[must_use]
pub(crate) fn project_visible_text(text: &str, hidden: &[(usize, usize)]) -> (String, Vec<Option<usize>>) {
let mut visible = String::with_capacity(text.len());
let mut map = Vec::new();
let mut vis_idx = 0usize;
let mut hi = 0usize;
for (byte_pos, ch) in text.char_indices() {
while hi < hidden.len() && byte_pos >= hidden[hi].1 {
hi += 1;
}
let is_hidden = hi < hidden.len() && byte_pos >= hidden[hi].0 && byte_pos < hidden[hi].1;
if is_hidden {
map.push(None);
} else {
map.push(Some(vis_idx));
visible.push(ch);
vis_idx += 1;
}
}
(visible, map)
}
/// Drop hidden characters from already-parsed styled spans while preserving each
/// span's style, keeping the ANSI colors of the surviving characters intact.
///
/// `map` is the per-char index map produced by [`project_visible_text`]; the spans
/// are iterated in the same (stripped-text) char order the map is indexed by. Spans
/// that become empty after filtering are dropped.
#[must_use]
pub(crate) fn retain_visible_spans(spans: Vec<Span<'_>>, map: &[Option<usize>]) -> Vec<Span<'static>> {
let mut out = Vec::with_capacity(spans.len());
let mut char_idx = 0usize;
for span in spans {
let mut content = String::new();
for ch in span.content.chars() {
if map.get(char_idx).copied().flatten().is_some() {
content.push(ch);
}
char_idx += 1;
}
if !content.is_empty() {
out.push(Span::styled(content, span.style));
}
}
out
}
/// Convert the matched character positions of `matches` (in full-text coordinates)
/// into visible-text char indices, dropping any that fall inside hidden ranges.
///
/// `map` is the per-char index map produced by [`project_visible_text`]. The result
/// is sorted ascending and deduplicated, ready to feed a `Matches::CharIndices`.
#[must_use]
pub(crate) fn project_match_indices(text: &str, matches: &Matches, map: &[Option<usize>]) -> Vec<usize> {
let full_indices: Vec<usize> = match matches {
Matches::CharIndices(indices) => indices.clone(),
Matches::CharRange(start, end) => (*start..*end).collect(),
Matches::ByteRange(start, end) => text
.char_indices()
.enumerate()
.filter(|(_, (byte_pos, _))| *byte_pos >= *start && *byte_pos < *end)
.map(|(char_idx, _)| char_idx)
.collect(),
Matches::None => Vec::new(),
};
let mut visible: Vec<usize> = full_indices
.into_iter()
.filter_map(|ci| map.get(ci).copied().flatten())
.collect();
visible.sort_unstable();
visible.dedup();
visible
}
#[cfg(test)]
#[path = "item_tests.rs"]
mod test;

View file

@ -30,6 +30,7 @@ pub struct SkimItemReaderOption {
use_ansi_color: bool,
transform_fields: Vec<FieldRange>,
matching_fields: Vec<FieldRange>,
hidden_fields: Vec<FieldRange>,
delimiter: Regex,
line_ending: u8,
show_error: bool,
@ -44,6 +45,7 @@ impl Default for SkimItemReaderOption {
use_ansi_color: false,
transform_fields: Vec::new(),
matching_fields: Vec::new(),
hidden_fields: Vec::new(),
delimiter: Regex::new(DELIMITER_STR).unwrap(),
show_error: false,
disable_pattern: None,
@ -69,6 +71,11 @@ impl SkimItemReaderOption {
.iter()
.filter_map(|f| if f.is_empty() { None } else { FieldRange::from_str(f) })
.collect(),
hidden_fields: options
.hide_nth
.iter()
.filter_map(|f| if f.is_empty() { None } else { FieldRange::from_str(f) })
.collect(),
delimiter: options.delimiter.clone(),
show_error: options.show_cmd_error,
disable_pattern: options.disable_pattern.clone(),
@ -137,6 +144,23 @@ impl SkimItemReaderOption {
self
}
/// Sets the fields to hide from display (while keeping them searchable)
#[must_use]
pub fn hide_nth<'a, T>(mut self, hide_nth: T) -> Self
where
T: Iterator<Item = &'a str>,
{
self.hidden_fields = hide_nth.filter_map(FieldRange::from_str).collect();
self
}
/// Sets the hidden fields directly
#[must_use]
pub fn hidden_fields(mut self, hidden_fields: Vec<FieldRange>) -> Self {
self.hidden_fields = hidden_fields;
self
}
/// Enables reading null-terminated lines instead of newline-terminated
#[must_use]
pub fn read0(mut self, enable: bool) -> Self {
@ -441,7 +465,8 @@ impl SkimItemReader {
&opt.transform_fields,
&opt.matching_fields,
&opt.delimiter,
);
)
.hidden_fields(&opt.hidden_fields, &opt.delimiter);
if opt.disable_pattern.as_ref().is_some_and(|re| re.is_match(line)) {
item.disable();
}

View file

@ -487,3 +487,171 @@ fn test_display_ansi_item_with_no_matches() {
assert!(text.contains("red"));
assert!(text.contains("text"));
}
#[test]
fn test_normalize_ranges_sorts_and_merges() {
// Overlapping and touching ranges are merged; empty ranges dropped; result sorted.
assert_eq!(normalize_ranges(&[(5, 8), (0, 3)]), vec![(0, 3), (5, 8)]);
assert_eq!(normalize_ranges(&[(0, 4), (2, 6)]), vec![(0, 6)]);
assert_eq!(normalize_ranges(&[(0, 3), (3, 6)]), vec![(0, 6)]);
assert_eq!(normalize_ranges(&[(2, 2), (0, 1)]), vec![(0, 1)]);
assert!(normalize_ranges(&[]).is_empty());
}
#[test]
fn test_project_visible_text_removes_hidden_ranges() {
// Hide bytes 6..10 ("RED ") from "apple RED 001".
let (visible, map) = project_visible_text("apple RED 001", &[(6, 10)]);
assert_eq!(visible, "apple 001");
// Chars 0..6 ("apple ") map to themselves, 6..10 ("RED ") are hidden,
// and the trailing "001" is shifted left by 4 positions.
assert_eq!(map[0], Some(0)); // 'a'
assert_eq!(map[5], Some(5)); // ' '
assert_eq!(map[6], None); // 'R'
assert_eq!(map[9], None); // ' '
assert_eq!(map[10], Some(6)); // '0'
assert_eq!(map[12], Some(8)); // '1'
}
#[test]
fn test_project_match_indices_drops_hidden_and_remaps() {
use crate::Matches;
let (_visible, map) = project_visible_text("apple RED 001", &[(6, 10)]);
// A match spanning both a visible char ('e' at 4) and hidden chars (7,8) keeps
// only the visible one, remapped into visible coordinates (unchanged here).
let indices = project_match_indices("apple RED 001", &Matches::CharIndices(vec![4, 7, 8]), &map);
assert_eq!(indices, vec![4]);
// A byte range covering "001" (bytes 10..13) maps to visible chars 6,7,8.
let indices = project_match_indices("apple RED 001", &Matches::ByteRange(10, 13), &map);
assert_eq!(indices, vec![6, 7, 8]);
// A match entirely inside the hidden field yields nothing.
let indices = project_match_indices("apple RED 001", &Matches::CharRange(6, 9), &map);
assert!(indices.is_empty());
}
#[test]
fn test_hidden_ranges_keep_text_searchable() {
use crate::field::FieldRange;
use regex::Regex;
let delimiter = Regex::new(r"\s+").unwrap();
// Hide field 2 ("RED") but keep it searchable.
let item = DefaultSkimItem::new("apple RED 001", false, &[], &[], &delimiter)
.hidden_fields(&[FieldRange::Single(2)], &delimiter);
// text() (used for matching) retains the hidden field, so it stays searchable.
assert_eq!(item.text(), "apple RED 001");
// hidden_ranges exposes the field's byte range (including its trailing delimiter).
assert_eq!(item.hidden_ranges(), Some(&[(6, 10)][..]));
}
#[test]
fn test_hidden_field_removed_from_display() {
use crate::field::FieldRange;
use crate::{DisplayContext, Matches, SkimItem};
use ratatui::style::Style;
use regex::Regex;
let delimiter = Regex::new(r"\s+").unwrap();
let item = DefaultSkimItem::new("apple RED 001", false, &[], &[], &delimiter)
.hidden_fields(&[FieldRange::Single(2)], &delimiter);
let context = DisplayContext {
score: 0,
matches: Matches::None,
container_width: 80,
base_style: Style::default(),
matched_style: Style::default(),
};
let line = item.display(context);
let rendered: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
// The hidden field is gone from what is displayed, but the rest remains.
assert_eq!(rendered, "apple 001");
assert!(!rendered.contains("RED"));
}
#[test]
fn test_hidden_field_match_not_highlighted() {
use crate::field::FieldRange;
use crate::{DisplayContext, Matches, SkimItem};
use ratatui::style::{Color, Style};
use regex::Regex;
let delimiter = Regex::new(r"\s+").unwrap();
let item = DefaultSkimItem::new("apple RED 001", false, &[], &[], &delimiter)
.hidden_fields(&[FieldRange::Single(2)], &delimiter);
// Simulate a match on the hidden "RED" (chars 6,7,8 in the full text).
let context = DisplayContext {
score: 0,
matches: Matches::CharIndices(vec![6, 7, 8]),
container_width: 80,
base_style: Style::default(),
matched_style: Style::default().bg(Color::Yellow),
};
let line = item.display(context);
let rendered: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
assert_eq!(rendered, "apple 001");
// No span carries the highlight background, since the matched chars are hidden.
assert!(line.spans.iter().all(|span| span.style.bg != Some(Color::Yellow)));
}
#[test]
fn test_hidden_field_preserves_ansi_colors() {
use crate::field::FieldRange;
use crate::{DisplayContext, Matches, SkimItem};
use ratatui::style::{Color, Style};
use regex::Regex;
let delimiter = Regex::new(r"\s+").unwrap();
// Two colored, space-separated fields: green "one" and red "two".
let item = DefaultSkimItem::new(
"\x1b[32mone\x1b[0m \x1b[31mtwo\x1b[0m",
true, // ansi_enabled
&[],
&[],
&delimiter,
)
.hidden_fields(&[FieldRange::Single(1)], &delimiter); // hide the first (green) field
// The hidden field is still part of the matchable text.
assert_eq!(item.text(), "one two");
let context = DisplayContext {
score: 0,
matches: Matches::None,
container_width: 80,
base_style: Style::default(),
matched_style: Style::default(),
};
let line = item.display(context);
let rendered: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
// "one " (field 1 plus its trailing delimiter) is gone; "two" remains.
assert_eq!(rendered, "two");
// The surviving field keeps its ANSI red foreground; the hidden green is gone.
assert!(
line.spans.iter().any(|span| span.style.fg == Some(Color::Red)),
"surviving field should keep its ANSI red foreground"
);
assert!(
line.spans.iter().all(|span| span.style.fg != Some(Color::Green)),
"hidden field's ANSI green foreground should not appear"
);
}
#[test]
fn test_hidden_field_ansi_highlight_remapped() {
use crate::field::FieldRange;
use crate::{DisplayContext, Matches, SkimItem};
use ratatui::style::{Color, Style};
use regex::Regex;
let delimiter = Regex::new(r"\s+").unwrap();
let item = DefaultSkimItem::new("\x1b[32mone\x1b[0m \x1b[31mtwo\x1b[0m", true, &[], &[], &delimiter)
.hidden_fields(&[FieldRange::Single(1)], &delimiter); // hide green "one"
// Match "two" — chars 4,5,6 in the full stripped text "one two".
let context = DisplayContext {
score: 0,
matches: Matches::CharIndices(vec![4, 5, 6]),
container_width: 80,
base_style: Style::default(),
matched_style: Style::default().bg(Color::Yellow),
};
let line = item.display(context);
let rendered: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
assert_eq!(rendered, "two");
// The match on the visible field is highlighted (remapped to visible coords 0..3)
// while its ANSI red foreground is preserved alongside the highlight background.
assert!(line.spans.iter().any(|span| span.style.bg == Some(Color::Yellow)));
assert!(line.spans.iter().any(|span| span.style.fg == Some(Color::Red)));
}

View file

@ -200,6 +200,27 @@ pub struct SkimOptions {
)]
pub with_nth: Vec<String>,
/// Fields to hide from display while keeping them searchable
///
/// Takes the same comma-separated field index expressions as **nth**. The listed
/// fields are removed from the displayed line but remain part of the text used for
/// matching, so a query can still match them. Characters in the hidden fields are
/// ignored for match highlighting and horizontal scrolling.
///
/// See **nth** for the field index expression syntax.
#[cfg_attr(
feature = "cli",
arg(
long,
default_value = "",
help_heading = "Search",
verbatim_doc_comment,
value_delimiter = ',',
allow_hyphen_values = true,
)
)]
pub hide_nth: Vec<String>,
/// Delimiter between fields
///
/// In regex format, defaults to AWK-style. Escape sequences like \x00, \t, \n are supported.
@ -1115,6 +1136,7 @@ impl Default for SkimOptions {
tiebreak: vec![RankCriteria::Score, RankCriteria::Begin, RankCriteria::End],
nth: Default::default(),
with_nth: Default::default(),
hide_nth: Default::default(),
delimiter: Regex::new(r"[\t\n ]+").unwrap(),
exact: Default::default(),
regex: Default::default(),

View file

@ -74,6 +74,17 @@ pub trait SkimItem: AsAny + Send + Sync + 'static {
None
}
/// Byte ranges of `text()` that are hidden from display (via `--hide-nth`).
///
/// Characters inside these ranges are removed from the rendered line and ignored
/// for match highlighting and horizontal scrolling, but stay part of `text()` so
/// they remain searchable. Ranges are expressed as (`start_byte`, `end_byte`) and
/// are expected to be sorted and non-overlapping. Returns `None` when nothing is
/// hidden.
fn hidden_ranges(&self) -> Option<&[(usize, usize)]> {
None
}
/// Returns true if the item should be disabled
/// Disabled items cannot be selected
fn disabled(&self) -> bool {

View file

@ -92,8 +92,29 @@ impl<'a> ItemRenderer<'a> {
out: &mut Vec<ListItem<'static>>,
) -> usize {
let item_text = item.item.text();
let sub_lines = self.split_sub_lines(item_text.as_ref());
let (match_start_char, match_end_char) = Self::matched_range(item_text.as_ref(), item.matched_range.as_ref());
// When fields are hidden (--hide-nth), project the display text and match
// positions into the visible coordinate space so hidden characters are ignored
// for sub-line splitting, highlighting, and horizontal scrolling. `display()`
// performs the same projection, keeping the styled line consistent with these
// positions.
let (display_text, match_start_char, match_end_char): (std::borrow::Cow<'_, str>, usize, usize) =
match item.item.hidden_ranges() {
Some(hidden) if !hidden.is_empty() => {
let (visible, map) = crate::helper::item::project_visible_text(item_text.as_ref(), hidden);
let matches = Self::display_matches(item.matched_range.as_ref());
let indices = crate::helper::item::project_match_indices(item_text.as_ref(), &matches, &map);
let (start, end) = match (indices.first(), indices.last()) {
(Some(first), Some(last)) => (*first, *last + 1),
_ => (0, 0),
};
(std::borrow::Cow::Owned(visible), start, end)
}
_ => {
let (start, end) = Self::matched_range(item_text.as_ref(), item.matched_range.as_ref());
(item_text, start, end)
}
};
let sub_lines = self.split_sub_lines(display_text.as_ref());
let mut added = 0usize;
// Collect rows for this item into a temporary buffer so we can reverse

View file

@ -47,3 +47,30 @@ insta_test!(test_prompt_ansi, ["a"], &["--prompt", "\x1b[1;34mprompt\x1b[0m noco
@snap;
@snap_color;
});
// --ansi combined with --hide-nth: the hidden (red) middle field is removed from
// the rendered line, while the surviving green/plain fields keep their ANSI colors.
// The color snapshot confirms the green foreground survives and the red one is gone.
insta_test!(
test_ansi_hide_nth,
@bytes b"\x1b[32mgreen\x1b[0m \x1b[31mred\x1b[0m plain\n",
&["--ansi", "--delimiter", " ", "--hide-nth", "2"],
{
@snap;
@snap_color;
}
);
// The hidden ANSI field stays searchable: matching its text ("red") still selects
// the item even though the field is not shown, and no highlight leaks onto the
// visible text.
insta_test!(
test_ansi_hide_nth_searchable,
@bytes b"\x1b[32mgreen\x1b[0m \x1b[31mred\x1b[0m plain\n",
&["--ansi", "--delimiter", " ", "--hide-nth", "2"],
{
@type "red";
@snap;
@snap_color;
}
);

View file

@ -87,6 +87,46 @@ insta_test!(opt_with_nth_range_desc, ["f1,f2,f3,f4"], &["--delimiter", ",", "--w
@snap;
});
insta_test!(opt_hide_nth_1, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth", "1"], {
@snap;
});
insta_test!(opt_hide_nth_2, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth", "2"], {
@snap;
});
insta_test!(opt_hide_nth_last, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth=-1"], {
@snap;
});
insta_test!(opt_hide_nth_range, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth", "2..3"], {
@snap;
});
insta_test!(opt_hide_nth_multi, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth", "1,3"], {
@snap;
});
// A hidden field stays searchable: querying "f2" still matches the item even though
// the field is not displayed, and the hidden characters carry no highlight.
insta_test!(opt_hide_nth_still_searchable, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth", "2"], {
@snap;
@type "f2";
@snap;
});
// A very wide hidden field is ignored for horizontal scrolling: the short visible
// field renders in full with no scroll ellipsis, even though the raw line is 1000+
// columns wide.
insta_test!(
opt_hide_nth_hscroll,
[&format!("{} target", ["a"; 1000].join(""))],
&["--delimiter", " ", "--hide-nth", "1"],
{
@snap;
}
);
insta_test!(opt_nth_1, ["f1,f2,f3,f4"], &["--delimiter", ",", "--nth", "1"], {
@snap;
@char '1';

View file

@ -0,0 +1,29 @@
---
source: tests/ansi.rs
description: "input: bytes b\"\\x1b[32mgreen\\x1b[0m \\x1b[31mred\\x1b[0m plain\\n\"\noptions: --ansi --delimiter --hide-nth 2"
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> green plain "
" 1/1 0/0"
"> "
cursor: (24, 3)

View file

@ -0,0 +1,11 @@
---
source: tests/ansi.rs
description: "input: bytes b\"\\x1b[32mgreen\\x1b[0m \\x1b[31mred\\x1b[0m plain\\n\"\noptions: --ansi --delimiter --hide-nth 2"
---
(21, 0..1) ">" fg=Indexed(161)
(21, 1..2) " " fg=Indexed(168)
(21, 2..7) "green" fg=Green bg=Indexed(236)
(21, 7..13) " plain" bg=Indexed(236)
(22, 0..5) " 1/1" fg=Indexed(144)
(22, 77..80) "0/0" fg=Indexed(144)
(23, 0..2) "> " fg=Indexed(110)

View file

@ -0,0 +1,29 @@
---
source: tests/ansi.rs
description: "input: bytes b\"\\x1b[32mgreen\\x1b[0m \\x1b[31mred\\x1b[0m plain\\n\"\noptions: --ansi --delimiter --hide-nth 2\nafter:\n @type \"red\""
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> green plain "
" 1/1 0/0"
"> red "
cursor: (24, 6)

View file

@ -0,0 +1,11 @@
---
source: tests/ansi.rs
description: "input: bytes b\"\\x1b[32mgreen\\x1b[0m \\x1b[31mred\\x1b[0m plain\\n\"\noptions: --ansi --delimiter --hide-nth 2"
---
(21, 0..1) ">" fg=Indexed(161)
(21, 1..2) " " fg=Indexed(168)
(21, 2..7) "green" fg=Green bg=Indexed(236)
(21, 7..13) " plain" bg=Indexed(236)
(22, 0..5) " 1/1" fg=Indexed(144)
(22, 77..80) "0/0" fg=Indexed(144)
(23, 0..2) "> " fg=Indexed(110)

View file

@ -0,0 +1,29 @@
---
source: tests/options.rs
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 1"
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> f2,f3,f4 "
" 1/1 0/0"
"> "
cursor: (24, 3)

View file

@ -0,0 +1,29 @@
---
source: tests/options.rs
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 2"
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> f1,f3,f4 "
" 1/1 0/0"
"> "
cursor: (24, 3)

View file

@ -0,0 +1,29 @@
---
source: tests/options.rs
description: "input: items [&format!(\"{} target\", [\"a\"; 1000].join(\"\"))]\noptions: --delimiter --hide-nth 1"
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> target "
" 1/1 0/0"
"> "
cursor: (24, 3)

View file

@ -0,0 +1,29 @@
---
source: tests/options.rs
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth=-1"
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> f1,f2,f3, "
" 1/1 0/0"
"> "
cursor: (24, 3)

View file

@ -0,0 +1,29 @@
---
source: tests/options.rs
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 1,3"
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> f2,f4 "
" 1/1 0/0"
"> "
cursor: (24, 3)

View file

@ -0,0 +1,29 @@
---
source: tests/options.rs
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 2..3"
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> f1,f4 "
" 1/1 0/0"
"> "
cursor: (24, 3)

View file

@ -0,0 +1,29 @@
---
source: tests/options.rs
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 2"
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> f1,f3,f4 "
" 1/1 0/0"
"> "
cursor: (24, 3)

View file

@ -0,0 +1,29 @@
---
source: tests/options.rs
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 2\nafter:\n @type \"f2\""
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> f1,f3,f4 "
" 1/1 0/0"
"> f2 "
cursor: (24, 5)