Files
community-plugins/tmux-provider/tmux_provider.luau
T
mguandGitHub 8d183a1dcb feat: add tmux-provider plugin (#151)
* feat: add tmux-provider plugin

* fix: invalid thumbnail name

* fix: invalid translation keys

* docs: README updated to include launcher provider entry

Also fixed wording for requirements (validation failed for some reason?)
2026-07-29 15:00:07 -04:00

238 lines
5.8 KiB
Luau

local tinsert = table.insert
local tsort = table.sort
local sformat = string.format
-- Per-query in-flight state: both tmux and tmuxp results must land before
-- we render, since either can be requested for the same query.
local sessionsCache
local tmuxpCache
local pendingQuery: string? = nil
local pendingTmux = false
local pendingTmuxp = false
local function tmuxInstalled(): boolean
return noctalia.commandExists("tmux")
end
local function tmuxpInstalled(): boolean
return noctalia.commandExists("tmuxp")
end
local function shellQuote(s: string): string
return "'" .. s:gsub("'", "'\"'\"'") .. "'"
end
-- Parses `tmux ls` output into a list of { name, attached }
-- Format per line: "name: N windows (created ...) [(attached)]"
local function parseTmuxLs(output: string)
local sessions = {}
for line in output:gmatch("[^\r\n]+") do
local name = line:match("^([^:]+):")
if name then
local attached = line:find("%(attached%)") ~= nil
tinsert(sessions, { name = name, attached = attached })
end
end
return sessions
end
-- Parses `tmuxp ls --json` output into a list of { name, session_name }
local function parseTmuxpLs(output: string)
local configs = {}
local data, err = noctalia.json.decode(output)
if not data or not data.workspaces then
if err then
noctalia.log("tmux-provider: failed to parse tmuxp ls --json output: " .. tostring(err))
end
return configs
end
for _, workspace in ipairs(data.workspaces) do
if workspace.name then
tinsert(configs, {
name = workspace.name,
session_name = workspace.session_name or workspace.name,
})
end
end
return configs
end
-- Builds the combined result list: running tmux sessions first, then tmuxp
-- configs that aren't already running as a live session.
local function buildResults(query: string)
local results = {}
local runningNames = {}
if sessionsCache then
for _, session in ipairs(sessionsCache) do
runningNames[session.name] = true
local score: number?
if query == "" then
score = 0
else
score = noctalia.fuzzyScore(query, session.name)
end
if score ~= nil then
tinsert(results, {
id = "attach:" .. session.name,
title = session.name,
subtitle = session.attached and noctalia.tr("session_attached")
or noctalia.tr("session_detached"),
glyph = "terminal-2",
score = score,
})
end
end
end
if tmuxpCache then
for _, config in ipairs(tmuxpCache) do
if not runningNames[config.session_name] then
local score: number?
if query == "" then
score = -1 -- rank below live sessions when query is empty
else
score = noctalia.fuzzyScore(query, config.name)
end
if score ~= nil then
tinsert(results, {
id = "tmuxp:" .. config.session_name,
title = config.name,
subtitle = noctalia.tr("tmuxp_config"),
glyph = "file-text",
score = score,
})
end
end
end
end
tsort(results, function(a, b)
return (a.score or 0) > (b.score or 0)
end)
return results
end
local function showResults(query: string)
local results = buildResults(query)
if #results > 0 then
launcher.setResults(query, results)
return
end
if not tmuxInstalled() then
launcher.setResults(query, {
{
id = "",
title = noctalia.tr("tmux_not_found"),
subtitle = noctalia.tr("tmux_not_found_subtitle"),
glyph = "alert-triangle",
},
})
return
end
launcher.setResults(query, {
{
id = "",
title = noctalia.tr("no_sessions_found"),
subtitle = noctalia.tr("no_sessions_subtitle"),
glyph = "loader",
},
})
end
-- Renders once every in-flight fetch for this query has landed.
local function maybeShowResults(query: string)
if pendingQuery ~= query then
return -- a newer query superseded this one
end
if pendingTmux or pendingTmuxp then
return -- still waiting on one of the two sources
end
showResults(query)
end
local function refreshSessions(query: string)
pendingQuery = query
if tmuxInstalled() then
pendingTmux = true
noctalia.runAsync("tmux ls", function(result)
if result.exitCode == 0 then
sessionsCache = parseTmuxLs(result.stdout)
else
-- tmux exits non-zero both on "no server running" (expected,
-- empty list) and on real errors. We can't reliably match the
-- "no server" message across tmux versions/locales, so we just
-- treat any non-zero exit as "no sessions" and log it at low
-- severity for diagnostics rather than surfacing it as an error.
if result.stderr and result.stderr ~= "" then
noctalia.log("tmux-provider: tmux ls: " .. result.stderr)
end
sessionsCache = {}
end
pendingTmux = false
maybeShowResults(query)
end)
else
sessionsCache = {}
pendingTmux = false
end
local use_tmuxp: boolean = noctalia.getConfig("use_tmuxp") or false
if use_tmuxp and tmuxpInstalled() then
pendingTmuxp = true
noctalia.runAsync("tmuxp ls --json", function(result)
if result.exitCode == 0 then
tmuxpCache = parseTmuxpLs(result.stdout)
else
noctalia.log(
"tmux-provider: tmuxp ls --json failed: "
.. tostring(result.stderr or result.stdout)
)
tmuxpCache = {}
end
pendingTmuxp = false
maybeShowResults(query)
end)
else
tmuxpCache = {}
pendingTmuxp = false
end
-- In case both sources were skipped synchronously (e.g. neither installed)
maybeShowResults(query)
end
function onQuery(query: string)
query = noctalia.string.trim(query)
refreshSessions(query)
end
function onActivate(id: string)
if id == "" then
return
end
local kind, name = id:match("^(%a+):(.+)$")
if not kind or not name then
noctalia.log("tmux-provider: onActivate received malformed id: " .. tostring(id))
return
end
if kind == "attach" then
noctalia.runInTerminal(sformat("tmux attach -t %s", shellQuote(name)))
elseif kind == "tmuxp" then
noctalia.runInTerminal(sformat("tmuxp load %s", shellQuote(name)))
end
end