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:
- Matus Gura matus.gura@gmail.com
- Nathan Typanski ntypanski@gmail.com
- Sébastien Celles s.celles@gmail.com
- Yuri D'Elia wavexx@thregr.org
The highlighted position is shown in the top-left corner of the screen. The contents of that cell are shown next to it.
- Rust command-line application distributed as the
tabviewbinary. - 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 as12G. - Persistent header row toggling.
- Lexical, natural, and numeric sorting by the current column.
- Dynamic column width and gap adjustment.
- Full-text incremental search with
nandNresult 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
F1or?. - Example screenshots in the
screenshots/directory.
- Rust toolchain for installation with Cargo.
- Optional clipboard support can be enabled with the
clipboardCargo feature. - Saved views and SQLite support are enabled by default through the
saved-viewsandsqliteCargo features. - Elasticsearch support is opt-in through the
elasticsearchCargo feature. - Build with
--no-default-featuresto omit both optional capabilities, or enable either one explicitly (for example,--no-default-features --features saved-viewsbuilds 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.
Install the latest published release:
cargo install tabviewInstall from a local checkout:
cargo install --path .Build with clipboard support:
cargo install tabview --features clipboard
cargo install tabview --features elasticsearchFrom 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.txtRead 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.txtRuntime 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.
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.
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 -
silentThe Rust rewrite supports the tabview CLI only. The former Python import API
(import tabview and tabview.view(...)) is not part of the supported surface.
Tabview loads theme settings from $XDG_CONFIG_HOME/tabview/config.yml, or
~/.config/tabview/config.yml when XDG_CONFIG_HOME is unset:
theme: cmdzroTheme 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_blueNamed 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.
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: RepositoryColumn 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 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.
cargo test --all-features
cargo clippy --all-targets --all-features -- -D warnings| 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. |