Files
zterm-navigator.nvim/lua/zterm-navigator/init.lua
T
Zacharias-Brohn 1855fba32f powerline?
2025-12-16 22:14:20 +01:00

415 lines
11 KiB
Lua

-- zterm-navigator: Seamless navigation between Neovim windows and ZTerm panes
--
-- When you press the navigation key (e.g., Alt+Arrow):
-- - If there's a Neovim window in that direction, navigate to it
-- - If not, send an OSC sequence to ZTerm to navigate to the next pane
--
-- Also provides vim-tpipeline-like statusline integration:
-- - Sends neovim's statusline to ZTerm's terminal statusline
-- - Updates on mode change, buffer change, cursor move, etc.
local M = {}
-- Track if statusline integration is active
M._statusline_enabled = false
M._statusline_augroup = nil
-- Default configuration
M.config = {
-- Key mappings for navigation
-- Set to false to disable a direction
left = "<A-Left>",
right = "<A-Right>",
up = "<A-Up>",
down = "<A-Down>",
-- Statusline integration (vim-tpipeline style)
statusline = {
-- Enable statusline integration (auto-detected if nil)
enabled = nil,
-- Use neovim's statusline setting, or provide a custom one
-- If nil, uses vim.o.statusline
statusline = nil,
-- Disable neovim's builtin statusline when sending to ZTerm
hide_nvim_statusline = true,
},
}
-- Send raw escape sequence to the terminal, bypassing neovim's terminal handling.
-- We spawn a shell process to write directly to the TTY, which bypasses neovim completely.
local function send_to_tty(str)
-- Use printf in a subprocess - this writes directly to the terminal
-- The subprocess inherits the TTY and can write to it directly
vim.fn.jobstart({ 'printf', '%s', str }, {
detach = true,
on_exit = function() end,
})
end
-- Send OSC 51 command to ZTerm for pane navigation
local function zterm_navigate(direction)
-- OSC 51;navigate;<direction> ST
-- Using BEL (\007) as string terminator for better compatibility
local osc = string.format("\027]51;navigate;%s\007", direction)
send_to_tty(osc)
end
-- Get the window number in a given direction, or 0 if none exists
local function get_window_in_direction(direction)
local dir_char = ({
left = "h",
right = "l",
up = "k",
down = "j",
})[direction]
if not dir_char then
return 0
end
-- Get current window number
local current_winnr = vim.fn.winnr()
-- Get window number in the specified direction
local target_winnr = vim.fn.winnr(dir_char)
-- If winnr() returns the same window, there's no window in that direction
if target_winnr == current_winnr then
return 0
end
return target_winnr
end
-- Navigate in the given direction
-- First tries to move within Neovim, falls back to ZTerm pane navigation
local function navigate(direction)
local target_win = get_window_in_direction(direction)
if target_win ~= 0 then
-- There's a Neovim window in that direction, navigate to it
vim.cmd("wincmd " .. ({
left = "h",
right = "l",
up = "k",
down = "j",
})[direction])
else
-- No Neovim window, ask ZTerm to navigate
zterm_navigate(direction)
end
end
-- Public navigation functions
function M.navigate_left()
navigate("left")
end
function M.navigate_right()
navigate("right")
end
function M.navigate_up()
navigate("up")
end
function M.navigate_down()
navigate("down")
end
-- ============================================================================
-- Statusline Integration (vim-tpipeline style)
-- ============================================================================
-- Check if we're running inside ZTerm
local function is_zterm()
local term = vim.env.TERM or ""
local term_program = vim.env.TERM_PROGRAM or ""
local zterm_env = vim.env.ZTERM or ""
-- ZTerm sets TERM to "zterm" or similar
return term:match("zterm") ~= nil
or term_program:match("[Zz]term") ~= nil
or zterm_env ~= ""
end
-- Debug function to test statusline sending
function M.debug_statusline()
local is_in_zterm = is_zterm()
print("TERM=" .. (vim.env.TERM or "nil"))
print("TERM_PROGRAM=" .. (vim.env.TERM_PROGRAM or "nil"))
print("is_zterm=" .. tostring(is_in_zterm))
print("statusline_enabled=" .. tostring(M._statusline_enabled))
print("Sending test statusline...")
send_to_tty("\027]51;statusline;\027[38;2;255;0;0mTEST\027[0m\007")
print("Done. Check ZTerm statusline.")
end
-- Force send a test statusline (for debugging)
function M.test_statusline()
send_to_tty("\027]51;statusline;\027[38;2;255;100;0mNeovim Test\027[0m\007")
end
-- Send statusline content to ZTerm via OSC 51
local function send_statusline(content)
-- OSC 51;statusline;<content> ST
local osc = string.format("\027]51;statusline;%s\007", content or "")
send_to_tty(osc)
end
-- Clear the ZTerm statusline (restore default)
local function clear_statusline()
send_statusline("")
end
-- Get the rendered statusline with ANSI escape codes
local function get_rendered_statusline()
-- Get the statusline string to evaluate
local stl = M.config.statusline.statusline or vim.o.statusline
-- If empty, try to get a sensible default
if not stl or stl == "" then
-- Build a simple default statusline
stl = " %f %m%r%h%w %= %y [%l,%c] %P "
end
-- Use nvim_eval_statusline to get the rendered content with highlights
local ok, result = pcall(vim.api.nvim_eval_statusline, stl, {
winid = vim.api.nvim_get_current_win(),
highlights = true,
fillchar = " ",
})
if not ok then
return nil
end
-- Convert highlights to ANSI escape codes
local output = {}
local str = result.str
local highlights = result.highlights or {}
-- Sort highlights by start position
table.sort(highlights, function(a, b) return a.start < b.start end)
local pos = 1
for i, hl in ipairs(highlights) do
-- Add any text before this highlight
if hl.start > pos then
-- No highlight for this segment, use reset
table.insert(output, "\027[0m")
table.insert(output, str:sub(pos, hl.start))
end
-- Determine the end of this highlight
local hl_end
if i < #highlights then
hl_end = highlights[i + 1].start
else
hl_end = #str
end
-- Get the highlight colors
local hl_info = vim.api.nvim_get_hl(0, { name = hl.group, link = false })
local ansi = {}
-- Foreground color
if hl_info.fg then
local r = bit.rshift(bit.band(hl_info.fg, 0xFF0000), 16)
local g = bit.rshift(bit.band(hl_info.fg, 0x00FF00), 8)
local b = bit.band(hl_info.fg, 0x0000FF)
table.insert(ansi, string.format("38;2;%d;%d;%d", r, g, b))
end
-- Background color
if hl_info.bg then
local r = bit.rshift(bit.band(hl_info.bg, 0xFF0000), 16)
local g = bit.rshift(bit.band(hl_info.bg, 0x00FF00), 8)
local b = bit.band(hl_info.bg, 0x0000FF)
table.insert(ansi, string.format("48;2;%d;%d;%d", r, g, b))
end
-- Bold
if hl_info.bold then
table.insert(ansi, "1")
end
-- Italic
if hl_info.italic then
table.insert(ansi, "3")
end
-- Underline
if hl_info.underline then
table.insert(ansi, "4")
end
-- Build the ANSI sequence
if #ansi > 0 then
table.insert(output, "\027[" .. table.concat(ansi, ";") .. "m")
else
table.insert(output, "\027[0m")
end
-- Add the highlighted text
table.insert(output, str:sub(hl.start + 1, hl_end))
pos = hl_end + 1
end
-- Add any remaining text
if pos <= #str then
table.insert(output, "\027[0m")
table.insert(output, str:sub(pos))
end
-- Reset at the end
table.insert(output, "\027[0m")
return table.concat(output)
end
-- Update the ZTerm statusline
local function update_statusline()
if not M._statusline_enabled then
return
end
local content = get_rendered_statusline()
if content then
send_statusline(content)
end
end
-- Saved laststatus value to restore on disable
M._saved_laststatus = nil
-- Enable statusline integration
function M.enable_statusline()
if M._statusline_enabled then
return
end
M._statusline_enabled = true
-- Hide neovim's statusline if configured
if M.config.statusline.hide_nvim_statusline then
M._saved_laststatus = vim.o.laststatus
vim.o.laststatus = 0
end
-- Create autocommands for statusline updates
M._statusline_augroup = vim.api.nvim_create_augroup("ZTermStatusline", { clear = true })
-- Events that should trigger a statusline update
local events = {
"ModeChanged", -- Mode changes (normal, insert, visual, etc.)
"BufEnter", -- Entering a buffer
"BufWritePost", -- After writing a file
"FileChangedShellPost",
"WinEnter", -- Entering a window
"CursorMoved", -- Cursor moved (for position info)
"CursorMovedI", -- Cursor moved in insert mode
"DiagnosticChanged", -- LSP diagnostics changed
}
vim.api.nvim_create_autocmd(events, {
group = M._statusline_augroup,
callback = function()
-- Defer slightly to batch rapid updates
vim.defer_fn(update_statusline, 10)
end,
})
-- Clear statusline on exit
vim.api.nvim_create_autocmd("VimLeavePre", {
group = M._statusline_augroup,
callback = function()
clear_statusline()
end,
})
-- Initial update
update_statusline()
end
-- Disable statusline integration
function M.disable_statusline()
if not M._statusline_enabled then
return
end
M._statusline_enabled = false
-- Remove autocommands
if M._statusline_augroup then
vim.api.nvim_del_augroup_by_id(M._statusline_augroup)
M._statusline_augroup = nil
end
-- Restore neovim's statusline
if M._saved_laststatus then
vim.o.laststatus = M._saved_laststatus
M._saved_laststatus = nil
end
-- Clear the ZTerm statusline
clear_statusline()
end
-- Toggle statusline integration
function M.toggle_statusline()
if M._statusline_enabled then
M.disable_statusline()
else
M.enable_statusline()
end
end
-- ============================================================================
-- Setup
-- ============================================================================
-- Setup function to configure keymaps
function M.setup(opts)
-- Merge user options with defaults
opts = opts or {}
M.config = vim.tbl_deep_extend("force", M.config, opts)
-- Set up keymaps
local keymap_opts = { noremap = true, silent = true }
if M.config.left then
vim.keymap.set({"n", "t"}, M.config.left, M.navigate_left,
vim.tbl_extend("force", keymap_opts, { desc = "Navigate left (window/pane)" }))
end
if M.config.right then
vim.keymap.set({"n", "t"}, M.config.right, M.navigate_right,
vim.tbl_extend("force", keymap_opts, { desc = "Navigate right (window/pane)" }))
end
if M.config.up then
vim.keymap.set({"n", "t"}, M.config.up, M.navigate_up,
vim.tbl_extend("force", keymap_opts, { desc = "Navigate up (window/pane)" }))
end
if M.config.down then
vim.keymap.set({"n", "t"}, M.config.down, M.navigate_down,
vim.tbl_extend("force", keymap_opts, { desc = "Navigate down (window/pane)" }))
end
-- Set up statusline integration
local statusline_enabled = M.config.statusline.enabled
if statusline_enabled == nil then
-- Auto-detect: enable if running in ZTerm
statusline_enabled = is_zterm()
end
if statusline_enabled then
-- Defer to ensure neovim is fully loaded
vim.defer_fn(function()
M.enable_statusline()
end, 100)
end
end
return M