257 lines
9.0 KiB
Lua
257 lines
9.0 KiB
Lua
-- lazysecrets.nvim: lazysecrets in a floating terminal, themed from the
|
|
-- colorscheme. No dependencies beyond Neovim 0.11 and lazysecrets.
|
|
--
|
|
-- lazysecrets layers config files given comma-separated in
|
|
-- LAZYSECRETS_CONFIG_FILE, later ones winning per key. This writes a theme
|
|
-- file from highlight groups and layers it on top of the user's own
|
|
-- config, so the user's settings stay and only the colours follow nvim.
|
|
--
|
|
-- require("lazysecrets").setup({ ... }) -- optional, to change M.config
|
|
-- require("lazysecrets").open() -- browse
|
|
-- require("lazysecrets").open({ args = { "lazysecrets://..." } }) -- open a link
|
|
|
|
local M = {}
|
|
|
|
---@class lazysecrets.Color
|
|
---@field fg? string|string[] highlight group whose foreground is the colour; with several, the first that has one
|
|
---@field bg? string|string[] highlight group whose background is the colour
|
|
---@field bold? boolean
|
|
|
|
M.config = {
|
|
cmd = "lazysecrets",
|
|
theme_path = vim.fn.stdpath("cache") .. "/lazysecrets-theme.yaml",
|
|
-- lazysecrets theme roles (`lazysecrets` README, Theme) from highlight
|
|
-- groups. A role taking a foreground can still read a group's
|
|
-- background (`bg`), e.g. the search match colour from Search.
|
|
-- stylua: ignore
|
|
theme = {
|
|
activeBorderColor = { fg = "MatchParen", bold = true },
|
|
inactiveBorderColor = { fg = "FloatBorder" },
|
|
selectedLineBgColor = { bg = "Visual" },
|
|
inactiveViewSelectedLineBgColor = { bg = "CursorLine" },
|
|
defaultFgColor = { fg = "Normal" },
|
|
optionsTextColor = { fg = "Function" },
|
|
searchMatchColor = { bg = "IncSearch", bold = true },
|
|
errorColor = { fg = "DiagnosticError" },
|
|
warningColor = { fg = "DiagnosticWarn" },
|
|
successColor = { fg = "DiagnosticOk", bold = true },
|
|
noticeColor = { fg = "DiagnosticOk" },
|
|
mutedColor = { fg = "Comment" },
|
|
favouriteColor = { fg = "DiagnosticWarn" },
|
|
valueColor = { fg = "String" },
|
|
azureColor = { fg = "Function" },
|
|
-- Not Constant: many schemes give it the search/match colour.
|
|
databricksColor = { fg = { "@variable.builtin", "Exception" } },
|
|
repoColor = { fg = "Statement" },
|
|
},
|
|
-- The floating window: width and height as fractions of the editor,
|
|
-- border as nvim_open_win takes it.
|
|
win = { width = 0.9, height = 0.9, border = "rounded" },
|
|
}
|
|
|
|
local dirty = true -- write the theme on first open, and after :colorscheme
|
|
|
|
---@param v lazysecrets.Color
|
|
---@return string[] # a theme value: "#rrggbb", then attributes
|
|
local function color(v)
|
|
local out = {}
|
|
local which = v.fg and "fg" or "bg"
|
|
local groups = v.fg or v.bg
|
|
for _, name in ipairs(type(groups) == "table" and groups or { groups }) do
|
|
local hl = vim.api.nvim_get_hl(0, { name = name, link = false })
|
|
if hl and hl[which] then
|
|
table.insert(out, string.format("#%06x", hl[which]))
|
|
break
|
|
end
|
|
end
|
|
if v.bold then
|
|
table.insert(out, "bold")
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function write_theme()
|
|
local lines = { "# Written by lazysecrets.nvim from the colorscheme; changes are overwritten.", "theme:" }
|
|
local roles = vim.tbl_keys(M.config.theme)
|
|
table.sort(roles)
|
|
for _, role in ipairs(roles) do
|
|
local c = color(M.config.theme[role])
|
|
if #c > 0 then -- a group without that colour: lazysecrets keeps its default
|
|
local quoted = vim.tbl_map(function(s)
|
|
return string.format("%q", s)
|
|
end, c)
|
|
table.insert(lines, string.format(" %s: [%s]", role, table.concat(quoted, ", ")))
|
|
end
|
|
end
|
|
vim.fn.mkdir(vim.fn.fnamemodify(M.config.theme_path, ":h"), "p")
|
|
vim.fn.writefile(lines, M.config.theme_path)
|
|
dirty = false
|
|
end
|
|
|
|
-- The user's config files, as lazysecrets would find them, then the theme.
|
|
local function config_files()
|
|
local files = vim.tbl_filter(function(f)
|
|
return f:match("%S")
|
|
end, vim.split(vim.env.LAZYSECRETS_CONFIG_FILE or "", ",", { plain = true }))
|
|
if #files == 0 then
|
|
local base = vim.env.XDG_CONFIG_HOME or (vim.env.HOME .. "/.config")
|
|
files = { base .. "/lazysecrets/config.yaml" } -- a missing file is fine
|
|
end
|
|
if not vim.tbl_contains(files, M.config.theme_path) then
|
|
table.insert(files, M.config.theme_path)
|
|
end
|
|
return table.concat(files, ",")
|
|
end
|
|
|
|
-- Where lazysecrets starts: the current file's folder when it's in a git
|
|
-- repo, so that repo opens with the files nearest this one first; else
|
|
-- nvim's working directory.
|
|
local function start_dir()
|
|
local file = vim.api.nvim_buf_get_name(0)
|
|
local dir = vim.fs.dirname(file)
|
|
if vim.bo.buftype == "" and file ~= "" and vim.fn.isdirectory(dir) == 1 and vim.fs.root(dir, ".git") then
|
|
return dir
|
|
end
|
|
return vim.fn.getcwd()
|
|
end
|
|
|
|
-- One lazysecrets at a time: hidden when you leave its window, shown
|
|
-- again by the next open, and replaced when opened with other arguments
|
|
-- or from another repo.
|
|
local term = {} ---@type { buf?: integer, win?: integer, job?: integer, key?: string, cfg?: table, stopped?: boolean }
|
|
|
|
local function running()
|
|
return term.buf and vim.api.nvim_buf_is_valid(term.buf) and term.job and vim.fn.jobwait({ term.job }, 0)[1] == -1
|
|
end
|
|
|
|
local function geometry()
|
|
local w = term.cfg or M.config.win
|
|
local lines = vim.o.lines - vim.o.cmdheight
|
|
local width = math.floor(vim.o.columns * w.width)
|
|
local height = math.floor(lines * w.height)
|
|
return {
|
|
relative = "editor",
|
|
width = width,
|
|
height = height,
|
|
col = math.max(0, math.floor((vim.o.columns - width) / 2) - 1), -- less the border
|
|
row = math.max(0, math.floor((lines - height) / 2) - 1),
|
|
style = "minimal",
|
|
border = w.border,
|
|
}
|
|
end
|
|
|
|
local function show()
|
|
if not (term.win and vim.api.nvim_win_is_valid(term.win)) then
|
|
term.win = vim.api.nvim_open_win(term.buf, true, geometry())
|
|
vim.wo[term.win].winhighlight = "NormalFloat:Normal"
|
|
else
|
|
vim.api.nvim_set_current_win(term.win)
|
|
end
|
|
vim.cmd.startinsert()
|
|
end
|
|
|
|
local function hide()
|
|
if term.win and vim.api.nvim_win_is_valid(term.win) then
|
|
vim.api.nvim_win_close(term.win, true)
|
|
end
|
|
term.win = nil
|
|
end
|
|
|
|
-- The last line lazysecrets printed: why it stopped, when it failed.
|
|
local function last_line(buf)
|
|
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
|
for i = #lines, 1, -1 do
|
|
if lines[i]:match("%S") then
|
|
return vim.trim(lines[i])
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param opts? { args?: string[], win?: table, cwd?: string }
|
|
function M.open(opts)
|
|
opts = opts or {}
|
|
if vim.fn.executable(M.config.cmd) ~= 1 then
|
|
vim.notify("lazysecrets isn't installed (no `" .. M.config.cmd .. "` on PATH)", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
if running() and vim.api.nvim_get_current_buf() == term.buf then
|
|
return show() -- from the float itself: there's no other repo to be in
|
|
end
|
|
local args = opts.args or {}
|
|
local cwd = opts.cwd or start_dir() -- before the float is current
|
|
local key = table.concat({ vim.fs.root(cwd, ".git") or cwd, unpack(args) }, "\0")
|
|
if running() and term.key == key then
|
|
return show()
|
|
end
|
|
if running() then
|
|
term.stopped = true
|
|
vim.fn.jobstop(term.job)
|
|
end
|
|
hide()
|
|
if dirty then
|
|
write_theme()
|
|
end
|
|
|
|
local buf = vim.api.nvim_create_buf(false, true)
|
|
local this = { buf = buf, key = key, cfg = vim.tbl_extend("force", M.config.win, opts.win or {}) }
|
|
term = this
|
|
show()
|
|
this.job = vim.fn.jobstart(vim.list_extend({ M.config.cmd }, args), {
|
|
term = true,
|
|
cwd = cwd,
|
|
env = { LAZYSECRETS_CONFIG_FILE = config_files() },
|
|
on_exit = function(_, code)
|
|
vim.schedule(function()
|
|
local why = code ~= 0 and not this.stopped and vim.api.nvim_buf_is_valid(buf) and last_line(buf)
|
|
if term == this then
|
|
hide()
|
|
term = {}
|
|
end
|
|
if vim.api.nvim_buf_is_valid(buf) then
|
|
vim.api.nvim_buf_delete(buf, { force = true })
|
|
end
|
|
if why then
|
|
vim.notify(why, vim.log.levels.ERROR)
|
|
end
|
|
end)
|
|
end,
|
|
})
|
|
-- Scheduled: closing a window from its own WinLeave isn't allowed.
|
|
-- Not if it's been replaced, or is current again by then.
|
|
vim.api.nvim_create_autocmd("WinLeave", {
|
|
buffer = buf,
|
|
callback = function()
|
|
vim.schedule(function()
|
|
if term == this and vim.api.nvim_get_current_win() ~= term.win then
|
|
hide()
|
|
end
|
|
end)
|
|
end,
|
|
})
|
|
end
|
|
|
|
-- Here rather than in setup(), which is optional: they're needed once
|
|
-- the module is loaded, and before that there's nothing to update.
|
|
local group = vim.api.nvim_create_augroup("lazysecrets", { clear = true })
|
|
vim.api.nvim_create_autocmd("ColorScheme", {
|
|
group = group,
|
|
callback = function()
|
|
dirty = true
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd("VimResized", {
|
|
group = group,
|
|
callback = function()
|
|
if term.win and vim.api.nvim_win_is_valid(term.win) then
|
|
vim.api.nvim_win_set_config(term.win, geometry())
|
|
end
|
|
end,
|
|
})
|
|
|
|
---@param opts? table merged into M.config
|
|
function M.setup(opts)
|
|
M.config = vim.tbl_deep_extend("force", M.config, opts or {})
|
|
end
|
|
|
|
return M
|