Core Principle
When an external process needs to inspect a live Neovim session, it must compose its queries out of pure read-only APIs (nvim_list_wins, nvim_win_get_buf, nvim_buf_get_name, nvim_win_get_width/height, vim.fn.win_screenpos, nvim_get_hl). Never :normal, never mode switches, never zooming. And when mapping a Neovim window to outer terminal coordinates, use vim.fn.win_screenpos (absolute, 1-indexed, includes tabline offset) and not nvim_win_get_position (editor-area-relative, excludes tabline).
Why This Matters
Live Neovim sessions are user-owned state. A read feature (preview, status, picker) that mutates mode to scrape contents is a latent user-facing bug. Neovim’s API already exposes enough inspection primitives that mutation is never required; reaching for :normal or window-zoom is a sign you haven’t found the right read API yet. And mixing up the two coordinate systems silently produces off-by-tabline captures that look right for tabline-less Neovim setups and break otherwise.
Evidence/Examples
- Terminal mode is sticky over RPC: calling
nvim_exec2(":normal ...")against a window in terminal mode fails withE5108: Can't re-enter normal mode from terminal mode. Any design that wants this is wrong. - Mutation leaks even on failure: an RPC that zooms a terminal window before failing can leave the user’s input state corrupted (keystrokes producing garbage until restart).
win_screenposvsnvim_win_get_position:win_screenposreturns absolute 1-indexed screen coords including tabline,nvim_win_get_positionreturns{row, col}relative to the editor area. The latter is off by the tabline height (and more if there’s awinbar/command line) when used as outer-terminal coordinates.nvim_get_hl(0, {name="Normal"}).bgreturns the background as a packed 24-bit integer, not a hex string. Decompose viamath.floor(bg/65536)%256,math.floor(bg/256)%256,bg%256to get R/G/B.
Implications
- External previewers/inspectors for Neovim should treat the session as strictly read-only. If a design requires mode switching to work, redesign it.
- When feeding Neovim window geometry to tools that speak in absolute screen coordinates (
tmux capture-pane, external renderers), reach forwin_screenposfirst. Only usenvim_win_get_positionwhen you already know you’re working in editor-area coordinates. - Treat any highlight-group color fetched via
nvim_get_hlas an integer to be decomposed, not a string to be used directly.
Related Ideas
- Tmux capture-pane Color Semantics — the consumer of the
win_screenposcoordinates in the tmux Claude session manager. - Fzf Preview and Popup Control — where the captured window content is rendered.
- REPL & Terminals - Py Podcast — broader terminal emulator concepts.
Questions
- Is there a single read-only RPC for “give me the rendered contents of this terminal window as a rectangular block of cells with styling”, instead of the current geometry-plus-external-capture approach?
- Does
winbaror floating window overlays perturbwin_screenposin ways the tabline case doesn’t expose?