Skip to content

About

Browse, filter, and preview Airtable records from Neovim.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

85 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Airtable_idbbncOsuL_1

airtable.nvim

Browse, filter, and preview Airtable records from Neovim.

Define one or more pickers (named views with their own filters, sort, and result layout), open them with a single command or keymap, and view full records as markdown. Includes a live preview, comment browsing, quick actions to open or copy a record's Airtable URL, and optional safe editing of specific fields you explicitly configure.

Requirements

Installation

vim.pack (built-in, Neovim ≥ 0.12)
vim.pack.add({
  'https://github.com/nvim-lua/plenary.nvim',
  'https://github.com/folke/snacks.nvim',
  'https://github.com/maxdlr/honcho.nvim',
  'https://github.com/maxdlr/airtable.nvim',
})
lazy.nvim
{
  'you/airtable.nvim',
  dependencies = { 'nvim-lua/plenary.nvim', 'folke/snacks.nvim', 'maxdlr/honcho.nvim' },
  opts = { --[[ see Configuration below ]] },
}
packer.nvim
use({
  'you/airtable.nvim',
  requires = { 'nvim-lua/plenary.nvim', 'folke/snacks.nvim', 'maxdlr/honcho.nvim' },
  config = function() require('airtable').setup({ --[[ see Configuration below ]] }) end,
})

Authentication

Your Airtable personal access token is never stored in config, it's read from an environment variable at runtime.

# ~/.zshrc, ~/.bashrc, etc. — never commit this value.
export AIRTABLE_TOKEN="pat..."

Generate a token at airtable.com/create/tokens with the data.records:read scope, and grant it access to your base. If you configure buffer.editable (see below), the token also needs data.records:write.

Configuration

Default config (click to expand)
require('airtable').setup({
  token_env = 'AIRTABLE_TOKEN',
  base_id = '',
  table_name = '',
  page_size = 20,
  default_filter = 'All records',

  buffer = {
    fields = {
      { key = 'title', field = 'Title' },
      { key = 'status', field = 'Status' },
      { key = 'description', field = 'Description' },
    },
  },

  pickers = {
    {
      name = 'All records',
      result_line = {
        { field = 'Status' },
        { field = 'Title' },
        { field = 'Description' },
      },
    },
  },
})

Minimum config

require('airtable').setup({
  token_env = 'AIRTABLE_TOKEN', -- name of the env var holding your token (not the token itself)
  base_id = 'appXXXXXXXXXXXXXX',
  table_name = 'Tickets',
})

If you skip pickers entirely, :Airtable lists every record in table_name — no filters required to get started.

Every field name above must match your Airtable base's actual column names exactly (case-sensitive) — there's no universal default, since this varies by base.

Recommended config

require('airtable').setup({
  token_env = 'AIRTABLE_TOKEN',
  base_id = 'appXXXXXXXXXXXXXX',                      -- your Airtable base id
  table_name = 'Tickets',                             -- exact table name (or its "tbl..." id) in that base
  page_size = 20,                                     -- records fetched per API page (Airtable max: 100)
  default_filter = 'Assigned to me',                  -- picker opened by `:Airtable` with no argument

  buffer = {
    fields = {                                        -- rendered in this exact order when a record is opened
      { key = 'title', field = 'Title' },             -- key="title" is special: the H1 heading
      { key = 'status', field = 'Status' },           -- other keys become their own "## <Key>" section
      { key = 'description', field = 'Description' },
    },

    -- Optional, off by default. Adds an "Edit <field>" action to the record view's
    -- <CR> menu. This is a WRITE operation — see "Editing fields" below.
    editable = {
      { field = 'Status', type = 'select' },                       -- opens a picker of the field's choices
      { field = 'PR link', type = 'text', name = 'Edit PR link' }, -- opens a small editable buffer
    },
  },

  pickers = {                                           -- one or more named views, switch between them by name
    {
      name = 'Assigned to me',                          -- shown in the picker title; used to select this view
      filters = {                                       -- conditions combined with AND (omit to list everything)
        { field = 'Assignee', value = 'Your Name' },    -- matches even array-shaped fields
        { field = 'Type', value = 'Bug', only = true }, -- only=true: exact match instead
        { field = 'Assignee', value = 'you@co.com', by = 'email' }, -- exact collaborator email match
      },
      sort = { field = 'Priority', order = 'asc' },     -- optional; order: 'asc' or 'desc'
      result_line = {                                   -- columns shown per row, left to right
        { field = 'Title' },                            -- hl omitted: defaults to an identifier color
        { field = 'Status', hl = '#FFA500' },           -- hl: a highlight group or hex color
      },
    },
  },
})

Record styling

Opening a record (and its picker preview) styles each buffer.fields entry based on its key name, no configuration needed:

  • Keys like status, assignee, reviewer, type, priority, or label render as a colored pill — the color is derived deterministically from the field's value, so the same value always gets the same color.
  • Keys like name, title, or id render with heading-style emphasis.
  • Everything else renders as plain text with a left border bar.

Colors adapt to your colorscheme and never break the render — if a color can't be resolved for any reason, the plugin falls back to plain, unstyled text.

Advanced settings (optional, click to expand)

buffer.style.section_border_character: left border glyph

Plain-style sections (heading + body) get a left border bar down the whole section. Defaults to "▌"; override it with any single character. Sections listed in buffer.editable get a distinct border color (default: orange, "#FFA500"), override with editable_section_border_color:

buffer = {
  fields = { --[[ ... ]] },
  editable = { --[[ ... ]] },
  style = {
    section_border_character = '|',
    editable_section_border_color = '#D1FF1B',
  },
},

result_line_prefix: conditional icons

Add a leading icon to a picker's result line based on a record's field value — useful for status/priority/assignee-style visual markers. Evaluated locally against already-fetched data (no extra requests). The first matching condition wins; no match means no icon.

pickers = {
  {
    name = 'Tickets',
    result_line = {
      { field = 'Title' },
      { field = 'Status' },
    },
    result_line_prefix = {
      -- condition uses the same shape as `filters`
      { '📝', { field = 'Status', value = 'In progress' } },
      { '▶️', { field = 'Status', value = 'To do' } },
      { '✅', { field = 'Status', value = 'Done', only = true } },
      -- icon can also be a table to color it: { icon = '...', color = <hl group or hex> }
      { { icon = '󰲶', color = '#FFFFFF' }, { field = 'Status', value = 'Blocked' } },
    },
  },
},

result_line[].hl: conditional per-value colors

Instead of one fixed color per section, hl can be a list of { value, color } rules:

result_line = {
  {
    field = 'Status',
    hl = {
      { value = 'To do', color = '#E32424' },
      { value = 'In progress', color = '#FFBF5E' },
      { value = 'Done', color = '#5F94E3' },
    },
  },
  { field = 'Title' },
},

date_format

Add date_format explicitly to a buffer.fields or result_line entry to override the auto-detected default:

Value Result
date_format = 'datetime' (default) 01/01/2026 - 21:09
date_format = 'date' (default) 01/01/2026
date_format = 'time' (default) 21:09
buffer = {
  fields = {
    { key = 'title', field = 'Title' },
    { key = 'created_at', field = 'Created At', date_format = 'datetime' },
  },
},
pickers = {
  {
    name = 'Tickets',
    result_line = {
      { field = 'Title' },
      { field = 'Created At', date_format = 'time' }, -- override: show only "21:09"
    },
  },
},
date_formats: customizing the templates

Override the display template for each mode globally. Placeholders: {DD}, {MM}, {YYYY}, {HH}, {mm}.

require('airtable').setup({
  -- ...
  date_formats = {
    datetime = '{YYYY}-{MM}-{DD} {HH}h{mm}', -- default: '{DD}/{MM}/{YYYY} - {HH}:{mm}'
    date = '{YYYY}-{MM}-{DD}',               -- default: '{DD}/{MM}/{YYYY}'
    time = '{HH}h{mm}',                      -- default: '{HH}:{mm}'
  },
})

Usage

:Airtable                  " open the default picker
:Airtable Open bugs        " open a specific picker by name
:AirtableResume             " reopen the last viewed record, or the default picker if none

Search

Typing in the picker fuzzy-matches the visible result line by default (title, status, whatever result_line shows). Prefix your query with -- to instead search the full content of every buffer.fields value — description, notes, anything not shown in the row — useful for finding a record by pasting a snippet, a feature flag name, or any text that only appears in a long field. Entirely local (no extra API calls): it searches the records already fetched for the current picker.

feature-flag-xyz         " matches only if it's in the visible row
--feature-flag-xyz       " matches anywhere in the record's configured fields

Pressing <CR> on a record opens an action menu: open in buffer, open in browser, copy URL, and an "Edit <field>" action for every buffer.editable entry (same choices/text flow as the record buffer below). Every action keeps the picker open except opening in a buffer.

Inside a record buffer, press <CR> for quick actions: open in browser, browse comments, copy the record's URL, refresh the buffer, or edit a field (if buffer.editable is configured). With the cursor on a URL, o opens it and c copies it. With the cursor on a section, e edits that field directly, same as the <CR> menu's "Edit" action. If the section isn't listed in buffer.editable, it toasts instead of editing.

Editing fields

This writes to Airtable. Only fields you explicitly list in buffer.editable can ever be edited.

  • type = 'select' — opens a picker listing the field's valid choices. Pressing <CR> on a choice saves it immediately.
  • type = 'text' — opens a large centered floating buffer prefilled with the field's current value. <C-CR> saves, q discards.

After a successful edit, the record buffer refreshes in place.

Keymaps

vim.keymap.set('n', '<leader>aa', function() require('airtable').open() end, { desc = 'Airtable' })
vim.keymap.set('n', '<leader>ab', function() require('airtable').open('Open bugs') end, { desc = 'Airtable: open bugs' })
vim.keymap.set('n', '<leader>ar', function() require('airtable').resume() end, { desc = 'Airtable: resume' })

Scope

  • Read-only by default. The only write operation is editing a field you've explicitly listed in buffer.editable.
  • No local caching — every command re-fetches from the API.

About

Browse, filter, and preview Airtable records from Neovim.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages