Files
community-plugins/ssh-launcher/launcher.luau
T
CleboostandGitHub a2b1062856 feat(ssh-launcher): add /ssh launcher for SSH config hosts (#138)
* feat(ssh-launcher): add /ssh launcher for SSH config hosts

Provide a launcher provider that reads ~/.ssh/config, lists host aliases
with fuzzy search, and opens an SSH session in the terminal on activate.

* docs(ssh-launcher): update store thumbnail

Replace the plugin card image with the final SSH Launcher artwork,
blur server IP addresses, and fix the displayed category tags.

* fix(ssh-launcher): parse SSH config directives case-insensitively

Match Host, Hostname, and User keywords regardless of casing so
lowercase blocks are included, consistent with OpenSSH behavior.
2026-07-29 08:48:14 -04:00

202 lines
4.6 KiB
Luau

--!nonstrict
local cachedHosts = nil
local function trim(s)
return noctalia.string.trim(s or "")
end
local function shellQuote(s)
return "'" .. s:gsub("'", "'\\''") .. "'"
end
local function getConfigPath()
local configured = noctalia.getConfig("config_path")
if type(configured) == "string" and configured ~= "" then
return noctalia.expandPath(configured)
end
return noctalia.expandPath("~/.ssh/config")
end
local function getMaxResults()
local n = noctalia.getConfig("max_results")
return (type(n) == "number" and n > 0) and math.floor(n) or 30
end
local function statusRow(title, subtitle, glyph)
return { id = "", title = title, subtitle = subtitle, glyph = glyph }
end
local function isValidHostAlias(alias)
return alias ~= "*" and not alias:match("[*?]")
end
local function hostSubtitle(host)
if host.user and host.hostname then
return host.user .. "@" .. host.hostname
end
return host.hostname or host.user or ""
end
local function parseSshConfig(content)
local entries = {}
local current = nil
local function flushBlock()
if not current or #current.aliases == 0 then
return
end
local subtitle = hostSubtitle(current)
for _, alias in current.aliases do
table.insert(entries, { alias = alias, subtitle = subtitle })
end
end
for line in content:gmatch("[^\r\n]+") do
local trimmed = trim(line)
if trimmed ~= "" and not trimmed:match("^#") then
local key, value = trimmed:match("^(%S+)%s+(.+)$")
if key and value then
key = key:lower()
if key == "host" then
flushBlock()
current = { aliases = {}, hostname = nil, user = nil }
for alias in value:gmatch("%S+") do
if isValidHostAlias(alias) then
table.insert(current.aliases, alias)
end
end
elseif current then
if key == "hostname" then
current.hostname = trim(value)
elseif key == "user" then
current.user = trim(value)
end
end
end
end
end
flushBlock()
return entries
end
local function loadHosts()
local content = noctalia.readFile(getConfigPath())
if type(content) ~= "string" or content == "" then
return nil
end
return parseSshConfig(content)
end
local function launchTerminal(cmd)
local term = trim(noctalia.getConfig("terminal") or "")
if term == "" then
return noctalia.runInTerminal(cmd)
end
local first = term:match("^%S+") or term
local bin = first:match("([^/]+)$") or first
local separator = (bin == "gnome-terminal" or bin == "kgx" or bin == "ptyxis") and "--" or "-e"
return noctalia.runAsync(term .. " " .. separator .. " sh -lc " .. shellQuote(cmd))
end
local function fuzzyScoreForHost(filter, host)
for _, text in { host.alias, host.subtitle } do
if text ~= "" then
local score = noctalia.fuzzyScore(filter, text)
if score then
return score
end
end
end
end
local function makeRows(hosts, filter)
local rows = {}
local limit = getMaxResults()
for _, host in hosts do
local score = filter == "" and nil or fuzzyScoreForHost(filter, host)
if filter == "" or score ~= nil then
table.insert(rows, {
id = host.alias,
title = host.alias,
subtitle = host.subtitle ~= "" and host.subtitle or nil,
glyph = "terminal",
score = score,
})
end
end
if filter == "" then
table.sort(rows, function(a, b)
return a.title:lower() < b.title:lower()
end)
while #rows > limit do
table.remove(rows)
end
end
return rows
end
local function showResults(query, hosts, filter)
local rows = makeRows(hosts, filter)
if #rows == 0 then
launcher.setResults(query, {
statusRow(
noctalia.tr("no-hosts-found"),
filter ~= "" and noctalia.tr("filter-empty", { filter = filter }) or noctalia.tr("config-empty"),
"terminal"
),
})
else
launcher.setResults(query, rows)
end
end
function onQuery(query)
local filter = trim(query)
if not noctalia.commandExists("ssh") then
launcher.setResults(query, {
statusRow(noctalia.tr("err-no-ssh"), noctalia.tr("err-no-ssh-subtitle"), "triangle-alert"),
})
return
end
if cachedHosts then
showResults(query, cachedHosts, filter)
return
end
launcher.setResults(query, {
statusRow(noctalia.tr("loading"), noctalia.tr("loading-subtitle"), "loader"),
})
local hosts = loadHosts()
if not hosts then
launcher.setResults(query, {
statusRow(noctalia.tr("config-missing"), getConfigPath(), "file-x"),
})
return
end
cachedHosts = hosts
showResults(query, hosts, filter)
end
function onActivate(id)
if id == "" then
return
end
local extra = trim(noctalia.getConfig("extra_args") or "")
local cmd = "ssh " .. shellQuote(id) .. (extra ~= "" and (" " .. extra) or "")
if not launchTerminal(cmd) then
noctalia.notifyError("SSH Launcher", noctalia.tr("err-terminal"))
end
end