Butler is a tool that allows you to easily start and stop a predefined list of processes based on your working directory. I created it because I started using ThePrimeagen's git-worktree.nvim plugin and it made my previous tmux workflow more or less impossible to use.
When switching worktrees, I need my servers to be running from the correct directories. If I don't kill them and restart them from the new worktree directory, they continue serving code from a previous worktree that I'm not working with at the moment.
Install using your favorite plugin manager. For example in vim-plug:
" butler.nvim depends on processes.nvim
Plug 'brandoncc/processes.nvim'
Plug 'brandoncc/butler.nvim'
The plugin has the following default configuration:
local config = {
-- Signals to send to the process to kill it, starting on the left.
kill_signals = { 'TERM', 'KILL' },
-- Each signal is given the kill_timeout length of time in seconds to exit
-- before trying the next signal.
kill_timeout = 1,
-- If you would like to see messages such as "Killing process 123 with signal
-- TERM", enable this.
log_kill_signals = false,
-- Butler currently supports tmux and native neovim terminals.
interface = 'native'
}
You can customize the plugin with the setup()
function:
lua require("butler").setup({ kill_timeout = 0.5 })
Butler is configured using a json file that is located at
$HOME/.config/butler.nvim/config.json
(pull requests to improve flexibility are welcome).
Paths are expanded using the vim expand()
function, so $HOME
, ~
, etc work
as expected.
{
"$HOME/dev/project-a": [
{
"name": "dev server",
"cmd": "bin/server"
},
{
"name": "dev repl",
"cmd": "bin/console"
}
],
"$HOME/dev/project-b": [
{
"name": "dev server",
"cmd": "bin/other-server"
}
],
"$HOME/dev": [
{
"name": "work timer",
"cmd": "timer"
}
]
}
Project paths can be nested, and all paths which match (are a substring of) your current working directory will be used. This is particularly useful with worktrees since you can use your bare repo directory in the configuration and all worktree subdirectories will inherit the processes. You can also set processes for specific worktrees by adding configurations for their exact paths.
In the configuration example above, the following is a list of the servers that will be started for different working directories:
bin/server
timer
bin/other-server
timer
timer
Processes are started in specially-marked buffers. When stopping processes, butler looks for these specially-marked buffers, not buffers that happen to be running the same command.
That means butler will stop any process running in these specially-marked buffers, even if it isn't the process that butler originally started.
This also means you can run the same command in a different terminal buffer and butler will not stop it. Butler only stops processes in buffers it creates.
lua require("butler").start()
Stopping servers kills their processes and then deletes their buffers. The processes are stopped using the kill signals provided in the configuration. The signals are each tried until the process is successfully killed or there are no more signals to try.
lua require("butler").stop()
Restarting servers stops them, which deletes their buffers, and then starts new ones. If you have changed your working directory (with worktrees, for example), the new servers will be running in the new working directory.
lua require("butler").restart()
I have the following configuration for git-worktree.nvim:
local Worktree = require("git-worktree")
Worktree.on_tree_change(function(op)
if op == Worktree.Operations.Switch then
require("butler").restart()
end
end)
This configuration has the effect of starting servers the first time you enter a worktree, and restarting them as you move into other worktrees.
Each interface provides its own way to jump to processes, which can be accessed with:
lua require("butler").processes()
When using the native interface, a telescope picker is provided to assist in
jumping between processes. If telescope is not installed, the processes()
function simply say that.
When using the tmux interface, the tmux choose-tree
command is used to assist
in choosing a process. The interface is filtered so that only butler-managed
tmux panes are shown.
Butler can be easily extended by adding more interfaces. At this time, there are two interfaces: 'native' and 'tmux'.
The main interface file should be located at
lua/butler/interfaces/your-interface.lua
. For this file name, you would use:
lua require("butler").setup({ interface = 'your-interface' })
Interfaces must return a table which has :new(fn)
defined on it, with the
following structure:
{
start_servers: function(commands),
stop_servers: function(),
is_available: function(),
}
start_servers()
will be called with the commands from your json config that
match the current working directory. The list will contain the full command
table for each, not just the command.cmd
strings.
stop_servers()
should stop any processes that the interface started, and do
any necessary cleanup.
is_available()
should return a boolean that lets butler know if the interface
can be used. For example, the tmux interface is not available if neovim is not
running within a tmux session.
The function can optionally return a second value which is a string containing a message that will be displayed to the user when the interface is unavailable. If provided, it is displayed to the user as "[your message], butler is falling back to native".
If no message is provided, the default message to the user is "[interface name] is not available, butler is falling back to native".
When the function that gets passed to :new()
is called, it returns a copy of
the latest butler config. This config contains any values set with
require("butler").setup()
, regardless of when setup()
was called.
If you would like your interface to offer a way to jump to processes that butler
is managing, you can also export a function on your interface table called
choose_process()
. This function is called by require("butler").processes()
.
A minimal interface example is available here.
If you have any ideas how the plugin could be improved or extended, please open an issue so we can discuss them. Contributions are welcome!