Skip to main content

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.

Learning Focus

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
  • e to edit the option value
  • Changes apply immediately — no restart needed
  • Ctrl+S to 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 d to delete a row
  • Press U to undo — row comes back
  • Press R to 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:

CommandAction
Space ff EnterFilter-friendly: <col> <op> <value> (natural language)
Space fk EnterFilter rows by keyword (comma-separated, OR across columns)
Space fx EnterFilter rows by Python expression
Space fclear EnterRestore 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
ConditionExample
isstatus is pending,completed
is notstatus is not shipped
containsemail contains gmail
starts withname starts with A
ends withemail 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:

CommandAction
Space rn EnterAdd 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:

ColumnValueExample
noSequential number (max + 1)19
pids- + sequential (zero-padded)s-19
c_dateCreation datetime YYYY-MM-DD-HH-MM2026-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:

CommandAction
Space backfill-cdate EnterFill empty c_date with current YYYY-MM-DD-HH-MM

Smart Date Insert

Insert a date value into the current cell:

CommandAction
Space today EnterInsert today's date (today, tomorrow, yesterday, +N, -N)
Space t EnterShortcut — 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

# ~/.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

ProblemCauseFix
.visidatarc not loadedFile has syntax errorRun python3 ~/.visidatarc to check
Option not persistingSet via Options Sheet but not savedPress Ctrl+S inside Options Sheet
Custom key conflictsKey already bound to somethingCheck with z Ctrl+H (command sheet)
Aggregator not appearingFunction not registeredVerify 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

What's Next