VisiData Configuration Basics
VisiData loads ~/.visidatarc at startup as a Python file. Any valid Python code can appear here — from simple options.key = value settings to custom aggregator definitions and key bindings.
Think of .visidatarc as your personal VisiData profile: it runs every time you open vd, so anything you put there becomes your permanent default behavior.
Start with the Options Sheet (Shift+O) to discover options visually, then migrate the ones you use most into ~/.visidatarc for persistence.
The Options Sheet (Shift+O)
Before editing any file, explore options interactively inside VisiData:
Shift+O # open Options Sheet (global — all sheets)
zO # open Options Sheet (sheet-specific — current sheet only)
gO # open ~/.visidatarc as a text sheet
Inside the Options Sheet:
- Arrow keys to navigate
eto edit the option value- Changes apply immediately — no restart needed
Ctrl+Sto save the current value to~/.visidatarc
Change an Option Live
vd ~/github/practice-folder/visidata/01-loading/01-employees.csv
# Press Shift+O → Options Sheet opens
# Press / → search for: undo → press Enter
# Press e on the 'undo' row → change False to True → press Enter
# Press q → return to sheet
# Now undo (U) is active for this session
To make it permanent:
# Still in Options Sheet:
# Press Ctrl+S → option saves to ~/.visidatarc
Setting Options in .visidatarc
Create or edit ~/.visidatarc:
# ~/.visidatarc
# Enable undo/redo (highly recommended — off by default)
options.undo = True
# Confirm before quitting a modified sheet
options.quitguard = True
# Default column width (0 = auto)
options.default_width = 20
# Date format for @ (date) type columns
options.disp_date_fmt = '%Y-%m-%d'
# Default save format
options.save_filetype = 'csv'
# Skip N rows at the top of files (global default)
options.skip = 0
# Header row count
options.header = 1
# CSV delimiter default
options.csv_delimiter = ','
# Encoding default
options.encoding = 'utf-8-sig'
# Show null values as this symbol
options.disp_note_none = '⌀'
Enable Undo (Most Important Setting)
By default, undo is off. This means gd (bulk delete) cannot be undone if you make a mistake.
Add this to ~/.visidatarc:
options.undo = True
Now:
- Press
dto delete a row - Press
Uto undo — row comes back - Press
Rto redo
Change Date Display Format
If your dates are in DD/MM/YYYY format but VisiData shows them as YYYY-MM-DD:
# ~/.visidatarc
options.disp_date_fmt = '%d/%m/%Y'
Before:
hire_date
2020-03-15
2019-11-01
After:
hire_date
15/03/2020
01/11/2019
Custom Key Bindings
You can define entirely new commands with addCommand or bind keys with bindkey:
# ~/.visidatarc
# Bind F5 to open frequency table
bindkey('KEY_F(5)', 'freq-col')
# Bind F6 to open describe sheet
bindkey('KEY_F(6)', 'describe')
# Bind Ctrl+R to reload current sheet
bindkey('^R', 'reload-sheet')
Find command longnames with z Ctrl+H inside VisiData (shows all commands for current sheet with longnames).
Example: In-Place Filter Commands
The config on this server defines in-place filter commands. They are registered as longname commands — run them with Space:
| Command | Action |
|---|---|
Space ff Enter | Filter-friendly: <col> <op> <value> (natural language) |
Space fk Enter | Filter rows by keyword (comma-separated, OR across columns) |
Space fx Enter | Filter rows by Python expression |
Space fclear Enter | Restore all rows (preserves any added rows) |
Unlike z| which only marks rows (requires " to open a new sheet), these commands hide non-matching rows directly in the current sheet. A master list (_all_rows) tracks all rows so new rows added during a filter survive fclear — the addRows method is monkey-patched to append to both the visible view and the master list.
Filter commands (35-filter.py, 36-filter-friendly.py under common-config):
# Monkey-patch addRows so new rows go to _all_rows (master list)
_addRows_original = Sheet.addRows
def _addRows_wrapper(sheet, rows, index=None):
if hasattr(sheet, "_all_rows"):
if index is None:
sheet._all_rows.extend(rows)
else:
for i, row in enumerate(rows):
sheet._all_rows.insert(index + i, row)
return _addRows_original(sheet, rows, index=index)
Sheet.addRows = _addRows_wrapper
# Filter commands filter against _all_rows
def csv_filter_friendly(sheet, col, op, values):
# Builds a Python expression from natural language syntax
# e.g. "status is pending,completed"
...
def csv_filter_keyword(sheet):
...
sheet.rows[:] = [row for row in sheet._all_rows if ...]
sheet.recalc()
def csv_filter_expr(sheet):
...
sheet.rows[:] = [row for row in sheet._all_rows if ...]
sheet.recalc()
# fclear restores from _all_rows (all original + any added rows)
def csv_filter_clear(sheet):
sheet.rows[:] = sheet._all_rows
delattr(sheet, "_all_rows")
sheet.recalc()
ff — Filter-Friendly (natural language):
Type Space ff Enter then enter: <column> <condition> <value> [and/or <value> ...]
Space ff Enter # prompt: filter friendly: status is pending,completed
Space ff Enter # prompt: filter friendly: name contains john
Space ff Enter # prompt: filter friendly: email ends with gmail.com
Space fclear Enter # restore all rows
| Condition | Example |
|---|---|
is | status is pending,completed |
is not | status is not shipped |
contains | email contains gmail |
starts with | name starts with A |
ends with | email ends with .com |
Usage:
vd shortcuts.csv
Space # open command palette
# Type: ff
# Enter: status is pending,completed
# → only rows where status = "pending" OR "completed"
# Add a new row
a
# → no, pid, c_date auto-filled
Space
# Type: fclear
# Enter
# → all rows restored, including the new one you added
Example: Row Numbering Command
The common-config VisiData setup also defines a custom row-numbering command named rn. Run it from the command palette:
| Command | Action |
|---|---|
Space rn Enter | Add a leftmost no column with 1, 2, 3... row numbers |
Use this when you want a predictable row-number column at the far left without manually adding a blank column, renaming it, and filling it.
Implementation (from 50-row-numbering.py under common-config):
from visidata import SettableColumn, Sheet, vd
@Sheet.api
def add_no_sequence_column(sheet):
col = SettableColumn('no', type=int)
sheet.addColumn(col, index=0)
for rownum, row in enumerate(sheet.rows, start=1):
col.setValue(row, rownum)
sheet.cursorVisibleColIndex = 0
vd.status(f"added '{col.name}' column with sequence numbering")
Sheet.addCommand(
None,
'rn',
'add_no_sequence_column()',
'add leftmost no column with sequence row numbering',
)
Usage:
Space rn Enter # creates no as the first column, numbered 1..N
This is different from VisiData's built-in i: i adds an incremental column near the cursor, while rn always inserts the row-number column at the far left and names it no automatically.
Auto-Fill on Add Row
When you press a to add a new row, the common-config scripts auto-populate these columns:
| Column | Value | Example |
|---|---|---|
no | Sequential number (max + 1) | 19 |
pid | s- + sequential (zero-padded) | s-19 |
c_date | Creation datetime YYYY-MM-DD-HH-MM | 2026-07-02-14-35 |
The sequential scan reads from _all_rows (the full master list, not the filtered view), so it always finds the correct next number even when a filter is active.
Implementation (from 38-auto-cdate.py under common-config):
def _next_no(sheet):
src = getattr(sheet, '_all_rows', sheet.rows)
col = sheet.column('no')
vals = [int(col.getTypedValue(r)) for r in src if col.getTypedValue(r)]
return max(vals) + 1 if vals else 1
def _cdate_addRows(sheet, rows, index=None):
seq = _next_no(sheet)
now = datetime.now().strftime("%Y-%m-%d-%H-%M")
for row in rows:
if cdate_col: cdate_col.setValue(row, now)
if no_col and seq is not None:
no_col.setValue(row, seq)
pid_col.setValue(row, f"s-{seq:02d}")
seq += 1
return original_addRows(sheet, rows, index=index)
Backfill Empty c_date
Fill empty c_date cells on existing rows:
| Command | Action |
|---|---|
Space backfill-cdate Enter | Fill empty c_date with current YYYY-MM-DD-HH-MM |
Smart Date Insert
Insert a date value into the current cell:
| Command | Action |
|---|---|
Space today Enter | Insert today's date (today, tomorrow, yesterday, +N, -N) |
Space t Enter | Shortcut — insert today's date |
Custom Aggregators
Add your own aggregators (like median) that appear in + prompts:
# ~/.visidatarc
def median(values):
L = sorted(v for v in values if v is not None)
if not L:
return None
return L[len(L) // 2]
vd.aggregator('median', median, 'median value')
After adding this:
# Move to a numeric column
+
# Enter: median
# Frequency table now shows median per group
Recommended Starter Config
# ~/.visidatarc — VisiData starter config for sysadmins
# Core safety
options.undo = True
options.quitguard = True
# Saving
options.save_filetype = 'csv'
options.encoding = 'utf-8-sig'
# Date handling
options.disp_date_fmt = '%Y-%m-%d %H:%M:%S'
# Memory safety
options.min_memory_mb = 100
# Display
options.disp_sidebar = True
options.wrap = False
options.disp_note_none = '⌀'
# Custom aggregators
def median(values):
L = sorted(v for v in values if v is not None)
return L[len(L)//2] if L else None
vd.aggregator('median', median, 'median value')
# Custom key bindings
bindkey('KEY_F(5)', 'freq-col') # F5 → frequency table
bindkey('KEY_F(6)', 'describe') # F6 → describe sheet
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
.visidatarc not loaded | File has syntax error | Run python3 ~/.visidatarc to check |
| Option not persisting | Set via Options Sheet but not saved | Press Ctrl+S inside Options Sheet |
| Custom key conflicts | Key already bound to something | Check with z Ctrl+H (command sheet) |
| Aggregator not appearing | Function not registered | Verify vd.aggregator(...) call syntax |
Hands-On Practice
# Open your config file
nano ~/.visidatarc
# Add:
options.undo = True
options.quitguard = True
# Save and exit
# Verify in VisiData
vd ~/github/practice-folder/visidata/03-join/01-servers.csv
# Try: edit a cell (e), make a change, press U → should undo
# Try: gq → should prompt if sheet is modified