Initial commit

This commit is contained in:
keb authored and Karl-Einar Bergstrøm committed 2026-10-12 19:26:53 +02:00
commit e5740bada3
8 files changed
+772

No files matched your search

+256
View File
@@ -0,0 +1,256 @@
-- 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