2026-07-06 19:51:29 -05:00
|
|
|
---@brief [[
|
|
|
|
|
--- watchexec.nvim window module.
|
|
|
|
|
--- Manages the output buffer and window (float or split), including
|
|
|
|
|
--- creation, display, text appending, and keymap-driven close.
|
|
|
|
|
---@brief ]]
|
|
|
|
|
|
|
|
|
|
local config = require("watchexec.config")
|
|
|
|
|
|
|
|
|
|
local M = {}
|
|
|
|
|
|
|
|
|
|
---@class watchexec.WindowState
|
|
|
|
|
---@field buf integer|nil
|
|
|
|
|
---@field win integer|nil
|
|
|
|
|
---@field visible boolean
|
|
|
|
|
|
|
|
|
|
---@type watchexec.WindowState
|
|
|
|
|
local state = {
|
|
|
|
|
buf = nil,
|
|
|
|
|
win = nil,
|
|
|
|
|
visible = false,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
---Create or reuse the output buffer.
|
|
|
|
|
---Sets buffer-local options and keymaps (<Esc> and q) to close the window.
|
|
|
|
|
---@return integer buf
|
|
|
|
|
function M.create_buf()
|
|
|
|
|
local existing = state.buf
|
2026-07-18 18:19:01 -05:00
|
|
|
|
2026-07-06 19:51:29 -05:00
|
|
|
if existing and vim.api.nvim_buf_is_valid(existing) then
|
|
|
|
|
return existing
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local buf = vim.api.nvim_create_buf(false, true)
|
|
|
|
|
|
|
|
|
|
state.buf = buf
|
|
|
|
|
vim.api.nvim_set_option_value("bufhidden", "hide", { buf = buf })
|
|
|
|
|
vim.api.nvim_set_option_value("filetype", "watchexec-output", { buf = buf })
|
|
|
|
|
vim.api.nvim_set_option_value("modifiable", false, { buf = buf })
|
|
|
|
|
|
|
|
|
|
pcall(vim.api.nvim_buf_set_name, buf, "watchexec://output")
|
|
|
|
|
|
|
|
|
|
vim.keymap.set("n", "<Esc>", function()
|
|
|
|
|
M.close()
|
|
|
|
|
end, { buffer = buf, nowait = true, desc = "Close watchexec window" })
|
|
|
|
|
|
|
|
|
|
vim.keymap.set("n", "q", function()
|
|
|
|
|
M.close()
|
|
|
|
|
end, { buffer = buf, nowait = true, desc = "Close watchexec window" })
|
|
|
|
|
|
|
|
|
|
return buf
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---Open the output window.
|
|
|
|
|
---Creates a float or split window per configuration, or reuses an existing one.
|
|
|
|
|
function M.open()
|
|
|
|
|
local cfg = config.get()
|
|
|
|
|
local buf = M.create_buf()
|
|
|
|
|
|
|
|
|
|
local win = state.win
|
|
|
|
|
|
|
|
|
|
if win and vim.api.nvim_win_is_valid(win) then
|
|
|
|
|
vim.api.nvim_win_set_buf(win, buf)
|
|
|
|
|
vim.api.nvim_set_current_win(win)
|
|
|
|
|
state.visible = true
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if cfg.window.type == "float" then
|
|
|
|
|
---@type watchexec.FloatOpts
|
|
|
|
|
local float = cfg.window.float
|
|
|
|
|
local width = float.width <= 1 and math.floor(vim.o.columns * float.width) or float.width
|
|
|
|
|
local height = float.height <= 1 and math.floor(vim.o.lines * float.height) or float.height
|
|
|
|
|
local row = float.row <= 1 and math.floor((vim.o.lines - height) * float.row) or float.row
|
|
|
|
|
local col = float.col <= 1 and math.floor((vim.o.columns - width) * float.col) or float.col
|
|
|
|
|
|
|
|
|
|
state.win = vim.api.nvim_open_win(buf, true, {
|
|
|
|
|
relative = float.relative or "editor",
|
|
|
|
|
width = width,
|
|
|
|
|
height = height,
|
|
|
|
|
row = row,
|
|
|
|
|
col = col,
|
|
|
|
|
style = "minimal",
|
|
|
|
|
border = cfg.window.border or "single",
|
|
|
|
|
})
|
|
|
|
|
else
|
|
|
|
|
local split = cfg.window.split
|
|
|
|
|
local size = cfg.window.size
|
|
|
|
|
local is_vertical = split == "left" or split == "right"
|
|
|
|
|
local dir = (split == "below" or split == "right") and "belowright" or "aboveleft"
|
|
|
|
|
local cmd = dir .. " " .. (is_vertical and size .. "vnew" or size .. "new")
|
|
|
|
|
|
|
|
|
|
vim.cmd(cmd)
|
|
|
|
|
|
|
|
|
|
local split_win = vim.api.nvim_get_current_win()
|
|
|
|
|
state.win = split_win
|
|
|
|
|
vim.api.nvim_win_set_buf(split_win, buf)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
|
|
|
|
|
|
|
|
|
if #lines == 1 and lines[1] == "" then
|
|
|
|
|
vim.api.nvim_set_option_value("modifiable", true, { buf = buf })
|
|
|
|
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { " No job running. Use :WatchexecRun <command> to start one.", "" })
|
|
|
|
|
vim.api.nvim_set_option_value("modifiable", false, { buf = buf })
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
state.visible = true
|
|
|
|
|
require("watchexec.indicator").refresh()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function M.close()
|
|
|
|
|
local win = state.win
|
|
|
|
|
|
|
|
|
|
if win and vim.api.nvim_win_is_valid(win) then
|
|
|
|
|
vim.api.nvim_win_close(win, true)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
state.win = nil
|
|
|
|
|
state.visible = false
|
|
|
|
|
require("watchexec.indicator").refresh()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function M.toggle()
|
|
|
|
|
if state.visible then
|
|
|
|
|
M.close()
|
|
|
|
|
else
|
|
|
|
|
M.open()
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---Close the window and delete the buffer entirely.
|
|
|
|
|
function M.cleanup()
|
|
|
|
|
M.close()
|
|
|
|
|
|
|
|
|
|
local buf = state.buf
|
|
|
|
|
|
|
|
|
|
if buf and vim.api.nvim_buf_is_valid(buf) then
|
|
|
|
|
vim.api.nvim_buf_delete(buf, { force = true })
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
state.buf = nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---Clear the output buffer and reset to the waiting placeholder.
|
|
|
|
|
function M.clear()
|
|
|
|
|
local buf = state.buf
|
|
|
|
|
|
|
|
|
|
if not buf or not vim.api.nvim_buf_is_valid(buf) then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
vim.api.nvim_set_option_value("modifiable", true, { buf = buf })
|
|
|
|
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { " No job running. Use :WatchexecRun <command> to start one.", "" })
|
|
|
|
|
vim.api.nvim_set_option_value("modifiable", false, { buf = buf })
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---Append text to the output buffer.
|
|
|
|
|
---On first append, replaces the "waiting for output" placeholder.
|
2026-07-18 18:19:01 -05:00
|
|
|
---When overwrite is true and the buffer has content, replaces the last line
|
|
|
|
|
---instead of appending (handles \\r-based progress overwrites).
|
2026-07-06 19:51:29 -05:00
|
|
|
---Truncates the buffer when max_lines is exceeded.
|
|
|
|
|
---Auto-scrolls to the bottom when enabled.
|
|
|
|
|
---@param text string
|
2026-07-18 18:19:01 -05:00
|
|
|
---@param overwrite boolean|nil
|
|
|
|
|
function M.append(text, overwrite)
|
2026-07-06 19:51:29 -05:00
|
|
|
local buf = state.buf
|
|
|
|
|
|
|
|
|
|
if not buf or not vim.api.nvim_buf_is_valid(buf) then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local cfg = config.get()
|
|
|
|
|
vim.api.nvim_set_option_value("modifiable", true, { buf = buf })
|
|
|
|
|
|
|
|
|
|
local current = vim.api.nvim_buf_line_count(buf)
|
|
|
|
|
local lines = vim.split(text, "\n", { plain = true })
|
|
|
|
|
local first_line = vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1] or ""
|
|
|
|
|
|
|
|
|
|
if first_line:match("^ No job running") then
|
|
|
|
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
2026-07-18 18:19:01 -05:00
|
|
|
elseif overwrite and current > 0 then
|
|
|
|
|
vim.api.nvim_buf_set_lines(buf, current - 1, -1, false, lines)
|
2026-07-06 19:51:29 -05:00
|
|
|
else
|
|
|
|
|
vim.api.nvim_buf_set_lines(buf, current, -1, false, lines)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if cfg.max_lines and vim.api.nvim_buf_line_count(buf) > cfg.max_lines then
|
|
|
|
|
local overflow = vim.api.nvim_buf_line_count(buf) - cfg.max_lines
|
|
|
|
|
vim.api.nvim_buf_set_lines(buf, 0, overflow, false, {})
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
vim.api.nvim_set_option_value("modifiable", false, { buf = buf })
|
|
|
|
|
|
|
|
|
|
local scroll_win = state.win
|
|
|
|
|
|
|
|
|
|
if cfg.auto_scroll and scroll_win and vim.api.nvim_win_is_valid(scroll_win) then
|
|
|
|
|
local line_count = vim.api.nvim_buf_line_count(buf)
|
|
|
|
|
vim.api.nvim_win_set_cursor(scroll_win, { line_count, 0 })
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---Recalculate float window dimensions after terminal resize.
|
|
|
|
|
---No-op for split windows or when no window is displayed.
|
|
|
|
|
function M.resize_float()
|
|
|
|
|
local win = state.win
|
|
|
|
|
|
|
|
|
|
if not win or not vim.api.nvim_win_is_valid(win) then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local cfg = config.get()
|
|
|
|
|
|
|
|
|
|
if cfg.window.type ~= "float" then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---@type watchexec.FloatOpts
|
|
|
|
|
local float = cfg.window.float
|
|
|
|
|
local width = float.width <= 1 and math.floor(vim.o.columns * float.width) or float.width
|
|
|
|
|
local height = float.height <= 1 and math.floor(vim.o.lines * float.height) or float.height
|
|
|
|
|
local row = float.row <= 1 and math.floor((vim.o.lines - height) * float.row) or float.row
|
|
|
|
|
local col = float.col <= 1 and math.floor((vim.o.columns - width) * float.col) or float.col
|
|
|
|
|
|
|
|
|
|
vim.api.nvim_win_set_config(win, {
|
|
|
|
|
relative = float.relative or "editor",
|
|
|
|
|
width = width,
|
|
|
|
|
height = height,
|
|
|
|
|
row = row,
|
|
|
|
|
col = col,
|
|
|
|
|
})
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---Check whether the output window is currently displayed.
|
|
|
|
|
---@return boolean
|
|
|
|
|
function M.is_visible()
|
|
|
|
|
return state.visible
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---Return the output buffer handle, or nil if not yet created.
|
|
|
|
|
---@return integer|nil
|
|
|
|
|
function M.get_buf()
|
|
|
|
|
return state.buf
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---Return the output window handle, or nil if not yet created.
|
|
|
|
|
---@return integer|nil
|
|
|
|
|
function M.get_win()
|
|
|
|
|
return state.win
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return M
|