commit f2c0a5f8c6446ca58c1b306a7e04c8e58d366c6c Author: rnhmjoj Date: Tue Sep 6 17:09:28 2022 +0200 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29beda7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2022 TJ DeVries, Michele Guerini Rocco + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..06f278d --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +# Frameline + +### Very minimal framework to write a status/tabline + +Frameline is a Lua library for writing your own statusline (and also a tabline) +for Neovim. This is not a ready-made statusline plugin, it's essential just the +*frame*, you have to built the rest by yourself. + +Frameline is around 200 lines of Lua with no external dependencies. + +## Examples + +1. A statusline with the usual components: mode, git branch, filename, + encoding, etc. + + ```lua + local frameline = require 'frameline' + local utils = frameline.utils + + -- Some components + function mode(win, buf) + if not win.is_active then return end + + local k = vim.api.nvim_get_mode().mode + local modes = { + n={'Normal', 'Normal'}, + i={'Insert', 'Insert'}, + R={'Replas', 'Insert'}, + v={'Visual', 'Visual'}, + V={'V⋅Line', 'VLine'}, + t={'Termin', 'Term'}, + ['']={'V⋅Bloc', 'VBloc'} + } + return utils.highlight('Mode'..modes[k][2], ' '..modes[k][1]..' ') + end + + function branch(_, buf) + local head = vim.fn.FugitiveHead() + if buf.modifiable and head ~= "" then return '⚑ '..head end + end + + function filename(_, buf) + local delta = "" + if buf.modified then delta = 'Δ' end + if not buf.modifiable then delta = '∇' end + local fname = buf.name ~= "" and utils.filename or 'new-file' + return delta..fname + end + + function readonly(_, buf) + if buf.modifiable and buf.readonly then return '∅' end + end + + function encoding(_, buf) + return buf.fileencoding ~= "" and buf.fileencoding or nil + end + + function filetype(_, buf) + return buf.filetype ~= "" and buf.filetype or 'no ft' + end + + -- Statusline + frameline.setup_statusline(function() + local segments = {} + + -- Left section + table.insert(segments, utils.subsection{items={mode}}) + table.insert(segments, utils.subsection{ + separator=' → ', + items={branch, filename, readonly}, + }) + table.insert(segments, utils.split) + + -- Right section + table.insert(segments, utils.subsection{ + user=2, + separator=':', stop=' ', + items={utils.line_number, utils.column_number}, + }) + table.insert(segments, utils.subsection{ + user=1, + separator=' ∘ ', + items={utils.percent, encoding, filetype} + }) + + return segments + end) + ``` + +2. A tabline that shows the current tab, open tabs and date. + + ```lua + -- Tabline + frameline.setup_tabline(function() + local segments = {} + local api = vim.api + local color = '%#StatusLine#' + + -- Tabs + local current = api.nvim_get_current_tabpage() + local label = ' %d %s ' + for i, tab in pairs(api.nvim_list_tabpages()) do + -- tab -> active win -> active buf -> name + local active_buf = api.nvim_win_get_buf(api.nvim_tabpage_get_win(tab)) + local name = api.nvim_buf_get_name(active_buf) + name = vim.fn.fnamemodify(name, ':t') -- filename only + + local group = tab == current and 'TablineTabCur' or 'TablineTab' + table.insert(segments, utils.highlight(group, label:format(i, name))) + end + + table.insert(segments, color) + table.insert(segments, utils.split) + + -- Current date + table.insert(segments, vim.fn.strftime("%a %H:%M")) + + return table.concat(segments) + end) + ``` + +## License + +Frameline is licensed under the MIT license. +See the accompanying file LICENSE or https://mit-license.org/. diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..7548c0a --- /dev/null +++ b/default.nix @@ -0,0 +1,17 @@ +{ lib, runCommand }: + +runCommand "nvim-frameline" + { pname = "nvim-frameline"; + version = "0.1.0"; + + meta = with lib; { + description = "Very minimal framework to write a status/tabline"; + homepage = "https://maxwell.ydns.eu/git/rnhmjoj/nvim-frameline"; + license = licenses.mit; + maintainers = [ maintainers.rnhmjoj ]; + platforms = platforms.all; + }; + } + '' + install -m644 -D ${./frameline.lua} "$out/lua/frameline/init.lua" + '' diff --git a/frameline.lua b/frameline.lua new file mode 100644 index 0000000..aeea182 --- /dev/null +++ b/frameline.lua @@ -0,0 +1,224 @@ +-- Very minimal framework to write a status/tabline +-- Based on a (literally) stripped-down express_line. +-- License: MIT +-- Copyright © 2022 TJ DeVries, Michele Guerini Rocco + + +local fl = {} + +-- Misc utilities +fl.utils = { + -- Built-in segments + line_number = '%l', + column_number = '%c', + percent = "%p%%", + filename = '%t', + split = '%=', + + -- Highlights a string + highlight = function(group, content) + return ('%%#%s#'):format(group) .. content .. '%*' + end, + + -- Creates a subsection {highlight, items, separator, stop} + subsection = function(cfg) + local segments = {} + + if cfg.user ~= nil then + table.insert(segments, ('%%%s*'):format(cfg.user)) + end + if cfg.highlight ~= nil then + table.insert(segments, ('%%#%s#'):format(cfg.highlight)) + end + + table.insert(segments, function(win, buf) + local res, last_active = "", false + + for i, item in pairs(cfg.items) do + local part = type(item) == 'string' and item or item(win, buf) + if part and last_active then + res = res .. (cfg.separator or ' ') + end + if part then + res = res .. part + last_active = true + end + end + return res + end) + table.insert(segments, cfg.stop or ' ') + + if highlight ~= nil or user ~= nil then + table.insert(segments, '%*') + end + + return segments + end +} + +-- Neovim Buffer class +local Buffer = {} +local buf_props = { + name = function(buf) return vim.api.nvim_buf_get_name(buf.bufnr) end, + is_active = function(buf) return buf.bufnr == vim.api.nvim_get_current_buf() end, +} +function Buffer:new(bufnr) + if bufnr == 0 then + bufnr = vim.api.nvim_buf_get_number(0) + end + + return setmetatable({ bufnr = bufnr, }, { __index=function(t, k) + if Buffer[k] ~= nil then + t[k] = Buffer[k] + elseif buf_props[k] ~= nil then + t[k] = buf_props[k](t) + else + t[k] = vim.api.nvim_buf_get_option(t.bufnr, k) + end + return t[k] + end + }) +end + +-- Neovim Window class +local Window = {} +local win_props = { + width = function(win) return vim.api.nvim_win_get_width(win.winid) end, + height = function(win) return vim.api.nvim_win_get_height(win.winid) end, + is_active = function(win) return win.winid == vim.api.nvim_get_current_win() end +} +function Window:new(winid) + return setmetatable({winid=winid}, { __index=function(t, k) + local result + if Window[k] ~= nil then + result = Window[k] + elseif win_props[k] ~= nil then + result = win_props[k](t) + end + return result + end + }) +end + +-- Evaluates the statusline segments +local processor = function(items, window, buffer) + local winid = window.winid + + return function() + if not vim.api.nvim_win_is_valid(winid) then return end + + buffer = Buffer:new(buffer.bufnr) + local waiting = {} + local statusline = {} + local effects = {} + + for k, v in ipairs(items) do + local ok, result, effect + if type(v) == 'string' then + ok, result = true, v + elseif type(v) == 'function' then + ok, result, effect = pcall(v, window, buffer) + else + ok = false + end + + if not ok then + statusline[k] = '' + else + if type(result) == 'thread' then + table.insert(waiting, {index=k, thread=result, effect=effect}) + else + statusline[k], effects[k] = result, effect + end + end + end + + local remaining = table.getn(waiting) + local completed = 0 + + local start = os.time() + while start + 2 > os.time() do + if remaining == completed then + break + end + + for i = 1, remaining do + local wait_val = waiting[i] + + if wait_val ~= nil then + local index, thread = wait_val.index, wait_val.thread + local _, res = coroutine.resume(thread, window, buffer) + + if coroutine.status(thread) == 'dead' then + statusline[index] = res + -- Remove + completed = completed + 1 + waiting[i] = nil + end + end + end + end + + -- Filter nils and concat + local final = {} + for k, v in ipairs(statusline) do + if effects[k] then v = effects[k](v) end + if v then table.insert(final, v) end + end + + return table.concat(final, "") + end +end + +-- Creates a table of all window/buffers +local get_new_windows_table = function() + return setmetatable({}, {__index = function(self, winid) + local val = setmetatable({}, { + __index = function(win_table, bufnr) + + if not fl.generator then return function() return '' end end + + local window = Window:new(winid) + local buffer = Buffer:new(bufnr) + local items = vim.tbl_flatten(fl.generator(window, buffer)) + local p = processor(items, window, buffer) + + rawset(win_table, bufnr, p) + return p + end, + }) + + rawset(self, winid, val) + return val + end, + }) +end + +-- Draws the status/tabline +function draw_statusline(winid) + local bufnr = vim.api.nvim_win_get_buf(winid) + return fl._window_statuslines[winid][bufnr]() +end +function draw_tabline() + return fl._tabline() +end + +-- Set up a statusline per window +fl.setup_statusline = function(generator) + fl._window_statuslines = get_new_windows_table() + fl.generator = generator + vim.api.nvim_create_autocmd( + {'BufWinEnter','WinEnter'}, + {pattern='*', callback=function() + vim.wo.statusline = "%!luaeval('draw_statusline("..vim.api.nvim_get_current_win()..")')" + end + }) +end + +-- Set up a global tabline +fl.setup_tabline = function(generator) + fl._tabline = generator + vim.cmd('set tabline=%!v:lua.draw_tabline()') +end + +return fl