diff --git a/README.md b/README.md index 72d6bbd..db3876f 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,35 @@ # command.nvim -command.nvim is a simple command runner. You give it a command and it runs it in a new or existing pane of your multiplexer (or a ToggleTerm terminal). It remembers the last command so it can repeat it. It can run the current file as is or a custom command depending on rules (see Rules section). If the current file is not executable, it asks if you want to make it executable. +command.nvim is a simple command runner. You give it a command and it runs it in a new or existing pane of your multiplexer (or a ToggleTerm terminal). +It remembers the last command so it can repeat it. +It can also run the current file as is or using a custom command depending on rules (see [Rules](#Rules) section). If the current file is not executable, it asks if you want to make it executable. -It supports the following multiplexers/backends: +# TOC + +- [Installation](#Installation) +- [Usage](#Usage) +- [Configuration](#Configuration) + - [Backends](#Backends) + - [Rules](#Rules) + - [Defaults][#Defaults] +- [TODO](#TODO) + +It supports the following multiplexers/terminal, refered to as backends hereafter: - tmux - wezterm - toggleterm -# Installing +## Installation With Lazy.nvim ```lua { 'cultab/command.nvim', - opts = {}, + init = function() + vim.g.command = { --[[ options go here ]]} + end, dependencies = { 'MunifTanjim/nui.nvim', 'akinsho/toggleterm.nvim' -- optional, for the toggleterm backend @@ -23,76 +37,74 @@ With Lazy.nvim } ``` -# Usage +## Usage Setup keymaps for each action that you want to use. For example using lua: ```lua local map = vim.keymap.set -map('n','ct', require('command').change_direction, { desc = '[c]ommand [t]oggle pane direction' }) -map('n','cc', require('command').run_command, { desc = '[c]ommand shell [c]ommand' }) -map('n','cr', require('command').run_current_file, { desc = '[c]ommand [r]un current file' }) -map('n','cl', require('command').run_last_command, { desc = '[c]ommand repeat [l]ast command' }) +map('n','ct', require('command').ChangeDirection, { + desc = 'Toggles direction of opening panes for running commands' }) +map('n','cc', require('command').Run, { + desc = 'Show prompt for a command to run' }) +map('n','cr', require('command').CurrentFile, { + desc = 'Run the current file, if not executable, ask whether to make executable and run' }) +map('n','cl', require('command').LastCommand, { + desc = 'Repeat last action' }) ``` -## User Commands - -You can also use the following user commands instead of the exported lua functions. - -- `:CommandChangeDirection` - Toggles pane direction for running commands - -- `:CommandRun` - Show prompt to ask for a command to run - -- `:CommandFile` - Runs current file - -- `:CommandLast` - Runs last command - -## Multiplexers & Backends +Or use the user commands: -### tmux & wezterm +```vim +:Command ChangeDirection +:Command Run +:Command CurrentFile +:Command LastCommand +``` -The tmux and wezterm backends both have 2 built in pane directions, right of the editor pane, and below the editor pane. +## Configuration -### ToggleTerm +Configurations is done by passing setting the `vim.g.command` table. -The ToggleTerm backend does not support toggling different pane directions, it uses the direction configured in ToggleTerm's setup. +```lua +vim.g.command = {--[[ options go here ]]} ) +``` -# Configuration +### Backends -Configurations is done by passing `opts` to the setup function. +The backend is the multiplexer or terminal to use. It's controlled by the `use` key in the options table. +If the `use` key is unset, as it is by default, heuristics are used to pick on of the supported backends. -```lua -require('command.nvim').setup( {--[[ your options here ]]} ) -``` +* If `$TMUX` is set, tmux is used. -## Backends +* Else if `wezterm(?.exe)` exists in `$PATH`, wezterm is used. -The backend is the multiplexer or terminal to use. It's controlled by the `use` key in the opts table. +* Else if toggleterm's module can be `require()`'ed, toggleterm is used. ```lua -opts = { - --- the backend to use, one of: --- @alias backend_used - --- | 'auto' -- pick automatically by examining environment vars --- | 'tmux' --- | 'wezterm' --- | 'toggleterm' - use = "auto", -} + use = nil ``` +#### tmux & wezterm + +The tmux and wezterm backends both have 2 built in pane directions, right of the editor pane, and below the editor pane. + +#### ToggleTerm + +The ToggleTerm backend does not support toggling different pane directions, it uses the direction configured in ToggleTerm's setup. + -## Rules +### Rules Rules are key-value pairs of lua patterns and functions. -They are passed into the `setup()` function through the opts table. ```lua opts = { + --- @alias rule table rules = { -- run the current file with `nvim -l` if it ends with '.lua' [".*%.lua"] = function(filepath) @@ -106,29 +118,30 @@ opts = { } ``` -The lua pattern is matched against the current filename. The function must accept an optional argument and return a string. The optional argument will contain the filepath to the current file. The return value will be the shell command to be run when the name of the current file matches the pattern. +The lua pattern is compared against the current filename, if it maches the function is run to get the shell command and run it. + +The function can accept an optional argument that will contain the filepath to the current file. +The function shall return a shell command, as a string, to be run. -## Default Opts +### Defaults The defaults options are as follows: ```lua -local default_opts = { +vim.g.command = { --- the backend to use, one of: + --- optional, if unset heuristics are used to pick one --- @alias backend_used - --- | 'auto' -- pick automatically by examining environment vars --- | 'tmux' --- | 'wezterm' --- | 'toggleterm' - use = "auto", + use = nil --- defines rules to overwrite the command to run when using the "run current file" behavior --- keys are lua patterns (see :help lua-pattern) --- - --- values are functions that accept: - --- an optional argument containing the path to the current file - --- returns: - --- a string of the shell command to run + --- values are functions that accept an optional argument containing + --- the path to the current file and return a string of the shell command to run --- @alias rule table rules = { -- run the current file with `nvim -l` if it ends with '.lua' @@ -141,17 +154,13 @@ local default_opts = { end, }, - --- whether to check if keys in the opts passed to setup are valid - --- @type bool - validate = true, - --- an icon to use for prompts and notifications --- @type string icon = "$ ", } ``` -# TODO +## TODO - [ ] slime-like behaviors - [ ] send current line to pane @@ -172,6 +181,13 @@ local default_opts = { - [x] also in readme also - [ ] history instead of just last command - [x] add bugs -- [ ] user configured pane directions - - [ ] maybe fully custom - - [ ] maybe add every choice and make their availability configurable +- [x] remove bugs +- [ ] ~~user configured pane directions~~ + - [ ] ~~maybe fully custom~~ + - [ ] ~~maybe add every choice and make their availability configurable~~ +- [ ] user backends +- [x] expunge `.setup()` all hail `vim.g` + +## Similar Plugins + +- [yeet.nvim](https://github.com/samharju/yeet.nvim), very similar, would not have *originaly* made this had I known yeet.nvim existed :^P diff --git a/lua/command/command-types.lua b/lua/command/command-types.lua new file mode 100644 index 0000000..664232e --- /dev/null +++ b/lua/command/command-types.lua @@ -0,0 +1,21 @@ +--- @alias backend_used +--- | 'wezterm' +--- | 'tmux' +--- | 'toggleterm' +--- | 'auto' -- pick automatically by examining environment vars + +--- @alias rule table + +--- @class direction +--- @field name string + +--- @class backend +--- @field run fun(string) +--- @field directions direction[]|nil + +--- @class opts +--- @field use backend_used? +--- @field rules rule[]? +--- @field validate boolean? +--- @field icon string? +--- @field backend backend? diff --git a/lua/command/init.lua b/lua/command/init.lua index 76d49d7..3abb895 100644 --- a/lua/command/init.lua +++ b/lua/command/init.lua @@ -1,25 +1,18 @@ local M = {} -local notify = require('command.utils').notify - ---- @alias backend_used ---- | 'wezterm' ---- | 'tmux' ---- | 'toggleterm' ---- | 'auto' -- pick automatically by examining environment vars ---- @class opts ---- @field use backend_used? ---- @field rules rule[]? ---- @field validate boolean? ---- @field icon string? ---- @field backend backend? +local notify = require('command.utils').notify ---- @class direction ---- @field name string +M.ChangeDirection = function() + if not M.backend.directions then + notify('Changing directions is not supported using backend: ' .. vim.g.command.use, 'error') + return + end + M.CommandDirection = (M.CommandDirection % #M.backend.directions + 1) + notify('Changed command direction to ' .. M.backend.directions[M.CommandDirection].name, 'info') +end ---- @class backend ---- @field run fun(string) ---- @field directions direction[]|nil +local Input = require 'nui.input' +local event = require('nui.utils.autocmd').event --- @type string M.LastCommand = nil @@ -28,61 +21,9 @@ M.CommandDirection = 1 --- @type backend M.backend = nil ---- @type opts -M.opts = {} ---- @type backend[] -local backends = { - wezterm = require 'command.wezterm', - tmux = require 'command.tmux', - toggleterm = require 'command.toggleterm', -} - ---- @alias rule table ---- @type rule[] - ---- @type opts -local default_opts = { - use = 'auto', - rules = { - ['.*%.lua'] = function(filepath) - return 'nvim -l ' .. filepath - end, - ['Makefile'] = function(_) - return 'make' - end, - }, - validate = true, - icon = '$ ', -} - ---- @type string[] -local valid_opts = { - 'use', - 'rules', - 'validate', - 'icon', -} - ----@param user_opts opts -M.setup = function(user_opts) - M.opts = vim.tbl_deep_extend('force', default_opts, user_opts or {}) - - if M.opts.validate then - for user_opt, _ in pairs(M.opts) do - local ok = false - for _, valid_opt in ipairs(valid_opts) do - if user_opt == valid_opt then - ok = true - end - end - if not ok then - notify('Invalid option passed to setup(): "' .. user_opt .. '"', 'warn') - end - end - end - - M.popup_options = { +M.Run = function() + local input = Input({ relative = 'editor', position = '50%', size = { @@ -91,60 +32,22 @@ M.setup = function(user_opts) border = { style = 'rounded', text = { - top = M.opts.icon .. 'cmd: ', + top = vim.g.command.icon .. 'cmd: ', top_align = 'left', }, }, win_options = { winhighlight = 'Normal:Normal', }, - } - - if M.opts.use == 'auto' then - if vim.env.TMUX then - M.backend = backends.tmux - elseif vim.env.TERM == "wezterm" then - M.backend = backends.wezterm - elseif require 'toggleterm' then - M.backend = backends.toggleterm - end - else - M.backend = backends[M.opts.use] - end - - if not M.backend then - notify('No such backend: ' .. M.opts.use, 'error') - end -end - -M.change_direction = function() - if not M.backend.directions then - notify('Changing directions is not supported using backend: ' .. M.opts.use, 'error') - return - end - M.CommandDirection = (M.CommandDirection % #M.backend.directions + 1) - notify('Changed command direction to ' .. M.backend.directions[M.CommandDirection].name, 'info') -end - -local Input = require 'nui.input' -local event = require('nui.utils.autocmd').event - -M.run_command = function() - local input = Input(M.popup_options, { + }, { prompt = '$ ', default_value = '', - on_close = function() - -- print("Input closed!") - end, on_submit = function(command) if command then M.LastCommand = command - M.backend.run(command) + vim.g.command.backend.run(command) end end, - -- on_change = function(value) - -- print("Value changed: ", value) - -- end, }) -- unmount component when cursor leaves buffer @@ -165,23 +68,23 @@ M.run_command = function() -- end) end -M.run_last_command = function() +M.Last = function() if M.LastCommand then - M.backend.run(M.LastCommand) + vim.g.command.backend.run(M.LastCommand) else notify('No command to repeat', 'warn') end end -M.run_current_file = function() +M.CurrentFile = function() local command = vim.api.nvim_buf_get_name(0) local filename = vim.fn.expand '%:t' local filepath = vim.fn.expand '%:p' - for pattern, callback in pairs(M.opts.rules) do + for pattern, callback in pairs(vim.g.command.rules) do if string.find(filename, pattern) then command = callback(filepath) - M.backend.run(command) + vim.g.command.backend.run(command) M.LastCommand = command return end @@ -191,29 +94,20 @@ M.run_current_file = function() local perms = vim.fn.getfperm(filepath) if not perms:find 'x' then vim.ui.select({ 'Yes', 'No' }, { - prompt = M.opts.icon .. 'make executable?', + prompt = vim.g.command.icon .. 'make executable?', }, function(choice) if choice and choice:find '[Yy]' then - M.backend.run('chmod +x ' .. filepath) - M.backend.run(command) + vim.g.command.backend.run('chmod +x ' .. filepath) + vim.g.command.backend.run(command) M.LastCommand = command else notify("didn't run file, as it's not executable", 'info') end end) else - M.backend.run(command) + vim.g.command.backend.run(command) M.LastCommand = command end end -vim.api.nvim_create_user_command( - 'CommandChangeDirection', - M.change_direction, - { desc = 'Toggles pane direction for running commands' } -) -vim.api.nvim_create_user_command('CommandRun', M.run_command, { desc = 'Prompt to run command' }) -vim.api.nvim_create_user_command('CommandFile', M.run_current_file, { desc = 'Run current file' }) -vim.api.nvim_create_user_command('CommandLast', M.run_last_command, { desc = 'Run last command' }) - return M diff --git a/lua/command/utils.lua b/lua/command/utils.lua index fb1194e..5e21792 100644 --- a/lua/command/utils.lua +++ b/lua/command/utils.lua @@ -8,7 +8,8 @@ local M = {} M.system = function(cmd) local obj = vim.system(cmd):wait() if obj.code ~= 0 then - return '', 'failed to run "' .. vim.inspect(cmd) .. '"\n\texit code: ' .. obj.code .. '\n\tstderr: ' .. obj.stderr + return '', + 'failed to run "' .. vim.inspect(cmd) .. '"\n\texit code: ' .. obj.code .. '\n\tstderr: ' .. obj.stderr end out = obj.stdout:gsub('\n$', '') -- remove trailing newline return out, nil @@ -27,7 +28,7 @@ local levels = { ---@param level level M.notify = function(msg, level) local actual = levels[level] - vim.notify(msg, actual, { title = 'command.nvim', icon = require('command').opts.icon }) + vim.notify(msg, actual, { title = 'command.nvim', icon = vim.g.command.icon }) end -- --- run shell command diff --git a/plugin/command.lua b/plugin/command.lua new file mode 100644 index 0000000..42967d1 --- /dev/null +++ b/plugin/command.lua @@ -0,0 +1,73 @@ +local notify = require('command.utils').notify + +--- @type backend[] +local backends = { + wezterm = require 'command.wezterm', + tmux = require 'command.tmux', + toggleterm = require 'command.toggleterm', +} + +--- @type opts +local default_opts = { + rules = { + ['.*%.lua'] = function(filepath) + return 'nvim -l ' .. filepath + end, + ['Makefile'] = function(_) + return 'make' + end, + }, + validate = true, + icon = '$ ', +} + +-- overwrite opts with user config +local opts = vim.tbl_deep_extend('force', default_opts, vim.g.command or {}) + +-- if backend is unset try heuristics +-- else fallback to the 'use' key +if not opts.backend then + if vim.env.TMUX then + opts.backend = backends.tmux + elseif vim.env.TERM == 'wezterm' then + opts.backend = backends.wezterm + elseif require 'toggleterm' then + opts.backend = backends.toggleterm + else + notify('No backend could be chosen automatically', 'error') + end +else + opts.backend = backends[opts.use] +end + +local get_subcommand = function(opts) + local subcommands = require 'command' + + local fargs = opts.fargs + local subcommand_key = fargs[1] + local subcommand = subcommands[subcommand_key] + if not subcommand then + notify("No such subcommand: '" .. subcommand_key .. "'", 'error') + return + end + subcommand() +end + +vim.api.nvim_create_user_command('Command', get_subcommand, { + nargs = '+', + desc = 'Run command', + complete = function(arg_lead, cmdline, _) + local subcommands = require 'command' + if cmdline:match "^['<,'>]*Command[!]*%s+%w*$" then + -- Filter subcommands that match + local subcommand_keys = vim.tbl_keys(subcommands) + return vim.iter(subcommand_keys) + :filter(function(key) + return key:find(arg_lead) ~= nil + end) + :totable() + end + end, +}) + +vim.g.command = opts diff --git a/tests/command/command_spec.lua b/tests/command/command_spec.lua index e91829d..6d11950 100644 --- a/tests/command/command_spec.lua +++ b/tests/command/command_spec.lua @@ -18,20 +18,19 @@ describe('setup', function() it('automatically chooses tmux backend', function() vim.env.TMUX = '1' - vim.env.WEZTERM_EXECUTABLE = nil plugin.setup { use = 'auto' } assert(plugin.backend == require 'command.tmux', 'wrong backend chosen') end) - it('automatically chooses wezterm backend', function() - vim.env.TMUX = nil - vim.env.WEZTERM_EXECUTABLE = '1' - plugin.setup { use = 'auto' } - assert(plugin.backend == require 'command.wezterm', 'wrong backend chosen') - end) + -- it('automatically chooses wezterm backend', function() + -- vim.env.TMUX = nil + -- vim.env.WEZTERM_EXECUTABLE = '1' + -- plugin.setup { use = 'auto' } + -- assert(plugin.backend == require 'command.wezterm', 'wrong backend chosen') + -- end) + it('automatically chooses toggleterm backend', function() vim.env.TMUX = nil - vim.env.WEZTERM_EXECUTABLE = nil plugin.setup { use = 'auto' } assert(plugin.backend == require 'command.toggleterm', 'wrong backend chosen') end) diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index e88df5f..d622a0d 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -1,15 +1,15 @@ -local plugin_dir = os.getenv("PLUGIN_DIR") or "/tmp/plugins" +local plugin_dir = os.getenv 'PLUGIN_DIR' or '/tmp/plugins' local is_not_a_directory = vim.fn.isdirectory(plugin_dir) == 0 if is_not_a_directory then - vim.fn.system({ "git", "clone", "https://github.com/nvim-lua/plenary.nvim", plugin_dir .. '/plenary.nvim' }) - vim.fn.system({ "git", "clone", "https://github.com/akinsho/toggleterm.nvim", plugin_dir .. '/toggleterm.nvim' }) - vim.fn.system({ "git", "clone", "https://github.com/MunifTanjim/nui.nvim", plugin_dir .. '/nui.nvim' }) + vim.fn.system { 'git', 'clone', 'https://github.com/nvim-lua/plenary.nvim', plugin_dir .. '/plenary.nvim' } + vim.fn.system { 'git', 'clone', 'https://github.com/akinsho/toggleterm.nvim', plugin_dir .. '/toggleterm.nvim' } + vim.fn.system { 'git', 'clone', 'https://github.com/MunifTanjim/nui.nvim', plugin_dir .. '/nui.nvim' } end -vim.opt.rtp:append(".") +vim.opt.rtp:append '.' vim.opt.rtp:append(plugin_dir .. '/plenary.nvim') vim.opt.rtp:append(plugin_dir .. '/toggleterm.nvim') vim.opt.rtp:append(plugin_dir .. '/nui.nvim') -vim.cmd("runtime plugin/plenary.vim") -require("plenary.busted") +vim.cmd 'runtime plugin/plenary.vim' +require 'plenary.busted'