Skip to content

Instantly share code, notes, and snippets.

@Dima-369
Created July 25, 2025 06:19
Show Gist options
  • Select an option

  • Save Dima-369/ec1fc3a523a80aa456ef10070781c2e3 to your computer and use it in GitHub Desktop.

Select an option

Save Dima-369/ec1fc3a523a80aa456ef10070781c2e3 to your computer and use it in GitHub Desktop.
Automatically rename terminal buffers to running command
local M = {}
-- Configuration
local config = {
update_interval = 2000, -- Update every 2 seconds
enable_auto_rename = true,
}
local group = vim.api.nvim_create_augroup('DynamicTermName', { clear = true })
local timers = {} -- Store timers for each terminal buffer
-- Function to get the currently running command in a terminal
local function get_running_command(bufnr, callback)
if not vim.api.nvim_buf_is_valid(bufnr) or vim.bo[bufnr].buftype ~= 'terminal' then
callback(nil)
return
end
-- First try to get info from b:term_title (more responsive)
local ok, term_title = pcall(vim.api.nvim_buf_get_var, bufnr, 'term_title')
if ok and term_title and term_title ~= "" then
-- Extract command from terminal title
local cmd_from_title = term_title:match("([^:]+)$") -- Get part after last colon
if cmd_from_title and cmd_from_title ~= "" and
not cmd_from_title:match("^[~/]") and -- Not a path
cmd_from_title ~= "fish" and cmd_from_title ~= "bash" and cmd_from_title ~= "zsh" then
callback(cmd_from_title:gsub("^%s+", ""):gsub("%s+$", "")) -- Trim whitespace
return
end
end
-- Fallback to process inspection
local terminal_job_id = vim.b[bufnr].terminal_job_id
local job_pid = terminal_job_id and vim.fn.jobpid(terminal_job_id)
if not job_pid or job_pid <= 0 then
callback(nil)
return
end
-- Check if terminal job is still running
local channel = vim.api.nvim_buf_get_option(bufnr, 'channel')
if channel and channel ~= 0 then
local running = vim.fn.jobwait({channel}, 0)[1] == -1
if not running then
callback("exited")
return
end
end
-- Get child processes of the shell
local cmd = string.format("pgrep -P %d 2>/dev/null | head -1", job_pid)
vim.fn.jobstart(cmd, {
on_stdout = function(_, data)
local child_pid = nil
for _, line in ipairs(data) do
if line and line ~= "" then
child_pid = vim.trim(line)
break
end
end
if child_pid then
-- Get the full command line of the child process
local ps_cmd = string.format("ps -o args= -p %s 2>/dev/null", child_pid)
vim.fn.jobstart(ps_cmd, {
on_stdout = function(_, ps_data)
local full_command = nil
for _, line in ipairs(ps_data) do
if line and line ~= "" then
full_command = vim.trim(line)
break
end
end
if full_command then
-- Extract just the command name and first few args
local parts = vim.split(full_command, " ")
local cmd_name = vim.fn.fnamemodify(parts[1], ":t")
-- Include first argument if it's not a flag
if #parts > 1 and not parts[2]:match("^%-") then
callback(cmd_name .. " " .. parts[2])
else
callback(cmd_name)
end
else
callback(nil)
end
end,
on_exit = function(_, exit_code)
if exit_code ~= 0 then
callback(nil)
end
end,
})
else
callback(nil)
end
end,
on_exit = function(_, exit_code)
if exit_code ~= 0 then
callback(nil)
end
end,
})
end
-- Function to update terminal buffer name
local function update_terminal_name(bufnr)
if not vim.api.nvim_buf_is_valid(bufnr) or vim.bo[bufnr].buftype ~= 'terminal' then
return
end
get_running_command(bufnr, function(command)
-- Ensure buffer is still valid
if not vim.api.nvim_buf_is_valid(bufnr) or not vim.b[bufnr].terminal_job_id then
return
end
local display_name
if command == "exited" then
display_name = "[TERM] exited"
elseif command and command ~= "" and command ~= "fish" and command ~= "bash" and command ~= "zsh" then
display_name = "[TERM] " .. command
else
-- Show shell name when no specific command is running
local shell = vim.fn.fnamemodify(vim.o.shell, ':t')
display_name = "[TERM] " .. shell
end
-- Get current buffer name to avoid unnecessary updates
local current_name = vim.api.nvim_buf_get_name(bufnr)
local current_display = vim.fn.fnamemodify(current_name, ':t')
if current_display ~= display_name then
-- Create unique name by including buffer number
local unique_name = display_name .. "-" .. bufnr
-- Use pcall to handle naming conflicts gracefully
local success = pcall(vim.api.nvim_buf_set_name, bufnr, unique_name)
if not success then
-- Fallback with timestamp if naming still fails
local timestamp = os.time()
local fallback_name = display_name .. "-" .. bufnr .. "-" .. timestamp
pcall(vim.api.nvim_buf_set_name, bufnr, fallback_name)
end
end
end)
end
-- Function to start monitoring a terminal buffer
local function start_monitoring(bufnr)
if timers[bufnr] then
timers[bufnr]:stop()
end
timers[bufnr] = vim.loop.new_timer()
timers[bufnr]:start(0, config.update_interval, vim.schedule_wrap(function()
update_terminal_name(bufnr)
end))
end
-- Function to stop monitoring a terminal buffer
local function stop_monitoring(bufnr)
if timers[bufnr] then
timers[bufnr]:stop()
timers[bufnr]:close()
timers[bufnr] = nil
end
end
-- Setup function
function M.setup(opts)
if opts then
config = vim.tbl_extend("force", config, opts)
end
if not config.enable_auto_rename then
return
end
-- Start monitoring when terminal opens
vim.api.nvim_create_autocmd('TermOpen', {
group = group,
callback = function()
local bufnr = vim.api.nvim_get_current_buf()
vim.defer_fn(function()
start_monitoring(bufnr)
end, 500) -- Small delay to let terminal initialize
end,
})
-- Monitor terminal title changes for immediate updates (more responsive)
vim.api.nvim_create_autocmd('TermRequest', {
group = group,
callback = function(args)
-- Update name when terminal sends title update sequences
if args.data and args.data.sequence then
local seq = args.data.sequence
-- OSC 0, 1, 2 are window/icon/title setting sequences
if seq:match('\027]0;') or seq:match('\027]1;') or seq:match('\027]2;') then
vim.defer_fn(function()
update_terminal_name(args.buf)
end, 50)
end
end
end,
})
-- Stop monitoring when terminal closes
vim.api.nvim_create_autocmd('BufDelete', {
group = group,
callback = function()
local bufnr = tonumber(vim.fn.expand('<abuf>'))
if bufnr then
stop_monitoring(bufnr)
end
end,
})
-- Also monitor when entering terminal buffers
vim.api.nvim_create_autocmd('BufEnter', {
pattern = 'term://*',
group = group,
callback = function()
local bufnr = vim.api.nvim_get_current_buf()
if not timers[bufnr] then
start_monitoring(bufnr)
end
end,
})
end
-- Manual rename function
function M.rename_current_terminal()
local bufnr = vim.api.nvim_get_current_buf()
if vim.bo[bufnr].buftype == 'terminal' then
update_terminal_name(bufnr)
vim.notify("Terminal buffer renamed", vim.log.levels.INFO)
else
vim.notify("Not in a terminal buffer", vim.log.levels.WARN)
end
end
-- Function to rename all terminal buffers
function M.rename_all_terminals()
local count = 0
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_valid(bufnr) and
vim.api.nvim_get_option_value("buftype", { buf = bufnr }) == "terminal" then
update_terminal_name(bufnr)
count = count + 1
end
end
vim.notify("Renamed " .. count .. " terminal buffers", vim.log.levels.INFO)
end
return M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment