summaryrefslogtreecommitdiffstats
path: root/lua/core/statusbar
diff options
context:
space:
mode:
Diffstat (limited to 'lua/core/statusbar')
-rw-r--r--lua/core/statusbar/components.lua315
1 files changed, 315 insertions, 0 deletions
diff --git a/lua/core/statusbar/components.lua b/lua/core/statusbar/components.lua
new file mode 100644
index 0000000..cce3127
--- /dev/null
+++ b/lua/core/statusbar/components.lua
@@ -0,0 +1,315 @@
+if not pcall(require, "el") then return end
+
+local job = require "plenary.job"
+local el_sub = require "el.subscribe"
+
+local M = {}
+
+function M.extract_hl(spec)
+ if not spec or vim.tbl_isempty(spec) then return end
+ local hl_name, hl_opts = { "El" }, {}
+ for attr, val in pairs(spec) do
+ if type(val) == "table" then
+ table.insert(hl_name, attr)
+ assert(vim.tbl_count(val) == 1)
+ local hl, what = next(val)
+ local hlID = vim.fn.hlID(hl)
+ if hlID > 0 then
+ table.insert(hl_name, hl)
+ local col = vim.fn.synIDattr(hlID, what)
+ if col and #col > 0 then
+ table.insert(hl_name, what)
+ hl_opts[attr] = col
+ end
+ end
+ else
+ -- bold, underline, etc
+ hl_opts[attr] = val
+ end
+ end
+ hl_name = table.concat(hl_name, "_")
+ -- if highlight exists, verify it has
+ -- the correct colorscheme highlights
+ local newID = vim.fn.hlID(hl_name)
+ if newID > 0 then
+ for what, expected in pairs(hl_opts) do
+ local res = vim.fn.synIDattr(newID, what)
+ if type(expected) == "boolean" then
+ -- synIDattr returns '1' for boolean
+ res = res and res == "1" and true
+ end
+ if res ~= expected then
+ -- need to regen the highlight
+ -- print("color mismatch", hl_name, what, "e:", expected, "c:", res)
+ newID = 0
+ end
+ end
+ end
+ if newID == 0 then
+ vim.api.nvim_set_hl(0, hl_name, hl_opts)
+ end
+ return hl_name
+end
+
+local function set_hl(hls, s)
+ if not hls or not s then return s end
+ hls = type(hls) == "string" and { hls } or hls
+ for _, hl in ipairs(hls) do
+ if vim.fn.hlID(hl) > 0 then
+ return ("%%#%s#%s%%0*"):format(hl, s)
+ end
+ end
+ return s
+end
+
+local function wrap_fnc(opts, fn)
+ return function(window, buffer)
+ -- buf_autocmd doesn't send win
+ if not window and buffer then
+ window = { win_id = vim.fn.bufwinid(buffer.bufnr) }
+ end
+ if opts.hide_inactive and window and
+ window.win_id ~= vim.api.nvim_get_current_win() then
+ return ""
+ end
+ return fn(window, buffer)
+ end
+end
+
+function M.mode(opts)
+ opts = opts or {}
+ return wrap_fnc(opts, function(_, _)
+ local fmt = opts.fmt or "%s%s"
+ local mode = vim.api.nvim_get_mode().mode
+ local mode_data = opts.modes and opts.modes[mode]
+ local hls = mode_data and mode_data[3]
+ local icon = opts.hl_icon_only and set_hl(hls, opts.icon) or opts.icon
+ mode = mode_data and mode_data[1]:upper() or mode
+ mode = (fmt):format(icon or "", mode)
+ return not opts.hl_icon_only and set_hl(hls, mode) or mode
+ end)
+end
+
+function M.git_branch(opts)
+ opts = opts or {}
+ return el_sub.buf_autocmd("el_git_branch", "BufEnter",
+ wrap_fnc(opts, function(_, buffer)
+ -- Try fugitive first as it's most reliable
+ local branch = vim.g.loaded_fugitive == 1 and
+ vim.fn.FugitiveHead() or nil
+ -- buffer can be null and code will crash with:
+ -- E5108: Error executing lua ... 'attempt to index a nil value'
+ if not buffer or not (buffer.bufnr > 0) then
+ return
+ end
+ -- fugitive is empty or not loaded, try gitsigns
+ if not branch or #branch == 0 then
+ local ok, res = pcall(vim.api.nvim_buf_get_var,
+ buffer.bufnr, "gitsigns_head")
+ if ok then branch = res end
+ end
+ -- last resort run git command
+ if not branch then
+ local j = job:new {
+ command = "git",
+ args = { "branch", "--show-current" },
+ cwd = vim.fn.fnamemodify(buffer.name, ":h"),
+ }
+
+ local ok, result = pcall(function()
+ return vim.trim(j:sync()[1])
+ end)
+ if ok then
+ branch = result
+ end
+ end
+
+ if branch and #branch > 0 then
+ local fmt = opts.fmt or "%s %s"
+ local icon = opts.icon or ""
+ return set_hl(opts.hl, (fmt):format(icon, branch))
+ end
+ end))
+end
+
+local function git_changes_formatter(opts)
+ local specs = {
+ insert = {
+ regex = "(%d+) insertions?",
+ icon = opts.icon_insert or "+",
+ hl = opts.hl_insert,
+ },
+ change = {
+ regex = "(%d+) files? changed",
+ icon = opts.icon_change or "~",
+ hl = opts.hl_change,
+ },
+ delete = {
+ regex = "(%d+) deletions?",
+ icon = opts.icon_delete or "-",
+ hl = opts.hl_delete,
+ },
+ }
+ return function(_, _, s)
+ local result = {}
+ for k, v in pairs(specs) do
+ local count = nil
+ if type(s) == "string" then
+ -- 'git diff --shortstat' output
+ -- from 'git_changes_all'
+ count = tonumber(string.match(s, v.regex))
+ else
+ -- map from 'git_changes_buf'
+ count = s[k]
+ end
+ if count and count > 0 then
+ table.insert(result, set_hl(v.hl, ("%s%d"):format(v.icon, count)))
+ end
+ end
+ return table.concat(result, " ")
+ end
+end
+
+-- requires gitsigns
+function M.git_changes_buf(opts)
+ opts = opts or {}
+ local formatter = opts.formatter or git_changes_formatter(opts)
+ return wrap_fnc(opts, function(window, buffer)
+ local stats = {}
+ if buffer and buffer.bufnr > 0 then
+ local ok, res = pcall(vim.api.nvim_buf_get_var,
+ buffer.bufnr, "vgit_status")
+ if ok then stats = res end
+ end
+ if buffer and buffer.bufnr > 0 then
+ local ok, res = pcall(vim.api.nvim_buf_get_var,
+ buffer.bufnr, "gitsigns_status_dict")
+ if ok then stats = res end
+ end
+ local counts = {
+ insert = stats.added > 0 and stats.added or nil,
+ change = stats.changed > 0 and stats.changed or nil,
+ delete = stats.removed > 0 and stats.removed or nil,
+ }
+ if not vim.tbl_isempty(counts) then
+ local fmt = opts.fmt or "%s"
+ local out = formatter(window, buffer, counts)
+ return out and fmt:format(out) or nil
+ else
+ -- el functions that return a
+ -- string must not return nil
+ return ""
+ end
+ end)
+end
+
+function M.git_changes_all(opts)
+ opts = opts or {}
+ local formatter = opts.formatter or git_changes_formatter(opts)
+ return el_sub.buf_autocmd("el_git_changes", "BufWritePost",
+ wrap_fnc(opts, function(window, buffer)
+ if not buffer or
+ not (buffer.bufnr > 0) or
+ vim.bo[buffer.bufnr].bufhidden ~= "" or
+ vim.bo[buffer.bufnr].buftype == "nofile" or
+ vim.fn.filereadable(buffer.name) ~= 1 then
+ return
+ end
+
+ local j = job:new {
+ command = "git",
+ args = { "diff", "--shortstat" },
+ -- makes no sense to run for one file as
+ -- 'file(s) changed' will always be 1
+ -- args = { "diff", "--shortstat", buffer.name },
+ cwd = vim.fn.fnamemodify(buffer.name, ":h"),
+ }
+
+ local ok, git_changes = pcall(function()
+ return formatter(window, buffer, vim.trim(j:sync()[1]))
+ end)
+
+ if ok then
+ local fmt = opts.fmt or "%s"
+ return git_changes and fmt:format(git_changes) or nil
+ end
+ end))
+end
+
+function M.lsp_srvname(opts)
+ local fmt = opts.fmt or "%s"
+ local icon = opts.icon or ""
+ local buf_clients = vim.lsp.buf_get_clients(0)
+ if not buf_clients or #buf_clients == 0 then
+ return ""
+ end
+ local names = ""
+ for i, c in ipairs(buf_clients) do
+ if i > 1 then names = names .. ", " end
+ names = names .. c.name
+ end
+ return icon..fmt:format(names)
+end
+
+local function diag_formatter(opts)
+ return function(_, buffer, counts)
+ local items = {}
+ local icons = {
+ ["errors"] = { opts.icon_err or "x", opts.hl_err or "DiagnosticError"},
+ ["warnings"] = { opts.icon_warn or "!", opts.hl_warn or "DiagnosticWarn"},
+ ["infos"] = { opts.icon_info or "i", opts.hl_info or "DiagnosticInfo"},
+ ["hints"] = { opts.icon_hint or "h", opts.hl_hint or "DiagnosticHint"},
+ }
+ for _, k in ipairs({ "errors", "warnings", "infos", "hints" }) do
+ if counts[k] > 0 then
+ table.insert(items,
+ set_hl(icons[k][2], ("%s:%s"):format(icons[k][1], counts[k])))
+ end
+ end
+ local fmt = opts.fmt or "%s"
+ if vim.tbl_isempty(items) then
+ return ""
+ else
+ return fmt:format(table.concat(items, " "))
+ end
+ end
+end
+
+local function get_buffer_counts(diagnostic, _, buffer)
+ local counts = { 0, 0, 0, 0 }
+ local diags = diagnostic.get(buffer.bufnr)
+ if diags and not vim.tbl_isempty(diags) then
+ for _, d in ipairs(diags) do
+ if tonumber(d.severity) then
+ counts[d.severity] = counts[d.severity] + 1
+ end
+ end
+ end
+ return {
+ errors = counts[1],
+ warnings = counts[2],
+ infos = counts[3],
+ hints = counts[4],
+ }
+end
+
+function M.diagnostics(opts)
+ opts = opts or {}
+ local formatter = opts.formatter or diag_formatter(opts)
+ return el_sub.buf_autocmd("el_buf_diagnostic", "LspAttach,DiagnosticChanged",
+ wrap_fnc(opts, function(window, buffer)
+ return formatter(window, buffer, get_buffer_counts(vim.diagnostic, window, buffer))
+ end))
+end
+
+function M.line(opts)
+ opts = opts or {}
+ local fmt = opts.fmt or "%s"
+ return wrap_fnc(opts, function(_, _)
+ local al = vim.api.nvim_buf_line_count(0)
+ local cl = vim.api.nvim_win_get_cursor(0)[1]
+ return (fmt):format(cl.."/"..al.." "..math.floor((cl / al) * 100).."%%")
+ end)
+end
+
+return M