feat: bug fixes, lazy.nvim-style controls, auto-update checking, repo polish

Phase 1 - Bug fixes:
- Fix extmark rendering in text.lua (table vs string hl handling)
- Fix timer leak: WinClosed autocmd now triggers view.close()
- Add GOBIN/GOPATH/~/go/bin awareness to is_installed() and new bin_path()
- Reject update_all() while tasks running (race condition fix)
- Implement _toggle_details with full binary info expansion

Phase 2 - Lazy.nvim-style controls:
- U = Update all, u = update cursor binary
- I = Install all missing, i = install cursor binary
- x = Abort, CR = toggle details, q/Esc = close
- Line-to-binary mapping for cursor-aware actions

Phase 3 - Auto-update checking:
- New checker module with go list/go version -m integration
- auto_check config option (daily/weekly/custom frequency)
- Persistent state in stdpath('data')/glaze/state.json
- :GlazeCheck command for manual checks
- Update indicators in UI (version info + arrows)

Phase 4 - Repo polish:
- MIT LICENSE file
- Doughnut-themed README with badges and Why Glaze? section
- Updated help docs with all new features/keybinds/API
This commit is contained in:
2026-02-19 00:47:19 +00:00
parent 60c2fd8739
commit 2ac8eb1899
9 changed files with 674 additions and 48 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Tai Groot
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,16 +1,33 @@
# glaze.nvim 󱓞
# 🍩 glaze.nvim
> A **Mason/Lazy-style** interface for managing Go binaries across Neovim plugins.
> **Go + Lazy = Glaze** — A Mason/Lazy-style manager for Go binaries in Neovim.
> Charmbracelet-inspired aesthetic. Zero duplication. One source of truth.
>
> *Like a fresh doughnut glaze — smooth, sweet, and holds everything together.*
![Lua](https://img.shields.io/badge/Lua-2C2D72?style=flat&logo=lua&logoColor=white)
![Neovim](https://img.shields.io/badge/Neovim%200.9+-57A143?style=flat&logo=neovim&logoColor=white)
![Go Required](https://img.shields.io/badge/Go-required-00ADD8?style=flat&logo=go&logoColor=white)
![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
## 🤔 Why Glaze?
Every Go-based Neovim plugin reinvents the wheel: each one ships its own
`go install` wrapper, its own update command, its own version checking.
**Glaze stops the madness.** Register your binaries once, manage them all from
one beautiful UI. Plugin authors get a two-line integration. Users get a single
`:Glaze` command.
## ✨ Features
- **Centralized binary management** — Register binaries from any plugin, update them all at once
- **Lazy.nvim-style UI** — Floating window with progress bars, spinners, and status indicators
- **Lazy.nvim-style UI** — Floating window with progress bars, spinners, and status indicators
- **Cursor-aware keybinds** — `u` updates the binary under your cursor, `U` updates all
- **Parallel installations** — Configurable concurrency for fast updates
- **Auto-update checking** — Daily/weekly checks with non-intrusive notifications
- **GOBIN/GOPATH awareness** — Finds binaries even if not in PATH
- **Detail expansion** — Press `<CR>` to see URL, install path, version info
- **Charmbracelet aesthetic** — Pink/magenta color scheme that matches the Charm toolchain
- **Zero config for dependents** — Just register and go
- **Callback support** — Get notified when your binary is updated
@@ -51,26 +68,30 @@ glaze.register("mods", "github.com/charmbracelet/mods")
| `:Glaze` | Open the Glaze UI |
| `:GlazeUpdate [name]` | Update all or specific binary |
| `:GlazeInstall [name]` | Install missing or specific binary |
| `:GlazeCheck` | Manually check for available updates |
### Keybinds (in Glaze UI)
| Key | Action |
|-----|--------|
| `u` | Update all binaries |
| `i` | Install missing binaries |
| `U` | Update ALL binaries |
| `u` | Update binary under cursor |
| `I` | Install all missing binaries |
| `i` | Install binary under cursor |
| `x` | Abort running tasks |
| `<CR>` | Toggle details (URL, path, version) |
| `q` / `<Esc>` | Close window |
## 🔌 For Plugin Authors
Register your plugin's binaries as a dependency:
Register your plugin's binaries as a dependency — two lines is all it takes:
```lua
-- In your plugin's setup or init:
local ok, glaze = pcall(require, "glaze")
if ok then
glaze.register("mytool", "github.com/me/mytool", {
plugin = "myplugin.nvim", -- Shows in UI
plugin = "myplugin.nvim",
callback = function(success)
if success then
vim.notify("mytool updated!")
@@ -86,7 +107,6 @@ You can still expose plugin-specific commands that delegate to Glaze:
```lua
vim.api.nvim_create_user_command("MyPluginUpdate", function()
local glaze = require("glaze")
require("glaze.runner").update({ "mytool" })
end, {})
```
@@ -108,6 +128,10 @@ require("glaze").setup({
},
concurrency = 4, -- Max parallel installations
go_cmd = { "go" }, -- Auto-detects goenv if available
auto_check = {
enabled = true, -- Auto-check for updates
frequency = "daily", -- "daily", "weekly", or hours as number
},
})
```
@@ -139,7 +163,8 @@ glaze.unregister(name) -- Remove a binary
glaze.binaries() -- Get all registered binaries
-- Status
glaze.is_installed(name) -- Check if binary exists
glaze.is_installed(name) -- Check if binary exists (PATH + GOBIN + GOPATH)
glaze.bin_path(name) -- Get full path to binary
glaze.status(name) -- "installed", "missing", or "unknown"
-- Runner (for programmatic control)
@@ -152,15 +177,22 @@ runner.abort() -- Stop all tasks
runner.is_running() -- Check if tasks are running
runner.tasks() -- Get current task list
runner.stats() -- Get { total, done, error, running, pending }
-- Update checker
local checker = require("glaze.checker")
checker.check() -- Check for updates (with notifications)
checker.auto_check() -- Check only if enough time has passed
checker.get_update_info() -- Get cached update info
```
## 🤝 Related Projects
- [freeze.nvim](https://github.com/taigrr/freeze.nvim) — Screenshot code with freeze
- [neocrush.nvim](https://github.com/taigrr/neocrush.nvim) — AI-powered coding assistant
- [blast.nvim](https://github.com/taigrr/blast.nvim) — Code activity tracking
- [lazy.nvim](https://github.com/folke/lazy.nvim) — UI inspiration
- [mason.nvim](https://github.com/williamboman/mason.nvim) — Concept inspiration
## 📄 License
MIT
MIT © [Tai Groot](https://github.com/taigrr)

View File

@@ -1,4 +1,4 @@
*glaze.txt* Centralized Go binary management for Neovim plugins
*glaze.txt* 🍩 Centralized Go binary management for Neovim plugins
Author: Tai Groot <tai@taigrr.com>
License: MIT
@@ -10,19 +10,25 @@ CONTENTS *glaze-contents*
1. Introduction ........................... |glaze-introduction|
2. Setup .................................. |glaze-setup|
3. Commands ............................... |glaze-commands|
4. API .................................... |glaze-api|
5. Configuration .......................... |glaze-config|
6. Highlights ............................. |glaze-highlights|
7. For Plugin Authors ..................... |glaze-plugin-authors|
4. Keybinds ............................... |glaze-keybinds|
5. API .................................... |glaze-api|
6. Configuration .......................... |glaze-config|
7. Highlights ............................. |glaze-highlights|
8. Auto-update Checking ................... |glaze-auto-check|
9. For Plugin Authors ..................... |glaze-plugin-authors|
==============================================================================
1. INTRODUCTION *glaze-introduction*
Glaze is a centralized manager for Go binaries used by Neovim plugins. Instead
of each plugin implementing its own binary installer, Glaze provides:
Glaze (Go + Lazy = Glaze 🍩) is a centralized manager for Go binaries used by
Neovim plugins. Instead of each plugin implementing its own binary installer,
Glaze provides:
- A unified UI for managing all Go binaries
- Parallel installation with progress tracking
- A unified lazy.nvim-style UI for managing all Go binaries
- Parallel installation with progress tracking
- Cursor-aware keybinds for individual binary control
- Automatic update checking with notifications
- GOBIN/GOPATH awareness
- A simple registration API for plugin authors
==============================================================================
@@ -50,8 +56,26 @@ Using lazy.nvim:
*:GlazeInstall*
:GlazeInstall [name] Install missing binaries, or a specific one.
*:GlazeCheck*
:GlazeCheck Manually check for available updates.
==============================================================================
4. API *glaze-api*
4. KEYBINDS *glaze-keybinds*
These keybinds are active in the Glaze UI window:
Key Action ~
`U` Update ALL registered binaries
`u` Update the binary under the cursor
`I` Install all missing binaries
`i` Install the binary under the cursor
`x` Abort all running tasks
`<CR>` Toggle detail expansion (URL, path, version, errors)
`q` Close the Glaze window
`<Esc>` Close the Glaze window
==============================================================================
5. API *glaze-api*
*glaze.setup()*
glaze.setup({opts})
@@ -78,14 +102,38 @@ glaze.binaries()
*glaze.is_installed()*
glaze.is_installed({name})
Returns true if the binary is in PATH.
Returns true if the binary is found in PATH, $GOBIN, $GOPATH/bin,
or ~/go/bin.
*glaze.bin_path()*
glaze.bin_path({name})
Returns the full path to the binary, or nil if not found.
*glaze.status()*
glaze.status({name})
Returns "installed", "missing", or "unknown".
*glaze-runner-api*
Runner API (require("glaze.runner")):
runner.update({names}) Update specific binaries
runner.update_all() Update all registered binaries
runner.install({names}) Install specific binaries
runner.install_missing() Install all missing binaries
runner.abort() Stop all running tasks
runner.is_running() Check if tasks are running
runner.tasks() Get current task list
runner.stats() Get task statistics
*glaze-checker-api*
Checker API (require("glaze.checker")):
checker.check() Check for updates (with notifications)
checker.auto_check() Check only if enough time has passed
checker.get_update_info() Get cached update info table
==============================================================================
5. CONFIGURATION *glaze-config*
6. CONFIGURATION *glaze-config*
Default configuration:
>lua
@@ -103,11 +151,15 @@ Default configuration:
},
concurrency = 4,
go_cmd = { "go" }, -- Auto-detects goenv
auto_check = {
enabled = true, -- Auto-check for updates on setup
frequency = "daily", -- "daily", "weekly", or hours (number)
},
})
<
==============================================================================
6. HIGHLIGHTS *glaze-highlights*
7. HIGHLIGHTS *glaze-highlights*
All highlight groups are prefixed with "Glaze":
@@ -121,9 +173,27 @@ All highlight groups are prefixed with "Glaze":
GlazeRunning In-progress status
GlazeProgressDone Progress bar (filled)
GlazeProgressTodo Progress bar (empty)
GlazeVersion Version info
GlazeTime Timing info
==============================================================================
7. FOR PLUGIN AUTHORS *glaze-plugin-authors*
8. AUTO-UPDATE CHECKING *glaze-auto-check*
Glaze can automatically check for newer versions of your registered binaries.
When enabled, Glaze checks on setup if enough time has passed since the last
check. It uses `go list -m -json <module>@latest` to find the latest version
and `go version -m <binary>` to determine the installed version.
State is stored in: `vim.fn.stdpath("data") .. "/glaze/state.json"`
Use `:GlazeCheck` to manually trigger a check at any time.
In the UI, binaries with available updates show a ⬆ indicator with version
info (e.g., "v1.0.0 → v1.1.0").
==============================================================================
9. FOR PLUGIN AUTHORS *glaze-plugin-authors*
Register your binaries in your plugin's setup:
>lua

257
lua/glaze/checker.lua Normal file
View File

@@ -0,0 +1,257 @@
---@brief [[
--- glaze.nvim update checker
--- Checks for newer versions of registered Go binaries
---@brief ]]
local M = {}
---@class GlazeUpdateInfo
---@field name string Binary name
---@field installed_version? string Currently installed version
---@field latest_version? string Latest available version
---@field has_update boolean Whether an update is available
---@type table<string, GlazeUpdateInfo>
M._update_info = {}
---@type boolean
M._checking = false
local STATE_FILE = vim.fn.stdpath("data") .. "/glaze/state.json"
---Read persisted state from disk.
---@return table
local function read_state()
local ok, content = pcall(vim.fn.readfile, STATE_FILE)
if not ok or #content == 0 then
return {}
end
local decode_ok, data = pcall(vim.json.decode, table.concat(content, "\n"))
if not decode_ok then
return {}
end
return data or {}
end
---Write state to disk.
---@param state table
local function write_state(state)
local dir = vim.fn.fnamemodify(STATE_FILE, ":h")
vim.fn.mkdir(dir, "p")
local json = vim.json.encode(state)
vim.fn.writefile({ json }, STATE_FILE)
end
---Get the frequency in seconds from config.
---@return number seconds
local function get_frequency_seconds()
local glaze = require("glaze")
local freq = glaze.config.auto_check.frequency
if freq == "daily" then
return 86400
elseif freq == "weekly" then
return 604800
elseif type(freq) == "number" then
return freq * 3600
end
return 86400
end
---Get installed version of a binary by parsing `go version -m` output.
---@param name string Binary name
---@param callback fun(version: string?)
local function get_installed_version(name, callback)
local glaze = require("glaze")
local bin_path = glaze.bin_path(name)
if not bin_path then
callback(nil)
return
end
vim.fn.jobstart({ "go", "version", "-m", bin_path }, {
stdout_buffered = true,
on_stdout = function(_, data)
if not data then
callback(nil)
return
end
local output = table.concat(data, "\n")
-- Parse "mod\tmodule/path\tv1.2.3\th1:..." or "path\tmodule/path"
local version = output:match("\tmod\t[^\t]+\t(v[^\t%s]+)")
or output:match("\tpath\t[^\n]+\n[^\t]*\tmod\t[^\t]+\t(v[^\t%s]+)")
callback(version)
end,
on_exit = function(_, code)
if code ~= 0 then
callback(nil)
end
end,
})
end
---Check for the latest version of a module using go list.
---@param url string Module URL
---@param callback fun(version: string?)
local function get_latest_version(url, callback)
local glaze = require("glaze")
local cmd = vim.list_extend({}, glaze.config.go_cmd)
vim.list_extend(cmd, { "list", "-m", "-json", url .. "@latest" })
vim.fn.jobstart(cmd, {
stdout_buffered = true,
env = { GOFLAGS = "" },
on_stdout = function(_, data)
if not data then
callback(nil)
return
end
local output = table.concat(data, "\n")
local decode_ok, result = pcall(vim.json.decode, output)
if decode_ok and result and result.Version then
callback(result.Version)
else
callback(nil)
end
end,
on_exit = function(_, code)
if code ~= 0 then
callback(nil)
end
end,
})
end
---Get cached update info.
---@return table<string, GlazeUpdateInfo>
function M.get_update_info()
return M._update_info
end
---Check for updates on all registered binaries.
---@param opts? { silent?: boolean }
function M.check(opts)
opts = opts or {}
local glaze = require("glaze")
local binaries = glaze.binaries()
if vim.tbl_count(binaries) == 0 then
if not opts.silent then
vim.notify("Glaze: no binaries registered", vim.log.levels.INFO)
end
return
end
if M._checking then
if not opts.silent then
vim.notify("Glaze: already checking for updates", vim.log.levels.INFO)
end
return
end
M._checking = true
local remaining = 0
local updates_found = 0
for name, binary in pairs(binaries) do
remaining = remaining + 2 -- installed version + latest version
local info = {
name = name,
installed_version = nil,
latest_version = nil,
has_update = false,
}
M._update_info[name] = info
get_installed_version(name, function()
-- callback receives version from the jobstart; re-check via closure
end)
end
-- Simplified: check each binary sequentially-ish
remaining = vim.tbl_count(binaries)
for name, binary in pairs(binaries) do
local info = M._update_info[name]
get_installed_version(name, function(installed)
info.installed_version = installed
get_latest_version(binary.url, function(latest)
info.latest_version = latest
if installed and latest and installed ~= latest then
info.has_update = true
updates_found = updates_found + 1
end
remaining = remaining - 1
if remaining <= 0 then
M._checking = false
-- Save check timestamp
local state = read_state()
state.last_check = os.time()
state.update_info = {}
for n, i in pairs(M._update_info) do
state.update_info[n] = {
installed_version = i.installed_version,
latest_version = i.latest_version,
has_update = i.has_update,
}
end
write_state(state)
if not opts.silent then
if updates_found > 0 then
vim.schedule(function()
vim.notify("Glaze: " .. updates_found .. " update(s) available", vim.log.levels.INFO)
end)
else
vim.schedule(function()
vim.notify("Glaze: all binaries up to date", vim.log.levels.INFO)
end)
end
elseif updates_found > 0 then
vim.schedule(function()
vim.notify("Glaze: " .. updates_found .. " update(s) available", vim.log.levels.INFO)
end)
end
-- Refresh UI if open
vim.schedule(function()
local ok, view = pcall(require, "glaze.view")
if ok and view._float and view._float:valid() then
view.render()
end
end)
end
end)
end)
end
end
---Auto-check if enough time has passed since last check.
function M.auto_check()
local state = read_state()
local last_check = state.last_check or 0
local now = os.time()
local freq = get_frequency_seconds()
-- Load cached update info
if state.update_info then
for name, info in pairs(state.update_info) do
M._update_info[name] = {
name = name,
installed_version = info.installed_version,
latest_version = info.latest_version,
has_update = info.has_update or false,
}
end
end
if (now - last_check) >= freq then
M.check({ silent = true })
end
end
return M

View File

@@ -119,12 +119,18 @@ function Float:mount()
self:close()
end, "Close")
-- Auto-close on WinClosed
-- Auto-close on WinClosed — also trigger view cleanup to stop timer
vim.api.nvim_create_autocmd("WinClosed", {
pattern = tostring(self.win),
once = true,
callback = function()
self:close()
-- Trigger view close to clean up timer
local ok, view = pcall(require, "glaze.view")
if ok then
view.close()
else
self:close()
end
end,
})

View File

@@ -18,10 +18,15 @@ local M = {}
---@field plugin? string Plugin that registered this binary
---@field callback? fun(success: boolean) Optional callback after install/update
---@class GlazeAutoCheckConfig
---@field enabled boolean Whether to auto-check for updates
---@field frequency string|number Frequency: "daily", "weekly", or hours as number
---@class GlazeConfig
---@field ui GlazeUIConfig
---@field concurrency number Max parallel installations
---@field go_cmd string[] Go command (supports goenv)
---@field auto_check GlazeAutoCheckConfig
---@class GlazeUIConfig
---@field border string Border style
@@ -50,6 +55,10 @@ M.config = {
},
concurrency = 4,
go_cmd = { "go" },
auto_check = {
enabled = true,
frequency = "daily",
},
}
---@type table<string, GlazeBinary>
@@ -100,6 +109,17 @@ function M.setup(opts)
return vim.tbl_keys(M._binaries)
end,
})
vim.api.nvim_create_user_command("GlazeCheck", function()
require("glaze.checker").check()
end, { desc = "Check for binary updates" })
-- Auto-check for updates
if M.config.auto_check.enabled then
vim.defer_fn(function()
require("glaze.checker").auto_check()
end, 3000)
end
end
---Register a binary for management.
@@ -129,10 +149,79 @@ function M.binaries()
end
---Check if a binary is installed.
---Checks PATH, $GOBIN, $GOPATH/bin, and $(go env GOBIN).
---@param name string Binary name
---@return boolean
function M.is_installed(name)
return vim.fn.executable(name) == 1
-- Check PATH first
if vim.fn.executable(name) == 1 then
return true
end
-- Check $GOBIN
local gobin = os.getenv("GOBIN")
if gobin and gobin ~= "" then
local path = gobin .. "/" .. name
if vim.uv.fs_stat(path) then
return true
end
end
-- Check $GOPATH/bin
local gopath = os.getenv("GOPATH")
if gopath and gopath ~= "" then
local path = gopath .. "/bin/" .. name
if vim.uv.fs_stat(path) then
return true
end
end
-- Check default ~/go/bin
local home = os.getenv("HOME") or os.getenv("USERPROFILE") or ""
local default_path = home .. "/go/bin/" .. name
if vim.uv.fs_stat(default_path) then
return true
end
return false
end
---Get the install path for a binary if found.
---@param name string Binary name
---@return string? path Full path to the binary, or nil
function M.bin_path(name)
-- Check PATH
local which = vim.fn.exepath(name)
if which ~= "" then
return which
end
-- Check $GOBIN
local gobin = os.getenv("GOBIN")
if gobin and gobin ~= "" then
local path = gobin .. "/" .. name
if vim.uv.fs_stat(path) then
return path
end
end
-- Check $GOPATH/bin
local gopath = os.getenv("GOPATH")
if gopath and gopath ~= "" then
local path = gopath .. "/bin/" .. name
if vim.uv.fs_stat(path) then
return path
end
end
-- Check default ~/go/bin
local home = os.getenv("HOME") or os.getenv("USERPROFILE") or ""
local default_path = home .. "/go/bin/" .. name
if vim.uv.fs_stat(default_path) then
return default_path
end
return nil
end
---Get binary installation status.

View File

@@ -139,6 +139,12 @@ end
local function run(names, mode)
local glaze = require("glaze")
-- Reject if already running (race condition fix)
if M._running then
vim.notify("Glaze: tasks already running. Wait or abort first.", vim.log.levels.WARN)
return
end
-- Check for Go
local go_check = glaze.config.go_cmd[1]
if vim.fn.executable(go_check) ~= 1 then

View File

@@ -57,6 +57,17 @@ function Text:append(str, hl, opts)
return self
end
---Append a virtual text extmark on the current line (empty string segment).
---@param extmark GlazeExtmark Extmark options (virt_text, virt_text_win_col, etc.)
---@return GlazeText
function Text:append_extmark(extmark)
if #self._lines == 0 then
self:nl()
end
table.insert(self._lines[#self._lines], { str = "", hl = extmark })
return self
end
---@return GlazeText
function Text:nl()
table.insert(self._lines, {})
@@ -106,7 +117,7 @@ function Text:render(buf, ns)
vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)
for l, line in ipairs(self._lines) do
if lines[l] ~= "" then
if lines[l] ~= "" or true then -- process even empty lines for extmarks
local col = self.padding
for _, segment in ipairs(line) do
local width = vim.fn.strwidth(segment.str)
@@ -114,12 +125,23 @@ function Text:render(buf, ns)
if extmark then
if type(extmark) == "string" then
extmark = { hl_group = extmark, end_col = col + width }
-- Simple highlight group string
if width > 0 then
pcall(vim.api.nvim_buf_set_extmark, buf, ns, l - 1, col, {
hl_group = extmark,
end_col = col + width,
})
end
elseif type(extmark) == "table" then
-- Full extmark table (virt_text, etc.)
local extmark_col = extmark.col or col
local opts = vim.tbl_extend("force", {}, extmark)
opts.col = nil -- col is positional, not an extmark option
if not opts.end_col and width > 0 and opts.hl_group then
opts.end_col = extmark_col + width
end
pcall(vim.api.nvim_buf_set_extmark, buf, ns, l - 1, extmark_col, opts)
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

View File

@@ -11,9 +11,26 @@ M._float = nil
---@type number?
M._timer = nil
---@type table<string, boolean>
M._expanded = {}
---Line-to-binary mapping for cursor-aware actions.
---@type table<number, string>
M._line_map = {}
local SPINNERS = { "", "", "", "" }
local SPINNER_IDX = 1
---Get the binary name under the cursor.
---@return string? name Binary name or nil
function M._get_cursor_binary()
if not M._float or not M._float:valid() then
return nil
end
local line = vim.api.nvim_win_get_cursor(M._float.win)[1]
return M._line_map[line]
end
---Open the Glaze UI.
function M.open()
local Float = require("glaze.float").Float
@@ -23,17 +40,35 @@ function M.open()
end
M._float = Float.new({
title = " 󱓞 Glaze ",
title = " 🍩 Glaze ",
})
-- Set up keymaps
M._float:map("u", function()
-- Set up keymaps — lazy.nvim-style controls
M._float:map("U", function()
require("glaze.runner").update_all()
end, "Update all")
end, "Update all binaries")
M._float:map("u", function()
local name = M._get_cursor_binary()
if name then
require("glaze.runner").update({ name })
else
vim.notify("Move cursor to a binary line to update it", vim.log.levels.INFO)
end
end, "Update binary under cursor")
M._float:map("i", function()
local name = M._get_cursor_binary()
if name then
require("glaze.runner").install({ name })
else
vim.notify("Move cursor to a binary line to install it", vim.log.levels.INFO)
end
end, "Install binary under cursor")
M._float:map("I", function()
require("glaze.runner").install_missing()
end, "Install missing")
end, "Install all missing")
M._float:map("x", function()
require("glaze.runner").abort()
@@ -58,6 +93,7 @@ end
function M._start_timer()
if M._timer then
vim.fn.timer_stop(M._timer)
M._timer = nil
end
M._timer = vim.fn.timer_start(100, function()
@@ -80,8 +116,14 @@ function M._start_timer()
end
---@private
---Toggle detail expansion for the binary under the cursor.
function M._toggle_details()
-- TODO: Implement detail expansion
local name = M._get_cursor_binary()
if not name then
return
end
M._expanded[name] = not M._expanded[name]
M.render()
end
---Render the UI.
@@ -92,6 +134,7 @@ function M.render()
local glaze = require("glaze")
local runner = require("glaze.runner")
local checker = require("glaze.checker")
local Text = require("glaze.text").Text
local text = Text.new()
@@ -99,9 +142,12 @@ function M.render()
local icons = glaze.config.ui.icons
-- Reset line map
M._line_map = {}
-- Header
text:nl()
text:append(" ", "GlazeIcon"):append("Glaze", "GlazeH1"):append(" Go Binary Manager", "GlazeComment"):nl()
text:append(" 🍩 ", "GlazeIcon"):append("Glaze", "GlazeH1"):append(" Go Binary Manager", "GlazeComment"):nl()
text:nl()
-- Stats / Progress
@@ -113,12 +159,19 @@ function M.render()
-- Keybinds
text:append(" ", nil, { indent = 2 })
text:append(" U ", "GlazeButtonActive"):append(" Update All ", "GlazeButton")
text:append(" ")
text:append(" u ", "GlazeButtonActive"):append(" Update ", "GlazeButton")
text:append(" ")
text:append(" i ", "GlazeButtonActive"):append(" Install ", "GlazeButton")
text:append(" I ", "GlazeButtonActive"):append(" Install All ", "GlazeButton")
text:append(" ")
text:append(" i ", "GlazeButtonActive"):append(" Install ", "GlazeButton")
text:nl()
text:append(" ", nil, { indent = 2 })
text:append(" x ", "GlazeButtonActive"):append(" Abort ", "GlazeButton")
text:append(" ")
text:append("", "GlazeButtonActive"):append(" Details ", "GlazeButton")
text:append(" ")
text:append(" q ", "GlazeButtonActive"):append(" Close ", "GlazeButton")
text:nl():nl()
@@ -137,7 +190,22 @@ function M.render()
local binaries = glaze.binaries()
local binary_count = vim.tbl_count(binaries)
text:append("Binaries", "GlazeH2"):append(" (" .. binary_count .. ")", "GlazeComment"):nl()
-- Count updates available
local updates_available = 0
local update_info = checker.get_update_info()
for _ in pairs(update_info) do
updates_available = updates_available + 1
end
local header_suffix = " (" .. binary_count .. ")"
if updates_available > 0 then
header_suffix = header_suffix
end
text:append("Binaries", "GlazeH2"):append(header_suffix, "GlazeComment")
if updates_available > 0 then
text:append(" " .. updates_available .. " update(s) available", "GlazeRunning")
end
text:nl()
text:nl()
if binary_count == 0 then
@@ -156,7 +224,7 @@ function M.render()
end)
for _, item in ipairs(sorted) do
M._render_binary(text, item.binary, icons)
M._render_binary(text, item.binary, icons, update_info)
end
end
@@ -181,14 +249,19 @@ function M._render_progress(text, stats)
local done_width = math.floor(done_ratio * width + 0.5)
if stats.done < stats.total then
text:append("", {
text:append_extmark({
virt_text_win_col = 2,
virt_text = { { string.rep("", done_width), "GlazeProgressDone" } },
})
text:append("", {
text:append_extmark({
virt_text_win_col = 2 + done_width,
virt_text = { { string.rep("", width - done_width), "GlazeProgressTodo" } },
})
else
text:append_extmark({
virt_text_win_col = 2,
virt_text = { { string.rep("", width), "GlazeProgressDone" } },
})
end
end
@@ -208,6 +281,9 @@ function M._render_task(text, task, icons)
icon, icon_hl = icons.pending, "GlazePending"
end
local line_num = text:row()
M._line_map[line_num] = task.binary.name
text:append(" " .. icon .. " ", icon_hl)
text:append(task.binary.name, "GlazeBinary")
@@ -230,14 +306,18 @@ end
---@param text GlazeText
---@param binary GlazeBinary
---@param icons GlazeIcons
---@param update_info? table<string, GlazeUpdateInfo>
---@private
function M._render_binary(text, binary, icons)
function M._render_binary(text, binary, icons, update_info)
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"
local line_num = text:row()
M._line_map[line_num] = binary.name
text:append(" " .. icon .. " ", icon_hl)
text:append(binary.name, "GlazeBinary")
@@ -245,11 +325,54 @@ function M._render_binary(text, binary, icons)
text:append(" (" .. binary.plugin .. ")", "GlazePlugin")
end
-- Show update available indicator
if update_info and update_info[binary.name] then
local info = update_info[binary.name]
if info.has_update then
text:append("", "GlazeRunning")
if info.installed_version and info.latest_version then
text:append(" " .. info.installed_version .. "" .. info.latest_version, "GlazeTime")
end
elseif info.installed_version then
text:append("" .. info.installed_version, "GlazeVersion")
end
end
text:nl()
text:append(binary.url, "GlazeUrl", { indent = 6 }):nl()
-- Expanded details
if M._expanded[binary.name] then
text:append("URL: ", "GlazeComment", { indent = 6 })
text:append(binary.url, "GlazeUrl"):nl()
local bin_path = glaze.bin_path(binary.name)
if bin_path then
text:append("Path: ", "GlazeComment", { indent = 6 })
text:append(bin_path, "GlazeUrl"):nl()
end
if binary.plugin then
text:append("Plugin: ", "GlazeComment", { indent = 6 })
text:append(binary.plugin, "GlazePlugin"):nl()
end
-- Show last error output from tasks
local runner = require("glaze.runner")
for _, task in ipairs(runner.tasks()) do
if task.binary.name == binary.name and task.status == "error" and #task.output > 0 then
text:append("Error: ", "GlazeError", { indent = 6 }):nl()
for _, line in ipairs(task.output) do
text:append(line, "GlazeError", { indent = 8 }):nl()
end
break
end
end
text:nl()
end
end
---Close the UI.
---Close the UI and clean up timer.
function M.close()
if M._timer then
vim.fn.timer_stop(M._timer)