Skip to content
 
 

Repository files navigation

Tabview

View delimited text, JSON, NDJSON, local SQLite databases, and Elasticsearch in a spreadsheet-like terminal interface.

This project is functional but future development will be sporadic and limited. For a more fully featured CSV viewer/spreadsheet app, check out the Visidata project.

Posted by Scott Hansen tech@firecat53.net

Original code forked from http://www.amk.ca/files/simple/tabview.txt.

Contributed by A.M. Kuchling amk@amk.ca.

Other contributors:

The highlighted position is shown in the top-left corner of the screen. The contents of that cell are shown next to it.

Features

  • Rust command-line application distributed as the tabview binary.
  • Spreadsheet-like view for visualizing tabular data.
  • Automatic or explicit delimited, JSON, and NDJSON input selection.
  • Read-only browsing of local SQLite databases through Turso.
  • ES|QL browsing of Elasticsearch indices and data streams.
  • RFC 6901 JSON Pointer selection for tables embedded in response documents.
  • Incremental indexing for large seekable inputs and typed JSON scalar values.
  • Vim-like navigation, including h, j, k, l, g, G, marks, and numeric prefixes such as 12G.
  • Persistent header row toggling.
  • Lexical, natural, and numeric sorting by the current column.
  • Dynamic column width and gap adjustment.
  • Full-text incremental search with n and N result navigation.
  • Current-column filter-in and filter-out with text, regex, and numeric modes.
  • Full-cell popup with Enter.
  • Optional clipboard support for yanking the current cell.
  • In-place reload when data changes.
  • Built-in keybinding help with F1 or ?.
  • Example screenshots in the screenshots/ directory.

Requirements

  • Rust toolchain for installation with Cargo.
  • Optional clipboard support can be enabled with the clipboard Cargo feature.
  • Saved views and SQLite support are enabled by default through the saved-views and sqlite Cargo features.
  • Elasticsearch support is opt-in through the elasticsearch Cargo feature.
  • Build with --no-default-features to omit both optional capabilities, or enable either one explicitly (for example, --no-default-features --features saved-views builds without SQLite).
  • Tokio is part of every build and provides the application runtime used for background source-query work across SQLite and file-backed sources.

Installation

Install the latest published release:

cargo install tabview

Install from a local checkout:

cargo install --path .

Build with clipboard support:

cargo install tabview --features clipboard
cargo install tabview --features elasticsearch

Usage

From the command line:

tabview <filename>
tabview <filename> --start_pos 6,5
tabview <filename> +6:5
tabview <filename> --encoding iso8859-1 +6:
tabview <filename> --delimiter '\t' --quoting QUOTE_NONE
tabview <filename> --width mode
tabview <filename> --width max
tabview <filename> --width 20
tabview <filename> --view cat-shards
tabview <filename> --no-view
tabview response.json --json-path /hits/hits
tabview repositories.json --object-mode entries
tabview settings.json --object-mode record
tabview records.ndjson --format ndjson
tabview response.data --format json --schema-scan full
tabview sample/us-counties.sqlite3
tabview sample/us-counties.sqlite3 --format sqlite --table counties
tabview --format elasticsearch https://localhost:9200 --table logs-*
tabview --format elasticsearch https://localhost:9200 \
  --query 'FROM logs-* | WHERE log.level == "error" | SORT @timestamp DESC'
tabview data.csv --output table
tabview --interactive data.csv
tabview --interactive --output table data.csv > edited.txt

Read from standard input:

cat data.csv | tabview -
cat records.ndjson | tabview --format ndjson -
cat data.csv | tabview --output table - > table.txt
cat data.csv | tabview --interactive --output table - > edited.txt

Runtime and serialization are separate. --interactive/-i forces the TUI; --output table/-o table selects fixed-width text output. Combining them runs the TUI and writes the final live view after a normal quit. Hiding columns, formatting, filtering, and sorting in that session affect the emitted result. Using -i without -o is view-only and writes no final table.

With neither option, terminal stdout selects the TUI and redirected or piped stdout selects plain table output automatically. Table output never uses raw mode or the alternate screen. It emits every configured row and visible column, uses no aggregate terminal-width limit, and leaves wrapping, paging, or truncation to downstream tools. --color auto and --color never produce plain bytes; --color always opts into theme-derived ANSI styling.

When stdin supplies table data during interactive operation, Tabview uses the controlling terminal for UI input and drawing. It continues draining finite stdin in the background while the TUI is active, and an explicit final export waits for EOF so late rows and columns are included. Redirect output to a different path from the input: shells truncate redirection targets before Tabview starts. The current table format is fixed-width text and does not preserve CSV or JSON syntax, so write it to a text destination rather than replacing the source. Future serializers such as CSV and Markdown can be added as new --output values without changing --interactive.

--format auto|delimited|json|ndjson|sqlite|elasticsearch defaults to auto. An unambiguous URL scheme can select a source format: libsql:// resolves to SQLite and file:// resolves to a local path. HTTP(S) remains ambiguous and therefore requires --format elasticsearch; Tabview never probes arbitrary remote content to guess its type. Filename extensions are considered before bounded content probing; SQLite's SQLite format 3 signature is recognized before any text decoding. An explicit format always wins. Delimited-only options imply delimited input under auto unless the input has a SQLite signature, and are rejected for SQLite and explicitly selected structured formats.

SQLite sources

Tabview opens local SQLite-format files through Turso. If a database contains one selectable table or compatible ordinary view, it is selected automatically. If several are available, the interactive application presents a simple table picker; direct batch output instead requires --table <name> or saved source.table so it cannot choose silently or emit a partial result. SQLite data cannot be read from stdin, and Turso Cloud, libsql://, and other remote SQLite URLs are not connected yet (the scheme is reserved now so remote libSQL support can use the same target model). SQLite support is compiled by the default-enabled sqlite Cargo feature; a build without that feature omits Turso, --format sqlite, and --table while retaining the shared Tokio application runtime.

SQLite source queries are bounded to 1,000 rows by default. Source filters and SQLite-native source sorting run before that limit. View filters, rich local sorts, search, formatting, and hiding then operate only on the fixed bounded result and never refill it. The Source Configuration modal (u) edits the source limit, filters, and native sort as one staged query. The View Configuration modal (V) manages source-independent local presentation, while Column Info (i) remains the quick current-column editor. Existing f/F and sort keybindings remain view-only.

--query accepts one read-only, row-producing SQLite statement. SELECT and CTE queries are composed as a derived table so source filters, source sorting, and the hard limit remain enforced. Multiple statements, writes, state-changing pragmas, unbound parameters, and non-tabular statements are rejected while the database remains opened with storage-level read-only flags.

The Query modal (p) shows the logical parameterized SQLite SELECT, typed parameters, and a safely rendered copyable statement. It also calls out active local view transforms, because they are intentionally absent from the SQL. The private extra-row truncation probe is never shown in the reusable SQL.

Tabview opens SQLite through Turso with storage-level read-only flags before creating a connection, so viewing does not convert a rollback-journal database to WAL, create journal/WAL/shared-memory sidecars, or modify existing database or sidecar bytes. The connection is confined behind a read-only facade, PRAGMA query_only is enabled and verified as defense in depth, and generated source predicates use bound parameters. Ordinary tables and capability-probed ordinary views are selectable. Virtual tables—including existing FTS5 and RTree tables—are reported as unsupported; shadow and SQLite-internal objects are hidden. Turso's optional FTS support is disabled because Tabview does not select existing SQLite FTS virtual tables. SQLite-enabled builds use Turso's mimalloc feature as Tabview's global allocator.

SQLite declared types are displayed as source metadata and used only as conservative initial hints. SQLite values remain dynamically typed at runtime, and observed integer, real, text, blob, and null values can widen those hints. Rowid tables and declared primary-key tuples provide stable cursor/mark identity across source-query replacement. Views and tables without a stable key reset identity-dependent state.

A public-domain Census Bureau database with 1,000 county records is included at sample/us-counties.sqlite3; see sample/README.md for its provenance and column-selection details.

Elasticsearch sources

Build with --features elasticsearch, then provide an HTTP(S) cluster endpoint:

tabview --format elasticsearch https://elastic.example:9200
tabview --format elasticsearch https://elastic.example:9200 --table logs-*
tabview --format elasticsearch https://elastic.example:9200 \
  --query 'FROM logs-* | KEEP @timestamp, message | SORT @timestamp DESC'

Without --table or --query, interactive mode discovers visible, open non-dot indices and data streams and displays them in separate picker sections. Aliases are intentionally omitted from discovery, but an alias supplied explicitly through --table is passed to Elasticsearch and works normally. Direct non-interactive output requires --table or --query, because it cannot ask the user to choose a target.

--table generates an ES|QL FROM query with _index and _id metadata. --query is a complete opaque ES|QL base query; Tabview does not parse or validate its FROM targets. Source Configuration can add safely quoted filters and sorts and change the hard limit (1,000 rows by default). Tabview privately requests one extra row to distinguish a complete result from a limited one; the reusable query shown by the Query popup retains the configured limit.

For a selected index or data stream, Tabview reads mappings and field capabilities to build a catalog including nested fields, multifields, runtime fields, and cross-index conflicts. ES|QL response columns remain authoritative for the displayed result schema because commands such as STATS, EVAL, and KEEP can change it. Query-only startup skips mapping discovery. Successful schema-changing queries activate atomically; failed or superseded requests leave the prior result visible.

Authentication and custom trust are environment-only:

ELASTIC_API_KEY=... tabview --format elasticsearch https://elastic.example:9200 --table logs-*
ELASTIC_USERNAME=elastic ELASTIC_PASSWORD=... \
  tabview --format elasticsearch https://elastic.example:9200 --table logs-*
ELASTIC_CA_CERT=/path/to/ca.pem \
  tabview --format elasticsearch https://elastic.example:9200 --table logs-*

API-key and username/password modes are mutually exclusive. Credentials in the endpoint URL are rejected, and URL query strings, fragments, and userinfo are never included in diagnostics or saved-view identities. The cluster principal needs permission to resolve index metadata and read mappings/field capabilities for picker/table mode, plus permission to run ES|QL and read the target data. Partial ES|QL results are labeled in the TUI and warned on stderr; stdout remains table data only.

The optional adapter pins the official Rust client at 9.1.0-alpha.1, uses its native-tls backend, and adds the client, HTTP, TLS, and URL dependency graph only when the feature is enabled. It is tested against the versioned Elasticsearch integration fixture. Server-side async-query progress, connection profiles, and aliases in the picker are outside this change.

For structured formats, --object-mode auto|record|entries controls how a selected object becomes rows. record keeps compatibility behavior and opens the object as one row. entries opens each direct member as a row, preserving source order; the member name is a synthetic first text column with canonical identity @key. The same option is format-neutral so future structured input adapters can use it too. It is not valid for arrays, scalars, delimited input, or NDJSON row streams.

auto, the default, detects entries only when the bounded sample has at least three members, every sampled value is an object, and at least 75 percent share a direct child field with the same value kind. Detection examines at most 64 entries or 1 MiB, finishing the entry that crosses the byte bound. Use explicit record or entries for reproducible scripts and saved views. Improvements to future default detection do not override an explicit mode.

--json-path uses RFC 6901 JSON Pointer, not JSONPath, and selection happens before object-mode resolution. For example, --json-path /hits/hits selects Elasticsearch search hits while ignoring response metadata. Selected arrays remain rows. For NDJSON the pointer is resolved in each complete document and the selected object or array remains that document's single row.

Nested objects are flattened to canonical row-relative pointers. Nested arrays remain atomic JSON cells. Native null, boolean, integer, floating-point, and text values remain distinct; notably, JSON null is not an empty string.

Use as the pager for MySQL by setting these options in ~/.my.cnf:

pager=tabview -d '\t' --quoting QUOTE_NONE -
silent

The Rust rewrite supports the tabview CLI only. The former Python import API (import tabview and tabview.view(...)) is not part of the supported surface.

Color Themes

Tabview loads theme settings from $XDG_CONFIG_HOME/tabview/config.yml, or ~/.config/tabview/config.yml when XDG_CONFIG_HOME is unset:

theme: cmdzro

Theme files live in tabview/themes/*.yml or tabview/themes/*.yaml under the same config directory. If both name.yml and name.yaml exist, .yml wins. If no theme is configured, tabview uses the built-in cmdzro theme based on ~/.config/nvim/colors/cmdzro.vim: neutral gray text, blue reserved for UI surfaces, yellow reserved for search and UI emphasis, and red reserved for errors or unhealthy states.

Theme colors accept 16-color names, 256-color palette values, and 32-bit hex:

name: ops-dark
mode: auto # auto, ansi16, ansi256, hex32, or truecolor

palette:
  text: "#AFAFAFFF"
  gray: gray
  muted: palette(240)
  ui_blue: palette(19)
  blue: blue
  dark_blue: palette(19)
  cyan: cyan
  dark_cyan: dark-cyan
  green: dark-green
  magenta: magenta
  yellow: yellow
  error: dark-red
  teal: "#25A39AFF"

identifiers:
  colors: [bright-green, magenta, cyan, white]

styles:
  table:
    location:
      fg: gray
      bg: black
    current_cell:
      fg: cyan
      bg: dark_blue
    divider:
      fg: gray
    header:
      fg: dark_cyan
      modifiers: [bold]
    header_selected:
      fg: cyan
      modifiers: [bold]
    header_glyph:
      fg: muted
    cell:
      fg: text
    selected:
      fg: text
      bg: dark_blue
    hidden_marker:
      fg: muted
  popup:
    background:
      fg: text
      bg: dark_blue
    border:
      fg: cyan
      bg: dark_blue
    title:
      fg: gray
      bg: dark_blue
    body:
      fg: text
      bg: dark_blue
    disabled:
      fg: muted
      bg: dark_blue
    active:
      fg: gray
      bg: dark_blue
    action:
      fg: cyan
      bg: dark_blue
    option_selected:
      fg: cyan
      bg: dark_blue
  search:
    highlight:
      fg: yellow
      modifiers: [underline]
  message:
    footer:
      fg: yellow
      bg: ui_blue

Named 16-color values use tabview's built-in cmdzro base palette; in truecolor mode they resolve to those RGB values, while mode: ansi16 emits ANSI colors for the terminal palette.

See sample/config/themes/cmdzro.yml for a complete theme file. The theme schema is shipped at schemas/theme.schema.json.

Saved Views

By default, tabview loads user-defined YAML views from $XDG_CONFIG_HOME/tabview/views, or ~/.config/tabview/views when XDG_CONFIG_HOME is unset. This POSIX-style path is used on every platform, including macOS. Files ending in .yml and .yaml are accepted. If both name.yml and name.yaml exist, .yml wins and a footer warning is shown.

Views match the opened input basename only. Remote endpoints use a sanitized non-secret basename such as https_elastic.example_9200; this avoids host collisions without persisting credentials, query strings, or fragments. Filename entries are classified as exact strings, globs containing *, ?, or [, or regexes that start with ^ or end with $. Exact matches win before globs, then regexes. Use --view <name> to force a view by file stem, or --no-view to disable loading and saving for that run.

Saved views can define sparse per-column state:

name: cat-shards
filenames:
  - cat_shards.txt
source: {}
view:
  nulls: last
  columns:
    shard:
      type: integer
      width: header
      align: left
      nulls: first
    "*count":
      type: integer
      format: locale
      width: content
    segment:
      type: text
      visible: false
  sort:
    - column: shard
      direction: asc
      kind: numeric
  filters:
    - column: "*count"
      action: in
      kind: numeric
      condition: ">0"

A keyed-object view can pin its row shape and address the synthetic key column independently of its display label:

name: repositories
filenames: [repositories.json]
source:
  format: json
  object_mode: entries
view:
  columns:
    "@key":
      label: Repository

Column keys match headers case-insensitively. Exact keys win over wildcard keys; wildcard ties use the most literal characters, then lexical order. Supported type aliases are string, text, date, ip, number, float, integer, semver, boolean, char, bit, and word. Formats include plain, locale, mask, uppercase, lowercase, char, bit, and word. Number masks support 0, 0.00, #,##0, and #,##0.00 forms. locale uses the system POSIX locale with en_US fallback, or view.locale. Headers are prefixed first with sort state, then filter state: for ascending sort, for descending sort, + for filter-in, - for filter-out, and ± for multiple filters. Truncation applies after those prefix markers.

Source options under source are selected before the table opens. View formatting and local operations live under view. Source-option precedence is explicit CLI options, then the selected saved view, then defaults. Supplying --schema-scan default therefore overrides a saved source.schema_scan: full for one invocation. When a view is written for an object table, tabview saves the resolved explicit source.object_mode (record or entries) so later detector improvements do not change that view's shape. Non-object tables omit it. Native sources may persist either source.table or source.query, never both. For Elasticsearch, source.query stores only the configured ES|QL base text; composed predicates, sorting, and the private extent probe remain separate.

For delimited, JSON, and NDJSON sources, saved source filters stream decoded logical records before the source limit. Use column: "*" for a grep-style whole-record filter. Quoted multiline CSV fields remain part of one logical record. Source sorting is deliberately unavailable for file adapters that cannot provide it without unbounded materialization; use view.sort for a local sort over the bounded result instead.

Relational saved column keys use the SQLite source name when it is unique. Duplicate result names are deterministic occurrence keys such as name#1 and name#2; an unsuffixed duplicate is rejected as ambiguous.

Structured column configuration should use exact, case-sensitive canonical JSON Pointers such as /_source/user/email; keyed-object member names use @key, regardless of whether its display label is name or _key. An unambiguous compact display label is accepted as a fallback. A column can set label without changing its canonical identity or raw data. View-level and per-column nulls: first|last control direction-independent sort placement, with the column policy winning over the view policy and last as the built-in default.

Large Files and Schema Discovery

Large seekable inputs are opened with incremental logical-row indexing. CSV offsets come from the CSV parser, so quoted multiline records remain one row. Navigation requests additional bounded ranges; viewport rendering, current-cell popups, and yanks do not require a full-table clone.

JSON schema discovery examines up to 100 MiB of selected logical-row payload by default and finishes the row crossing that boundary. A schema stopped at the bound is provisional: newly encountered canonical paths append on the right, earlier rows receive nulls, and existing labels and order remain fixed. Use --schema-scan full when every column and inferred source type must be known before the first data frame.

Exact sorting, filtering, maximum-width calculation, full auto-range profiling, and similar whole-dataset operations may need to index or materialize the selected table. Their cost grows with the complete source even when initial opening was bounded. Stdin and encodings that cannot safely use byte offsets use materialized storage.

Columns can also define ordered conditional color rules. The first matching rule wins, and colors affect only cell styling; raw values, formatted values, sorting, filtering, search, copy, and popups are unchanged.

view:
  columns:
    active:
      type: boolean
      colors:
        - match:
            true: green
            false: muted
    prirep:
      type: string
      colors:
        - match:
            p: darkgreen
            r: blue
    used_percent:
      type: number
      colors:
        - range:
            "<10": red
            ">=90": red
        - gradient:
            mode: auto
            steps: 8
            colors: [green, yellow]
    latency_ms:
      type: number
      colors:
        - gradient:
            mode: fixed
            stops:
              0: green
              100: yellow
              500: red
    ip_address:
      type: ip
      colors:
        - identifiers:
            colors: auto
    host:
      type: string
      colors:
        - identifiers:
            colors: [cyan, "palette(198)", "#25A39AFF"]

The identifiers rule is for string-like discrete values. It assigns each unique rendered value in the column, such as an IP address or host name, to a stable generated color. colors: auto uses the active theme's [identifiers].colors families; a view can override those families with a color array. Each family generates 16 dark-to-light shades, and identifiers cycle across families before advancing shades. The darkest generated shade is kept at the ANSI dark/dim foreground equivalent for that family rather than near-black.

Press v to inspect the current generated YAML. Press s to save it to the loaded view file, or to a placeholder file named from the current input with only the last extension replaced by .yml. Existing files ask for y/n confirmation. Saves are atomic and create the views directory as needed.

The schema for editor validation is shipped at schemas/view.schema.json. See sample/cat-shards.view.yml for a complete example.

Development

cargo test --all-features
cargo clippy --all-targets --all-features -- -D warnings

Keybindings

Key Action
F1, ? Show keybindings.
Cursor keys, h, j, k, l Move the highlighted cell, scrolling if required.
q, Q Quit.
Home, ^, Ctrl-a Move to the start of this row.
End, $, Ctrl-e Move to the end of this row.
[num]| Go to column num, or the first column when num is omitted.
PgUp, PgDn, J, K Move a page up or down.
H, L Move a page left or right.
g Go to the top of the current column.
[num]G Go to row num, or the bottom of the current column when num is omitted.
Ctrl-g Show file and data information.
Insert, m Mark the current cell.
Delete, ' Return to the marked cell, if any.
Enter View full cell contents in a popup.
/ Search.
i Edit the current column view configuration, sort state, and filter action.
u Edit staged source filters, native sort, and source limit.
V Show source-independent view configuration.
p Show and copy the active source SQL when available.
f, F Filter in or filter out rows by the current column. Tab cycles text, regex, and numeric modes; submitting an empty condition clears filters for the current column.
n Go to the next search result.
N Go to the previous search result.
t Toggle fixed header row.
<, > Decrease or increase all column widths.
,, . Decrease or increase the current column width.
-, + Decrease or increase the column gap.
s, S Sort the current column lexically, ascending or descending.
a, A Sort the current column naturally, ascending or descending.
#, @ Sort the current column numerically, ascending or descending.
r Reload file or input data and reset sort order.
y Yank the rendered current cell to the clipboard when clipboard support is enabled.
Y Yank the raw current cell to the clipboard when clipboard support is enabled.
v Show the saved view modal when saved views are enabled.
[num]z Toggle variable column width mode between mode and max, or set all columns to width num.
[num]Z Maximize the current column, or set the current column to width num.
[num]chh, [num]chl Hide visible columns to the left or right of the current column.
chj, chk Hide the current column.
[num]cHh, [num]cHl Show adjacent hidden columns to the left or right.
csk, csj, csx Sort the current column ascending, sort descending, or clear its sort key.
[num][ Skip to the previous row value change.
[num]] Skip to the next row value change.
[num]{ Skip to the previous column value change.
[num]} Skip to the next column value change.

About

Rust rataui command line CSV and tabular data viewer

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages