Skip to content

Lua API

This page documents what you can use inside ~/.config/tmux/tpane/*.lua. Use it as a reference when you want to move tmux options, key bindings, status bar config, plugins, pane helpers, or small bits of behavior into Lua.

A minimal setup looks like this:

text
# ~/.config/tmux/tmux.conf
run-shell -b 'tpane'
lua
-- ~/.config/tmux/tpane/init.lua
tpane.opt.mouse = true
tpane.bind("h", tpane.pane.select("left"))
tpane.statusline { right = { tpane.widgets.clock } }

Reload from inside tmux with:

sh
tpane reload

Check errors with:

sh
tpane status

Config files

tpane loads top-level Lua files from:

text
~/.config/tmux/tpane

Set TPANE_CONFIG_DIR to load config from somewhere else.

Files in subdirectories are not loaded automatically. Use require for shared modules:

lua
local colors = require("theme.colors") -- ~/.config/tmux/tpane/theme/colors.lua

Options

Use tpane.opt for normal tmux options:

lua
tpane.opt.mouse = true              -- set -g mouse on
tpane.opt.history_limit = 5000      -- set -g history-limit 5000
tpane.opt.mode_keys = "vi"          -- set -g mode-keys vi
tpane.opt.renumber_windows = true   -- set -g renumber-windows on
tpane.opt.escape_time = 0           -- set -g escape-time 0

Use tpane.append when you would use set -ga in tmux:

lua
tpane.append("update_environment", "TERM")
tpane.append("update_environment", "TERM_PROGRAM")

tpane.options is the table form of the same API. Use it when you want to set several related options together:

lua
tpane.options {
  status_style = { bg = "default" },
  pane_border_style = { fg = "#51576d" },
}

Nested tables are flattened into tmux option names, so this is equivalent:

lua
tpane.options {
  status = { style = { bg = "default" } },
  pane = { border = { style = { fg = "#51576d" } } },
}

Key bindings

tpane.bind connects a key to an action.

lua
tpane.bind("h", tpane.pane.select("left"))
tpane.bind("j", tpane.pane.select("down"))
tpane.bind("k", tpane.pane.select("up"))
tpane.bind("l", tpane.pane.select("right"))

Bindings use the tmux prefix by default. Pass prefix = false for root bindings:

lua
tpane.bind("M-Left", tpane.pane.resize("left", 10), { prefix = false })

Copy-mode bindings go in tmux's copy-mode key table. This example enters copy mode with <prefix> [ and then uses v, r, and y inside copy mode:

lua
tpane.bind("[", "copy-mode")
tpane.bind("v", tpane.copy.begin(), { mode = "copy" })
tpane.bind("r", tpane.copy.rectangle(), { mode = "copy" })
tpane.bind("y", tpane.copy.copy(), { mode = "copy" })

mode = "copy" maps to tmux's copy-mode-vi table. If you want another tmux key table, pass it directly:

lua
tpane.bind("y", tpane.copy.copy(), { table = "copy-mode" })

A binding can also run a Lua functions. The callback receives the pane that pressed the key:

lua
tpane.bind("L", function(pane)
  tpane.toggle(pane, "logs")
end)

Use raw tmux commands when there is no helper:

lua
tpane.bind("R", "source-file ~/.config/tmux/tmux.conf ; display 'reloaded'")

For multiple raw commands, use tpane.raw:

lua
tpane.bind("C-S-l", tpane.raw({
  "swap-window -t +1",
  "select-window -t +1",
}), { prefix = false })

Remove a binding with tpane.unbind:

lua
tpane.unbind("C-b")
tpane.unbind("v", { mode = "copy" })

Actions

Actions are values you pass to tpane.bind. They are helpers for common tmux commands.

Pane actions:

lua
-- Move between panes.
tpane.bind("h", tpane.pane.select("left"))
tpane.bind("j", tpane.pane.select("down"))
tpane.bind("k", tpane.pane.select("up"))
tpane.bind("l", tpane.pane.select("right"))

-- Resize panes without the prefix.
tpane.bind("M-Left", tpane.pane.resize("left", 10), { prefix = false })
tpane.bind("M-Right", tpane.pane.resize("right", 10), { prefix = false })

-- Split panes.
tpane.bind("%", tpane.pane.split("right", { cwd = "pane" }))
tpane.bind('"', tpane.pane.split("down", { cwd = "pane" }))

cwd = "pane" is tpane shorthand for tmux's -c "#{pane_current_path}". Use it with tpane.pane.split or tpane.window.new when the new pane/window should start in the current pane's directory.

Window actions:

lua
-- New window in the current pane's directory.
tpane.bind("c", tpane.window.new({ cwd = "pane" }))

-- Reorder the current window.
tpane.bind(">", tpane.window.swap("next"))
tpane.bind("<", tpane.window.swap("prev"))

Copy-mode actions:

lua
-- Enter copy mode with <prefix> [, then select/copy with vi-like keys.
tpane.bind("[", "copy-mode")
tpane.bind("v", tpane.copy.begin(), { mode = "copy" })
tpane.bind("r", tpane.copy.rectangle(), { mode = "copy" })
tpane.bind("y", tpane.copy.copy(), { mode = "copy" })

tpane.copy.begin({ rectangle = true }) starts selection and toggles rectangle mode in one binding:

lua
tpane.bind("C-v", tpane.copy.begin({ rectangle = true }), { mode = "copy" })

Send the prefix key to the pane:

lua
-- Optional. This makes <prefix> C-a send C-a to the program in the pane.
-- Useful for nested tmux sessions.
tpane.bind("C-a", tpane.key.prefix())

This is the Lua version of tmux's bind C-a send-prefix. You only need it if you want a way to pass the prefix key through to the pane.

Status bar

tpane.statusline defines what appears on the left and right side of the tmux status bar.

lua
tpane.statusline {
  position = "top",
  left = { tpane.widgets.session, tpane.widgets.tabs },
  right = { tpane.widgets.clock },
}

The values in left and right are widgets. A widget can be a built-in widget, a custom Lua widget, or a job-backed widget.

Built-in widgets

WidgetDescription
tpane.widgets.sessionCurrent tmux session.
tpane.widgets.hostHostname from tmux.
tpane.widgets.clockCurrent time, like 14:30.
tpane.widgets.dateCurrent date, like Jun 25.
tpane.widgets.prefixShows when the tmux prefix key is active.
tpane.widgets.tabstmux window list.
tpane.widgets.cpu(opts)CPU usage. Works on Linux and macOS.
tpane.widgets.memory(opts)Used memory. Works on Linux and macOS.
tpane.widgets.battery(opts)Battery status with icons. Works on Linux and macOS.
tpane.widgets.player(opts)Current playing track. Uses playerctl, Music or Spotify.

Plain widgets are used directly:

lua
tpane.statusline {
  left = { tpane.widgets.session, tpane.widgets.tabs },
  right = { tpane.widgets.clock },
}

Widgets that take opts start background work. Call them once, store the result, and use that result in the statusline:

lua
local cpu = tpane.widgets.cpu({ every = "2s" })
local memory = tpane.widgets.memory({ every = "5s" })
local battery = tpane.widgets.battery({ every = "30s" })

tpane.statusline {
  right = { cpu, memory, battery, tpane.widgets.clock },
}

Custom widgets

Use tpane.widget when you want to render your own widget. The function runs when tpane renders the status bar.

lua
local cwd = tpane.widget(function(ctx)
  return ctx.pane and ctx.pane.cwd_basename or ""
end)

tpane.statusline {
  left = { tpane.widgets.session, cwd },
  right = { tpane.widgets.clock },
}

The ctx argument contains the tmux state available while rendering:

FieldDescription
ctx.sessionCurrent session name.
ctx.windowCurrent window id, like @2.
ctx.paneCurrent pane object, or nil.
ctx.panesAll pane objects known to tpane.

A widget can return a string:

lua
local server = tpane.widget(function()
  return "server"
end)

To style the text, return a table with text plus style fields:

lua
local status = tpane.widget(function()
  return { text = "ok", fg = "green", bold = true }
end)

That renders ok in green and bold in the status bar. Return nil or an empty string to show nothing.

Style fields:

FieldValueDescription
textstringText to show.
fgstringForeground color, like "green" or "#8caaee".
bgstringBackground color.
boldbooleanBold text.
dimbooleanDim text.
italicsbooleanItalic text.
blinkbooleanBlinking text.
reversebooleanSwap foreground and background.
hiddenbooleanHidden text.
strikethroughbooleanStruck-through text.
underscoreboolean or stringUnderline. Strings are passed to tmux as underline style.
alignstringtmux alignment style value.
fillstringtmux fill style value.

Shell command widgets

Use tpane.job for status bar data that comes from a shell command. Jobs run in the background on their own interval, so the status bar does not block while the command is running.

lua
local uptime = tpane.job({
  every = "1m",
  timeout = "5s",
  cmd = "uptime",
})

tpane.statusline {
  right = { uptime },
}

every and timeout can be seconds or a string ending in s, m, or h. timeout defaults to 10s.

Multiline status bar

Use rows for a multiline status bar:

lua
tpane.statusline {
  position = "top",
  rows = {
    { left = { tpane.widgets.session }, right = { tpane.widgets.clock } },
    { left = { tpane.widgets.tabs }, right = { tpane.widgets.prefix } },
  },
}

Window tabs

Use tpane.tabline to configure tmux window tabs:

lua
tpane.tabline {
  label = "cwd", -- cwd, name, or a raw tmux format string
  prefix = "",
  suffix = "",
  inactive = { fg = "#777777" },
  current = { fg = "#8caaee", bold = true },
}

Plugins and themes

Use tpane.use to load a plugin from your Lua config:

lua
tpane.use("sensible")
tpane.use("vim-navigator")
tpane.use("yank")
tpane.use("themes")
tpane.use("pane-detection")

External plugins use the same function with a repo spec:

lua
tpane.use("theme", {
  repo = "https://github.com/example/tpane-theme.git",
  branch = "main",
})

See plugins.md for the built-in plugins, git plugin options, and plugin management commands.

Themes are applied with tpane.theme after loading the themes plugin:

lua
tpane.use("themes")
tpane.theme("Catppuccin Mocha")
tpane.theme("Gruvbox Dark", { transparent = true })

List bundled themes with:

sh
tpane themes

Reusable panes

Reusable panes are named panes that tpane can show, hide, and restore. Use them for logs, REPLs, shells, database consoles, or watchers that should keep running when hidden.

Register the pane once:

lua
tpane.register_pane("logs", {
  side = "bottom",
  size = "25%",
  command = "tail -f logs/app.log",
})

Control it from a key binding:

lua
tpane.bind("L", function(pane)
  tpane.toggle(pane, "logs")
end)

tpane.bind("M-L", function(pane)
  tpane.expand(pane, "logs")
end, { prefix = false })

toggle shows or hides it. Hidden panes are stashed, so the process keeps running. expand shows it and zooms the layout around it.

Options:

lua
tag = "logs"                 -- defaults to registered name
name = "logs"                -- stash name, defaults to registered name
side = "bottom"              -- bottom | top | right | left
size = "25%"
full = true                  -- split across the full window
anchor = { tag = "editor" }  -- optional target pane for split/unstash
command = "tail -f app.log"
title = "logs"
label = "logs"
blocked_message = "..."      -- shown instead of hiding a blocked pane

Use tpane.split for a one-off split instead of a registered pane:

lua
local pane = tpane.split(current, {
  side = "bottom",
  size = "25%",
  command = "zsh",
})

Pane objects

A pane object is how tpane represents one tmux pane in Lua. It gives you the pane id, current directory, command, window, state, and helper methods.

tpane passes pane objects to callbacks that work with a specific pane:

lua
-- Key handler: pane is the pane that pressed the key.
tpane.bind("L", function(pane)
  tpane.toggle(pane, "logs")
end)

-- Widget: ctx.pane is the pane being used to render the status bar.
local cwd = tpane.widget(function(ctx)
  return ctx.pane and ctx.pane.cwd_basename or ""
end)

-- Kind detection: pane is each pane tpane is scanning.
tpane.kind {
  name = "editor",
  detect = function(pane)
    return pane:running("nvim")
  end,
}

Pane fields

FieldDescription
pane.idtmux pane id, like %3. Use this when calling low-level tmux helpers.
pane.pidRoot process pid for the pane.
pane.cwdCurrent directory of the pane.
pane.cwd_basenameLast path component of pane.cwd.
pane.commandtmux pane_current_command, usually the foreground command name.
pane.sessiontmux session name.
pane.windowtmux window id, like @2.
pane.activetrue when the pane is focused.
pane.zoomedtrue when the pane's window is zoomed.
pane.kindKind detected by tpane.kind, if any.
pane.labelLabel shown by tpane for this pane, if any.
pane.tagCustom tag stored on the pane. Useful for finding panes later.
pane.homeHome window for a stashed pane.
pane.stateCurrent pane state, if one was detected or set.

Pane methods

MethodDescription
pane:running(name)Returns true if the pane's process tree contains a command with that name.
pane:var(name)Reads a tmux pane variable, such as @my_var or @tpane_tag.
pane:set(values)Sets tpane pane metadata: kind, label, state, tag, or home.
pane:capture()Returns the pane's visible text, like tmux capture-pane.
pane:proc_tree()Returns the pane's process tree. Use it for custom process checks.

Examples:

lua
-- Label panes running psql.
tpane.kind {
  name = "database",
  detect = function(pane)
    return pane:running("psql")
  end,
}

-- Show the current directory in the status bar.
local cwd = tpane.widget(function(ctx)
  return ctx.pane and ctx.pane.cwd_basename or ""
end)

-- Mark the current pane so it can be found later.
tpane.bind("T", function(pane)
  pane:set { tag = "terminal", label = "terminal" }
end)

-- Jump back to the marked pane.
tpane.bind("G", function()
  local terminal = tpane.find { tag = "terminal" }
  if terminal then
    tpane.tmux.select(terminal.id)
  end
end)

Use pane:capture() to read the visible text in a pane. This is useful when the process name is not enough and the output tells you what is happening:

lua
-- Mark a worker pane as blocked/working by reading its output.
tpane.kind {
  name = "worker",
  match = "worker",
  state = function(pane)
    local text = pane:capture()
    if text:match("blocked") then return "blocked" end
    if text:match("running") then return "working" end
  end,
}

Use pane:proc_tree() to inspect all processes running under the pane. This is useful for commands hidden behind shells, scripts, package managers, etc:

lua
-- Detect a test watcher even if the pane command is just `zsh` or `npm`.
tpane.kind {
  name = "test watcher",
  detect = function(pane)
    return pane:proc_tree():any(function(proc)
      return proc.argv:match("--watch") ~= nil
    end)
  end,
}

A process has:

FieldDescription
proc.pidProcess id.
proc.ppidParent process id.
proc.argvFull command line.

Use tpane.find when you want to do something with a pane you tagged or identified earlier:

lua
tpane.bind("G", function()
  local logs = tpane.find { tag = "logs" }
  if logs then
    tpane.tmux.select(logs.id)
  end
end)

tpane.find returns the first match. tpane.find_all returns every match. All fields in the query must match.

Kinds and states

Use kinds when you want tpane to recognize what a pane is and reuse that information elsewhere.

For example let's say you want to label a panel that if it's running psql, it should be labeled as database:

lua
tpane.kind { name = "database", match = "psql" }

Now you can act on that pane without remembering its tmux id:

lua
tpane.bind("D", function()
  local db = tpane.find { kind = "database" }
  if db then
    tpane.tmux.select(db.id)
  end
end)

You can also show the recognized pane type in the status bar:

lua
local pane_label = tpane.widget(function(ctx)
  return ctx.pane and ctx.pane.label or ""
end)

Matching panes

Use match for a simple command-name check:

lua
-- Any pane running nvim becomes an editor pane.
tpane.kind { name = "editor", match = "nvim" }

Use detect when the check needs more than a command name. It receives a pane and returns true when the pane should get that kind:

lua
-- A node process only counts as a server when it is in a server directory.
tpane.kind {
  name = "server",
  detect = function(pane)
    return pane:running("node") and pane.cwd:match("/server$") ~= nil
  end,
}

Labels

The label is the display text for a matched pane. By default it is the kind name. Use label when the display text should include pane-specific information:

lua
tpane.kind {
  name = "editor",
  match = "nvim",
  label = function(pane)
    return "editor " .. pane.cwd_basename
  end,
}

If Neovim is running in /home/me/project, the label is editor project.

States

A state is a short status attached to a pane. Use it when the pane can be working, blocked, waiting, done, etc.

lua
-- Read a worker pane's output and expose it as pane.state.
tpane.kind {
  name = "worker",
  match = "worker",
  state = function(pane)
    local text = pane:capture()
    if text:match("blocked") then return "blocked" end
    if text:match("running") then return "working" end
    return "idle"
  end,
}

Then use that state in a widget:

lua
local state = tpane.widget(function(ctx)
  return ctx.pane and ctx.pane.state or ""
end)

Built-in states:

StateMeaning
approvalWaiting for approval.
blockedBlocked or needs attention.
workingWork is in progress.
done_unseenFinished, but the pane has not been focused yet.
idle_seenFinished/idle and already seen.

If a state function returns done, tpane shows it as done_unseen until the pane is focused.

Add your own state style with tpane.state:

lua
tpane.state("waiting", { color = "yellow", glyph = "…" })

Store

tpane.store persists small Lua values across reloads.

It is shared by the whole tpane config for the current tmux server. It is not scoped per plugin, so choose keys that will not collide with other config or plugins.

lua
tpane.store.set("my-plugin.counter", 1)
tpane.store.set("layout.last-project", "tpane")

Avoid generic keys in plugins:

lua
tpane.store.set("counter", 1)
tpane.store.set("enabled", true)

Read and delete values with the same key:

lua
local count = tpane.store.get("my-plugin.counter")
tpane.store.delete("my-plugin.counter")

Values may be strings, numbers, booleans, tables, or nil.

Events

Use tpane.on to register callbacks for tmux changes. The callback is called when that event happens.

EventCallback argumentWhen it fires
ticknoneOn each tpane scan.
pane:newpane objectWhen tpane sees a new pane.
pane:focuspane objectWhen the active pane changes.
window:closewindow id, like @2When a window disappears.
state:changepane id, like %3When a pane state changes.

Example: show a tmux message when focusing a pane recognized as a database:

lua
tpane.on("pane:focus", function(pane)
  if pane.kind == "database" then
    tpane.tmux.display { target = pane.id, message = "database pane" }
  end
end)

Example: keep a counter of how many panes were created in this tmux server:

lua
tpane.on("pane:new", function()
  local count = tpane.store.get("stats.panes-created") or 0
  tpane.store.set("stats.panes-created", count + 1)
end)

Example: react when a worker pane changes state:

lua
tpane.on("state:change", function(pane_id)
  local pane = tpane.pane(pane_id)
  if pane.state == "blocked" then
    tpane.tmux.display { target = pane.id, message = "worker is blocked" }
  end
end)

Keep event callbacks short. If you need to run a shell command on a timer, use tpane.job instead of tick.

Workspaces

A workspace is a named tmux layout. Register it in Lua, then apply it from a key binding or command.

lua
tpane.workspace {
  name = "dev",
  windows = {
    { name = "app", command = "zsh" },
    {
      name = "logs",
      panes = {
        { side = "bottom", size = "30%", command = "tail -f app.log" },
      },
    },
  },
}

tpane.bind("D", function()
  tpane.apply_workspace("dev")
end)

Format helpers

Use tpane.fmt for tmux conditionals that do not have Lua equivalents:

lua
tpane.fmt.prefix("", "")
tpane.fmt.when("window_zoomed_flag", "Z", "")

Low-level tmux helpers

Use these when the higher-level helpers are not enough:

lua
local window = tpane.tmux.new_window { name = "logs", cwd = pane.cwd, command = "zsh" }
tpane.tmux.select_window(window)
tpane.tmux.send_keys { target = pane.id, keys = "npm test", enter = true }
tpane.tmux.split { target = pane.id, dir = "below", size = "25%", cwd = pane.cwd }
tpane.tmux.stash { pane = pane.id, window = pane.window, cwd = pane.cwd, name = "hidden" }
tpane.tmux.unstash { pane = hidden.id, target = pane.id, horizontal = true, size = "35%" }
tpane.tmux.unzoom(pane.window)
tpane.tmux.select(pane.id)
tpane.tmux.zoom(pane.id)
tpane.tmux.display { target = pane.id, message = "message" }

Public API

APIPurpose
tpane.use(name_or_spec)Load a built-in or git plugin.
tpane.optSet tmux options with assignment, like tpane.opt.mouse = true.
tpane.append(name, value)Append to tmux options such as update-environment.
tpane.options(table)Set nested tmux options.
tpane.bind(key, action, opts)Bind a key.
tpane.unbind(key, opts)Remove a key binding.
tpane.raw(command)Build an action from raw tmux command text.
tpane.pane.*Pane actions.
tpane.window.*Window actions.
tpane.copy.*Copy-mode actions.
tpane.key.*Key helpers such as tpane.key.prefix().
tpane.widget(fn)Create a widget handle.
tpane.widgets.*Built-in widget handles and factories.
tpane.job(opts)Run shell-backed widget data in the background.
tpane.statusline(opts)Configure the tmux statusline.
tpane.theme(name_or_palette[, opts])Apply a theme from the themes plugin.
tpane.tabline(opts)Configure tmux window tabs.
tpane.register_pane(name, opts)Register a reusable pane definition.
tpane.split(target, opts)Split/open a reusable pane.
tpane.toggle(target, opts)Toggle a reusable pane.
tpane.show(target, opts)Show a reusable pane.
tpane.hide(target, opts)Hide a reusable pane.
tpane.expand(target, opts)Zoom or expand a pane.
tpane.workspace(def)Register a named layout.
tpane.apply_workspace(name)Apply a registered layout once.
tpane.panes()Return current pane objects.
tpane.find(query)Find one pane by fields.
tpane.find_all(query)Find all panes by fields.
tpane.kind(def)Register pane detection.
tpane.state(name, opts)Register state presentation.
tpane.on(event, fn)Register an event handler.
tpane.storePersistent Lua key-value store.
tpane.tmuxLow-level tmux helpers.
tpane.fmttmux format helpers.