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.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# SSH Launcher
|
||||
|
||||

|
||||
|
||||
SSH Launcher lists hosts from your OpenSSH config and opens an SSH session in a
|
||||
terminal from the Noctalia launcher.
|
||||
|
||||
## Plugin
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| ID | `cleboost/ssh-launcher` |
|
||||
| Entry | Launcher provider: `launcher` |
|
||||
| Launcher Prefix | `/ssh` |
|
||||
|
||||
## Requirements
|
||||
|
||||
Install [OpenSSH](https://www.openssh.com/) and ensure `ssh` is available on
|
||||
`PATH`.
|
||||
|
||||
## Usage
|
||||
|
||||
Open the Noctalia launcher and type `/ssh` to list hosts from your SSH config.
|
||||
Continue typing to filter by host alias, hostname, or user, then select one to
|
||||
open a terminal running `ssh <host>`.
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `config_path` | `file` | `~/.ssh/config` | Path to your SSH config file. |
|
||||
| `max_results` | `int` | `30` | Maximum number of hosts shown. |
|
||||
| `extra_args` | `string` | `""` | Extra arguments appended to every `ssh` command. |
|
||||
| `terminal` | `string` | `""` | Custom terminal command. Empty uses Noctalia's terminal discovery. |
|
||||
|
||||
## Notes
|
||||
|
||||
Hosts are read from simple `Host` entries in your SSH config. Wildcard patterns
|
||||
such as `Host *` or `Host *.example.com` are ignored. `Include` directives are
|
||||
not expanded in this version. The host list is cached for the launcher session.
|
||||
@@ -0,0 +1,201 @@
|
||||
--!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
|
||||
@@ -0,0 +1,49 @@
|
||||
id = "cleboost/ssh-launcher"
|
||||
name = "SSH Launcher"
|
||||
version = "1.0.0"
|
||||
plugin_api = 3
|
||||
author = "Cleboost"
|
||||
license = "MIT"
|
||||
icon = "terminal"
|
||||
description = "Connect to SSH hosts from ~/.ssh/config via /ssh."
|
||||
tags = ["launcher", "network", "utility", "development"]
|
||||
dependencies = ["ssh"]
|
||||
|
||||
[[launcher_provider]]
|
||||
id = "launcher"
|
||||
entry = "launcher.luau"
|
||||
prefix = "ssh"
|
||||
glyph = "terminal"
|
||||
include_in_global_search = false
|
||||
debounce_ms = 0
|
||||
|
||||
[[setting]]
|
||||
key = "config_path"
|
||||
type = "file"
|
||||
label_key = "settings.config_path.label"
|
||||
description_key = "settings.config_path.description"
|
||||
default = "~/.ssh/config"
|
||||
|
||||
[[setting]]
|
||||
key = "max_results"
|
||||
type = "int"
|
||||
label_key = "settings.max_results.label"
|
||||
description_key = "settings.max_results.description"
|
||||
default = 30
|
||||
min = 1
|
||||
max = 100
|
||||
|
||||
[[setting]]
|
||||
key = "extra_args"
|
||||
type = "string"
|
||||
label_key = "settings.extra_args.label"
|
||||
description_key = "settings.extra_args.description"
|
||||
default = ""
|
||||
|
||||
[[setting]]
|
||||
key = "terminal"
|
||||
type = "string"
|
||||
label_key = "settings.terminal.label"
|
||||
description_key = "settings.terminal.description"
|
||||
default = ""
|
||||
advanced = true
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config-empty": "No SSH hosts found in config",
|
||||
"config-missing": "SSH config file not found",
|
||||
"err-no-ssh": "ssh command not found",
|
||||
"err-no-ssh-subtitle": "Install OpenSSH client and ensure ssh is on PATH",
|
||||
"err-terminal": "Could not open a terminal",
|
||||
"filter-empty": "Filter: \"{filter}\"",
|
||||
"loading": "Loading…",
|
||||
"loading-subtitle": "Reading SSH config",
|
||||
"no-hosts-found": "No SSH hosts found",
|
||||
"settings": {
|
||||
"config_path": {
|
||||
"description": "Path to your SSH config file.",
|
||||
"label": "SSH config path"
|
||||
},
|
||||
"extra_args": {
|
||||
"description": "Extra arguments appended to every ssh command (for example -A).",
|
||||
"label": "Extra SSH arguments"
|
||||
},
|
||||
"max_results": {
|
||||
"description": "Maximum number of hosts shown in the launcher.",
|
||||
"label": "Maximum results"
|
||||
},
|
||||
"terminal": {
|
||||
"description": "Custom terminal command. Leave empty to use Noctalia's terminal discovery.",
|
||||
"label": "Terminal command"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config-empty": "Aucun hôte SSH trouvé dans la config",
|
||||
"config-missing": "Fichier de config SSH introuvable",
|
||||
"err-no-ssh": "Commande ssh introuvable",
|
||||
"err-no-ssh-subtitle": "Installez le client OpenSSH et vérifiez que ssh est dans le PATH",
|
||||
"err-terminal": "Impossible d'ouvrir un terminal",
|
||||
"filter-empty": "Filtre : « {filter} »",
|
||||
"loading": "Chargement…",
|
||||
"loading-subtitle": "Lecture de la config SSH",
|
||||
"no-hosts-found": "Aucun hôte SSH trouvé",
|
||||
"settings": {
|
||||
"config_path": {
|
||||
"description": "Chemin vers votre fichier de config SSH.",
|
||||
"label": "Chemin de la config SSH"
|
||||
},
|
||||
"extra_args": {
|
||||
"description": "Arguments ajoutés à chaque commande ssh (par exemple -A).",
|
||||
"label": "Arguments SSH supplémentaires"
|
||||
},
|
||||
"max_results": {
|
||||
"description": "Nombre maximum d'hôtes affichés dans le launcher.",
|
||||
"label": "Nombre maximum de résultats"
|
||||
},
|
||||
"terminal": {
|
||||
"description": "Commande terminal personnalisée. Laissez vide pour la détection automatique de Noctalia.",
|
||||
"label": "Commande terminal"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user