Skip to content

Instantly share code, notes, and snippets.

@haolian9
Last active May 26, 2023 10:51
Show Gist options
  • Save haolian9/ac7fa319308e1e9ece18fb329fbf5711 to your computer and use it in GitHub Desktop.
Save haolian9/ac7fa319308e1e9ece18fb329fbf5711 to your computer and use it in GitHub Desktop.
interactive sudo for nvim
---@diagnostic disable: unused-local
--design choices/limits
--* sudo runs in a tty process
--* sudo should run in background
--* prompt user to enter his password when needed
--* singleton
--* sync but not blocking nvim
--
--supported forms:
--* [ ] sudo mv src dest
--* [ ] echo hello | sudo tee dest 1>/dev/null
--
--todo:
--* why sudo does not ask user to enter password every time in shell? but it does here.
local api = vim.api
local state = { term_width = nil, term_height = nil, bufnr = nil, term = nil, job = nil }
do
local cols, lines = vim.o.columns, vim.o.lines
state.term_width = math.min(cols, math.max(math.floor(cols * 0.8), 100))
state.term_height = math.min(lines, math.max(math.floor(lines * 0.3), 5))
end
state.bufnr = api.nvim_create_buf(false, true)
---@param output string[]
---@param gold string
---@return boolean
local function output_contains(output, gold)
for _, line in pairs(output) do
if string.find(line, gold, 1, true) then return true end
end
return false
end
local function show_prompt(winbar)
if not (state.win_id and api.nvim_win_is_valid(state.win_id)) then
local cols, lines = vim.o.columns, vim.o.lines
local width = state.term_width + 2
local height = state.term_height + 2
local x = math.floor((cols - width) / 2)
local y = lines - height
-- stylua: ignore
state.win_id = api.nvim_open_win(state.bufnr, true, {
relative = "editor", style = "minimal",
row = y, col = x, width = width, height = height,
})
else
api.nvim_set_current_win(state.win_id)
end
vim.wo[state.win_id].winbar = winbar
vim.cmd.startinsert()
end
state.term = api.nvim_open_term(state.bufnr, {
on_input = function(_, term, bufnr, data)
vim.fn.chansend(state.job, data)
-- todo: can be called on_exit for better UX
-- if data == "\r" then vim.schedule(function()
-- api.nvim_win_close(state.win_id, false)
-- state.win_id = nil
-- end) end
end,
})
local sudo_cmd = { "sudo", "vifm" }
state.job = vim.fn.jobstart(sudo_cmd, {
pty = true,
width = state.term_width,
height = state.term_height,
stdin = "pipe",
stdout_buffered = false,
stderr_buffered = false,
env = { LANG = "C" },
on_exit = function(job_id, exit_code, event)
vim.fn.chanclose(state.term)
api.nvim_buf_delete(state.bufnr, { force = false })
end,
on_stdout = function(job_id, data, event)
vim.fn.chansend(state.term, data)
if output_contains(data, "[sudo] password for") then show_prompt(string.format("%s - stdout", table.concat(sudo_cmd, " "))) end
end,
on_stderr = function(job_id, data, event)
if output_contains(data, "[sudo] password for") then show_prompt(string.format("%s - stderr", table.concat(sudo_cmd, " "))) end
vim.fn.chansend(state.term, data)
end,
})
@haolian9
Copy link
Author

the window that shows the terminal buffer has +2 width and height, so we can confirm that the jobstart(width, height) does take effects

@cryptomilk
Copy link

I dunno what creates the pty bigger than expected in my environment.

@cryptomilk
Copy link

Also I get asked every time unlock sudo. I guess because use a pty and it goes away.

@haolian9
Copy link
Author

what creates the pty bigger than expected in my environment.

according to your previous screen shot, i actually can not see anything abnormal. can you run a fullscreen tui program? so that you can check the borders.

get asked every time unlock sudo

that's what i want to know too, it is a bit annoying.

@haolian9
Copy link
Author

bigger than expected

dont be confused by the &winbar!

@cryptomilk
Copy link

get asked every time unlock sudo

that's what i want to know too, it is a bit annoying.

As the pty gets closed after sudo finishes, the sudo session is gone. Next time you open a new pty with a new sudo session.

@haolian9
Copy link
Author

i have no idea to reuse a pty in the processes which will be spawned serially right now. maybe it's possible to do it in the C realm using luajit's FFI, but that seems to be over complicated.

@haolian9
Copy link
Author

apparently libuv has no support for creating a pty: https://groups.google.com/g/libuv/c/EhwAn3y9wWo?pli=1.

the rabbit hole is deep enough, and cost enough.

@cryptomilk
Copy link

According to neovim developers jobstart should be avoided and uv.new_tty() should be used.

https://github.com/luvit/luv/blob/master/docs.md#uv_tty_t--tty-handle

@haolian9
Copy link
Author

haolian9 commented May 4, 2023

thanks for pointing out it! but sadly it's not an equilevant to openpty(3).
oh maybe i can access openpty via ffi, and then reuse the pty between spawn(sudo)?
dont know if it can work eventually, but it's worth a try!

@haolian9
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment