normalize_hyprland() dual-emits each of its 10 flat event names (bread.workspace.changed, bread.monitor.connected, bread.window.opened, etc.) alongside a bread.hyprland.<rest> equivalent, so portable automation can subscribe to bread.hyprland.* and be guaranteed compositor-specific coverage without also matching genuinely cross-backend events like bread.power.*. The bread.hyprland.event fallback was already namespaced and is unaffected. Gated behind [compat] legacy_hyprland_event_names (default true during the deprecation window) so the old names keep firing until modules migrate; set to false to emit only the namespaced names. EventNormalizer::with_legacy_hyprland_event_names(bool) is a builder on top of the existing ::new(dedup_window_ms), threaded through from config in main.rs. Bumps API_VERSION to 1.5.0 (additive), updates Documentation.md's Hyprland event reference with Deprecated/Since markers and the new [compat] option, refreshes the README config example and the dock-workflow/git-branch-widget examples to the namespaced names, and adds DEPRECATIONS.md tracking the deferred full removal once the window closes.
154 lines
5.7 KiB
Lua
154 lines
5.7 KiB
Lua
-- git-branch-widget — shows "<repo> <branch>" for whichever git repo the
|
|
-- currently focused terminal's *active tab* is sitting in, yellow when the
|
|
-- worktree is dirty. Hides entirely when focus isn't on a terminal, or the
|
|
-- focused tab isn't inside a git repo.
|
|
--
|
|
-- This is a plain Lua module doing its own OS-level legwork end to end —
|
|
-- no dedicated Rust adapter behind it. It combines four general-purpose
|
|
-- primitives that all already exist (or were added alongside this module
|
|
-- as small, non-kitty-specific additions): bread.hyprland.active_window()
|
|
-- for the focused window's class + pid, bread.fs.exists to probe for a
|
|
-- listening socket, bread.exec_capture to run `kitty @ ls` and read its
|
|
-- output, and bread.json.decode to parse it.
|
|
--
|
|
-- Why kitty remote control instead of /proc: a kitty *window* can host
|
|
-- several *tabs*, each a separate child shell process, and the kernel has
|
|
-- no notion of "which pty is currently displayed" — that's purely internal
|
|
-- kitty state. Walking /proc can find the window's child processes but
|
|
-- can't tell which one you're actually looking at. Kitty's own `kitty @ ls`
|
|
-- tracks focus precisely at the OS-window/tab/window level, so asking it
|
|
-- directly is the only way to get this exactly right for multi-tab windows.
|
|
--
|
|
-- Prerequisite — add to ~/.config/kitty/kitty.conf:
|
|
-- allow_remote_control socket-only
|
|
-- listen_on unix:/tmp/kitty-bread-{kitty_pid}
|
|
-- `{kitty_pid}` makes the socket path unique per kitty process, so this
|
|
-- works whether or not you run kitty in single-instance mode, and however
|
|
-- many separate kitty processes you have open — this module derives the
|
|
-- exact socket to ask from the focused window's own pid. Kitty only picks
|
|
-- up `listen_on` on (re)start, not a config reload, so existing kitty
|
|
-- windows need to be restarted once after adding this.
|
|
--
|
|
-- Drop-in: copy into ~/.config/bread/modules/. Needs `git` and the kitty
|
|
-- remote-control config above. Assumes the terminal's WM_CLASS is "kitty"
|
|
-- (edit TERMINAL_CLASS below for another terminal, if it has an equivalent
|
|
-- remote-control/introspection story).
|
|
|
|
local M = bread.module({ name = "git-branch-widget", version = "1.0.0" })
|
|
|
|
local TERMINAL_CLASS = "kitty"
|
|
|
|
local function shell_quote(s)
|
|
return "'" .. s:gsub("'", "'\\''") .. "'"
|
|
end
|
|
|
|
-- The exact cwd of the focused tab in the focused kitty window, or nil if
|
|
-- focus isn't on kitty, that kitty process hasn't been restarted since the
|
|
-- listen_on config was added, or nothing came back focused (shouldn't
|
|
-- happen for a window Hyprland itself says is focused, but `kitty @ ls`
|
|
-- reflects kitty's own state, not Hyprland's, so treat it as fallible).
|
|
local function focused_tab_cwd()
|
|
local win = bread.hyprland.active_window()
|
|
if type(win) ~= "table" or win.class ~= TERMINAL_CLASS or not win.pid then
|
|
return nil
|
|
end
|
|
|
|
local socket_path = "/tmp/kitty-bread-" .. win.pid
|
|
if not bread.fs.exists(socket_path) then
|
|
return nil
|
|
end
|
|
|
|
local ok, output = bread.exec_capture("kitty @ --to unix:" .. socket_path .. " ls")
|
|
if not ok then
|
|
return nil
|
|
end
|
|
|
|
local os_windows = bread.json.decode(output)
|
|
if type(os_windows) ~= "table" then
|
|
return nil
|
|
end
|
|
|
|
for _, osw in ipairs(os_windows) do
|
|
if osw.is_focused then
|
|
for _, tab in ipairs(osw.tabs or {}) do
|
|
if tab.is_focused then
|
|
for _, w in ipairs(tab.windows or {}) do
|
|
if w.is_focused then
|
|
return w.cwd
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function git_info(cwd)
|
|
local quoted = shell_quote(cwd)
|
|
|
|
local ok, toplevel = bread.exec_capture("git -C " .. quoted .. " rev-parse --show-toplevel")
|
|
if not ok then
|
|
return nil
|
|
end
|
|
toplevel = toplevel:gsub("%s+$", "")
|
|
local repo = toplevel:match("([^/]+)/?$") or toplevel
|
|
|
|
local repo_quoted = shell_quote(toplevel)
|
|
local branch_ok, branch = bread.exec_capture("git -C " .. repo_quoted .. " rev-parse --abbrev-ref HEAD")
|
|
if not branch_ok then
|
|
return nil
|
|
end
|
|
branch = branch:gsub("%s+$", "")
|
|
|
|
local _, status = bread.exec_capture("git -C " .. repo_quoted .. " status --porcelain")
|
|
local dirty = status:match("%S") ~= nil
|
|
|
|
return { repo = repo, branch = branch, dirty = dirty }
|
|
end
|
|
|
|
local function widget_root(info)
|
|
if not info then
|
|
return { type = "label", text = "" }
|
|
end
|
|
return {
|
|
type = "label",
|
|
text = info.repo .. " " .. info.branch,
|
|
style = { color = info.dirty and "yellow" or "dim" },
|
|
}
|
|
end
|
|
|
|
local function update()
|
|
local cwd = focused_tab_cwd()
|
|
local info = cwd and git_info(cwd) or nil
|
|
|
|
bread.widget.update("branch", {
|
|
visible = info ~= nil,
|
|
tooltip = info and ("git: " .. info.repo .. "@" .. info.branch .. (info.dirty and " (dirty)" or "")) or "",
|
|
root = widget_root(info),
|
|
})
|
|
end
|
|
|
|
function M.on_load()
|
|
local cwd = focused_tab_cwd()
|
|
local info = cwd and git_info(cwd) or nil
|
|
|
|
bread.widget.register({
|
|
id = "branch",
|
|
placement = "right_of_clock",
|
|
visible = info ~= nil,
|
|
tooltip = info and ("git: " .. info.repo .. "@" .. info.branch) or "",
|
|
root = widget_root(info),
|
|
})
|
|
|
|
-- Event-driven for instant updates on focus change, plus a poll to
|
|
-- catch a branch switch inside the same still-focused tab (e.g. `git
|
|
-- checkout` run without ever changing window focus), which produces no
|
|
-- focus event at all.
|
|
bread.on("bread.hyprland.window.focused", update)
|
|
bread.on("bread.hyprland.window.focus.changed", update)
|
|
bread.every(2000, update)
|
|
end
|
|
|
|
return M
|