Back to Skim

Skim Architecture

ARCHITECTURE.md

5.0.060.9 KB
Original Source

Skim Architecture

Table of Contents

  1. High-Level Overview
  2. Workspace & Crate Layout
  3. Entry Points
  4. Core Data Flow
  5. Operating Modes
  6. Item Ingestion Pipeline
  7. The Matching Subsystem
  8. TUI Subsystem
  9. Individual Widgets
  10. Key Bindings & Action System
  11. Preview System
  12. Output & Result Collection
  13. IPC / Listen Socket
  14. Theming
  15. History
  16. Pre-Selection
  17. Threading Model
  18. Important Call Sites (Cross-Reference)
  19. Public Library API

High-Level Overview

Skim (sk) is a terminal fuzzy-finder written in Rust, equivalent in spirit to fzf. It can operate as a standalone CLI binary or as an embedded library crate. At runtime it orchestrates four concurrent activities:

stdin / command
      │
      ▼
 ┌──────────┐   batched items   ┌──────────────┐
 │  Reader  │──────────────────▶│  ItemPool    │
 └──────────┘                   │  (Arc<…>)    │
                                └──────┬───────┘
                                       │ take()
                                       ▼
                                ┌──────────────┐
                                │   Matcher    │◀── query string
                                │  (parallel)  │
                                └──────┬───────┘
                                       │ ProcessedItems
                                       ▼
                   ┌──────────────────────────────────┐
                   │              TUI                  │
                   │  ┌────────┐  ┌────────┐           │
                   │  │ Input  │  │Preview │           │
                   │  ├────────┤  ├────────┤           │
                   │  │ItemList│  │ Header │           │
                   │  └────────┘  └────────┘           │
                   └──────────────────────────────────┘
                                       │
                                       ▼
                                  SkimOutput

The Reader pulls raw text from stdin or a shell command and converts it into Arc<dyn SkimItem> batches, depositing them into the shared ItemPool. The Matcher picks items up from the pool, evaluates every item against the current query string using a configured engine, and writes ranked MatchedItem results into ProcessedItems. The TUI renders four composable widgets (Input, ItemList, Preview, Header), drives a crossterm-based event loop, and converts user keystrokes into typed Action values that are dispatched back to the App state machine.


Workspace & Crate Layout

skim/                  ← workspace root
├── src/               ← single `skim` crate (lib + binary)
│   ├── bin/
│   │   └── main.rs    ← `sk` binary entry point
│   ├── lib.rs         ← library root; re-exports public types
│   ├── skim.rs        ← Skim<Backend> orchestrator
│   ├── options.rs     ← SkimOptions (all CLI / library options)
│   ├── output.rs      ← SkimOutput (returned to callers) + BinOptions/write_output (CLI serialization)
│   ├── reader.rs      ← Reader + ReaderControl + CommandCollector trait
│   ├── matcher.rs     ← Matcher + MatcherControl (parallel worker dispatcher)
│   ├── item.rs        ← ItemPool, MatchedItem, Rank, RankBuilder
│   ├── skim_item.rs   ← SkimItem trait
│   ├── 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)
│   ├── spinlock.rs    ← lightweight SpinLock<T>
│   ├── util.rs        ← printf helper, misc utilities
│   ├── popup/         ← tmux & zellij popup integration
│   │   ├── mod.rs           ← SkimPopup trait, run_with(), check_env(), SkimPopupOutput
│   │   ├── tmux.rs          ← TmuxPopup (builds/runs tmux display-popup)
│   │   └── zellij.rs        ← ZellijPopup (builds/runs zellij action new-floating-pane)
│   ├── prelude.rs     ← convenience re-exports
│   ├── manpage.rs     ← man-page generation (cli feature)
│   ├── shell.rs       ← shell completion generation (cli feature)
│   ├── engine/        ← match engine implementations
│   │   ├── mod.rs
│   │   ├── factory.rs       ← ExactOrFuzzyEngineFactory, AndOrEngineFactory, RegexEngineFactory
│   │   ├── andor.rs         ← AndEngine, OrEngine
│   │   ├── exact.rs         ← ExactEngine (prefix/postfix/inverse/exact string)
│   │   ├── fuzzy.rs         ← FuzzyEngine + FuzzyAlgorithm enum
│   │   ├── all.rs           ← MatchAllEngine (match-all / empty query)
│   │   ├── normalized.rs    ← NormalizedEngine (Unicode normalization wrapper)
│   │   ├── regexp.rs        ← RegexEngine (regex-mode)
│   │   ├── split.rs         ← SplitMatchEngine (--split-match)
│   │   └── util.rs          ← normalization helpers
│   ├── fuzzy_matcher/  ← raw fuzzy scoring algorithms
│   │   ├── mod.rs           ← FuzzyMatcher trait, MatchIndices type alias
│   │   ├── skim.rs          ← SkimMatcherV2
│   │   ├── clangd.rs        ← ClangdMatcher
│   │   ├── fzy.rs           ← FzyMatcher
│   │   ├── frizbee.rs       ← FrizbeeMatcher (typo-resistant)
│   │   └── arinae/          ← ArinaeMatcher (default; Smith-Waterman based)
│   ├── helper/         ← higher-level item helpers
│   │   ├── mod.rs
│   │   ├── item.rs          ← DefaultSkimItem (ANSI parsing, field transforms)
│   │   ├── item_reader.rs   ← SkimItemReader + SkimItemReaderOption (stdin/cmd → items)
│   │   ├── selector.rs      ← DefaultSkimSelector (pre-selection)
│   │   └── macros.rs        ← helper macros
│   └── tui/            ← terminal UI
│       ├── mod.rs            ← Size (fixed/percent/negative), Direction, BorderType, re-exports
│       ├── app.rs            ← App struct + render + event dispatch (central state machine)
│       ├── backend.rs        ← Tui<B> (ratatui terminal wrapper + crossterm event pump)
│       ├── event.rs          ← Event enum, Action enum, ActionCallback, parse_action
│       ├── widget.rs         ← SkimWidget trait + SkimRender result type
│       ├── input.rs          ← Input widget (query box + cursor + status info)
│       ├── item_list.rs      ← ItemList widget (scrollable match result list)
│       ├── item_renderer.rs  ← ItemRenderer (per-item ANSI/highlight rendering)
│       ├── preview.rs        ← Preview widget (plain text, PTY, or image preview pane)
│       ├── header.rs         ← Header widget (--header / --header-lines)
│       ├── statusline.rs     ← Info / InfoDisplay status bar modes
│       ├── layout.rs         ← LayoutTemplate + AppLayout (pre-computed areas)
│       ├── options.rs        ← TuiLayout enum, PreviewLayout struct
│       └── util.rs           ← cursor helpers, style merging
├── tests/             ← integration & snapshot tests
│   ├── common/
│   │   └── insta.rs   ← snap! / insta_test! macros for TUI snapshot testing
│   ├── snapshots/     ← committed .snap files
│   ├── ansi.rs        ← ANSI rendering tests
│   ├── options.rs     ← option coverage tests
│   ├── preview.rs     ← preview pane tests
│   └── …
├── benches/           ← criterion benchmarks
└── Cargo.toml

The single crate exports:

  • A library (lib): all types under skim::*, suitable for embedding.
  • A binary (sk, requires feature cli): the clap-based CLI.

The cli feature gates clap, clap_complete, shlex, env_logger, and clap_mangen.

The image feature (enabled by default) gates image preview support, including the image and ratatui-image dependencies, the ImageProtocol enum, the SkimOptions::image / SkimOptions::image_picker fields, and the PreviewContent::Image rendering path. With the feature off, the --image flag and its supporting code are compiled out entirely and neither image crate is pulled in.

The listen feature (enabled by default) gates the IPC socket that lets other processes drive skim via --listen / --remote, including the interprocess, ron, and serde dependencies, the SkimOptions::listen / SkimOptions::remote fields, and the serde derives on Action. See IPC / Listen Socket.


Entry Points

Binary (src/bin/main.rs)

main()
  │
  ├─ SkimOptions::from_env()          ← parses argv via clap (feature=cli)
  ├─ opts.build()                     ← applies defaults, loads history files
  │
  ├─ if opts.shell → generate_completions()   ← early exit
  ├─ if opts.man  → manpage::generate()       ← early exit
  ├─ if opts.remote → IPC relay mode          ← early exit
  │
  ├─ sk_main(opts)
  │     ├─ SkimItemReader::new(reader_opts)   ← configure stdin reader
  │     ├─ opts.cmd_collector = cmd_collector
  │     │
  │     ├─ if --popup/--tmux && check_env() → popup::run_with(&opts)
  │     │
  │     └─ else:
  │           ├─ if stdin not a TTY (piped) → cmd_collector.of_bufread(stdin)
  │           └─ Skim::run_with(opts, rx_item?)
  │
  └─ print output / write history / exit

Library (src/lib.rs + src/skim.rs)

Two public entry points exist on Skim:

MethodUse case
Skim::run_with(options, source)Takes a SkimItemReceiver channel (or None to use the configured command collector). The canonical entry point.
Skim::run_items(options, items)Convenience wrapper: accepts any IntoIterator<Item: SkimItem>, batches them through a bounded channel, and calls run_with.

Advanced embedders and tests can also drive the lifecycle manually: Skim::init, start, init_tui / init_tui_with, enter, run, output, plus accessors such as app, app_mut, tui_ref, tui_mut, app_and_tui, and event_sender.

The two high-level helpers return Result<SkimOutput>.


Core Data Flow

Initialisation sequence

Skim::run_with(options, source)
  │
  ├─ Skim::init(options, source)
  │     ├─ parse height (Size enum)
  │     ├─ ColorTheme::init_from_options(&options)
  │     ├─ Reader::from_options(&options).source(source)
  │     ├─ resolve cmd / expand initial_cmd (interactive mode)
  │     └─ App::from_options(options, theme, cmd)
  │           ├─ Input::from_options(…)
  │           ├─ Preview::from_options(…)
  │           ├─ Header::from_options(…)
  │           ├─ ItemList::from_options(…)
  │           ├─ ItemPool::from_options(…)
  │           ├─ Matcher::from_options(…)
  │           └─ LayoutTemplate::from_options(…)
  │
  ├─ Skim::start()
  │     ├─ reader.collect(item_pool, initial_cmd)  ← spawns reader thread(s)
  │     └─ app.restart_matcher(force=true)          ← kicks off first match pass
  │
  ├─ Skim::should_enter() → decides whether to open TUI
  │     (handles filter / select-1 / exit-0 / sync blocking)
  │
  ├─ if should_enter:
  │     ├─ Skim::init_tui()  → Tui::new_with_height(height)
  │     ├─ Skim::enter()     → tui.enter_terminal(); resolve image picker; listener; event task
  │     └─ Skim::run()       → async event loop (tick())
  │
  └─ Skim::output()         ← collect results + kill reader

Steady-state loop (Skim::tick())

Each call to tick() runs a tokio::select! on four concurrent futures:

BranchSourceAction
tui.next()crossterm keyboard/mouse/resize/paste eventsDispatch to app.handle_event()
matcher_interval.tick()10 ms periodic timer (adaptive: disabled once reader finishes and all items are matched)app.restart_matcher(false)
items_available.notified()Notify set by ItemPool::appendapp.restart_matcher(false)
listener.accept()IPC socket (when --listen)Parse RON-encoded Action, push to event queue

Operating Modes

Normal Interactive Mode

The default mode. The TUI is shown in full. Items arrive from stdin or a command, are matched against the live query, and displayed in the list. The user navigates with keyboard/mouse and presses Enter to accept.

Key files: src/skim.rs, src/tui/app.rs, src/tui/backend.rs

Filter Mode (--filter)

When --filter <query> is set, skim never opens the TUI.

Skim::should_enter() enters a busy-wait loop:

loop {
    if matcher.stopped() && reader.is_done() && pool.num_not_taken() == 0 {
        break;
    }
    sleep(1ms);
    app.restart_matcher(false);
}

Then app.item_list.items is populated from processed_items and output() is called immediately. The matched items are printed to stdout by the binary, one per line (or null-delimited with --print0).

In filter mode the FuzzyEngine is built with filter_mode = true, which uses fuzzy_match_range instead of fuzzy_indices to skip the per-character index computation and run faster.

Key files: src/skim.rs (should_enter()), src/engine/fuzzy.rs (filter_mode fast path), src/bin/main.rs (output loop)

Interactive / Command Mode (--interactive)

When --interactive is set together with --cmd <template>, the query box controls a shell command rather than a fuzzy filter. Every change to the input re-expands the template and issues a Reload event.

Template placeholders:

  • {} — the current query
  • {q} — alias for {}
  • {n} — ordinal of the current item

App::expand_cmd() handles placeholder expansion. On Action::ToggleInteractive, the mode flips between the query controlling the fuzzy filter and the query driving the command.

In interactive mode, the initial command is expanded against the initial query:

rust
// src/skim.rs Skim::init()
let initial_cmd = if app.options.interactive && app.options.cmd.is_some() {
    app.expand_cmd(&cmd, true)   // true = initial call
} else { cmd.clone() };

A Reload(new_cmd) event is handled at the Skim::tick() level (not App::handle_event()), because it must kill the reader and restart cleanly:

rust
// src/skim.rs tick()
if let Event::Reload(new_cmd) = &evt {
    self.handle_reload(&new_cmd.clone());
}

handle_reload():

  1. Kills reader_control (waits for all reader threads to stop)
  2. Clears ItemPool
  3. Clears ItemList (unless no_clear_if_empty)
  4. Calls app.restart_matcher(force=true)
  5. Starts a new reader.collect(…)

Key files: src/skim.rs (handle_reload, tick), src/tui/app.rs (expand_cmd, handle_actionRefreshCmd)

Select-1 / Exit-0 / Sync Modes

All three are handled in Skim::should_enter() before opening the TUI:

OptionMeaningBehaviour
--select-1Auto-accept if exactly one matchWaits until ≥ 2 matches or reader/matcher done; returns without TUI if exactly 1 match
--exit-0Exit immediately if no matchesWaits until ≥ 1 match or done; returns without TUI if 0 matches
--syncBlock until all items processedWaits until num_matched == usize::MAX (effectively waits for full scan)

ANSI Mode (--ansi)

When --ansi is set, SkimItemReaderOption::from_options sets use_ansi_color = true. Each input line then creates a DefaultSkimItem with:

  1. Escape detection: DefaultSkimItem::contains_ansi_escape() checks for \x1b.
  2. Stripping: strip_ansi() returns (stripped_text, ansi_info) where ansi_info is a Vec<(byte_pos, char_pos)> mapping from stripped to original coordinates.
  3. Matching: text() returns the stripped text; the matcher works on plain text.
  4. Display: display() reconstructs styled ratatui::text::Line using ansi_to_tui::IntoText, then overlays match-highlight spans on top.

The coordinate mapping is critical: match indices come back in terms of stripped text positions, but the highlight must be applied to the original ANSI-containing string. DefaultSkimItem::display() applies a char-index offset conversion using ansi_info.

Without --ansi, any ANSI escape codes are passed through to text() and displayed as literal characters. If the raw input happens to contain escape sequences (but --ansi is not set), escape_ansi() is called to make them visible.

ANSI input uses the same parallel pipeline as plain input — there is no separate serial path. DefaultSkimItem::new handles the stripping inline inside the worker threads.

Key files: src/helper/item.rs (DefaultSkimItem::new, strip_ansi, display)

When --popup [direction[,size[,size]]] (alias --tmux) is set, the binary calls check_and_run_popup(), which checks popup::check_env() and, if true, delegates to popup::run_with() instead of Skim::run_with().

check_env() returns true only when:

  • $_SKIM_POPUP is not set in the environment (prevents the child process from recursing back into popup mode), and
  • at least one supported multiplexer is detected: tmux ($TMUX set) or Zellij ($ZELLIJ set).

The popup flow:

  1. Creates a temp directory for IPC (/tmp/sk-popup-XXXXXXXX/).
  2. If stdin is piped, creates a named FIFO (tmp_stdin) and spawns a thread to relay stdin into it incrementally so the child can stream-read.
  3. Reconstructs the sk command line from std::env::args(), shell-quotes every retained argument, strips --popup/--tmux, --output-format, and --print-cmd, then appends --print-query --print-header --print-current --print-score.
  4. Forwards all SKIM_*, RUST*, and PATH environment variables to the child via the multiplexer's -e flag, plus _SKIM_POPUP=1 to prevent re-entry.
  5. Launches the popup via the appropriate backend:
    • tmux: tmux display-popup -E … sh -c <cmd> > stdout_file
    • Zellij: zellij action new-floating-pane … -- sh -c <cmd> > stdout_file
  6. Waits for the popup process to exit.
  7. Parses the structured stdout file (query\nheader\ncurrent_item\nitem1\nscore1\n…) into a synthetic SkimOutput; cmd is reconstructed from the parent options because --print-cmd is deliberately stripped.

The internal SkimPopup trait abstracts the two multiplexer backends:

rust
trait SkimPopup {
    fn from_options(options: &SkimOptions) -> Box<dyn SkimPopup>;
    fn add_env(&mut self, key: &str, value: &str);
    fn run_and_wait(&mut self, command: &str) -> std::io::Result<ExitStatus>;
}

TmuxPopup and ZellijPopup each implement this trait. The active backend is selected at runtime: Zellij takes priority if both are available.

The child sk process runs fully independently inside the popup. The parent reads back a synthetic SkimOutput from the captured file. Because _SKIM_POPUP=1 is set in the child's environment, check_env() returns false in the child, so it runs as a normal interactive skim session regardless of what SKIM_DEFAULT_OPTIONS contains. The stdin relay thread stops when the popup exits, avoiding broken-pipe noise if the child closes its FIFO early.

Key files: src/popup/mod.rs (run_with, check_env), src/popup/tmux.rs (TmuxPopup), src/popup/zellij.rs (ZellijPopup), src/bin/main.rs (check_and_run_popup)


Item Ingestion Pipeline

All inputs — plain stdin, --ansi, --nth/--with-nth, and shell commands — flow through a single unified parallel pipeline (parallel_bufread). There is no serial fallback path.

Source (stdin bytes or child process stdout)
  │
  │  SkimItemReader::of_bufread() or CommandCollector::invoke()
  │
  └── parallel_bufread()  (all inputs)
        ├─ Thread 1: I/O reader — reads 256 KB chunks, splits at line boundaries,
        │             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),
        │             send (seq, items) pairs
        ├─ Thread 1: reorder — collects (seq, items), emits in order through SkimItemReceiver;
        │             drops tx_pipeline_done on exit (signals killer thread)
        └─ Thread 1: killer — waits on rx_interrupt OR rx_pipeline_done (whichever fires first);
                      kills child process if one exists, then exits

SkimItemReceiver channel
  │
  │  Reader::collect()
  │  collect_items() spawns a thread that blocks on recv_timeout (5ms) from the channel
  │
  └── ItemPool::append(items)
        ├─ respects --tac (reverse order)
        ├─ respects --header-lines (reserves first N items)
        ├─ notifies items_available (Notify) to wake matcher
        └─ increments atomic length counter

DefaultSkimItem construction matrix

with_nthansitext fieldorig_textstripped_text
falsefalseoriginal lineNoneNone
falsetrueoriginal lineNonestripped (+ ansi_info)
truefalsetransformedoriginalNone
truetruetransformedoriginalstripped (+ ansi_info)

Fields /0 bytes are stripped from text (used for display/matching) but preserved in orig_text (used for output).


The Matching Subsystem

Match Engine Hierarchy

Engines are composable through the factory pattern. Starting from Matcher::create_engine_factory_with_builder():

options
  │
  ├── if regex mode:
  │     RegexEngineFactory
  │       └─ if normalize: NormalizedEngineFactory(RegexEngineFactory)
  │
  └── else (fuzzy/exact mode):
        ExactOrFuzzyEngineFactory
          └─ if split_match: SplitMatchEngineFactory(ExactOrFuzzyEngineFactory)
               └─ AndOrEngineFactory(SplitMatchEngineFactory | ExactOrFuzzyEngineFactory)
                    └─ if normalize: NormalizedEngineFactory(AndOrEngineFactory)

When create_engine_with_case(query, case) is called at match time, the factory chain parses the query string and builds a concrete engine tree:

query: "'abc def | ghi ^xyz"
  │
  AndOrEngineFactory.parse_andor()
    │
    ├─ "'abc"  → ExactOrFuzzyEngineFactory
    │              → ExactEngine (prefix=false, postfix=false, case-insensitive "abc")
    │
    ├─ "def | ghi" → OrEngine
    │                  ├─ FuzzyEngine("def")
    │                  └─ FuzzyEngine("ghi")
    │
    └─ "^xyz"  → ExactEngine(prefix=true, "xyz")

Query prefix semantics handled by ExactOrFuzzyEngineFactory::create_engine_with_case():

Prefix/suffixEngine type
'abcforce ExactEngine (toggle from default)
!abcExactEngine with inverse = true
^abcExactEngine with prefix = true
abc$ExactEngine with postfix = true
!^abcExactEngine inverse+prefix
!^abc$ExactEngine inverse+prefix+postfix (exact string, inverted)
plain abcFuzzyEngine (or ExactEngine if --exact)
empty / !MatchAllEngine

Fuzzy Algorithms

All algorithms implement the FuzzyMatcher trait with two methods:

  • fuzzy_indices(choice, pattern) → Option<(score, Vec<usize>)> — full match with per-character highlights
  • fuzzy_match_range(choice, pattern) → Option<(score, begin, end)> — fast path without highlight indices (used in filter mode)
AlgorithmFlagNotes
Arinae--algorithm arinae (default)Smith-Waterman with affine gaps; typo-resistant; picks last occurrence on ties when --last-match
SkimV2--algorithm skim_v2Skim's classic dynamic-programming scorer
Clangd--algorithm clangdClangd-style subsequence scoring
Fzy--algorithm fzyPort of the fzy C algorithm; supports --typos
Frizbee--algorithm frizbeeEdit-distance based; explicitly typo-tolerant (x86_64 and aarch64 only)

Typo tolerance is configured via Typos:

  • Typos::Disabled — no tolerance (default)
  • Typos::Smart — adaptive: query.len() / 4 typos allowed
  • Typos::Fixed(n) — exactly n typos

Parallel Matching

Matcher::run() dispatches work across the thread pool using thread_pool::parallel_work_queue():

Matcher::run(query, item_pool, thread_pool, …)
  │
  ├─ create matcher_engine from factory (synchronous)
  ├─ take items from pool synchronously (avoids race with restart)
  │
  └─ std::thread::spawn(coordinator closure)
        │
        ├─ shares items as Arc<[Arc<dyn SkimItem>]>
        │
        ├─ parallel_work_queue(pool, num_workers, items, CHUNK_SIZE=4096, …)
        │     │
        │     ├─ Worker threads (num_cpus - 1):
        │     │    ├─ atomically grab next chunk
        │     │    ├─ for each item: matcher_engine.match_item(item)
        │     │    ├─ flush processed/matched counters (Relaxed atomic)
        │     │    └─ accumulate into worker-local Vec<MatchedItem>
        │     │         └─ sort_unstable() on worker thread (parallel sort)
        │     │
        │     ├─ AtomicCounter barrier (lock-free AtomicUsize + thread::park/unpark)
        │     │
        │     └─ coordinator:
        │          └─ merge_worker_results(worker_results, no_sort, …)
        │               ├─ concatenate k sorted runs
        │               ├─ sort() (stable; driftsort detects k runs → O(n log k))
        │               └─ write into SpinLock<Option<ProcessedItems>>
        │
        └─ stopped.store(true)

Interruption is cooperative: each chunk checks interrupt.load(Relaxed) before processing. MatcherControl::kill() sets interrupt = true; MatcherControl::drop() also calls kill().

Ranking & Sorting

MatchedItem implements Ord through a lazy sort key computed by Rank::sort_key(criteria). Items can also be disabled: SkimItem::disabled() returns false by default, and --disable-pattern <regex> marks matching items as disabled in the default item type. Disabled items stay visible but are dimmed by ItemRenderer and cannot be selected.

Rank fields:

FieldDescription
scoreRaw match score (higher = better)
beginFirst matched character index
endLast matched character index
lengthTotal item text length in bytes
indexOrdinal position in the input stream
path_name_offsetByte offset after last / or \ (for path-name tiebreak)

RankCriteria variants (configurable via --tiebreak): Score, NegScore, Begin, NegBegin, End, NegEnd, Length, NegLength, Index, NegIndex, PathName, NegPathName.

MergeStrategy (in item_list.rs):

StrategyWhen used
ReplaceFresh match pass (query changed, full re-sort)
SortedMergeNew items arrived during a running match (merge-insert)
Append--no-sort mode

TUI Subsystem

Backend & Terminal Setup

Tui<B> (in src/tui/backend.rs) wraps ratatui::Terminal<B> and owns:

  • A tokio::sync::mpsc channel (event_tx / event_rx) of capacity 1 M for events.
  • A JoinHandle for a background Tokio task that reads crossterm::event::EventStream and sends Event values.
  • A CancellationToken to stop the background task.
  • A is_fullscreen flag that determines the ratatui::Viewport.

Viewport selection (Tui::new_with_height_and_backend()):

  • Size::Percent(100)Viewport::Fullscreen (enters alternate screen).
  • Size::Fixed(lines)Viewport::Fixed(Rect) with that many rows.
  • Size::Percent(p) → fixed viewport with terminal_height * p / 100 rows.
  • Size::Neg(lines) → fixed viewport with terminal_height - lines rows, saturating at zero.

Any fixed viewport is anchored at the current cursor position; the terminal is scrolled if needed to make room.

The default backend is CrosstermBackend<BufWriter<Stderr>>. Skim always draws to stderr so stdout remains clean for piped output.

Terminal lifecycle:

Tui::enter()
  ├─ enable_raw_mode()
  ├─ execute!(EnableMouseCapture, EnableBracketedPaste)
  ├─ if fullscreen: execute!(EnterAlternateScreen, cursor::Hide)
  └─ Tui::start()  ← spawns event pump task

Tui::exit()
  ├─ Tui::stop() / cancel()
  ├─ cleanup_terminal()
  │    ├─ execute!(DisableMouseCapture, DisableBracketedPaste, LeaveAlternateScreen, Show)
  │    └─ disable_raw_mode()
  └─ if inline: clear() + reset cursor to top of drawing area

A panic hook is installed once (PANIC_HOOK_SET: Once) to ensure cleanup_terminal() runs even on panics.

Image preview protocol detection is owned by Skim::enter(), not Tui::enter_terminal(): --image=detect temporarily ensures an alternate screen is active, queries ratatui_image::picker::Picker::from_query_stdio(), falls back to Picker::halfblocks() on failure, then stores the picker in both SkimOptions.image_picker and the Preview widget. --image=halfblocks skips detection and installs Picker::halfblocks() directly.

Event Loop

The crossterm event pump (background Tokio task in Tui::start()):

loop {
    select! {
        cancelled → break
        crossterm_event →
            Key(press)  → send Event::Key(key)
            Paste(text) → send Event::Paste(text)
            Mouse(m)    → send Event::Mouse(m)
            Resize(c,r) → send Event::Resize + Event::Render
            Error(e)    → send Event::Error(e)
        tick (1/12 s)  → send Event::Heartbeat
    }
}

The main loop (Skim::run()) calls tick() in a loop, which select!s on the same channel plus the matcher interval and IPC listener.

Frame rate is capped at 120 fps (FRAME_TIME_MS = 1000/120). App::handle_event(Heartbeat) checks needs_render (an AtomicBool set by the matcher when new results arrive) and emits Event::Render only when the last render was more than FRAME_TIME_MS ago.

App State

App (in src/tui/app.rs) is the single mutable application state. It contains:

FieldTypeRole
item_poolArc<ItemPool>Shared with reader; accumulates raw items
matcherMatcherEngine factory + case + rank config
matcher_controlMatcherControlHandle to stop/query current match pass
item_listItemListDisplay list + selection state
inputInputQuery text + cursor
previewPreviewPreview pane state + process handle
headerHeaderStatic + dynamic header lines
thread_poolArc<ThreadPool>Worker threads for matching
optionsSkimOptionsFull configuration snapshot
layout_templateLayoutTemplatePre-computed widget constraints
layoutAppLayoutLast-frame widget areas (updated in render)
needs_renderArc<AtomicBool>Signal from matcher → event loop
yank_registerStringCut/yank buffer
query_history / cmd_historyVec<String>History for ↑/↓ navigation

App::handle_event() dispatches on Event:

Event::Render     → tui.draw(|f| f.render_widget(&mut self, f.area()))
Event::Heartbeat  → update_spinner(); check pending_matcher_restart; throttled render
Event::RunPreview → run_preview(tui)
Event::Key(k)     → handle_key(k) → [Action…] → tui.event_tx.send(Event::Action)
Event::Action(a)  → handle_action(a) → [Event…] → tui.event_tx.send(…)
Event::Paste(t)   → input.insert_str(cleaned); on_query_changed()
Event::Resize(…)  → app.resize(); run_preview()
Event::Mouse(…)   → handle_mouse()
Event::PreviewReady → apply preview offset; needs_render()
Event::AppendItems  → item_pool.append(); restart_matcher(false)
Event::ClearItems   → item_pool.clear(); restart_matcher(true)
Event::Quit/Close   → tui.exit(); should_quit = true
Event::Reload(_)    → (handled by Skim::tick, not here)

App::restart_matcher(force):

restart_matcher(force)
  ├─ if !force && matcher not stopped → skip (debounce)
  ├─ if pool has no un-taken items && !force → skip
  ├─ kill existing matcher_control
  ├─ determine MergeStrategy
  │    ├─ Replace  → if query changed / force
  │    └─ SortedMerge / Append otherwise
  └─ matcher.run(query, pool, thread_pool, processed_items, strategy, no_sort, needs_render)
       → returns new MatcherControl

A 200 ms debounce (restart_matcher_debounced) is applied to query-change events to avoid thrashing the matcher on rapid typing.

Widget System

All TUI widgets implement SkimWidget:

rust
pub trait SkimWidget: Sized {
    fn from_options(options: &SkimOptions, theme: Arc<ColorTheme>) -> Self;
    fn render(&mut self, area: Rect, buf: &mut Buffer) -> SkimRender;
}

pub struct SkimRender {
    pub items_updated: bool,
    pub run_preview: bool,   // signals that preview should be (re)spawned
}

App itself implements ratatui::widgets::Widget via impl Widget for &mut App, which calls each sub-widget's render() and ORs their SkimRender results. After render, App sets cursor_pos to the absolute screen coordinates of the input cursor.

Layout Engine

LayoutTemplate pre-computes area splits from SkimOptions once and stores constraint trees. apply(area: Rect) → AppLayout is then a cheap, allocation-free split.

AppLayout has four optional areas:

rust
pub struct AppLayout {
    pub list_area: Rect,
    pub input_area: Rect,
    pub header_area: Option<Rect>,
    pub preview_area: Option<Rect>,
}

Layout is rebuilt on Event::Resize, when the header height changes (multiline header items arriving), and on TogglePreview.

Layout orientations (TuiLayout):

ModeDescription
DefaultInput at bottom, list above, header above list (bottom-to-top reading)
ReverseInput at top, list below (top-to-bottom reading)
ReverseListList at top, input at bottom

Preview placement is parsed from --preview-window:

  • Direction: left / right / up / down
  • Size: 50% (default), fixed cells, or negative cells (-N, meaning the non-preview side keeps N cells)
  • Modifiers: hidden, wrap, pty, +offset

Individual Widgets

Input Widget

Input (src/tui/input.rs) maintains:

  • value: String — the query text (primary mode)
  • alternate_value: String — the command text (interactive mode)
  • cursor_pos: usize — character-level cursor position

Text operations (used by handle_action):

  • insert(char) / insert_str(&str) — insert at cursor
  • delete(n) — delete n characters forward
  • delete_backward_word() / delete_to_beginning() / delete_forward_word()
  • move_cursor(delta) / move_cursor_to(pos) / move_to_end()
  • move_cursor_forward_word() / move_cursor_backward_word()
  • switch_mode() — swaps primary and alternate buffers (interactive mode toggle)

The StatusInfo struct rendered inside the input line shows: {spinner} {matched}/{total} ({processed}) [{matcher_mode}] [{multi_count}]

ItemList Widget

ItemList (src/tui/item_list.rs) maintains:

  • items: Vec<MatchedItem> — the currently displayed matched items
  • processed_items: Arc<SpinLock<Option<ProcessedItems>>> — shared with matcher
  • selection: Vec<usize> — indices of multi-selected items
  • current: usize — focused item index (0 = bottom in default layout)
  • offset: usize — scroll offset (number of items scrolled)
  • manual_hscroll: i16 — user-driven horizontal scroll

On each render, ItemList::render() checks processed_items and swaps them in atomically via the SpinLock. Depending on MergeStrategy:

  • Replace: replaces items entirely.
  • SortedMerge: performs an O(n+m) merge preserving order.
  • Append: extends items.

Selection state management:

  • toggle_at(idx) / toggle() / toggle_all() / select_all() / clear_selection()
  • scroll_by_rows(n) — scroll by terminal rows (accounting for multiline items)
  • scroll_by(n) — scroll by item count
  • jump_to_first() / jump_to_last()

Pre-selection is applied when items first appear: DefaultSkimSelector::should_select(index, item) is checked for each item and matching items are added to selection.

ItemRenderer

ItemRenderer (src/tui/item_renderer.rs) is an ephemeral struct created per render frame. It handles all per-item display concerns:

  1. Selector icon rendering> (single-select cursor) or configurable icon.
  2. Multi-select icon — space / > or configurable icon per selection state.
  3. Match highlight — builds a DisplayContext (score, Matches, width, base style, highlight style) and calls item.display(context) so custom items can render their own styled Line<'_>.
  4. Horizontal scrollcalc_hscroll() finds the first matched character and auto-scrolls to show it; apply_hscroll() clips spans accordingly.
  5. Tab expansionexpand_tabs() replaces \t with spaces at configurable width.
  6. Ellipsis truncation — replaces overflowing content with (or custom --ellipsis).
  7. Multiline items — when --multiline <sep> is set, splits item text on the separator and renders sub-lines.
  8. Score / index display — when feature flags ShowScore / ShowIndex are set.
  9. Disabled state — dims all spans when item.disabled() is true.

Preview Widget

Preview (src/tui/preview.rs) renders a side/top/bottom pane showing expanded information about the focused item. Its stored content is one of three variants:

Plain text mode (no pty): spawns sh -c <cmd> on Unix or cmd /c <cmd> on Windows. On Windows, Command::raw_arg is used so cmd.exe receives shell metacharacters exactly as written. The child captures stdout (capped at PREVIEW_MAX_BYTES), parses it with ansi_to_tui::IntoText, stores as PreviewContent::Text, and sends Event::PreviewReady.

PTY mode (--preview-window pty): creates a real pseudo-terminal pair via portable_pty. The child process sees a properly sized terminal (via ROWS/COLUMNS env and PTY dimensions). Output is parsed by a vt100::Parser with a scrollback buffer, stored as PreviewContent::Terminal(Arc<RwLock<vt100::Parser>>). This enables interactive preview programs (e.g. bat, delta).

Image mode (--image[=detect|halfblocks], requires the default image feature): treats the expanded preview command as an image path instead of executing it. A worker thread decodes the image with the image crate and stores PreviewContent::Image { source, protocol, size }. Rendering uses ratatui_image; detect builds an image protocol picker after entering the alternate screen, while halfblocks skips terminal capability detection and uses the portable half-block renderer. The protocol is rebuilt when the preview area changes so the image keeps its aspect ratio within the pane.

Preview::spawn():

kill() ← kill any running preview
reset scroll_y / scroll_x

if image mode:
  decode <cmd> as image path
  thread: content.write() = PreviewContent::Image or failure text
          → Event::PreviewReady

else if pty mode:
  init_pty()  ← create PtyPair
  resize PTY to current (rows, cols)
  spawn sh -c <cmd> in slave
  thread: read master → filter_and_respond_to_queries → vt100::Parser::process
          → Event::PreviewReady when EOF

else:
  sh -c <cmd>
  thread: wait for output → content.write() = PreviewContent::Text(…)
          → Event::PreviewReady

Scroll state: scroll_y, scroll_x (in lines/columns). page_up/down, scroll_up/down/left/right modify these. PreviewPosition supports fixed, percentage, and negative offsets. When PreviewReady fires, an optional offset expression (from --preview-window +expr) is evaluated to auto-scroll to the matched line.

Header Widget

Header (src/tui/header.rs) renders two kinds of content:

  • Static (--header <text>): shown at the top or bottom depending on layout; expanded for tab characters once at init.
  • Dynamic (--header-lines N): first N items from ItemPool::reserved() are treated as header lines instead of selectable items.

When --multiline <sep> is active, each header-line item may span multiple terminal rows. The height() method returns the total rows needed; if this changes between frames (multiline items arrive), App::render() detects it and rebuilds LayoutTemplate.

StatusLine / Info

StatusInfo is computed inside Input::render() from the current App state:

Left side:  "> " + spinner + " " + matched + "/" + total [+ "(N%)"]
Inline sep: " < " (when inline_info)
Right side: multi-select count when multi mode

InfoDisplay has four modes:

  • Default — separate line above the prompt
  • Inline — inside the prompt line (after the query text)
  • InlineRight — inside the prompt line, right-aligned
  • Hidden — not shown

Key Bindings & Action System

Key Parsing

parse_key(key_str) (in src/binds.rs) converts strings like "ctrl-a", "alt-shift-f", "f10", "enter" into crossterm::event::KeyEvent { code, modifiers }.

parse_action(raw) (in src/tui/event.rs) converts strings like "down:2", "execute(ls {})", "if-query-empty:reload+up" into Action variants.

parse_action_chain(chain) splits on + (respecting if-*{…+…} syntax) into Vec<Action>.

parse_keymap("key:action+action")(&str, Vec<Action>).

Default Key Map (get_default_key_map)

Notable defaults:

KeyAction
EnterAccept(None)
EscAbort
Ctrl-C / Ctrl-D / Ctrl-GAbort
/ Ctrl-K / Ctrl-PUp(1)
/ Ctrl-J / Ctrl-NDown(1)
TabToggle + Down(1)
Shift-Tab / BackTabToggle + Up(1)
Ctrl-ABeginningOfLine
Ctrl-EEndOfLine
Ctrl-UUnixLineDiscard
Ctrl-WUnixWordRubout
Ctrl-YYank
Ctrl-QToggleInteractive
Ctrl-RRotateMode
Shift-↑ / Shift-↓PreviewUp(1) / PreviewDown(1)
Alt-H / Alt-LScrollLeft(1) / ScrollRight(1)

User bindings from --bind key:action[+action] are parsed at startup and merged via KeyMap::add_keymaps().

Action Dispatch

Event::Key(k) → handle_key(k)
  ├─ lookup k in options.keymap → [Action…]
  ├─ fallback: Ctrl-C → Quit
  ├─ fallback: printable char → AddChar(c) or AddChar(uppercase)
  └─ emit Vec<Event::Action(a)> to event queue

Event::Action(a) → handle_action(a) → Vec<Event>

handle_action is a large match statement covering all ~70+ Action variants. Key action categories:

CategoryActions
NavigationUp/Down(n), HalfPageUp/Down, PageUp/Down, First/Last/Top
Text editingAddChar, BackwardChar/DeleteChar/Word, ForwardChar/Word, KillLine, Yank, UnixLineDiscard/WordRubout
SelectionToggle, ToggleAll, ToggleIn/Out, Select, SelectAll, DeselectAll, AppendAndSelect
QuerySetQuery, NextHistory, PreviousHistory
PreviewTogglePreview, PreviewUp/Down/Left/Right, PreviewPageUp/Down, RefreshPreview, SetPreviewCmd
CommandExecute(cmd), ExecuteSilent(cmd), Reload(cmd?), RefreshCmd
ModeToggleInteractive, ToggleSort, RotateMode
ConditionalIfQueryEmpty(then, else?), IfQueryNotEmpty(then, else?), IfNonMatched(then, else?)
LifecycleAccept(key?), Abort, Cancel
UIClearScreen, Redraw, SetHeader(text?), SelectRow(n)
CustomCustom(ActionCallback) — async or sync closure receiving &mut App

Action::Custom(ActionCallback) is the library extension point: callers can inject arbitrary async logic into the action pipeline without forking skim.


Preview System

The preview command string supports placeholder substitution via App::expand_cmd():

PlaceholderExpands to
{}text of the focused item
{q}current query string
{n}index of the focused item
{+}space-separated texts of all selected items
{+n}space-separated indices of selected items

Preview execution is debounced (DEBOUNCE_MS in run_preview). A change in the focused item or query triggers pending_preview_run = true; the next Heartbeat after the debounce window calls Preview::spawn().

The ItemPreview enum (returned by SkimItem::preview()) gives library users full control:

rust
pub enum ItemPreview {
    Command(String),          // run command, capture stdout
    Text(String),             // display plain text
    AnsiText(String),         // display ANSI-colored text
    CommandWithPos(String, PreviewPosition),
    TextWithPos(String, PreviewPosition),
    AnsiWithPos(String, PreviewPosition),
    Global,                   // fall back to --preview option
}

Output & Result Collection

Skim::output() is called after the event loop exits:

Skim::output()
  ├─ reader_control.kill()        ← stop reader threads
  ├─ is_abort = !matches!(final_event, Action::Accept)
  ├─ selected_items = app.results()
  │     └─ item_list.items[selection indices] or [current] if no multi
  ├─ query = app.input.to_string()
  ├─ cmd = (interactive? input : options.cmd_query? : initial_cmd)
  ├─ current = item_list.selected()   ← focused item
  └─ header = app.header.header

SkimOutput fields returned to caller:

rust
pub struct SkimOutput {
    pub final_event: Event,         // Action::Accept or Action::Abort
    pub is_abort: bool,
    pub final_key: crossterm::event::KeyEvent,
    pub query: String,
    pub cmd: String,
    pub selected_items: Vec<MatchedItem>,
    pub current: Option<MatchedItem>,
    pub header: String,
}

The output phase is SkimOutput::write_output(&mut out, &BinOptions) (src/output.rs), called by the CLI binary with a buffered stdout. BinOptions (also in src/output.rs, built via BinOptions::from_opts) captures the output-related flags. Keeping the serialization independent of stdout lets it be unit-tested by passing a Vec<u8>. It writes, in order:

  1. query if --print-query
  2. cmd if --print-cmd
  3. header if --print-header
  4. current item text if --print-current
  5. accept_key if --expect matched
  6. For each selected item: strips ANSI if --ansi && !--no-strip-ansi, prints text + score if --print-score
  7. If --output-format <template>: uses printf() to expand a format string with placeholders (this path is exclusive — it replaces steps 1–6)

Exit codes: 0 = items selected, 1 = no items selected, 130 = abort, 135 = tmux launch failed.


IPC / Listen Socket

This subsystem is gated behind the default listen Cargo feature, which also pulls in the interprocess, ron, and serde dependencies. With the feature off, the --listen / --remote flags, the Skim::listener field, the select! listener branch, and the serde derives on Action are all compiled out. The select! branch cannot carry a #[cfg] attribute (tokio's macro rejects it), so it stays in place but its future becomes a never-resolving pending() and its handler is unreachable (the stream type alias RemoteStream is uninhabited).

When --listen <socket_name> is set, Skim::init_listener() creates an interprocess local socket. The main event loop's select! accepts connections and spawns Tokio tasks to read RON-encoded Action values line by line:

listener.accept() → stream
  tokio::spawn:
    BufReader(stream).lines()
    for each line:
      ron::from_str::<Action>(&line)
      → tui.event_tx.send(Event::Action(act))

The remote client mode (--remote <socket_name>) reads action strings from stdin and sends them to an existing skim instance:

// src/bin/main.rs main()
if let Some(remote) = opts.remote {
    stream = LocalSocket::connect(socket_name)
    loop: read_line → parse_action_chain → ron::to_string → stream.write_all
}

This enables scripted control of a running skim session.


Theming

ColorTheme (src/theme.rs) holds 13 named ratatui::style::Style values:

FieldCovers
normalDefault item text
matchedHighlighted match characters
currentFocused item background
current_matchMatch highlights on focused item
queryQuery text in input box
spinnerSpinner animation character
infoStatus info line
promptPrompt character
cursorCursor indicator
selectedMulti-selected item marker
headerHeader text
borderBorder lines
scrollbarItem list scrollbar thumb

Built-in palettes: none, bw, default16, dark256, molokai256, light256, catppuccin_mocha, catppuccin_macchiato, catppuccin_latte, catppuccin_frappe.

Selected via --color base_theme[,component:color[:modifier]]. Individual component overrides use CSS-style RGB hex (#RRGGBB), ANSI 256-color indices, or named modifiers (bold, italic, underline, dim, reverse).

BorderType mirrors Ratatui's border styles but adds two internal no-border states:

  • None is the default --border=none; widgets do not draw boxes, but preview separators may still be drawn between panes.
  • ForceOff is set by --no-border; it disables all borders, including tmux/zellij popup borders.

Passing --border without a value means plain. Passing a value accepts Ratatui styles such as rounded, double, thick, dashed variants, and quadrant variants.


History

Query and command histories are managed in SkimOptions:

  • Loaded at startup via SkimOptions::init_histories() from files specified by --history-file / --cmd-history-file.
  • Stored in App::query_history / App::cmd_history.
  • Navigation with Action::NextHistory / Action::PreviousHistory uses history_index: Option<usize> and saved_input: String to restore the original input when returning to the live query.
  • Written back to file at exit in sk_main via write_history_to_file(), which deduplicates the last entry and enforces --history-size.

Pre-Selection

DefaultSkimSelector (src/helper/selector.rs) implements the Selector trait:

rust
pub trait Selector {
    fn should_select(&self, index: usize, item: &dyn SkimItem) -> bool;
}

Three modes (combinable):

  • first_n(N) — selects the first N items by index
  • preset(iter) — selects items whose text() is in a HashSet
  • regex(pattern) — selects items matching a regex

Configured via --pre-select-n, --pre-select-items, --pre-select-pat, --pre-select-file.

Applied during ItemList::from_options() and re-applied when items are appended.


Threading Model

Skim uses a mix of OS threads (for blocking I/O) and Tokio async tasks:

Main thread (Tokio runtime)
  ├─ Skim::run() async loop → tick() → select!
  │     ├─ process events synchronously (App::handle_event is sync)
  │     └─ block_in_place for ActionCallback::call (async closures)
  │
  └─ Tokio task: Tui event pump (crossterm EventStream + tick timer)

ThreadPool (N = num_cpus OS threads, persistent)
  ├─ Matcher coordinator (1 slot per match run)
  └─ Worker threads (N-1 slots per match run)

Reader threads (OS threads, per-invocation):
  ├─ collect_items thread: blocks on SkimItemReceiver (recv_timeout 5ms), calls ItemPool::append
  ├─ I/O reader thread: reads large byte chunks, splits lines, assigns sequence numbers
  ├─ Worker threads (N): parse lines, create DefaultSkimItem (ANSI strip + field transforms inline)
  ├─ Reorder thread: sequence-ordered output; drops tx_pipeline_done on EOF
  └─ Killer thread (command inputs only): waits for rx_interrupt or rx_pipeline_done;
       kills child process when either fires

Preview thread (OS thread, per preview spawn):
  └─ reads PTY/child stdout or decodes image path → PreviewContent Arc<RwLock>
      → sends Event::PreviewReady

IPC handler task (Tokio, per connection):
  └─ reads RON actions → sends Event::Action to TUI channel

Popup stdin relay thread (OS thread, only in --popup/--tmux mode):
  └─ copies stdin → FIFO for child sk process

Synchronization primitives used:

  • Arc<SpinLock<Option<ProcessedItems>>> — matcher-to-ItemList result handoff
  • Arc<AtomicBool>needs_render (matcher → event loop), stopped / interrupt (MatcherControl)
  • Arc<AtomicUsize>processed / matched counters, reader components_to_stop
  • Arc<tokio::sync::Notify>items_available (ItemPool → Skim::tick wakeup)
  • Arc<std::sync::RwLock<PreviewContent>> — preview thread → Preview widget
  • kanal::Sender/Receiver<Vec<Arc<dyn SkimItem>>> — item batches through pipeline

The global allocator is mimalloc (v3), chosen for its low-latency multi-threaded allocation characteristics critical to the concurrent item-creation pipeline.


Important Call Sites (Cross-Reference)

Call siteFileWhat it does
Skim::run_withsrc/skim.rs:58Top-level library entry point
Skim::run_itemssrc/skim.rs:100Convenience wrapper for iterator inputs
Skim::init_tuisrc/skim.rs:124Initialize default crossterm TUI backend
Skim::initsrc/skim.rs:143Constructs all subsystems from options
Skim::startsrc/skim.rs:185Starts reader + initial matcher pass
Skim::handle_reloadsrc/skim.rs:195Kills reader, clears pool, restarts
Skim::init_tui_withsrc/skim.rs:258Install a caller-provided TUI backend
Skim::entersrc/skim.rs:345Enter terminal, resolve image picker, start listener/event pump
Skim::should_entersrc/skim.rs:385Filter/select-1/exit-0/sync gate
Skim::outputsrc/skim.rs:488Collect & return SkimOutput
Skim::ticksrc/skim.rs:569Single async event loop iteration
App::from_optionssrc/tui/app.rs:260Build all widgets from options
App::run_previewsrc/tui/app.rs:414Expand cmd, debounce, call Preview::spawn
App::handle_eventsrc/tui/app.rs:536Dispatch all Event variants
App::handle_actionsrc/tui/app.rs:687Dispatch all Action variants
App::restart_matchersrc/tui/app.rs:1183Kill old match pass, start new one
App::expand_cmdsrc/tui/app.rs:1256Substitute {}, {q}, {n} etc.
Widget::render (App)src/tui/app.rs:128Root render; calls all sub-widgets
Matcher::runsrc/matcher.rs:~260Parallel match dispatch
merge_worker_resultssrc/matcher.rs:28Merge k sorted runs → ProcessedItems
ItemPool::appendsrc/item.rs:469Add items, notify matcher
ItemPool::takesrc/item.rs:502Take un-matched items for matcher
DefaultSkimItem::newsrc/helper/item.rs:58ANSI strip, field transform, ranges, disable pattern
SkimItemReader::parallel_bufreadsrc/helper/item_reader.rs:263Unified parallel pipeline (all inputs)
spawn_io_readersrc/helper/item_reader.rs:354I/O reader thread: chunk reads + line splitting
spawn_reorder_threadsrc/helper/item_reader.rs:458Reorder thread: ordered output + pipeline-done signal
Preview::spawnsrc/tui/preview.rs:319Start image, PTY, or plain preview worker
Tui::new_with_height_and_backendsrc/tui/backend.rs:77Terminal init + viewport sizing
Tui::entersrc/tui/backend.rs:126Enable raw mode + terminal setup
Tui::startsrc/tui/backend.rs:192Spawn crossterm EventStream task
popup::run_withsrc/popup/mod.rs:86Delegate to multiplexer popup + parse output
popup::check_envsrc/popup/mod.rs:72Guard: multiplexer present and not already in popup
check_and_run_popupsrc/bin/main.rs:131Check popup conditions, dispatch to popup::run_with
sk_mainsrc/bin/main.rs:144CLI orchestration + output printing
parse_keysrc/binds.rs:139"ctrl-a"KeyEvent
parse_action_chainsrc/binds.rs:211"down+select"Vec<Action>
Matcher::create_engine_factory_with_buildersrc/matcher.rs:189Build engine factory chain from options
ExactOrFuzzyEngineFactory::create_engine_with_casesrc/engine/factory.rs:93Parse query prefixes, build engine
AndOrEngineFactory::parse_andorsrc/engine/factory.rs:176Split query into AND/OR tree
FuzzyEngine::match_itemsrc/engine/fuzzy.rs:175Fuzzy match a single item
LayoutTemplate::from_optionssrc/tui/layout.rs:76Compute widget constraint tree
LayoutTemplate::applysrc/tui/layout.rs:165Split Rect into AppLayout
ItemRenderer::render_itemsrc/tui/item_renderer.rs:84Full per-item render pipeline
ColorTheme::init_from_optionssrc/theme.rs:56Parse --color spec

Public Library API

The minimum surface to embed skim in a Rust application:

rust
use skim::prelude::*;

// 1. Build options
let options = SkimOptionsBuilder::default()
    .height("40%")
    .multi(true)
    .preview(Some("cat {}"))
    .build()
    .unwrap();

// 2a. Run with a static item slice
let output = Skim::run_items(options, ["foo", "bar", "baz"]).unwrap();

// 2b. Or stream items through a channel
let (tx, rx) = unbounded::<Vec<Arc<dyn SkimItem>>>();
// … send batches to tx from another thread …
let output = Skim::run_with(options, Some(rx)).unwrap();

// 3. Inspect results
if !output.is_abort {
    for item in &output.selected_items {
        println!("{}", item.output());
    }
}

Implementing SkimItem for custom types:

rust
struct MyItem { id: u32, label: String }

impl SkimItem for MyItem {
    fn text(&self) -> Cow<'_, str> {
        Cow::Borrowed(&self.label)   // used for matching
    }
    fn display(&self, context: DisplayContext) -> ratatui::text::Line<'_> {
        context.to_line(self.text()) // default implementation; override for custom styling
    }
    fn output(&self) -> Cow<'_, str> {
        Cow::Owned(self.id.to_string())   // printed on accept
    }
    fn preview(&self, _ctx: PreviewContext<'_>) -> ItemPreview {
        ItemPreview::Text(format!("ID: {}\nLabel: {}", self.id, self.label))
    }
    fn disabled(&self) -> bool {
        false // disabled items are visible but cannot be selected
    }
}

DisplayContext exposes the current score, match spans (Matches::CharIndices, CharRange, ByteRange, or None), container width, and base/highlight styles. PreviewContext exposes the current query, command query, preview dimensions, current index/selection, and multi-selection lists.

Custom ActionCallback for inline async logic:

rust
let cb = ActionCallback::new(|app| async move {
    // mutate app state, return follow-up events
    Ok(vec![Event::Action(Action::Accept(None))])
});
// bind it: options.keymap.insert(key, vec![Action::Custom(cb)]);

CommandCollector trait for custom item sources (e.g. async databases):

rust
impl CommandCollector for MySource {
    fn invoke(&mut self, cmd: &str, components_to_stop: Arc<AtomicUsize>)
        -> (SkimItemReceiver, Sender<i32>)
    {
        // spawn a thread, send batches of Arc<dyn SkimItem>, return rx + kill-tx
    }
}

Set options.cmd_collector = Rc::new(RefCell::new(my_source)) before calling Skim::run_with.