Skip to content

Instantly share code, notes, and snippets.

@lamngockhuong
Last active September 15, 2026 15:13
Show Gist options
  • Select an option

  • Save lamngockhuong/095fc74e3dcb1b294aea5a080198519c to your computer and use it in GitHub Desktop.

Select an option

Save lamngockhuong/095fc74e3dcb1b294aea5a080198519c to your computer and use it in GitHub Desktop.
herdr-nvim-ctl — xem, tìm và dọn các tiến trình nvim ẩn mà plugin herdr-nvim giữ sau mỗi tab Herdr (hướng dẫn tiếng Việt)

herdr-nvim-ctl

🇬🇧 English · 🇻🇳 Tiếng Việt

A small CLI for inspecting and cleaning up the hidden nvim processes that the herdr-nvim plugin keeps alive behind every tab in Herdr.

The problem

herdr-nvim gives you a full-height Neovim sidebar, one key away. What makes it pleasant is also what confuses people: each tab keeps its own headless nvim process, and that process survives when you toggle the sidebar off. This is deliberate — buffers, cursor position and pending annotations are all still there when you toggle it back on.

The cost shows up a few hours into a session. ps returns a pile of nvim --headless --listen ... lines you don't remember starting. Some drag along vtsls, tsserver, biome and lazygit; a tab sitting on a TypeScript repo can reach 2 GB across its whole process tree. Worse, when you close a tab its nvim has nowhere to return to — it becomes an orphan and just holds memory.

This script answers three questions:

  • How many nvim processes are running, and which tab does each belong to as you see it on screen, not as an opaque id like wW:t2?
  • How do I get back to that tab and keep using it?
  • Which ones are orphans, and how do I clean them up safely?

Requirements

  • Herdr (the script must run inside a Herdr session, i.e. HERDR_ENV=1)
  • The chmarax.herdr-nvim plugin, installed
  • bash, jq, pgrep (Linux: procps; preinstalled on macOS)
  • watch is optional, only for the watch command. Without it (the macOS default) the script redraws the screen itself once a second.
  • Linux, WSL included, and macOS. On Linux it reads /proc/<pid>/cmdline and /proc/<pid>/status; macOS has no /proc, so it reads the same facts from ps.

Install

mkdir -p ~/.local/bin
curl -fsSL https://gist.githubusercontent.com/lamngockhuong/095fc74e3dcb1b294aea5a080198519c/raw/herdr-nvim-ctl \
  -o ~/.local/bin/herdr-nvim-ctl
chmod +x ~/.local/bin/herdr-nvim-ctl

Messages come out in English or Vietnamese depending on HERDR_NVIM_CTL_LANG; unset, it falls back to LC_ALL/LANG, then to English. Force one with export HERDR_NVIM_CTL_LANG=vi (or =en).

Check that ~/.local/bin is on your PATH:

case ":$PATH:" in *":$HOME/.local/bin:"*) echo "OK" ;; *) echo 'export PATH="$HOME/.local/bin:$PATH"  # add to ~/.zshrc or ~/.bashrc' ;; esac

Usage

List

herdr-nvim-ctl
PID      TAB       RAM     WORKSPACE                  TAB
264566   wV:t2     33MB    10. aidd-test              2. 2
268701   wW:t2     53MB    TAB CLOSED — run: herdr-nvim-ctl gc
271487   wW:t1     32MB    11. ~                      1. 1

The last two columns are what you actually see on Herdr's workspace and tab bars, so there is no id to decode. A row marked TAB CLOSED is an orphan: its tab is gone, it cannot be reopened, it can only be reaped.

Jump to the tab that owns a daemon

herdr-nvim-ctl focus wW:t2

Herdr switches to that workspace and tab. Once there, press prefix+i to bring the sidebar back with its previous state intact.

Reap orphans

herdr-nvim-ctl gc

This kills nothing on its own. It delegates to the plugin's own herdr-nvim daemon-gc, preserving the behavior its author designed: only daemons whose tab no longer exists are stopped, then their socket and state file are removed. Daemons belonging to open tabs are never touched.

The plugin already runs this opportunistically every time you press prefix+i, so you rarely need it by hand. Reach for it after closing a batch of tabs, when you want the memory back right away.

Stop a specific daemon

herdr-nvim-ctl kill wW:t2   # one tab
herdr-nvim-ctl kill all     # every daemon

It sends SIGTERM first so nvim can run its exit autocmds and write shada, waits up to 2 seconds, then escalates to SIGKILL only for processes that ignored it. Unsaved buffers are lost, so for a tab you still care about, focus it and quit nvim properly instead.

Watch live

herdr-nvim-ctl watch

Refreshes once a second. Run it in one pane and toggle the sidebar in another to watch the count change. Ctrl+C to quit.

Either spelling of a tab id works

Herdr calls a tab wR:t2, while its socket file is wR_t2.sock — filenames can't carry a colon. The two are easy to mix up, and plain herdr tab focus only accepts the colon form:

❯ herdr tab focus wR_t2
{"error":{"code":"tab_not_found","message":"tab wR_t2 not found"},"id":"cli:tab:focus"}

The script normalizes the argument, so focus and kill take both:

herdr-nvim-ctl focus wR_t2   # pasted from ls or ps
herdr-nvim-ctl focus wR:t2   # tab id

How it resolves the tab

Nothing is guessed from process names or ordering. The plugin always spawns nvim with:

nvim --headless --listen /run/user/<uid>/herdr-nvim/<workspace>_<tab>.sock ...

The script reads the process command line (/proc/<pid>/cmdline on Linux, ps on macOS), takes the socket filename, turns _ into : to get the tab id, then looks that up in herdr workspace list and herdr tab list for the displayed number and label. A tab id missing from those lists means the tab is closed — an orphaned daemon.

One caveat when scripting against this: ids like wW are stable, but workspace numbers shift whenever you create or close another workspace. Use ids in commands; use the numbers to know where to look on screen.

Safety

  • list, focus, gc and watch never stop a daemon belonging to an open tab.
  • kill is the only command that terminates processes, and only when you ask.
  • Process matching is pgrep -x nvim, an exact name match, so the Herdr server, agent panes and unrelated system processes are never in scope.
  • list and focus require HERDR_ENV=1 and refuse to run outside a Herdr session rather than acting on someone else's session.

What it depends on that could change

Herdr exposes no command that lists these daemons, so the script infers them from two internal conventions of the plugin:

  • nvim is spawned with --listen <dir>/<workspace>_<tab>.sock;
  • the socket filename replaces : with _, matching the plugin's own tab_key().

Both are implementation details, not a committed interface. If a future plugin release renames its sockets, the TAB column goes blank and kill finds nothing — patch socket_of_pid and tab_of_pid when that happens. gc is safe either way: it shells out to the plugin's own herdr-nvim daemon-gc.

License

MIT

herdr-nvim-ctl

🇻🇳 Tiếng Việt · 🇬🇧 English

Công cụ dòng lệnh để xem và dọn các tiến trình nvim ẩn mà plugin herdr-nvim giữ lại phía sau mỗi tab trong Herdr.

Vấn đề nó giải quyết

Plugin herdr-nvim cho bạn một sidebar Neovim bật tắt bằng một phím. Điểm hay của nó cũng là điểm khiến người dùng bối rối: mỗi tab giữ một tiến trình nvim headless riêng, và tiến trình đó sống tiếp sau khi bạn tắt sidebar. Đây là thiết kế có chủ đích, nhờ vậy buffer, vị trí con trỏ và các ghi chú đang chờ vẫn còn nguyên khi bạn bật lại.

Hệ quả là sau vài giờ làm việc, ps cho ra một loạt dòng nvim --headless --listen ... mà bạn không nhớ đã mở từ đâu. Vài trong số đó còn kéo theo cả vtsls, tsserver, biome, lazygit — một tab mở repo TypeScript lâu ngày có thể chiếm tới 2 GB. Tệ hơn, khi bạn đóng một tab, tiến trình nvim của nó không còn đường quay về: nó thành mồ côi và chỉ nằm đó ăn bộ nhớ.

Script này trả lời ba câu hỏi:

  • Đang có bao nhiêu nvim, mỗi cái thuộc tab nào trên giao diện (không phải ID nội bộ khó hiểu như wW:t2)?
  • Làm sao quay lại đúng tab đó để dùng tiếp?
  • Cái nào là mồ côi và dọn thế nào cho sạch?

Yêu cầu

  • Herdr (script phải chạy bên trong một phiên Herdr, tức HERDR_ENV=1)
  • Plugin chmarax.herdr-nvim đã cài
  • bash, jq, pgrep (Linux: gói procps; macOS có sẵn)
  • watch là tùy chọn, chỉ cho lệnh watch. Không có watch (mặc định trên macOS) thì script tự vẽ lại màn hình mỗi giây.
  • Linux (kể cả WSL) và macOS. Trên Linux script đọc /proc/<pid>/cmdline và /proc/<pid>/status; trên macOS không có /proc nên lấy cùng thông tin đó qua ps.

Cài đặt

mkdir -p ~/.local/bin
curl -fsSL https://gist.githubusercontent.com/lamngockhuong/095fc74e3dcb1b294aea5a080198519c/raw/herdr-nvim-ctl \
  -o ~/.local/bin/herdr-nvim-ctl
chmod +x ~/.local/bin/herdr-nvim-ctl

Script in thông báo tiếng Anh hoặc tiếng Việt tuỳ HERDR_NVIM_CTL_LANG; không đặt biến này thì nó nhìn LC_ALL/LANG, và mặc định là tiếng Anh. Muốn luôn tiếng Việt, thêm vào ~/.zshrc hoặc ~/.bashrc:

export HERDR_NVIM_CTL_LANG=vi

Kiểm tra ~/.local/bin đã nằm trong PATH:

case ":$PATH:" in *":$HOME/.local/bin:"*) echo "OK" ;; *) echo 'export PATH="$HOME/.local/bin:$PATH"  # thêm vào ~/.zshrc hoặc ~/.bashrc' ;; esac

Sử dụng

Liệt kê

herdr-nvim-ctl
PID      TAB       RAM     WORKSPACE                  TAB
264566   wV:t2     33MB    10. aidd-test              2. 2
268701   wW:t2     53MB    TAB ĐÃ ĐÓNG — chạy: herdr-nvim-ctl gc
271487   wW:t1     32MB    11. ~                      1. 1

Tổng: 3 tiến trình nvim.
Tới tab: herdr-nvim-ctl focus <TAB>   |   Hiện sidebar: prefix+i

Hai cột cuối là thứ bạn nhìn thấy trên thanh workspace và thanh tab của Herdr, nên không phải giải mã wV:t2 là tab nào nữa. Dòng TAB ĐÃ ĐÓNG là daemon mồ côi: tab của nó không còn, không mở lại được, chỉ có dọn.

Quay lại tab đang giữ một daemon

herdr-nvim-ctl focus wW:t2

Herdr chuyển sang đúng workspace và tab đó. Tới nơi, nhấn prefix+i để hiện sidebar nvim với nguyên trạng thái cũ.

Dọn daemon mồ côi

herdr-nvim-ctl gc
Đã dọn 1 daemon mồ côi. Còn lại: 2.

Lệnh này không tự tay tắt tiến trình nào. Nó gọi lại herdr-nvim daemon-gc của chính plugin, nên giữ nguyên hành vi mà tác giả plugin thiết kế: chỉ tắt daemon nào có tab không còn tồn tại, sau đó xoá socket và file trạng thái. Daemon của tab đang mở tuyệt đối không bị đụng tới.

Plugin cũng chạy ngầm lệnh dọn này mỗi lần bạn nhấn prefix+i, nên phần lớn thời gian bạn không cần gọi tay. Gọi khi vừa đóng hàng loạt tab và muốn lấy lại bộ nhớ ngay.

Tắt một daemon cụ thể

herdr-nvim-ctl kill wW:t2   # một tab
herdr-nvim-ctl kill all     # toàn bộ

Script gửi SIGTERM trước để nvim kịp chạy autocmd thoát và ghi shada, chờ tối đa 2 giây, rồi mới SIGKILL những tiến trình còn cứng đầu. Buffer chưa lưu sẽ mất, nên với tab còn dùng thì nên focus rồi thoát nvim đàng hoàng.

Theo dõi trực tiếp

herdr-nvim-ctl watch

Làm mới mỗi giây. Mở ở một pane riêng rồi bật tắt sidebar ở pane khác để thấy số lượng thay đổi. Ctrl+C để thoát.

Tab id viết kiểu nào cũng được

Herdr gọi tab là wR:t2, còn tên file socket lại là wR_t2.sock — socket không cho phép dấu hai chấm trong tên file. Hai dạng này rất dễ lẫn, và herdr tab focus gốc chỉ nhận dạng có dấu hai chấm:

❯ herdr tab focus wR_t2
{"error":{"code":"tab_not_found","message":"tab wR_t2 not found"},"id":"cli:tab:focus"}

Script tự quy đổi, nên focus và kill nhận cả hai:

herdr-nvim-ctl focus wR_t2   # dán từ ls hay ps
herdr-nvim-ctl focus wR:t2   # dạng tab id

Cách nó lần ra tab

Không đoán theo tên hay thứ tự tiến trình. Plugin luôn sinh nvim kèm tham số:

nvim --headless --listen /run/user/<uid>/herdr-nvim/<workspace>_<tab>.sock ...

Script đọc dòng lệnh của tiến trình (/proc/<pid>/cmdline trên Linux, ps trên macOS), lấy tên file socket, đổi _ thành : để ra tab id, rồi đối chiếu với herdr workspace list và herdr tab list để lấy số thứ tự cùng nhãn hiển thị. Tab id nào không có trong danh sách nghĩa là tab đã đóng, tức daemon mồ côi.

Lưu ý khi viết lệnh: ID như wW thì cố định, còn số thứ tự workspace thì đổi mỗi khi bạn tạo hoặc đóng workspace khác. Dùng ID cho lệnh, dùng số thứ tự để biết nhìn vào đâu trên màn hình.

An toàn

  • Bốn lệnh list, focus, gc, watch không tắt daemon của tab đang mở.
  • kill là lệnh duy nhất tắt tiến trình theo yêu cầu trực tiếp của bạn.
  • Script chỉ khớp tiến trình có tên đúng nvim (pgrep -x), nên không chạm vào server Herdr, các pane agent, hay tiến trình nào khác của hệ thống.
  • list và focus yêu cầu HERDR_ENV=1; chạy ngoài phiên Herdr thì báo lỗi thay vì thao tác nhầm vào phiên khác.

Nó dựa vào những gì có thể đổi

Herdr không có lệnh nào liệt kê các daemon này, nên script phải suy ra từ hai quy ước nội bộ của plugin:

  • nvim được sinh ra kèm --listen <thư mục>/<workspace>_<tab>.sock;
  • tên file socket đổi : thành _, đúng như hàm tab_key() trong mã nguồn plugin.

Cả hai đều là chi tiết cài đặt, không phải giao diện được cam kết. Nếu một bản plugin mới đổi cách đặt tên socket, cột TAB sẽ trống và kill không tìm thấy gì — lúc đó sửa lại hàm socket_of_pid và tab_of_pid. Phần gc thì an toàn vì nó gọi thẳng herdr-nvim daemon-gc của plugin.

Giấy phép

MIT

#!/usr/bin/env bash
# herdr-nvim-ctl — inspect and control the per-tab headless nvim daemons that
# the herdr-nvim plugin (chmarax.herdr-nvim) keeps alive behind each tab.
#
# Messages follow $HERDR_NVIM_CTL_LANG (vi|en), else $LANG, else English.
set -uo pipefail
# ---------------------------------------------------------------- messages --
# Two locales, one logic path: only these strings differ between languages.
case "${HERDR_NVIM_CTL_LANG:-${LC_ALL:-${LANG:-}}}" in
vi|vi_*|vi.*) LANG_MODE=vi ;;
*) LANG_MODE=en ;;
esac
if [ "$LANG_MODE" = vi ]; then
M_USAGE='herdr-nvim-ctl — xem và dọn các tiến trình nvim ẩn mà plugin herdr-nvim
giữ lại phía sau mỗi tab Herdr.
herdr-nvim-ctl liệt kê daemon kèm workspace/tab nhìn thấy được
herdr-nvim-ctl focus <tab> nhảy tới tab đang giữ daemon (ví dụ: wW:t2)
herdr-nvim-ctl gc dọn daemon của tab đã đóng
herdr-nvim-ctl kill <tab> tắt một daemon; "all" tắt toàn bộ
herdr-nvim-ctl watch đếm trực tiếp, làm mới mỗi giây
Ngôn ngữ: đặt HERDR_NVIM_CTL_LANG=vi hoặc =en'
M_NO_HERDR='Không chạy trong phiên Herdr (HERDR_ENV != 1).'
M_NO_CLI='Không tìm thấy lệnh herdr trong PATH.'
M_NO_JQ='Không tìm thấy lệnh jq trong PATH.'
M_WS_FAIL='Lệnh herdr workspace list thất bại.'
M_TABS_FAIL='Lệnh herdr tab list thất bại.'
M_NONE='Không có tiến trình nvim nào đang chạy.'
M_NOT_DAEMON='không phải daemon herdr-nvim'
M_TAB_CLOSED='TAB ĐÃ ĐÓNG — chạy: herdr-nvim-ctl gc'
M_TOTAL_1='Tổng:'
M_TOTAL_2='tiến trình nvim.'
M_HINT='Tới tab: herdr-nvim-ctl focus <TAB> | Hiện sidebar: prefix+i'
M_FOCUS_USAGE='Dùng: herdr-nvim-ctl focus <tab-id> (ví dụ: wW:t2)'
M_FOCUS_FAIL='Không focus được tab'
M_FOCUS_OK_1='Đã chuyển tới'
M_FOCUS_OK_2='Nhấn prefix+i để hiện sidebar nvim.'
M_NO_PLUGIN='Không tìm thấy binary plugin herdr-nvim.'
M_GC_FAIL='Lệnh daemon-gc thất bại.'
M_GC_OK_1='Đã dọn'
M_GC_OK_2='daemon mồ côi. Còn lại:'
M_KILL_USAGE='Dùng: herdr-nvim-ctl kill <tab-id|all>'
M_KILL_NONE='Không có daemon nào khớp'
M_KILL_OK='Đã tắt:'
M_BAD_CMD='Lệnh không hợp lệ:'
M_BAD_CMD_HINT='(chạy: herdr-nvim-ctl --help)'
else
M_USAGE='herdr-nvim-ctl — inspect and clean up the hidden nvim processes the
herdr-nvim plugin keeps alive behind every Herdr tab.
herdr-nvim-ctl list daemons with their visible workspace/tab
herdr-nvim-ctl focus <tab> jump to the tab owning a daemon (e.g. wW:t2)
herdr-nvim-ctl gc reap daemons whose tab no longer exists
herdr-nvim-ctl kill <tab> stop one daemon; "all" stops every daemon
herdr-nvim-ctl watch live count, refreshed every second
Language: set HERDR_NVIM_CTL_LANG=en or =vi'
M_NO_HERDR='Not running inside a Herdr session (HERDR_ENV != 1).'
M_NO_CLI='The herdr command was not found on PATH.'
M_NO_JQ='The jq command was not found on PATH.'
M_WS_FAIL='herdr workspace list failed.'
M_TABS_FAIL='herdr tab list failed.'
M_NONE='No nvim process is running.'
M_NOT_DAEMON='not a herdr-nvim daemon'
M_TAB_CLOSED='TAB CLOSED — run: herdr-nvim-ctl gc'
M_TOTAL_1='Total:'
M_TOTAL_2='nvim process(es).'
M_HINT='Jump to a tab: herdr-nvim-ctl focus <TAB> | Show sidebar: prefix+i'
M_FOCUS_USAGE='Usage: herdr-nvim-ctl focus <tab-id> (e.g. wW:t2)'
M_FOCUS_FAIL='Could not focus tab'
M_FOCUS_OK_1='Switched to'
M_FOCUS_OK_2='Press prefix+i to show the nvim sidebar.'
M_NO_PLUGIN='The herdr-nvim plugin binary was not found.'
M_GC_FAIL='daemon-gc failed.'
M_GC_OK_1='Reaped'
M_GC_OK_2='orphaned daemon(s). Remaining:'
M_KILL_USAGE='Usage: herdr-nvim-ctl kill <tab-id|all>'
M_KILL_NONE='No daemon matches'
M_KILL_OK='Stopped:'
M_BAD_CMD='Unknown command:'
M_BAD_CMD_HINT='(run: herdr-nvim-ctl --help)'
fi
# ----------------------------------------------------------------- helpers --
die() { printf '%s\n' "$*" >&2; exit 1; }
require_herdr() {
[ "${HERDR_ENV:-}" = 1 ] || die "$M_NO_HERDR"
command -v herdr >/dev/null || die "$M_NO_CLI"
command -v jq >/dev/null || die "$M_NO_JQ"
}
# Linux exposes process details through /proc; macOS/BSD have no /proc, so the
# same facts come from ps there. Probe once and branch in the two readers below.
if [ -r /proc/self/cmdline ]; then HAVE_PROC=1; else HAVE_PROC=0; fi
# The socket path is the only link from a process back to its tab: the plugin
# spawns `nvim --headless --listen <dir>/<workspace>_<tab>.sock`, mirroring its
# own `tab_key()` which replaces ':' with '_'.
socket_of_pid() {
local arg
if [ "$HAVE_PROC" = 1 ]; then
while IFS= read -r -d '' arg; do
case $arg in */herdr-nvim/*.sock) printf '%s' "$arg"; return 0 ;; esac
done < "/proc/$1/cmdline" 2>/dev/null
else
# Socket paths never contain spaces, so word splitting is safe here.
for arg in $(ps -ww -o args= -p "$1" 2>/dev/null); do
case $arg in */herdr-nvim/*.sock) printf '%s' "$arg"; return 0 ;; esac
done
fi
return 1
}
# pgrep -c is GNU-only; count lines instead so BSD pgrep works too.
nvim_pids() { pgrep -x nvim 2>/dev/null; }
nvim_count() { nvim_pids | grep -c . ; }
tab_of_pid() {
local sock key
sock=$(socket_of_pid "$1") || return 1
key=${sock##*/}
key=${key%.sock}
printf '%s' "${key/_/:}"
}
# Accept either form of the id: the tab id `wR:t2` or the socket stem `wR_t2`.
normalize_tab() { printf '%s' "${1/_/:}"; }
# Resident set size in MB. /proc reports VmRSS in kB, ps reports rss in kB too.
rss_mb_of_pid() {
local k v
if [ "$HAVE_PROC" = 1 ]; then
while read -r k v _; do
[ "$k" = VmRSS: ] && { printf '%s' "$((v / 1024))"; return 0; }
done < "/proc/$1/status" 2>/dev/null
else
v=$(ps -o rss= -p "$1" 2>/dev/null | tr -d ' ')
case $v in ''|*[!0-9]*) ;; *) printf '%s' "$((v / 1024))"; return 0 ;; esac
fi
printf '0'
}
plugin_bin() {
ls -t "$HOME"/.config/herdr/plugins/github/chmarax.herdr-nvim-*/bin/herdr-nvim 2>/dev/null | head -1
}
# ---------------------------------------------------------------- commands --
cmd_list() {
require_herdr
local ws tabs pids lookup p tab rss line tid wcol tcol count
ws=$(herdr workspace list) || die "$M_WS_FAIL"
tabs=$(herdr tab list) || die "$M_TABS_FAIL"
pids=$(nvim_pids)
[ -n "$pids" ] || { printf '%s\n' "$M_NONE"; return 0; }
# One jq pass joins tabs to their workspace; the loop below only reads it.
lookup=$(jq -n -r --argjson ws "$ws" --argjson tabs "$tabs" '
($ws.result.workspaces | map({(.workspace_id): "\(.number). \(.label)"}) | add) as $wmap
| $tabs.result.tabs[]
| [.tab_id, ($wmap[.workspace_id] // "?"), "\(.number). \(.label)"]
| @tsv')
printf '%-8s %-9s %-7s %-26s %s\n' PID TAB RAM WORKSPACE TAB
count=0
for p in $pids; do
count=$((count + 1))
tab=$(tab_of_pid "$p") || { printf '%-8s %-9s %-7s %s\n' "$p" '-' '-' "$M_NOT_DAEMON"; continue; }
rss=$(rss_mb_of_pid "$p")
wcol=
while IFS=$'\t' read -r tid line tcol; do
[ "$tid" = "$tab" ] && { wcol=$line; break; }
done <<< "$lookup"
if [ -z "$wcol" ]; then
printf '%-8s %-9s %-7s %s\n' "$p" "$tab" "${rss}MB" "$M_TAB_CLOSED"
else
printf '%-8s %-9s %-7s %-26s %s\n' "$p" "$tab" "${rss}MB" "$wcol" "$tcol"
fi
done
printf '\n%s %s %s\n%s\n' "$M_TOTAL_1" "$count" "$M_TOTAL_2" "$M_HINT"
}
cmd_focus() {
require_herdr
[ $# -eq 1 ] || die "$M_FOCUS_USAGE"
local tab; tab=$(normalize_tab "$1")
herdr tab focus "$tab" >/dev/null || die "$M_FOCUS_FAIL $tab."
printf '%s %s. %s\n' "$M_FOCUS_OK_1" "$tab" "$M_FOCUS_OK_2"
}
cmd_gc() {
local bin before after
bin=$(plugin_bin)
[ -x "$bin" ] || die "$M_NO_PLUGIN"
before=$(nvim_count)
"$bin" daemon-gc || die "$M_GC_FAIL"
after=$(nvim_count)
printf '%s %s %s %s.\n' "$M_GC_OK_1" "$((before - after))" "$M_GC_OK_2" "$after"
}
cmd_kill() {
[ $# -eq 1 ] || die "$M_KILL_USAGE"
local pids p sock alive
if [ "$1" = all ]; then
pids=$(nvim_pids)
else
local want; want=$(normalize_tab "$1")
pids=$(for p in $(nvim_pids); do [ "$(tab_of_pid "$p")" = "$want" ] && printf '%s\n' "$p"; done)
fi
[ -n "$pids" ] || die "$M_KILL_NONE '$1'."
# Quit the same way the plugin does (daemon.rs::send_quit) so nvim writes
# shada and runs its exit autocmds; fall back to signals if RPC is refused.
for p in $pids; do
sock=$(socket_of_pid "$p") || continue
nvim --headless --server "$sock" --remote-send '<cmd>qa!<cr>' >/dev/null 2>&1
done
alive=$pids
for _ in 1 2 3 4 5; do
sleep 0.4
alive=$(for p in $alive; do kill -0 "$p" 2>/dev/null && printf '%s ' "$p"; done)
[ -z "$alive" ] && break
done
[ -n "$alive" ] && { kill $alive 2>/dev/null; sleep 0.4; kill -9 $alive 2>/dev/null; }
# send_quit leaves the socket behind; the plugin unlinks it in the same step.
for p in $pids; do
sock=$(socket_of_pid "$p" 2>/dev/null) || continue
kill -0 "$p" 2>/dev/null || rm -f "$sock"
done
printf '%s %s\n' "$M_KILL_OK" "${pids//$'\n'/ }"
}
cmd_watch() {
# readlink -f is GNU-only on older systems; fall back to $0 as invoked.
local self
self=$(readlink -f "$0" 2>/dev/null) || self=$0
[ -n "$self" ] || self=$0
if command -v watch >/dev/null; then
watch -n 1 "$self list"
else
# macOS ships no watch(1): redraw the screen ourselves.
while :; do
printf '\033[H\033[2J'
"$self" list
sleep 1
done
fi
}
case "${1:-list}" in
list|ls|'') cmd_list ;;
focus|go) shift; cmd_focus "$@" ;;
gc|clean) cmd_gc ;;
kill|stop) shift; cmd_kill "$@" ;;
watch) cmd_watch ;;
-h|--help|help) printf '%s\n' "$M_USAGE" ;;
*) die "$M_BAD_CMD $1 $M_BAD_CMD_HINT" ;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment