mirror of
https://github.com/taigrr/glaze.nvim.git
synced 2026-04-02 11:19:16 -07:00
initial commit
This commit is contained in:
76
lua/glaze/colors.lua
Normal file
76
lua/glaze/colors.lua
Normal file
@@ -0,0 +1,76 @@
|
||||
---@brief [[
|
||||
--- glaze.nvim color definitions
|
||||
--- Charmbracelet-inspired palette with Lazy.nvim structure
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Charmbracelet-inspired colors (pink/magenta theme)
|
||||
M.colors = {
|
||||
-- Headers and accents
|
||||
H1 = { fg = "#FF6AD5", bold = true }, -- Charm pink
|
||||
H2 = { fg = "#C4A7E7", bold = true }, -- Soft purple
|
||||
Title = { fg = "#FF6AD5", bold = true },
|
||||
|
||||
-- Status indicators
|
||||
Done = { fg = "#9FFFCB" }, -- Mint green (success)
|
||||
Running = { fg = "#FFD866" }, -- Warm yellow (in progress)
|
||||
Pending = { fg = "#6E6A86" }, -- Muted gray
|
||||
Error = { fg = "#FF6B6B", bold = true }, -- Soft red
|
||||
|
||||
-- Content
|
||||
Normal = "NormalFloat",
|
||||
Binary = { fg = "#FF6AD5" }, -- Charm pink for binary names
|
||||
Url = { fg = "#7DCFFF", italic = true }, -- Bright blue for URLs
|
||||
Plugin = { fg = "#BB9AF7" }, -- Purple for plugin names
|
||||
Comment = "Comment",
|
||||
Dimmed = "Conceal",
|
||||
|
||||
-- Progress bar
|
||||
ProgressDone = { fg = "#FF6AD5" }, -- Charm pink
|
||||
ProgressTodo = { fg = "#3B3A52" }, -- Dark purple-gray
|
||||
|
||||
-- UI elements
|
||||
Border = { fg = "#FF6AD5" },
|
||||
Button = "CursorLine",
|
||||
ButtonActive = { bg = "#FF6AD5", fg = "#1A1B26", bold = true },
|
||||
Key = { fg = "#FFD866", bold = true }, -- Keybind highlights
|
||||
|
||||
-- Version info
|
||||
Version = { fg = "#9FFFCB" },
|
||||
Time = { fg = "#6E6A86", italic = true },
|
||||
|
||||
-- Icons
|
||||
Icon = { fg = "#FF6AD5" },
|
||||
IconDone = { fg = "#9FFFCB" },
|
||||
IconError = { fg = "#FF6B6B" },
|
||||
IconRunning = { fg = "#FFD866" },
|
||||
|
||||
Bold = { bold = true },
|
||||
Italic = { italic = true },
|
||||
}
|
||||
|
||||
M.did_setup = false
|
||||
|
||||
function M.set_hl()
|
||||
for name, def in pairs(M.colors) do
|
||||
local hl = type(def) == "table" and def or { link = def }
|
||||
hl.default = true
|
||||
vim.api.nvim_set_hl(0, "Glaze" .. name, hl)
|
||||
end
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
if M.did_setup then
|
||||
return
|
||||
end
|
||||
M.did_setup = true
|
||||
|
||||
M.set_hl()
|
||||
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
callback = M.set_hl,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
190
lua/glaze/float.lua
Normal file
190
lua/glaze/float.lua
Normal file
@@ -0,0 +1,190 @@
|
||||
---@brief [[
|
||||
--- glaze.nvim floating window (based on lazy.nvim)
|
||||
--- Creates centered floating window with backdrop
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class GlazeFloatOptions
|
||||
---@field size? { width: number, height: number }
|
||||
---@field border? string
|
||||
---@field title? string
|
||||
---@field zindex? number
|
||||
|
||||
---@class GlazeFloat
|
||||
---@field buf number
|
||||
---@field win number
|
||||
---@field opts GlazeFloatOptions
|
||||
---@field win_opts table
|
||||
---@field backdrop_buf? number
|
||||
---@field backdrop_win? number
|
||||
local Float = {}
|
||||
|
||||
local _id = 0
|
||||
|
||||
---@param opts? GlazeFloatOptions
|
||||
---@return GlazeFloat
|
||||
function Float.new(opts)
|
||||
local self = setmetatable({}, { __index = Float })
|
||||
return self:init(opts)
|
||||
end
|
||||
|
||||
---@param opts? GlazeFloatOptions
|
||||
function Float:init(opts)
|
||||
require("glaze.colors").setup()
|
||||
|
||||
_id = _id + 1
|
||||
self.id = _id
|
||||
|
||||
local config = require("glaze").config
|
||||
self.opts = vim.tbl_deep_extend("force", {
|
||||
size = config.ui.size,
|
||||
border = config.ui.border,
|
||||
zindex = 50,
|
||||
}, opts or {})
|
||||
|
||||
self.win_opts = {
|
||||
relative = "editor",
|
||||
style = "minimal",
|
||||
border = self.opts.border,
|
||||
zindex = self.opts.zindex,
|
||||
title = self.opts.title,
|
||||
title_pos = self.opts.title and "center" or nil,
|
||||
}
|
||||
|
||||
self:mount()
|
||||
return self
|
||||
end
|
||||
|
||||
function Float:layout()
|
||||
local function size(max, value)
|
||||
return value > 1 and math.min(value, max) or math.floor(max * value)
|
||||
end
|
||||
|
||||
self.win_opts.width = size(vim.o.columns, self.opts.size.width)
|
||||
self.win_opts.height = size(vim.o.lines - 4, self.opts.size.height)
|
||||
self.win_opts.row = math.floor((vim.o.lines - self.win_opts.height) / 2)
|
||||
self.win_opts.col = math.floor((vim.o.columns - self.win_opts.width) / 2)
|
||||
|
||||
if self.opts.border ~= "none" then
|
||||
self.win_opts.row = self.win_opts.row - 1
|
||||
self.win_opts.col = self.win_opts.col - 1
|
||||
end
|
||||
end
|
||||
|
||||
function Float:mount()
|
||||
self.buf = vim.api.nvim_create_buf(false, true)
|
||||
|
||||
-- Create backdrop
|
||||
local normal = vim.api.nvim_get_hl(0, { name = "Normal" })
|
||||
if normal.bg and vim.o.termguicolors then
|
||||
self.backdrop_buf = vim.api.nvim_create_buf(false, true)
|
||||
self.backdrop_win = vim.api.nvim_open_win(self.backdrop_buf, false, {
|
||||
relative = "editor",
|
||||
width = vim.o.columns,
|
||||
height = vim.o.lines,
|
||||
row = 0,
|
||||
col = 0,
|
||||
style = "minimal",
|
||||
focusable = false,
|
||||
zindex = self.opts.zindex - 1,
|
||||
})
|
||||
vim.api.nvim_set_hl(0, "GlazeBackdrop", { bg = "#000000", default = true })
|
||||
vim.wo[self.backdrop_win].winhighlight = "Normal:GlazeBackdrop"
|
||||
vim.wo[self.backdrop_win].winblend = 60
|
||||
vim.bo[self.backdrop_buf].buftype = "nofile"
|
||||
end
|
||||
|
||||
self:layout()
|
||||
self.win = vim.api.nvim_open_win(self.buf, true, self.win_opts)
|
||||
|
||||
-- Buffer settings
|
||||
vim.bo[self.buf].buftype = "nofile"
|
||||
vim.bo[self.buf].bufhidden = "wipe"
|
||||
vim.bo[self.buf].filetype = "glaze"
|
||||
|
||||
-- Window settings
|
||||
vim.wo[self.win].conceallevel = 3
|
||||
vim.wo[self.win].foldenable = false
|
||||
vim.wo[self.win].spell = false
|
||||
vim.wo[self.win].wrap = true
|
||||
vim.wo[self.win].winhighlight = "Normal:GlazeNormal,FloatBorder:GlazeBorder"
|
||||
vim.wo[self.win].cursorline = true
|
||||
|
||||
-- Keymaps
|
||||
self:map("q", function()
|
||||
self:close()
|
||||
end, "Close")
|
||||
self:map("<Esc>", function()
|
||||
self:close()
|
||||
end, "Close")
|
||||
|
||||
-- Auto-close on WinClosed
|
||||
vim.api.nvim_create_autocmd("WinClosed", {
|
||||
pattern = tostring(self.win),
|
||||
once = true,
|
||||
callback = function()
|
||||
self:close()
|
||||
end,
|
||||
})
|
||||
|
||||
-- Handle resize
|
||||
vim.api.nvim_create_autocmd("VimResized", {
|
||||
callback = function()
|
||||
if not self:valid() then
|
||||
return true
|
||||
end
|
||||
self:layout()
|
||||
vim.api.nvim_win_set_config(self.win, {
|
||||
relative = "editor",
|
||||
width = self.win_opts.width,
|
||||
height = self.win_opts.height,
|
||||
row = self.win_opts.row,
|
||||
col = self.win_opts.col,
|
||||
})
|
||||
if self.backdrop_win and vim.api.nvim_win_is_valid(self.backdrop_win) then
|
||||
vim.api.nvim_win_set_config(self.backdrop_win, {
|
||||
width = vim.o.columns,
|
||||
height = vim.o.lines,
|
||||
})
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@param key string
|
||||
---@param fn function
|
||||
---@param desc string
|
||||
function Float:map(key, fn, desc)
|
||||
vim.keymap.set("n", key, fn, { buffer = self.buf, nowait = true, desc = desc })
|
||||
end
|
||||
|
||||
function Float:valid()
|
||||
return self.win and vim.api.nvim_win_is_valid(self.win)
|
||||
end
|
||||
|
||||
function Float:close()
|
||||
vim.schedule(function()
|
||||
if self.backdrop_win and vim.api.nvim_win_is_valid(self.backdrop_win) then
|
||||
vim.api.nvim_win_close(self.backdrop_win, true)
|
||||
end
|
||||
if self.backdrop_buf and vim.api.nvim_buf_is_valid(self.backdrop_buf) then
|
||||
vim.api.nvim_buf_delete(self.backdrop_buf, { force = true })
|
||||
end
|
||||
if self.win and vim.api.nvim_win_is_valid(self.win) then
|
||||
vim.api.nvim_win_close(self.win, true)
|
||||
end
|
||||
if self.buf and vim.api.nvim_buf_is_valid(self.buf) then
|
||||
vim.api.nvim_buf_delete(self.buf, { force = true })
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Float:width()
|
||||
return self.win_opts.width
|
||||
end
|
||||
|
||||
M.Float = Float
|
||||
|
||||
return M
|
||||
148
lua/glaze/init.lua
Normal file
148
lua/glaze/init.lua
Normal file
@@ -0,0 +1,148 @@
|
||||
---@brief [[
|
||||
--- glaze.nvim - Centralized Go binary management for Neovim plugins
|
||||
---
|
||||
--- A Mason/Lazy-style interface for managing Go binaries across multiple plugins.
|
||||
--- Register your plugin's binaries once, update them all with a single command.
|
||||
---
|
||||
--- Usage:
|
||||
--- require("glaze").setup({})
|
||||
--- require("glaze").register("freeze", "github.com/charmbracelet/freeze")
|
||||
--- require("glaze").register("glow", "github.com/charmbracelet/glow")
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class GlazeBinary
|
||||
---@field name string Binary name (executable name)
|
||||
---@field url string Go module URL (without @version)
|
||||
---@field plugin? string Plugin that registered this binary
|
||||
---@field callback? fun(success: boolean) Optional callback after install/update
|
||||
|
||||
---@class GlazeConfig
|
||||
---@field ui GlazeUIConfig
|
||||
---@field concurrency number Max parallel installations
|
||||
---@field go_cmd string[] Go command (supports goenv)
|
||||
|
||||
---@class GlazeUIConfig
|
||||
---@field border string Border style
|
||||
---@field size { width: number, height: number }
|
||||
---@field icons GlazeIcons
|
||||
|
||||
---@class GlazeIcons
|
||||
---@field pending string
|
||||
---@field running string
|
||||
---@field done string
|
||||
---@field error string
|
||||
---@field binary string
|
||||
|
||||
---@type GlazeConfig
|
||||
M.config = {
|
||||
ui = {
|
||||
border = "rounded",
|
||||
size = { width = 0.7, height = 0.8 },
|
||||
icons = {
|
||||
pending = "○",
|
||||
running = "◐",
|
||||
done = "●",
|
||||
error = "✗",
|
||||
binary = "",
|
||||
},
|
||||
},
|
||||
concurrency = 4,
|
||||
go_cmd = { "go" },
|
||||
}
|
||||
|
||||
---@type table<string, GlazeBinary>
|
||||
M._binaries = {}
|
||||
|
||||
---@type number?
|
||||
M._ns = nil
|
||||
|
||||
---@param opts? GlazeConfig
|
||||
function M.setup(opts)
|
||||
M.config = vim.tbl_deep_extend("force", M.config, opts or {})
|
||||
M._ns = vim.api.nvim_create_namespace("glaze")
|
||||
|
||||
-- Auto-detect goenv
|
||||
if vim.fn.executable("goenv") == 1 then
|
||||
M.config.go_cmd = { "goenv", "exec", "go" }
|
||||
end
|
||||
|
||||
-- Create commands
|
||||
vim.api.nvim_create_user_command("Glaze", function()
|
||||
require("glaze.view").open()
|
||||
end, { desc = "Open Glaze UI" })
|
||||
|
||||
vim.api.nvim_create_user_command("GlazeUpdate", function(cmd)
|
||||
if cmd.args and cmd.args ~= "" then
|
||||
require("glaze.runner").update({ cmd.args })
|
||||
else
|
||||
require("glaze.runner").update_all()
|
||||
end
|
||||
end, {
|
||||
desc = "Update Go binaries",
|
||||
nargs = "?",
|
||||
complete = function()
|
||||
return vim.tbl_keys(M._binaries)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command("GlazeInstall", function(cmd)
|
||||
if cmd.args and cmd.args ~= "" then
|
||||
require("glaze.runner").install({ cmd.args })
|
||||
else
|
||||
require("glaze.runner").install_missing()
|
||||
end
|
||||
end, {
|
||||
desc = "Install Go binaries",
|
||||
nargs = "?",
|
||||
complete = function()
|
||||
return vim.tbl_keys(M._binaries)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---Register a binary for management.
|
||||
---@param name string Binary/executable name
|
||||
---@param url string Go module URL (e.g., "github.com/charmbracelet/freeze")
|
||||
---@param opts? { plugin?: string, callback?: fun(success: boolean) }
|
||||
function M.register(name, url, opts)
|
||||
opts = opts or {}
|
||||
M._binaries[name] = {
|
||||
name = name,
|
||||
url = url,
|
||||
plugin = opts.plugin,
|
||||
callback = opts.callback,
|
||||
}
|
||||
end
|
||||
|
||||
---Unregister a binary.
|
||||
---@param name string Binary name to unregister
|
||||
function M.unregister(name)
|
||||
M._binaries[name] = nil
|
||||
end
|
||||
|
||||
---Get all registered binaries.
|
||||
---@return table<string, GlazeBinary>
|
||||
function M.binaries()
|
||||
return M._binaries
|
||||
end
|
||||
|
||||
---Check if a binary is installed.
|
||||
---@param name string Binary name
|
||||
---@return boolean
|
||||
function M.is_installed(name)
|
||||
return vim.fn.executable(name) == 1
|
||||
end
|
||||
|
||||
---Get binary installation status.
|
||||
---@param name string Binary name
|
||||
---@return "installed"|"missing"|"unknown"
|
||||
function M.status(name)
|
||||
if not M._binaries[name] then
|
||||
return "unknown"
|
||||
end
|
||||
return M.is_installed(name) and "installed" or "missing"
|
||||
end
|
||||
|
||||
return M
|
||||
242
lua/glaze/runner.lua
Normal file
242
lua/glaze/runner.lua
Normal file
@@ -0,0 +1,242 @@
|
||||
---@brief [[
|
||||
--- glaze.nvim task runner
|
||||
--- Handles parallel Go binary installation/updates
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class GlazeTask
|
||||
---@field binary GlazeBinary
|
||||
---@field status "pending"|"running"|"done"|"error"
|
||||
---@field output string[]
|
||||
---@field start_time? number
|
||||
---@field end_time? number
|
||||
---@field job_id? number
|
||||
|
||||
---@type GlazeTask[]
|
||||
M._tasks = {}
|
||||
|
||||
---@type function?
|
||||
M._on_update = nil
|
||||
|
||||
---@type boolean
|
||||
M._running = false
|
||||
|
||||
---Get all current tasks.
|
||||
---@return GlazeTask[]
|
||||
function M.tasks()
|
||||
return M._tasks
|
||||
end
|
||||
|
||||
---Check if runner is active.
|
||||
---@return boolean
|
||||
function M.is_running()
|
||||
return M._running
|
||||
end
|
||||
|
||||
---Set update callback (called when task status changes).
|
||||
---@param fn function
|
||||
function M.on_update(fn)
|
||||
M._on_update = fn
|
||||
end
|
||||
|
||||
---@private
|
||||
function M._notify()
|
||||
if M._on_update then
|
||||
vim.schedule(M._on_update)
|
||||
end
|
||||
end
|
||||
|
||||
---@param binary GlazeBinary
|
||||
---@return GlazeTask
|
||||
local function create_task(binary)
|
||||
return {
|
||||
binary = binary,
|
||||
status = "pending",
|
||||
output = {},
|
||||
}
|
||||
end
|
||||
|
||||
---@param task GlazeTask
|
||||
local function run_task(task)
|
||||
local glaze = require("glaze")
|
||||
local cmd = vim.list_extend({}, glaze.config.go_cmd)
|
||||
table.insert(cmd, "install")
|
||||
table.insert(cmd, task.binary.url .. "@latest")
|
||||
|
||||
task.status = "running"
|
||||
task.start_time = vim.uv.hrtime()
|
||||
task.output = {}
|
||||
M._notify()
|
||||
|
||||
task.job_id = vim.fn.jobstart(cmd, {
|
||||
on_stdout = function(_, data)
|
||||
if data then
|
||||
for _, line in ipairs(data) do
|
||||
if line ~= "" then
|
||||
table.insert(task.output, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
on_stderr = function(_, data)
|
||||
if data then
|
||||
for _, line in ipairs(data) do
|
||||
if line ~= "" then
|
||||
table.insert(task.output, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
on_exit = function(_, code)
|
||||
task.end_time = vim.uv.hrtime()
|
||||
task.status = code == 0 and "done" or "error"
|
||||
task.job_id = nil
|
||||
|
||||
-- Call binary callback if set
|
||||
if task.binary.callback then
|
||||
vim.schedule(function()
|
||||
task.binary.callback(code == 0)
|
||||
end)
|
||||
end
|
||||
|
||||
M._notify()
|
||||
M._process_queue()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@private
|
||||
function M._process_queue()
|
||||
local glaze = require("glaze")
|
||||
local running = 0
|
||||
local pending = {}
|
||||
|
||||
for _, task in ipairs(M._tasks) do
|
||||
if task.status == "running" then
|
||||
running = running + 1
|
||||
elseif task.status == "pending" then
|
||||
table.insert(pending, task)
|
||||
end
|
||||
end
|
||||
|
||||
-- Start pending tasks up to concurrency limit
|
||||
local to_start = math.min(#pending, glaze.config.concurrency - running)
|
||||
for i = 1, to_start do
|
||||
run_task(pending[i])
|
||||
end
|
||||
|
||||
-- Check if all done
|
||||
if running == 0 and #pending == 0 then
|
||||
M._running = false
|
||||
M._notify()
|
||||
end
|
||||
end
|
||||
|
||||
---Run tasks for specified binaries.
|
||||
---@param names string[]
|
||||
---@param mode "install"|"update"
|
||||
local function run(names, mode)
|
||||
local glaze = require("glaze")
|
||||
|
||||
-- Check for Go
|
||||
local go_check = glaze.config.go_cmd[1]
|
||||
if vim.fn.executable(go_check) ~= 1 then
|
||||
vim.notify("Go is not installed. Please install Go first: https://go.dev/dl/", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Filter binaries
|
||||
local binaries = {}
|
||||
for _, name in ipairs(names) do
|
||||
local binary = glaze._binaries[name]
|
||||
if binary then
|
||||
if mode == "install" and glaze.is_installed(name) then
|
||||
-- Skip already installed
|
||||
else
|
||||
table.insert(binaries, binary)
|
||||
end
|
||||
else
|
||||
vim.notify("Unknown binary: " .. name, vim.log.levels.WARN)
|
||||
end
|
||||
end
|
||||
|
||||
if #binaries == 0 then
|
||||
if mode == "install" then
|
||||
vim.notify("All binaries already installed", vim.log.levels.INFO)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Create tasks
|
||||
M._tasks = {}
|
||||
for _, binary in ipairs(binaries) do
|
||||
table.insert(M._tasks, create_task(binary))
|
||||
end
|
||||
|
||||
M._running = true
|
||||
M._notify()
|
||||
M._process_queue()
|
||||
|
||||
-- Open UI
|
||||
require("glaze.view").open()
|
||||
end
|
||||
|
||||
---Update specific binaries.
|
||||
---@param names string[]
|
||||
function M.update(names)
|
||||
run(names, "update")
|
||||
end
|
||||
|
||||
---Update all registered binaries.
|
||||
function M.update_all()
|
||||
local glaze = require("glaze")
|
||||
run(vim.tbl_keys(glaze._binaries), "update")
|
||||
end
|
||||
|
||||
---Install specific binaries.
|
||||
---@param names string[]
|
||||
function M.install(names)
|
||||
run(names, "install")
|
||||
end
|
||||
|
||||
---Install all missing binaries.
|
||||
function M.install_missing()
|
||||
local glaze = require("glaze")
|
||||
local missing = {}
|
||||
for name, _ in pairs(glaze._binaries) do
|
||||
if not glaze.is_installed(name) then
|
||||
table.insert(missing, name)
|
||||
end
|
||||
end
|
||||
if #missing > 0 then
|
||||
run(missing, "install")
|
||||
else
|
||||
vim.notify("All binaries already installed", vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
|
||||
---Abort all running tasks.
|
||||
function M.abort()
|
||||
for _, task in ipairs(M._tasks) do
|
||||
if task.job_id then
|
||||
vim.fn.jobstop(task.job_id)
|
||||
task.status = "error"
|
||||
table.insert(task.output, "Aborted by user")
|
||||
end
|
||||
end
|
||||
M._running = false
|
||||
M._notify()
|
||||
end
|
||||
|
||||
---Get task statistics.
|
||||
---@return { total: number, done: number, error: number, running: number, pending: number }
|
||||
function M.stats()
|
||||
local stats = { total = #M._tasks, done = 0, error = 0, running = 0, pending = 0 }
|
||||
for _, task in ipairs(M._tasks) do
|
||||
stats[task.status] = (stats[task.status] or 0) + 1
|
||||
end
|
||||
return stats
|
||||
end
|
||||
|
||||
return M
|
||||
132
lua/glaze/text.lua
Normal file
132
lua/glaze/text.lua
Normal file
@@ -0,0 +1,132 @@
|
||||
---@brief [[
|
||||
--- glaze.nvim text rendering (based on lazy.nvim)
|
||||
--- Handles buffered text with highlight segments
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class GlazeTextSegment
|
||||
---@field str string
|
||||
---@field hl? string|GlazeExtmark
|
||||
|
||||
---@class GlazeExtmark
|
||||
---@field hl_group? string
|
||||
---@field col? number
|
||||
---@field end_col? number
|
||||
---@field virt_text? table
|
||||
---@field virt_text_win_col? number
|
||||
|
||||
---@class GlazeText
|
||||
---@field _lines GlazeTextSegment[][]
|
||||
---@field padding number
|
||||
---@field wrap number
|
||||
local Text = {}
|
||||
|
||||
function Text.new()
|
||||
local self = setmetatable({}, { __index = Text })
|
||||
self._lines = {}
|
||||
self.padding = 2
|
||||
self.wrap = 80
|
||||
return self
|
||||
end
|
||||
|
||||
---@param str string
|
||||
---@param hl? string|GlazeExtmark
|
||||
---@param opts? { indent?: number, wrap?: boolean }
|
||||
---@return GlazeText
|
||||
function Text:append(str, hl, opts)
|
||||
opts = opts or {}
|
||||
if #self._lines == 0 then
|
||||
self:nl()
|
||||
end
|
||||
|
||||
local lines = vim.split(str, "\n")
|
||||
for i, line in ipairs(lines) do
|
||||
if opts.indent then
|
||||
line = string.rep(" ", opts.indent) .. line
|
||||
end
|
||||
if i > 1 then
|
||||
self:nl()
|
||||
end
|
||||
-- Handle wrap
|
||||
if opts.wrap and str ~= "" and self:col() > 0 and self:col() + vim.fn.strwidth(line) + self.padding > self.wrap then
|
||||
self:nl()
|
||||
end
|
||||
table.insert(self._lines[#self._lines], { str = line, hl = hl })
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
---@return GlazeText
|
||||
function Text:nl()
|
||||
table.insert(self._lines, {})
|
||||
return self
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Text:row()
|
||||
return #self._lines == 0 and 1 or #self._lines
|
||||
end
|
||||
|
||||
---@return number
|
||||
function Text:col()
|
||||
if #self._lines == 0 then
|
||||
return 0
|
||||
end
|
||||
local width = 0
|
||||
for _, segment in ipairs(self._lines[#self._lines]) do
|
||||
width = width + vim.fn.strwidth(segment.str)
|
||||
end
|
||||
return width
|
||||
end
|
||||
|
||||
function Text:trim()
|
||||
while #self._lines > 0 and #self._lines[#self._lines] == 0 do
|
||||
table.remove(self._lines)
|
||||
end
|
||||
end
|
||||
|
||||
---@param buf number
|
||||
---@param ns number
|
||||
function Text:render(buf, ns)
|
||||
local lines = {}
|
||||
|
||||
for _, line in ipairs(self._lines) do
|
||||
local str = string.rep(" ", self.padding)
|
||||
for _, segment in ipairs(line) do
|
||||
str = str .. segment.str
|
||||
end
|
||||
if str:match("^%s*$") then
|
||||
str = ""
|
||||
end
|
||||
table.insert(lines, str)
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)
|
||||
|
||||
for l, line in ipairs(self._lines) do
|
||||
if lines[l] ~= "" then
|
||||
local col = self.padding
|
||||
for _, segment in ipairs(line) do
|
||||
local width = vim.fn.strwidth(segment.str)
|
||||
local extmark = segment.hl
|
||||
|
||||
if extmark then
|
||||
if type(extmark) == "string" then
|
||||
extmark = { hl_group = extmark, end_col = col + width }
|
||||
end
|
||||
---@cast extmark GlazeExtmark
|
||||
local extmark_col = extmark.col or col
|
||||
extmark.col = nil
|
||||
pcall(vim.api.nvim_buf_set_extmark, buf, ns, l - 1, extmark_col, extmark)
|
||||
end
|
||||
col = col + width
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.Text = Text
|
||||
|
||||
return M
|
||||
264
lua/glaze/view.lua
Normal file
264
lua/glaze/view.lua
Normal file
@@ -0,0 +1,264 @@
|
||||
---@brief [[
|
||||
--- glaze.nvim view/UI
|
||||
--- Lazy.nvim-style floating window with Charmbracelet aesthetic
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type GlazeFloat?
|
||||
M._float = nil
|
||||
|
||||
---@type number?
|
||||
M._timer = nil
|
||||
|
||||
local SPINNERS = { "◐", "◓", "◑", "◒" }
|
||||
local SPINNER_IDX = 1
|
||||
|
||||
---Open the Glaze UI.
|
||||
function M.open()
|
||||
local Float = require("glaze.float").Float
|
||||
|
||||
if M._float and M._float:valid() then
|
||||
M._float:close()
|
||||
end
|
||||
|
||||
M._float = Float.new({
|
||||
title = " Glaze ",
|
||||
})
|
||||
|
||||
-- Set up keymaps
|
||||
M._float:map("u", function()
|
||||
require("glaze.runner").update_all()
|
||||
end, "Update all")
|
||||
|
||||
M._float:map("i", function()
|
||||
require("glaze.runner").install_missing()
|
||||
end, "Install missing")
|
||||
|
||||
M._float:map("x", function()
|
||||
require("glaze.runner").abort()
|
||||
end, "Abort")
|
||||
|
||||
M._float:map("<CR>", function()
|
||||
M._toggle_details()
|
||||
end, "Toggle details")
|
||||
|
||||
-- Subscribe to runner updates
|
||||
require("glaze.runner").on_update(function()
|
||||
M.render()
|
||||
end)
|
||||
|
||||
-- Start update timer for spinner
|
||||
M._start_timer()
|
||||
|
||||
M.render()
|
||||
end
|
||||
|
||||
---@private
|
||||
function M._start_timer()
|
||||
if M._timer then
|
||||
vim.fn.timer_stop(M._timer)
|
||||
end
|
||||
|
||||
M._timer = vim.fn.timer_start(100, function()
|
||||
if not M._float or not M._float:valid() then
|
||||
if M._timer then
|
||||
vim.fn.timer_stop(M._timer)
|
||||
M._timer = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local runner = require("glaze.runner")
|
||||
if runner.is_running() then
|
||||
SPINNER_IDX = (SPINNER_IDX % #SPINNERS) + 1
|
||||
vim.schedule(function()
|
||||
M.render()
|
||||
end)
|
||||
end
|
||||
end, { ["repeat"] = -1 })
|
||||
end
|
||||
|
||||
---@private
|
||||
function M._toggle_details()
|
||||
-- TODO: Implement detail expansion
|
||||
end
|
||||
|
||||
---Render the UI.
|
||||
function M.render()
|
||||
if not M._float or not M._float:valid() then
|
||||
return
|
||||
end
|
||||
|
||||
local glaze = require("glaze")
|
||||
local runner = require("glaze.runner")
|
||||
local Text = require("glaze.text").Text
|
||||
|
||||
local text = Text.new()
|
||||
text.wrap = M._float:width() - 4
|
||||
|
||||
local icons = glaze.config.ui.icons
|
||||
|
||||
-- Header
|
||||
text:nl()
|
||||
text:append(" ", "GlazeIcon"):append("Glaze", "GlazeH1"):append(" Go Binary Manager", "GlazeComment"):nl()
|
||||
text:nl()
|
||||
|
||||
-- Stats / Progress
|
||||
local stats = runner.stats()
|
||||
if stats.total > 0 then
|
||||
M._render_progress(text, stats)
|
||||
text:nl()
|
||||
end
|
||||
|
||||
-- Keybinds
|
||||
text:append(" ", nil, { indent = 2 })
|
||||
text:append(" u ", "GlazeButtonActive"):append(" Update ", "GlazeButton")
|
||||
text:append(" ")
|
||||
text:append(" i ", "GlazeButtonActive"):append(" Install ", "GlazeButton")
|
||||
text:append(" ")
|
||||
text:append(" x ", "GlazeButtonActive"):append(" Abort ", "GlazeButton")
|
||||
text:append(" ")
|
||||
text:append(" q ", "GlazeButtonActive"):append(" Close ", "GlazeButton")
|
||||
text:nl():nl()
|
||||
|
||||
-- Tasks section (if running)
|
||||
if stats.total > 0 then
|
||||
text:append("Tasks", "GlazeH2"):append(" (" .. stats.done .. "/" .. stats.total .. ")", "GlazeComment"):nl()
|
||||
text:nl()
|
||||
|
||||
for _, task in ipairs(runner.tasks()) do
|
||||
M._render_task(text, task, icons)
|
||||
end
|
||||
text:nl()
|
||||
end
|
||||
|
||||
-- Registered binaries
|
||||
local binaries = glaze.binaries()
|
||||
local binary_count = vim.tbl_count(binaries)
|
||||
|
||||
text:append("Binaries", "GlazeH2"):append(" (" .. binary_count .. ")", "GlazeComment"):nl()
|
||||
text:nl()
|
||||
|
||||
if binary_count == 0 then
|
||||
text:append("No binaries registered yet.", "GlazeComment", { indent = 4 }):nl()
|
||||
text:append("Use ", "GlazeComment", { indent = 4 })
|
||||
text:append('require("glaze").register(name, url)', "GlazeUrl")
|
||||
text:append(" to add binaries.", "GlazeComment"):nl()
|
||||
else
|
||||
-- Sort by name
|
||||
local sorted = {}
|
||||
for name, binary in pairs(binaries) do
|
||||
table.insert(sorted, { name = name, binary = binary })
|
||||
end
|
||||
table.sort(sorted, function(a, b)
|
||||
return a.name < b.name
|
||||
end)
|
||||
|
||||
for _, item in ipairs(sorted) do
|
||||
M._render_binary(text, item.binary, icons)
|
||||
end
|
||||
end
|
||||
|
||||
text:trim()
|
||||
|
||||
-- Render to buffer
|
||||
vim.bo[M._float.buf].modifiable = true
|
||||
text:render(M._float.buf, glaze._ns)
|
||||
vim.bo[M._float.buf].modifiable = false
|
||||
end
|
||||
|
||||
---@param text GlazeText
|
||||
---@param stats table
|
||||
---@private
|
||||
function M._render_progress(text, stats)
|
||||
if not M._float then
|
||||
return
|
||||
end
|
||||
|
||||
local width = M._float:width() - 6
|
||||
local done_ratio = stats.total > 0 and (stats.done / stats.total) or 0
|
||||
local done_width = math.floor(done_ratio * width + 0.5)
|
||||
|
||||
if stats.done < stats.total then
|
||||
text:append("", {
|
||||
virt_text_win_col = 2,
|
||||
virt_text = { { string.rep("━", done_width), "GlazeProgressDone" } },
|
||||
})
|
||||
text:append("", {
|
||||
virt_text_win_col = 2 + done_width,
|
||||
virt_text = { { string.rep("━", width - done_width), "GlazeProgressTodo" } },
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
---@param text GlazeText
|
||||
---@param task GlazeTask
|
||||
---@param icons GlazeIcons
|
||||
---@private
|
||||
function M._render_task(text, task, icons)
|
||||
local icon, icon_hl
|
||||
if task.status == "done" then
|
||||
icon, icon_hl = icons.done, "GlazeIconDone"
|
||||
elseif task.status == "error" then
|
||||
icon, icon_hl = icons.error, "GlazeIconError"
|
||||
elseif task.status == "running" then
|
||||
icon, icon_hl = SPINNERS[SPINNER_IDX], "GlazeIconRunning"
|
||||
else
|
||||
icon, icon_hl = icons.pending, "GlazePending"
|
||||
end
|
||||
|
||||
text:append(" " .. icon .. " ", icon_hl)
|
||||
text:append(task.binary.name, "GlazeBinary")
|
||||
|
||||
-- Time taken
|
||||
if task.end_time and task.start_time then
|
||||
local ms = (task.end_time - task.start_time) / 1e6
|
||||
text:append(string.format(" %.0fms", ms), "GlazeTime")
|
||||
end
|
||||
|
||||
text:nl()
|
||||
|
||||
-- Show output for errors or running tasks
|
||||
if task.status == "error" and #task.output > 0 then
|
||||
for _, line in ipairs(task.output) do
|
||||
text:append(line, "GlazeError", { indent = 6 }):nl()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param text GlazeText
|
||||
---@param binary GlazeBinary
|
||||
---@param icons GlazeIcons
|
||||
---@private
|
||||
function M._render_binary(text, binary, icons)
|
||||
local glaze = require("glaze")
|
||||
local installed = glaze.is_installed(binary.name)
|
||||
|
||||
local icon = installed and icons.done or icons.pending
|
||||
local icon_hl = installed and "GlazeIconDone" or "GlazePending"
|
||||
|
||||
text:append(" " .. icon .. " ", icon_hl)
|
||||
text:append(binary.name, "GlazeBinary")
|
||||
|
||||
if binary.plugin then
|
||||
text:append(" (" .. binary.plugin .. ")", "GlazePlugin")
|
||||
end
|
||||
|
||||
text:nl()
|
||||
text:append(binary.url, "GlazeUrl", { indent = 6 }):nl()
|
||||
end
|
||||
|
||||
---Close the UI.
|
||||
function M.close()
|
||||
if M._timer then
|
||||
vim.fn.timer_stop(M._timer)
|
||||
M._timer = nil
|
||||
end
|
||||
if M._float then
|
||||
M._float:close()
|
||||
M._float = nil
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user