A ssh connections manager for neovim.
conn-manager-demo.mov
![Image](https://private-user-images.githubusercontent.com/3406908/404644350-41c9c586-390d-4cb4-8d0b-6954e0eece2c.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2Mzc0NzIsIm5iZiI6MTczOTYzNzE3MiwicGF0aCI6Ii8zNDA2OTA4LzQwNDY0NDM1MC00MWM5YzU4Ni0zOTBkLTRjYjQtOGQwYi02OTU0ZTBlZWNlMmMucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIxNSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMTVUMTYzMjUyWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MTliYmUyYzZmNzMzNTUxOGM2NGNkMmZhMWUzY2VmNzZhZDAwYjJkNDdkZmQ4NTVlOWQ0NzVmMGFmNDJlZWQwOCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.kGLxDaLaXAUh93oqqwEH-lhUs8ycnVVv9nTmUbRxBPk)
- neovim >= 0.10.0
- ssh client (ie.
ssh
command) - jq (optional)
Example setup for lazy.nvim
{
'epheien/conn-manager.nvim',
cmd = 'ConnManager',
config = function()
require('conn-manager').setup({
config_file = vim.fs.joinpath(vim.fn.stdpath('config') --[[@as string]], 'conn-manager.json'),
window_config = {
width = 36,
split = 'left',
vertical = true,
win = -1,
},
on_window_open = function(win)
vim.api.nvim_set_option_value('fillchars', vim.o.fillchars .. ',eob: ', { win = win })
end,
})
end,
}
Run :ConnManager
in neovim, and press a
to start. Press ?
for keymaps help.
:ConnManager [open|close|toggle]
You can press ?
to open keymaps help window in ConnManager
buffer.
Read setup_keymaps
function in lua/conn-manager/init.lua
for detail.
{
config_file = vim.fs.joinpath(vim.fn.stdpath('config') --[[@as string]], 'conn-manager.json'),
keymaps = true, -- add default keymaps
window_config = {
width = 30,
split = 'left',
vertical = true,
win = -1,
},
on_window_open = nil, -- params: (winid)
on_buffer_create = nil, -- params: (bufnr)
node = {
window_picker = nil, -- window_picker for builtin node open, params: (node)
on_open = nil, -- params: (node, fallback, opts)
icons = {
arrow_closed = ' ',
arrow_open = ' ',
closed_folder = ' ',
opened_folder = ' ',
terminal_conn = ' ',
},
},
help = {
cursorline = true,
sort_by = 'key',
winhl = 'NormalFloat:Normal',
window_config = {
relative = 'editor',
border = 'single',
row = 1,
col = 0,
style = 'minimal',
noautocmd = true,
},
},
filter = {
prefix = '[FILTER]: ',
},
save = {
on_read = nil, -- read hook, signature: (text) -> string
-- write hook, signature: (text) -> string|nil
-- return nil will bypass builtin file writing logic, so you must write file yourself.
on_write = nil,
}
}
lua/plugins/conn-manager.lua
local function on_node_open(node, fallback, opts)
local empty = require('utils').empty
if not opts or empty(opts.open_with) then
fallback()
return
end
if opts.open_with == 'tab' then
require('conn-manager').open_in_tab()
vim.api.nvim_set_option_value('winfixbuf', true, { win = 0 })
vim.t.title = node.config.display_name
return
end
local title = node.config.display_name
-- dump args to execute
local args = { 'ssh' }
if node.config.port then
vim.list_extend(args, { '-p', tostring(node.config.port) })
end
if not empty(node.config.username) then
vim.list_extend(args, { '-l', node.config.username })
end
if not empty(node.config.private_key_file) then
vim.list_extend(args, { '-i', vim.fn.expand(node.config.private_key_file) })
end
table.insert(args, node.config.computer_name)
local prefix
if opts.open_with == 'kitty' then
prefix = { 'open', '-n', '-a', 'kitty', '--args', '--title', title }
else
prefix = { 'open', '-n', '-a', 'alacritty', '--args', '--title', title, '-e' }
end
args = vim.list_extend(prefix, args)
vim.system(args, { stdout = false, stderr = false, detach = true })
end
---@param obj wintab.Wintab
---@return wintab.Component[]
local function get_buffer_components(obj) ---@diagnostic disable-line
local components = {}
for _, job in ipairs(require('conn-manager').tree.jobs) do
table.insert(
components,
require('wintab').Component.new(
job.bufnr,
string.format(' %s ', vim.b[job.bufnr].conn_manager_title)
)
)
end
return components
end
local wintab = { state = {} }
local function window_picker(node)
local winid = -1
if wintab.state.winid and vim.api.nvim_win_is_valid(wintab.state.winid) then
winid = wintab.state.winid
end
if vim.api.nvim_win_is_valid(winid) then
if not require('conn-manager.window').is_window_usable(winid) then
vim.api.nvim_win_set_buf(winid, vim.api.nvim_create_buf(true, true))
end
goto out
end
winid = require('conn-manager.window').pick_window_for_node_open(false)
if winid == -1 then
vim.cmd.tabnew()
vim.api.nvim_set_option_value('winfixbuf', true, { win = 0 })
vim.t.title = node.config.display_name
winid = vim.api.nvim_get_current_win()
end
::out::
if vim.api.nvim_get_option_value('winbar', { win = winid }) == '' then
wintab = require('wintab').init(winid, get_buffer_components)
end
return winid
end
return {
'epheien/conn-manager.nvim',
cmd = 'ConnManager',
dependencies = {
'epheien/wintab.nvim',
'nvchad/menu',
},
config = function()
require('conn-manager').setup({
config_file = vim.fs.joinpath(vim.fn.stdpath('config') --[[@as string]], 'conn-manager.json'),
window_config = {
width = 36,
split = 'left',
vertical = true,
win = -1,
},
on_window_open = function(win)
vim.api.nvim_set_option_value('statusline', '─', { win = win })
vim.api.nvim_set_option_value('fillchars', vim.o.fillchars .. ',eob: ', { win = win })
vim.api.nvim_set_option_value('winfixbuf', true, { win = win })
end,
node = {
on_open = on_node_open,
window_picker = window_picker,
},
on_buffer_create = function(bufnr)
vim.keymap.set(
'n',
't',
function() require('conn-manager').open({ open_with = 'tab' }) end,
{ buffer = bufnr, desc = 'Open in Tab' }
)
vim.keymap.set(
'n',
'.',
function() require('menu').open('conn-manager', { mouse = false, border = false }) end,
{ buffer = bufnr, desc = 'Menu' }
)
end,
save = {
on_read = function(text) return text end,
on_write = function(text)
if not vim.fn.executable('jq') then
return text
end
vim.system({ 'jq' }, {
stdin = text,
}, function(obj)
if obj.code ~= 0 then
vim.api.nvim_err_writeln(string.format('jq exit %d: %s', obj.code, obj.stderr))
return
end
local fname = require('conn-manager.config').config.config_file
local temp = fname .. '.tmp'
local file = io.open(temp, 'w')
if file then
file:write(obj.stdout)
file:close()
os.rename(temp, fname)
end
end)
end,
},
})
end,
}
vim.api.nvim_set_hl(0, 'ConnManagerFolder', { link = 'Directory', default = true })
vim.api.nvim_set_hl(0, 'ConnManagerCopyHL', { link = 'Added', default = true })
vim.api.nvim_set_hl(0, 'ConnManagerCutHL', { link = 'Changed', default = true })
vim.api.nvim_set_hl(0, 'ConnManagerLiveFilterPrefix', { link = 'PreProc', default = true })
vim.api.nvim_set_hl(0, 'ConnManagerLiveFilterValue', { link = 'ModeMsg', default = true })
Install menu and add this configuration.
lua/menus/conn-manager.lua
return {
{
name = ' Open with nvim terminal',
cmd = function() require('conn-manager').open({ open_with = '' }) end,
rtxt = 'on',
},
{
name = ' Open with nvim terminal in new tab',
cmd = function() require('conn-manager').open({ open_with = 'tab' }) end,
rtxt = 'ot',
},
{
name = ' Open with Alacritty.app',
cmd = function() require('conn-manager').open({ open_with = 'alacritty' }) end,
rtxt = 'oa',
},
{
name = ' Open with Kitty.app',
cmd = function() require('conn-manager').open({ open_with = 'kitty' }) end,
rtxt = 'ok',
},
{ name = 'separator' },
{
name = ' Add node',
cmd = require('conn-manager').add_node,
rtxt = 'a',
},
{
name = ' Add folder',
cmd = require('conn-manager').add_folder,
rtxt = 'A',
},
{ name = 'separator' },
{
name = ' Edit node',
cmd = require('conn-manager').modify,
rtxt = 'r',
},
{
name = ' Remove node',
cmd = require('conn-manager').remove,
rtxt = 'D',
},
{ name = 'separator' },
{
name = ' Cut',
cmd = require('conn-manager').cut_node,
rtxt = 'x',
},
{
name = ' Copy',
cmd = require('conn-manager').copy_node,
rtxt = 'c',
},
{
name = ' Paste',
cmd = function() require('conn-manager').paste_node() end,
rtxt = 'p',
},
{
name = ' Paste before node',
cmd = function() require('conn-manager').paste_node(true) end,
rtxt = 'p',
},
}
The password is stored in plain text in neovim, and the security needs to be ensured by yourself.
The connections config file saves sensitive information including passwords in plain text, do not upload this file to the public domain.
If you need to encrypt your connections config file, please use the on_read
and on_write
options,
refer Example Configuration
section for example.