feat: track Decman configuration

This commit is contained in:
2026-08-13 23:03:55 +02:00
parent 34acd648c9
commit f99cfeae79
96 changed files with 7120 additions and 0 deletions
@@ -0,0 +1,598 @@
---@diagnostic disable: undefined-global
local save = ya.sync(function(this, cwd, output)
if cwd:match("%.git[/\\]") or cwd:match("%.git$") then
this.output = nil
return
end
if cx.active.current.cwd == Url(cwd) then
this.output = output
ui.render()
end
end)
return {
setup = function(this, options)
options = options or {}
local config = {
order = options.order or {
"__spacer__",
"branch",
"remote_branch",
"__spacer__",
"tag",
"__spacer__",
"commit",
"__spacer__",
"behind_ahead_remote",
"__spacer__",
"stashes",
"__spacer__",
"state",
"__spacer__",
"staged",
"__spacer__",
"unstaged",
"__spacer__",
"untracked",
},
show_numbers = options.show_numbers == nil and true
or options.show_numbers,
show_branch = options.show_branch == nil and true or options.show_branch,
branch_prefix = options.branch_prefix or "",
branch_color = options.branch_color or "blue",
branch_symbol = options.branch_symbol or "",
branch_borders = options.branch_borders or "",
show_remote_branch = options.show_remote_branch == nil and true
or options.show_remote_branch,
always_show_remote_branch = options.always_show_remote_branch == nil
and false
or options.always_show_remote_branch,
always_show_remote_repo = options.always_show_remote_repo == nil
and false
or options.always_show_remote_repo,
remote_branch_prefix = options.remote_branch_prefix or ":",
remote_branch_color = options.remote_branch_color or "bright magenta",
show_tag = options.show_tag == nil and true or options.show_tag,
always_show_tag = options.always_show_tag == nil and false
or options.always_show_tag,
tag_color = options.tag_color or "magenta",
tag_symbol = options.tag_symbol == nil and "#" or options.tag_symbol,
show_commit = options.show_commit == nil and true or options.show_commit,
always_show_commit = options.always_show_commit == nil and false
or options.always_show_commit,
commit_color = options.commit_color or "bright magenta",
commit_symbol = options.commit_symbol == nil and "@"
or options.commit_symbol,
show_behind_ahead_remote = options.show_behind_ahead_remote == nil
and true
or options.show_behind_ahead_remote,
behind_remote_color = options.behind_remote_color or "bright magenta",
behind_remote_symbol = options.behind_remote_symbol or "⇣",
ahead_remote_color = options.ahead_remote_color or "bright magenta",
ahead_remote_symbol = options.ahead_remote_symbol or "⇡",
show_stashes = options.show_stashes == nil and true
or options.show_stashes,
stashes_color = options.stashes_color or "bright magenta",
stashes_symbol = options.stashes_symbol or "$",
show_state = options.show_state == nil and true or options.show_state,
show_state_prefix = options.show_state_prefix == nil and true
or options.show_state_prefix,
state_color = options.state_color or "red",
state_symbol = options.state_symbol or "~",
show_staged = options.show_staged == nil and true or options.show_staged,
staged_color = options.staged_color or "bright yellow",
staged_symbol = options.staged_symbol or "+",
show_unstaged = options.show_unstaged == nil and true
or options.show_unstaged,
unstaged_color = options.unstaged_color or "bright yellow",
unstaged_symbol = options.unstaged_symbol or "!",
show_untracked = options.show_untracked == nil and true
or options.show_untracked,
untracked_color = options.untracked_color or "bright blue",
untracked_symbol = options.untracked_symbol or "?",
}
--- @param data GitStatusData
function Header:render_branch(data)
local branch = data.branch
if not branch then
return nil
end
local left_border = config.branch_borders:sub(1, 1)
local right_border = config.branch_borders:sub(2, 2)
local branch_string = ""
if config.branch_symbol == "" then
branch_string = left_border .. branch .. right_border
else
branch_string = left_border
.. config.branch_symbol
.. branch
.. right_border
end
local branch_prefix = config.branch_prefix == "" and ""
or config.branch_prefix .. " "
return ui.Line({
ui.Span(branch_prefix),
ui.Span(branch_string):fg(config.branch_color),
})
end
--- @param data GitStatusData
function Header:render_remote_branch(data)
local branch = data.branch
local remote_branch = data.remote_branch
local remote_repo = data.remote_repo
if not remote_branch then
return nil
end
local show_remote = config.always_show_remote_branch
or branch ~= remote_branch
if not show_remote then
return nil
end
local remote_branch_label = config.always_show_remote_repo
and (remote_repo .. "/" .. remote_branch)
or remote_branch
return ui.Line({
ui.Span(config.remote_branch_prefix),
ui.Span(remote_branch_label):fg(config.remote_branch_color),
})
end
--- @param data GitStatusData
function Header:render_tag(data)
local branch = data.branch
local tag = data.tag
if not tag then
return nil
end
if not branch or config.always_show_tag then
return ui.Line({
ui.Span(config.tag_symbol),
ui.Span(tag):fg(config.tag_color),
})
end
end
--- @param data GitStatusData
function Header:render_commit(data)
local branch = data.branch
local tag = data.tag
local commit = data.commit
if not commit then
return nil
end
if (not branch and not tag) or config.always_show_commit then
return ui.Line({
ui.Span(config.commit_symbol),
ui.Span(commit):fg(config.commit_color),
})
end
end
--- @param data GitStatusData
function Header:render_behind_ahead_remote(data)
local behind = data.behind_remote
local ahead = data.ahead_remote
local behind_label = behind
and behind > 0
and ui.Span(
config.behind_remote_symbol .. (config.show_numbers and behind or "")
):fg(config.behind_remote_color)
local ahead_label = ahead
and ahead > 0
and ui.Span(
config.ahead_remote_symbol .. (config.show_numbers and ahead or "")
):fg(config.ahead_remote_color)
if ahead_label and behind_label then
return ui.Line({
behind_label,
ahead_label,
})
elseif ahead_label then
return ahead_label
elseif behind_label then
return behind_label
else
return nil
end
end
--- @param data GitStatusData
function Header:render_stashes(data)
local stashes_count = data.stashes
if not stashes_count then
return nil
end
local stashes_label = config.stashes_symbol
if config.show_numbers then
stashes_label = stashes_label .. stashes_count
end
return ui.Span(stashes_label):fg(config.stashes_color)
end
--- @param data GitStatusData
function Header:render_state(data)
local state_prefix = data.state_prefix
local unmerged_count = data.unmerged_count
local rebase_done = data.rebase_done
local rebase_total = data.rebase_total
if state_prefix then
local state_label = ""
if config.show_state_prefix then
state_label = state_prefix
if
state_prefix == "rebase-i"
and config.show_numbers
and rebase_done
and rebase_total
then
state_label = state_label
.. " "
.. rebase_done
.. "/"
.. rebase_total
end
end
local unmerged_label = ""
local symbol = ""
if unmerged_count and unmerged_count > 0 then
symbol = " " .. config.state_symbol
if config.show_numbers then
unmerged_label = tostring(unmerged_count)
end
end
return ui.Span(state_label .. symbol .. unmerged_label)
:fg(config.state_color)
end
return nil
end
--- @param data GitStatusData
function Header:render_staged(data)
local staged = data.staged
if staged then
local staged_label = config.staged_symbol
if config.show_numbers then
local filtered_staged = staged:gsub("^[%s]*%b()[%s]*", "")
local staged_count = 0
for line in filtered_staged:gmatch("[^\r\n]+") do
if line:match("%S") then
staged_count = staged_count + 1
end
end
staged_label = staged_label .. staged_count
end
return ui.Span(staged_label):fg(config.staged_color)
end
return nil
end
--- @param data GitStatusData
function Header:render_unstaged(data)
local unstaged = data.unstaged
if unstaged then
local unstaged_label = config.unstaged_symbol
if config.show_numbers then
local filtered_unstaged =
unstaged:gsub("^[%s]*%b()[\r\n]*", ""):gsub("^[%s]*%b()[\r\n]*", "")
local unstaged_count = 0
for line in filtered_unstaged:gmatch("[^\r\n]+") do
if line:match("%S") then
unstaged_count = unstaged_count + 1
end
end
unstaged_label = unstaged_label .. unstaged_count
end
return ui.Span(unstaged_label):fg(config.unstaged_color)
end
return nil
end
--- @param data GitStatusData
function Header:render_untracked(data)
local untracked = data.untracked
if untracked then
local untracked_label = config.untracked_symbol
if config.show_numbers then
local filtered_untracked = untracked:gsub("^[%s]*%b()[\r\n]*", "")
local untracked_count = 0
for line in filtered_untracked:gmatch("[^\r\n]+") do
if line:match("%S") then
untracked_count = untracked_count + 1
end
end
untracked_label = untracked_label .. untracked_count
end
return ui.Span(untracked_label):fg(config.untracked_color)
end
return nil
end
function Header:githead()
--- @type GitStatusData
local data = this.output
if not data then
return ui.Line({})
end
local head = {}
for _, key in ipairs(config.order) do
if key == "__spacer__" then
if head[#head] ~= " " then
table.insert(head, " ")
end
else
local fn = self["render_" .. key]
local is_shown = config["show_" .. key]
if fn and is_shown then
local value = fn(self, data)
if value then
table.insert(head, value)
end
end
end
end
return ui.Line(head)
end
Header:children_add(Header.githead, 2000, Header.LEFT)
local callback = function()
local cwd = cx.active.current.cwd
ya.emit("plugin", {
this._id,
ya.quote(tostring(cwd), true),
})
end
ps.sub("cd", callback)
ps.sub("rename", callback)
ps.sub("bulk", callback)
ps.sub("move", callback)
ps.sub("trash", callback)
ps.sub("delete", callback)
ps.sub("tab", callback)
end,
entry = function(_, job)
local args = job.args or job
---@class GitStatusData
---@field ahead_remote? integer
---@field behind_remote? integer
---@field branch? string
---@field commit? string
---@field rebase_done? integer
---@field remote_branch? string
---@field remote_repo? string
---@field staged? string
---@field stashes? integer
---@field state_prefix? "bisect"|"cherry"|"merge"|"rebase-i"|"revert"
---@field tag? string
---@field unmerged_count? integer
---@field unstaged? string
---@field untracked? string
local data = {}
--- @param status string
local get_behind_ahead_remote = function(status)
local diverged_ahead, diverged_behind =
status:match("have (%d+) and (%d+) different")
if diverged_ahead and diverged_behind then
data.behind_remote = tonumber(diverged_behind)
data.ahead_remote = tonumber(diverged_ahead)
else
local behind_remote = status:match("behind %S+ by (%d+) commits?")
local ahead_remote = status:match("ahead of %S+ by (%d+) commits?")
data.behind_remote = tonumber(behind_remote)
data.ahead_remote = tonumber(ahead_remote)
end
end
--- @param status string
local get_branch = function(status)
data.branch = status:match("On branch (%S+)")
end
--- @param status string
local get_stashes = function(status)
data.stashes = tonumber(status:match("Your stash currently has (%d+)"))
end
--- @param status string
local get_state = function(status)
local unmerged = status:match("Unmerged paths:%s*(.-)%s*\n\n")
if unmerged then
local filtered_unmerged =
unmerged:gsub("^[%s]*%b()[%s]*", ""):gsub("^[%s]*%b()[%s]*", "")
local count = 0
for line in filtered_unmerged:gmatch("[^\r\n]+") do
if line:match("%S") then
count = count + 1
end
end
data.unmerged_count = count
end
if status:find("git merge") then
data.state_prefix = "merge"
elseif status:find("git cherry%-pick") then
data.state_prefix = "cherry"
elseif status:find("git rebase") then
data.state_prefix = "rebase-i"
local commands_done = status:match("%((%d+) commands? done%)")
if commands_done then
data.rebase_done = commands_done
if not data.unmerged_count then
data.rebase_total = commands_done
else
data.rebase_total = data.unmerged_count
end
end
elseif status:find("git revert") then
data.state_prefix = "revert"
elseif status:find("git bisect") then
data.state_prefix = "bisect"
end
end
--- @param status string
local get_staged = function(status)
data.staged = status:match("Changes to be committed:%s*(.-)%s*\n\n")
end
--- @param status string
local get_unstaged = function(status)
data.unstaged =
status:match("Changes not staged for commit:%s*(.-)%s*\n\n")
end
--- @param status string
local get_untracked = function(status)
data.untracked = status:match("Untracked files:%s*(.-)%s*\n\n")
end
local get_status = function()
local cmd = Command("git")
:arg({
"status",
"--ignore-submodules=dirty",
"--branch",
"--show-stash",
"--ahead-behind",
})
:cwd(args[1])
:env("LANGUAGE", "en_US.UTF-8")
:stdout(Command.PIPED)
local cmd_output = cmd:output()
if cmd_output then
local status = cmd_output.stdout
get_branch(status)
get_behind_ahead_remote(status)
get_stashes(status)
get_state(status)
get_staged(status)
get_unstaged(status)
get_untracked(status)
end
end
local get_remote_branch = function()
local cmd = Command("git")
:arg({
"rev-parse",
"--abbrev-ref",
"--symbolic-full-name",
"@{upstream}",
})
:cwd(args[1])
:env("LANGUAGE", "en_US.UTF-8")
:stdout(Command.PIPED)
local cmd_output = cmd:output()
if cmd_output then
local remote_branch = cmd_output.stdout
data.remote_branch =
remote_branch:gsub("[\r\n]", ""):match("^[^/]+/(.+)")
data.remote_repo = remote_branch:gsub("[\r\n]", ""):match("^([^/]+)/")
end
end
--- @param log string
local get_tag = function(log)
data.tag = log:match("tag: ([^, )]+)")
end
--- @param log string
local get_commit = function(log)
data.commit = log:match("^commit%s+([a-f0-9]+)")
end
local get_git_log = function()
local cmd = Command("git")
:arg({ "log", "--format=commit %h%d", "-n", "1" })
:cwd(args[1])
:env("LANGUAGE", "en_US.UTF-8")
:stdout(Command.PIPED)
local cmd_output = cmd:output()
if cmd_output then
local log = cmd_output.stdout
get_tag(log)
get_commit(log)
end
end
get_status()
get_remote_branch()
get_git_log()
save(args[1], data)
end,
}