snailed
/
taolf
Archived
2
0
Fork 0

initial commit

This commit is contained in:
Luca Bilke 2024-01-28 22:50:38 +01:00
parent 69ab1efcff
commit 5bc3a78955
No known key found for this signature in database
GPG Key ID: AD6630D0A1E650AC
5 changed files with 308 additions and 474 deletions

9
lua/.luarc.json Normal file
View File

@ -0,0 +1,9 @@
{
"diagnostics.globals": [
"vim"
],
"diagnostics.disable": [
"duplicate-doc-field",
"duplicate-set-field"
]
}

View File

@ -1,85 +1,8 @@
local M = {}
local Config = require("lf.config")
local utils = require("lf.utils")
local uv = vim.loop
local api = vim.api
local fn = vim.fn
---Check Neovim version before setting mappings
---@param cfg Lf.Config
local function has_feature(cfg)
if not vim.keymap or not vim.keymap.set then
utils.err("lf.nvim mappings require Neovim 0.7.0 or higher", true)
cfg.mappings = false
end
end
---Make `Lf` become the file manager that opens whenever a directory buffer is loaded
---@param bufnr integer
---@return boolean
local function become_dir_fman(bufnr)
local bufname = api.nvim_buf_get_name(bufnr)
if bufname == "" then
return false
end
local stat = uv.fs_stat(bufname)
if type(stat) ~= "table" or (type(stat) == "table" and stat.type ~= "directory") then
return false
end
return true
end
local function setup_autocmds()
api.nvim_create_user_command("Lf", function(tbl)
require("lf").start(tbl.args)
end, {nargs = "*", complete = "file"})
if Config.data.default_file_manager or vim.g.lf_netrw then
local group = api.nvim_create_augroup("Lf_ReplaceNetrw", {clear = true})
if vim.g.loaded_netrwPlugin ~= 1 and not Config.data.disable_netrw_warning then
api.nvim_create_autocmd("FileType", {
desc = "Display message about Lf not being default file manager",
group = group,
pattern = "netrw",
once = true,
callback = function()
utils.warn([[Lf cannot be the default file manager with netrw enabled.]] ..
[[Put `vim.g.loaded_netrwPlugin` in your configuration.]])
end,
})
end
api.nvim_create_autocmd("VimEnter", {
desc = "Override the default file manager (i.e., netrw)",
group = group,
pattern = "*",
nested = true,
callback = function(a)
if fn.exists("#FileExplorer") then
api.nvim_create_augroup("FileExplorer", {clear = true})
end
end,
})
api.nvim_create_autocmd("BufEnter", {
desc = "After overriding default file manager, open Lf",
group = group,
pattern = "*",
once = true,
callback = function(a)
if become_dir_fman(a.buf) then
vim.defer_fn(function()
require("lf").start(a.file)
end, 1)
end
end,
})
end
end
---Setup the Lf plugin
---@param cfg Lf.Config
@ -89,10 +12,8 @@ function M.setup(cfg)
end
cfg = cfg or {}
has_feature(cfg)
M.__conf = cfg
Config.init()
setup_autocmds()
end
---Start the file manager
@ -115,11 +36,11 @@ function M.start(path, cfg)
else
-- Strict nil checks are needed because `nil` can be given as an argument
if path ~= nil and path_t ~= "string" then
utils.err("first argument must be a string")
api.nvim_err_writeln("first argument must be a string")
return
end
if cfg ~= nil and type(cfg) ~= "table" then
utils.err("second argument must be a table")
api.nvim_err_writeln("second argument must be a table")
return
end

View File

@ -1,7 +1,4 @@
local fn = vim.fn
local o = vim.o
local utils = require("lf.utils")
local api = vim.api
---@class Lf.Container
---@field data Lf.Config
@ -16,82 +13,23 @@ local Config = {
local default = {
default_cmd = "lf",
default_action = "drop",
default_actions = {
["<C-t>"] = "tabedit",
["<C-x>"] = "split",
["<C-v>"] = "vsplit",
["<C-o>"] = "tab drop",
["<C-e>"] = "edit",
["<C-g>"] = "argedit",
},
winblend = 10,
dir = "",
direction = "float",
border = "double",
height = fn.float2nr(fn.round(0.75 * o.lines)),
width = fn.float2nr(fn.round(0.75 * o.columns)),
dir = "gwd",
escape_quit = false,
focus_on_open = true,
mappings = true,
tmux = false,
default_file_manager = false,
disable_netrw_warning = true,
highlights = {
Normal = {link = "Normal"},
FloatBorder = {link = "FloatBorder"},
},
count = nil,
env = {
clear = false,
vars = {}, -- NOTE: this doesn't work for now
},
-- Layout configurations
layout_mapping = "<A-u>",
views = {
{width = 0.800, height = 0.800},
{width = 0.600, height = 0.600},
{width = 0.950, height = 0.950},
{width = 0.500, height = 0.500, col = 0, row = 0},
{width = 0.500, height = 0.500, col = 0, row = 0.5},
{width = 0.500, height = 0.500, col = 0.5, row = 0},
{width = 0.500, height = 0.500, col = 0.5, row = 0.5},
},
}
---Validate configuration values
---@param cfg Lf.Config existing configuration options
---@return Lf.Config
local function validate(cfg)
vim.validate({
default_cmd = {cfg.default_cmd, "s", false},
default_action = {cfg.default_action, "s", false},
default_actions = {cfg.default_actions, "t", false},
winblend = {cfg.winblend, {"n", "s"}, false},
dir = {cfg.dir, "s", false},
direction = {cfg.direction, "s", false},
border = {cfg.border, {"s", "t"}, false},
height = {cfg.height, {"n", "s"}, false},
width = {cfg.width, {"n", "s"}, false},
escape_quit = {cfg.escape_quit, "b", false},
focus_on_open = {cfg.focus_on_open, "b", false},
mappings = {cfg.mappings, "b", false},
tmux = {cfg.tmux, "b", false},
default_file_manager = {cfg.default_file_manager, "b", false},
disable_netrw_warning = {cfg.disable_netrw_warning, "b", false},
highlights = {cfg.highlights, "t", false},
count = {cfg.count, "n", true},
env = {cfg.env, "t", false},
env_vars = {cfg.env.vars, "t", false},
env_clear = {cfg.env.clear, "b", false},
-- Layout configurations
layout_mapping = {cfg.layout_mapping, "s", false},
views = {cfg.views, "t", false},
})
cfg.winblend = tonumber(cfg.winblend) --[[@as number]]
cfg.height = tonumber(cfg.height) --[[@as number]]
cfg.width = tonumber(cfg.width) --[[@as number]]
return cfg
end
---Set a configuration passed as a function argument (not through `setup`)
@ -100,7 +38,7 @@ end
function Config:override(cfg)
if type(cfg) == "table" then
self.data = vim.tbl_deep_extend("force", self.data, cfg) --[[@as Lf.Config]]
self.data = validate(self.data)
validate(self.data)
end
return self.data
end
@ -124,7 +62,7 @@ function Config.init()
local lf = require("lf")
-- Keep options from the `lf.setup()` call
Config.data = vim.tbl_deep_extend("keep", lf.__conf or {}, default) --[[@as Lf.Config]]
Config.data = validate(Config.data)
validate(Config.data)
lf.__conf = nil
Config.__loaded = true
end
@ -133,55 +71,17 @@ return setmetatable(Config, {
__index = function(self, key)
return rawget(self, key)
end,
__newindex = function(_self, key, val)
utils.warn(("do not set invalid config values: %s => %s"):format(key, val))
---@diagnostic disable-next-line: unused-local
__newindex = function(self, key, val)
api.nvim_err_writeln(("do not set invalid config values: %s => %s"):format(key, val))
end,
})
---@alias Lf.border.generic {[1]:string,[2]:string,[3]:string,[4]:string,[5]:string,[6]:string,[7]:string,[8]:string}
---@alias Lf.border "'none'"|"'single'"|"'double'"|"'rounded'"|"'solid'"|"'shadow'"|Lf.border.generic
---@alias Lf.direction "'vertical'"|"'horizontal'"|"'tab'"|"'float'"
---@alias Lf.directory "'gwd'"|"''"|nil|string
---@class Lf.views
---@field width number
---@field height number
---@field relative? "'editor'"|"'win'"|"'cursor'"|"'mouse'"
---@field win? integer For `relative='win'`
---@field anchor? "'NW'"|"'NE'"|"'SW'"|"'SE'" Which corner of float to place `(row, col)`
---@field bufpos? {row: number, col: number}
---@field row? integer|float
---@field col? integer|float
---@field focusable? boolean
---@field zindex? number
---@field style? "'minimal'"
---@field border? Lf.border Border kind
---@field title? string|{[1]: string, [2]: string}[] Can be a string or an array of tuples
---@field title_pos? "'left'"|"'center'"|"'right'"
---@field noautocmd? boolean
---@class Lf.env
---@field clear boolean Should environment variables be cleared?
---@field vars table<string, string|number> Hash of variables to be set on startup
---@class Lf.Config
---@field default_cmd? string Default `lf` command
---@field default_action? string Default action when `Lf` opens a file
---@field default_actions? table<string, string> Default action keybindings
---@field winblend? number Psuedotransparency level
---@field dir? Lf.directory Directory where `lf` starts ('gwd' is git-working-directory, ""/nil is CWD)
---@field direction? Lf.direction Window layout
---@field border? Lf.border Border kind
---@field width? integer Width of the *floating* window
---@field height? integer Height of the *floating* window
---@field escape_quit? boolean Whether escape should be mapped to quit
---@field focus_on_open? boolean Whether Lf should open focused on current file
---@field mappings? boolean Whether terminal buffer mappings should be set
---@field tmux? boolean Whether `tmux` statusline should be changed by this plugin
---@field default_file_manager? boolean Make lf the default file manager for neovim
---@field disable_netrw_warning? boolean Don't display a message when opening a directory with `default_file_manager` as true
---@field highlights? table<string, table<string, string>> Highlight table passed to `toggleterm`
---@field layout_mapping? string Keybinding to rotate through the window layouts
---@field views? Lf.views[] Table of layouts to be applied to `nvim_win_set_config`
---@field env? Lf.env Environment variables
---@field count? integer A number that triggers that specific terminal

View File

@ -1,24 +1,77 @@
local utils = require("lf.utils")
local ok, terminal = pcall(require, "toggleterm")
if not ok then
utils.err("toggleterm.nvim must be installed to use this program")
return
end
local fs = utils.fs
local cmd = vim.cmd
local api = vim.api
local fn = vim.fn
local uv = vim.loop
local o = vim.o
local fs = utils.fs
local map = utils.map
local ok, terminal = pcall(require, "toggleterm")
if not ok then
api.nvim_err_writeln("toggleterm.nvim must be installed to use this program")
return
end
local Config = require("lf.config")
-- local promise = require("promise")
-- local async = require("async")
---@class Terminal
---@alias Mode "n" | "i" | "?"
--- @class TerminalState
--- @field mode Mode
--- @class TermCreateArgs
--- @field newline_chr? string user specified newline chararacter
--- @field cmd? string a custom command to run
--- @field direction? string the layout style for the terminal
--- @field id number?
--- @field highlights table<string, table<string, string>>?
--- @field dir string? the directory for the terminal
--- @field count number? the count that triggers that specific terminal
--- @field display_name string?
--- @field hidden boolean? whether or not to include this terminal in the terminals list
--- @field close_on_exit boolean? whether or not to close the terminal window when the process exits
--- @field auto_scroll boolean? whether or not to scroll down on terminal output
--- @field float_opts table<string, any>?
--- @field on_stdout fun(t: Terminal, job: number, data: string[]?, name: string?)?
--- @field on_stderr fun(t: Terminal, job: number, data: string[], name: string)?
--- @field on_exit fun(t: Terminal, job: number, exit_code: number?, name: string?)?
--- @field on_create fun(term:Terminal)?
--- @field on_open fun(term:Terminal)?
--- @field on_close fun(term:Terminal)?
--- @class Terminal
--- @field newline_chr string
--- @field cmd string
--- @field direction string the layout style for the terminal
--- @field id number
--- @field bufnr number
--- @field window number
--- @field job_id number
--- @field highlights table<string, table<string, string>>
--- @field dir string the directory for the terminal
--- @field name string the name of the terminal
--- @field count number the count that triggers that specific terminal
--- @field hidden boolean whether or not to include this terminal in the terminals list
--- @field close_on_exit boolean? whether or not to close the terminal window when the process exits
--- @field auto_scroll boolean? whether or not to scroll down on terminal output
--- @field float_opts table<string, any>?
--- @field display_name string?
--- @field env table<string, string> environmental variables passed to jobstart()
--- @field clear_env boolean use clean job environment, passed to jobstart()
--- @field on_stdout fun(t: Terminal, job: number, data: string[]?, name: string?)?
--- @field on_stderr fun(t: Terminal, job: number, data: string[], name: string)?
--- @field on_exit fun(t: Terminal, job: number, exit_code: number?, name: string?)?
--- @field on_create fun(term:Terminal)?
--- @field on_open fun(term:Terminal)?
--- @field on_close fun(term:Terminal)?
--- @field _display_name fun(term: Terminal): string
--- @field __state TerminalState
--- @field new fun(self, term: TermCreateArgs?): Terminal
--- @field close fun(self)
--- @field open fun(self, size: number?, direction: string?)
local Terminal = require("toggleterm.terminal").Terminal
---@class Lf
@ -75,7 +128,6 @@ function Lf:new(config)
self:__set_argv()
self.bufnr = 0
self.view_idx = 1
self.action = self.cfg.default_action
-- Needs to be grabbed here before the terminal buffer is created
self.signcolumn = o.signcolumn
@ -91,20 +143,10 @@ function Lf:__create_term()
self.term = Terminal:new({
cmd = self.cfg.default_cmd,
dir = self.cfg.dir,
direction = self.cfg.direction,
winblend = self.cfg.winblend,
close_on_exit = true,
hidden = false,
clear_env = self.cfg.env.clear,
highlights = self.cfg.highlights,
display_name = "Lf",
count = self.cfg.count,
float_opts = {
border = self.cfg.border,
width = self.cfg.width,
height = self.cfg.height,
winblend = self.cfg.winblend,
},
})
end
@ -136,22 +178,26 @@ function Lf:__open_in(path)
if path == "gwd" or path == "git_dir" then
path = utils.git_dir()
end
path = fn.expand((path == "" or path == nil) and "%:p:h" or path)
local built = path
if type(path) == "string" then
path = fn.expand(path)
else
path = fn.expand("%:p:h")
end
local stat = uv.fs_stat(path)
if not type(stat) == "table" then
local cwd = uv.cwd()
stat = uv.fs_stat(cwd)
built = cwd
path = cwd --[[@as string]]
end
-- Should be fine, but just checking
if stat and not stat.type == "directory" then
built = fs.dirname(built)
path = fs.dirname(path)
end
self.term.dir = built
self.term.dir = path
self.curfile = fn.expand("%:p")
return self
@ -193,63 +239,26 @@ function Lf:__on_open(term)
self.bufnr = term.bufnr
self.winid = term.window
cmd("silent! doautocmd User LfTermEnter")
-- cmd("silent! doautocmd User LfTermEnter")
-- Wrap needs to be set, otherwise the window isn't aligned on resize
api.nvim_win_call(self.winid, function()
vim.wo.showbreak = "NONE"
vim.wo.wrap = true
vim.wo.sidescrolloff = 0
vim.wo.scrolloff = 0
vim.wo.scrollbind = false
end)
if self.cfg.tmux then
utils.tmux(true)
end
-- Not sure if this works
if self.cfg.mappings then
if self.cfg.escape_quit then
map(
"t",
"<Esc>",
"<Cmd>q<CR>",
{buffer = self.bufnr, desc = "Exit Lf"}
)
end
for key, mapping in pairs(self.cfg.default_actions) do
map("t", key, function()
-- Change default_action for easier reading in the callback
self.action = mapping
local res = utils.read_file(self.tmp_id)
self.id = tonumber(res)
fn.system({"lf", "-remote", ("send %d open"):format(self.id)})
end, {noremap = true, buffer = self.bufnr, desc = ("Lf %s"):format(mapping)})
end
if self.cfg.layout_mapping then
map("t", self.cfg.layout_mapping, function()
api.nvim_win_set_config(self.winid, utils.get_view(
self.cfg.views[self.view_idx],
self.bufnr,
self.signcolumn
))
self.view_idx = self.view_idx < #self.cfg.views
and self.view_idx + 1
or 1
end)
end
end
-- Don't know why whenever wrap is set in the terminal, a weird resize happens.
-- Because of that, this is needed here.
vim.defer_fn(function()
cmd("silent! doautoall VimResized")
end, 800)
-- -- Wrap needs to be set, otherwise the window isn't aligned on resize
-- api.nvim_win_call(self.winid, function()
-- vim.wo.showbreak = "NONE"
-- vim.wo.wrap = true
-- vim.wo.sidescrolloff = 0
-- vim.wo.scrolloff = 0
-- vim.wo.scrollbind = false
-- end)
--
-- if self.cfg.tmux then
-- utils.tmux(true)
-- end
--
-- -- Don't know why whenever wrap is set in the terminal, a weird resize happens.
-- -- Because of that, this is needed here.
-- vim.defer_fn(function()
-- cmd("silent! doautoall VimResized")
-- end, 800)
end
---@private
@ -257,16 +266,12 @@ end
---
---@param term Terminal
function Lf:__callback(term)
if self.cfg.tmux then
utils.tmux(false)
end
if
(self.action == "cd" or self.action == "lcd")
and uv.fs_stat(self.tmp_lastdir)
then
local last_dir = utils.read_file(self.tmp_lastdir)
if last_dir ~= nil and last_dir ~= uv.cwd() then
if type(last_dir) == "string" then
cmd(("%s %s"):format(self.action, fn.fnameescape(last_dir)))
return
end

View File

@ -1,66 +1,68 @@
local M = {}
--
local uv = vim.loop
local fn = vim.fn
local api = vim.api
local levels = vim.log.levels
local o = vim.o
---Echo a message with `nvim_echo`
---@param msg string message
---@param hl string highlight group
function M.echomsg(msg, hl)
hl = hl or "Title"
api.nvim_echo({{msg, hl}}, true, {})
end
---Display notification message
---@param msg string
---@param level number
---@param opts table?
function M.notify(msg, level, opts)
opts = vim.tbl_extend("force", opts or {}, {title = "lf.nvim"})
vim.notify(msg, level, opts)
end
---INFO message
---@param msg string
---@param print? boolean
---@param opts table?
function M.info(msg, print, opts)
if print then
M.echomsg(("[INFO]: %s"):format(msg), "Directory")
else
M.notify(msg, levels.INFO, opts)
end
end
---WARN message
---@param msg string
---@param print? boolean
---@param opts table?
function M.warn(msg, print, opts)
if print then
M.echomsg(("[WARN]: %s"):format(msg), "WarningMsg")
else
M.notify(msg, levels.WARN, opts)
end
end
---ERROR message
---@param msg string
---@param print? boolean
---@param opts table?
function M.err(msg, print, opts)
if print then
M.echomsg(("[ERROR]: %s"):format(msg), "ErrorMsg")
else
M.notify(msg, levels.ERROR, opts)
end
end
-- local api = vim.api
-- local levels = vim.log.levels
-- local o = vim.o
--
-- ---Echo a message with `nvim_echo`
-- ---@param msg string message
-- ---@param hl string highlight group
-- function M.echomsg(msg, hl)
-- hl = hl or "Title"
-- api.nvim_echo({{msg, hl}}, true, {})
-- end
--
-- ---Display notification message
-- ---@param msg string
-- ---@param level number
-- ---@param opts table?
-- function M.notify(msg, level, opts)
-- opts = vim.tbl_extend("force", opts or {}, {title = "lf.nvim"})
-- vim.notify(msg, level, opts)
-- end
--
-- ---INFO message
-- ---@param msg string
-- ---@param print? boolean
-- ---@param opts table?
-- function M.info(msg, print, opts)
-- if print then
-- M.echomsg(("[INFO]: %s"):format(msg), "Directory")
-- else
-- M.notify(msg, levels.INFO, opts)
-- end
-- end
--
-- ---WARN message
-- ---@param msg string
-- ---@param print? boolean
-- ---@param opts table?
-- function M.warn(msg, print, opts)
-- if print then
-- M.echomsg(("[WARN]: %s"):format(msg), "WarningMsg")
-- else
-- M.notify(msg, levels.WARN, opts)
-- end
-- end
--
-- ---ERROR message
-- ---@param msg string
-- ---@param print? boolean
-- ---@param opts table?
-- function M.err(msg, print, opts)
-- if print then
-- M.echomsg(("[ERROR]: %s"):format(msg), "ErrorMsg")
-- else
-- M.notify(msg, levels.ERROR, opts)
-- end
-- end
--
---@param path string
---@diagnostic disable-next-line: undefined-doc-name
---@return uv_fs_t|string
---@diagnostic disable-next-line: undefined-doc-name
---@return uv.aliases.fs_stat_table?
function M.read_file(path)
-- tonumber(444, 8) == 292
@ -70,153 +72,150 @@ function M.read_file(path)
uv.fs_close(fd)
return buffer, stat
end
---Create a neovim keybinding
---@param mode string vim mode in a single letter
---@param lhs string keys that are bound
---@param rhs string|function string or lua function that is mapped to the keys
---@param opts table? options set for the mapping
function M.map(mode, lhs, rhs, opts)
opts = opts or {}
opts.noremap = opts.noremap == nil and true or opts.noremap
vim.keymap.set(mode, lhs, rhs, opts)
end
---Get Neovim window height
---@return number
function M.height()
return o.lines - o.cmdheight
end
---Get neovim window width (minus signcolumn)
---@param bufnr number: Buffer number from the file that Lf is opened from
---@param signcolumn string: Signcolumn option set by the user, not the terminal buffer
---@return number
function M.width(bufnr, signcolumn)
if signcolumn:match("no") then
return o.columns
end
-- This is a rough estimate of the signcolumn
local width = #tostring(api.nvim_buf_line_count(bufnr))
local col = vim.split(signcolumn, ":")
if #col == 2 then
width = width + tonumber(col[2])
end
return o.columns - width
end
---Get the table that is passed to `api.nvim_win_set_config`
---@param opts table
---@param bufnr number Buffer number from the file that Lf is opened from
---@param signcolumn string Signcolumn option set by the user, not the terminal buffer
---@return table
function M.get_view(opts, bufnr, signcolumn)
opts = opts or {}
local width = opts.width
if width > o.columns then
M.err(("width (%d) cannot be greater than columns (%d)"):format(width, o.columns))
width = nil
end
width = width or M.width(bufnr, signcolumn)
if width < 0 then
if width > -1 then
width = fn.float2nr(o.columns + fn.round(width * o.columns))
else
width = o.columns + width
end
else
if width < 1 then
width = fn.float2nr(fn.round(width * o.columns))
end
end
local height = opts.height
if height > o.lines then
M.err(("height (%d) cannot be greater than lines (%d)"):format(height, o.lines))
height = nil
end
height = height or M.height()
if height < 0 then
if height > -1 then
height = fn.float2nr(o.lines + fn.round(height * o.lines))
else
height = o.lines + height
end
else
if height < 1 then
height = fn.float2nr(fn.round(height * o.lines))
end
end
local col = opts.col
and fn.float2nr(fn.round(opts.col * o.columns))
or math.ceil(o.columns - width) * 0.5 - 1
local row = opts.row
and fn.float2nr(fn.round(opts.row * o.lines))
or math.ceil(o.lines - height) * 0.5 - 1
return {
col = col, -- fractional allowed
row = row, -- fractional allowed
width = width, -- minimum of 1
height = height, -- minimum of 1
relative = "editor",
style = "minimal",
}
end
---@return string|nil
function M.git_dir()
---@diagnostic disable-next-line: missing-parameter
local gitdir = fn.system(("git -C %s rev-parse --show-toplevel"):format(fn.expand("%:p:h")))
if gitdir:match("^fatal:.*") then
M.info("Failed to get git directory")
return
end
return vim.trim(gitdir)
end
---Set the tmux statusline when opening/closing `Lf`
---@param disable boolean: whether the statusline is being enabled or disabled
function M.tmux(disable)
if not vim.env.TMUX then
return
end
if disable then
fn.system([[tmux set status off]])
fn.system([[tmux list-panes -F '\#F' | grep -q Z || tmux resize-pane -Z]])
else
fn.system([[tmux set status on]])
fn.system([[tmux list-panes -F '\#F' | grep -q Z && tmux resize-pane -Z]])
end
end
-- ╭──────────────────────────────────────────────────────────╮
-- │ vim.fs replacement │
-- ╰──────────────────────────────────────────────────────────╯
M.fs = {}
---Return basename of the given file entry
---
---@param file string: File or directory
---@return string: Basename of `file`
function M.fs.basename(file)
return fn.fnamemodify(file, ":t")
end
--
-- ---Create a neovim keybinding
-- ---@param mode string vim mode in a single letter
-- ---@param lhs string keys that are bound
-- ---@param rhs string|function string or lua function that is mapped to the keys
-- ---@param opts table? options set for the mapping
-- function M.map(mode, lhs, rhs, opts)
-- opts = opts or {}
-- opts.noremap = opts.noremap == nil and true or opts.noremap
-- vim.keymap.set(mode, lhs, rhs, opts)
-- end
--
-- ---Get Neovim window height
-- ---@return number
-- function M.height()
-- return o.lines - o.cmdheight
-- end
--
-- ---Get neovim window width (minus signcolumn)
-- ---@param bufnr number: Buffer number from the file that Lf is opened from
-- ---@param signcolumn string: Signcolumn option set by the user, not the terminal buffer
-- ---@return number
-- function M.width(bufnr, signcolumn)
-- if signcolumn:match("no") then
-- return o.columns
-- end
--
-- -- This is a rough estimate of the signcolumn
-- local width = #tostring(api.nvim_buf_line_count(bufnr))
-- local col = vim.split(signcolumn, ":")
-- if #col == 2 then
-- width = width + tonumber(col[2])
-- end
--
-- return o.columns - width
-- end
--
-- ---Get the table that is passed to `api.nvim_win_set_config`
-- ---@param opts table
-- ---@param bufnr number Buffer number from the file that Lf is opened from
-- ---@param signcolumn string Signcolumn option set by the user, not the terminal buffer
-- ---@return table
-- function M.get_view(opts, bufnr, signcolumn)
-- opts = opts or {}
--
-- local width = opts.width
-- if width > o.columns then
-- M.err(("width (%d) cannot be greater than columns (%d)"):format(width, o.columns))
-- width = nil
-- end
-- width = width or M.width(bufnr, signcolumn)
-- if width < 0 then
-- if width > -1 then
-- width = fn.float2nr(o.columns + fn.round(width * o.columns))
-- else
-- width = o.columns + width
-- end
-- else
-- if width < 1 then
-- width = fn.float2nr(fn.round(width * o.columns))
-- end
-- end
--
-- local height = opts.height
-- if height > o.lines then
-- M.err(("height (%d) cannot be greater than lines (%d)"):format(height, o.lines))
-- height = nil
-- end
-- height = height or M.height()
-- if height < 0 then
-- if height > -1 then
-- height = fn.float2nr(o.lines + fn.round(height * o.lines))
-- else
-- height = o.lines + height
-- end
-- else
-- if height < 1 then
-- height = fn.float2nr(fn.round(height * o.lines))
-- end
-- end
--
-- local col = opts.col
-- and fn.float2nr(fn.round(opts.col * o.columns))
-- or math.ceil(o.columns - width) * 0.5 - 1
-- local row = opts.row
-- and fn.float2nr(fn.round(opts.row * o.lines))
-- or math.ceil(o.lines - height) * 0.5 - 1
--
-- return {
-- col = col, -- fractional allowed
-- row = row, -- fractional allowed
-- width = width, -- minimum of 1
-- height = height, -- minimum of 1
-- relative = "editor",
-- style = "minimal",
-- }
-- end
--
-- ---@return string|nil
-- function M.git_dir()
-- ---@diagnostic disable-next-line: missing-parameter
-- local gitdir = fn.system(("git -C %s rev-parse --show-toplevel"):format(fn.expand("%:p:h")))
--
-- if gitdir:match("^fatal:.*") then
-- M.info("Failed to get git directory")
-- return
-- end
-- return vim.trim(gitdir)
-- end
--
-- ---Set the tmux statusline when opening/closing `Lf`
-- ---@param disable boolean: whether the statusline is being enabled or disabled
-- function M.tmux(disable)
-- if not vim.env.TMUX then
-- return
-- end
-- if disable then
-- fn.system([[tmux set status off]])
-- fn.system([[tmux list-panes -F '\#F' | grep -q Z || tmux resize-pane -Z]])
-- else
-- fn.system([[tmux set status on]])
-- fn.system([[tmux list-panes -F '\#F' | grep -q Z && tmux resize-pane -Z]])
-- end
-- end
--
-- -- ╭──────────────────────────────────────────────────────────╮
-- -- │ vim.fs replacement │
-- -- ╰──────────────────────────────────────────────────────────╯
--
-- M.fs = {}
--
-- ---Return basename of the given file entry
-- ---
-- ---@param file string: File or directory
-- ---@return string: Basename of `file`
-- function M.fs.basename(file)
-- return fn.fnamemodify(file, ":t")
-- end
---Return parent directory of the given file entry
---
---@param file string: File or directory
---@return string?: Parent directory of file
---@return string: Parent directory of file
function M.fs.dirname(file)
if file == nil then
return nil
end
return fn.fnamemodify(file, ":h")
end