From b4aa666ae8089472c9cd2008a55e3644ae198a12 Mon Sep 17 00:00:00 2001 From: martinko-nb995-btw Date: Thu, 20 Aug 2026 15:28:34 +0200 Subject: [PATCH 1/3] Add ssh-agent plugin --- ssh-agent/README.md | 102 +++++++ ssh-agent/panel.luau | 315 ++++++++++++++++++++++ ssh-agent/plugin.toml | 107 ++++++++ ssh-agent/service.luau | 478 +++++++++++++++++++++++++++++++++ ssh-agent/thumbnail.webp | Bin 0 -> 48734 bytes ssh-agent/translations/en.json | 37 +++ ssh-agent/widget.luau | 47 ++++ 7 files changed, 1086 insertions(+) create mode 100644 ssh-agent/README.md create mode 100644 ssh-agent/panel.luau create mode 100644 ssh-agent/plugin.toml create mode 100644 ssh-agent/service.luau create mode 100644 ssh-agent/thumbnail.webp create mode 100644 ssh-agent/translations/en.json create mode 100644 ssh-agent/widget.luau diff --git a/ssh-agent/README.md b/ssh-agent/README.md new file mode 100644 index 00000000..6e89c3de --- /dev/null +++ b/ssh-agent/README.md @@ -0,0 +1,102 @@ +# SSH Agent + +Manage your SSH keys and saved sessions directly from the Noctalia bar. This plugin keeps a small SSH agent running in the background, lets you add or remove keys, and provides a panel for quick session management. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `martasskv5/ssh-agent` | +| Entries | Bar widget: `ssh-agent-widget`; panel: `ssh-agent-panel`; service: `ssh-agent-service` | + +## Requirements + +Install the following on `PATH` for the plugin to work correctly: + +- `ssh-agent` +- `ssh-add` +- `zenity` + +For passphrase prompts, install one of these programs as well: + +- `ksshaskpass` +- `ssh-askpass` +- `lxqt-openssh-askpass` + +The plugin also uses `pkill`, `mkdir`, and the standard SSH agent socket pattern under `/tmp` to manage the agent lifecycle and temporary files. + +To make SSH tools available outside the plugin process, set `SSH_AUTH_SOCK` in your shell or desktop environment. For example: + +```sh +export SSH_AUTH_SOCK="/tmp/ssh-agent-$USER.sock" +``` + +For a compositor or window manager configuration, add the same value there as well so applications launched from the desktop environment can find the agent. + +### Example Niri config + +```kdl +environment { + SSH_ASKPASS "/usr/bin/ksshaskpass" + SSH_ASKPASS_REQUIRE "prefer" + SSH_AUTH_SOCK "/tmp/ssh-agent-.sock" +} +``` + +### Example Hyprland config + +```conf +env = SSH_ASKPASS,/usr/bin/ksshaskpass +env = SSH_ASKPASS_REQUIRE,prefer +env = SSH_AUTH_SOCK,/tmp/ssh-agent-.sock +``` + +## Usage + +Add the `SSH Agent` widget under Settings → Bar. + +To open the panel manually, run: + +```sh +noctalia msg panel-toggle martasskv5/ssh-agent:ssh-agent-panel +``` + +The panel lets you add SSH keys, remove them, and manage saved SSH sessions. If no SSH agent is running yet, the plugin will start one automatically and connect to it using the configured socket. + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `socket_path` | `string` | `""` | The path to the SSH agent socket. Leave empty to use the default per-user agent socket location. | +| `sessions_file` | `string` | `"~/.ssh/sessions.json"` | The path to the JSON file used to store saved SSH sessions. | +| `default_key_browse_path` | `string` | `"~/.ssh"` | The default folder shown when browsing for SSH private keys. | +| `terminal_command` | `string` | `""` | The terminal command used to launch SSH sessions. Leave empty to use the system default terminal. | +| `show_saved_sessions` | `boolean` | `true` | Whether saved SSH sessions are shown in the panel. | +| `show_notifications` | `boolean` | `true` | Whether the plugin shows success and failure notifications for SSH operations. | +| `auto_start_mode` | `string` | `"connect_existing"` | How the plugin decides whether to reuse an existing agent or start a new one. Supported values are `"connect_existing"`, `"ask_each_time"`, and `"create_new"`. | + +## IPC + +Refresh the plugin state and update the bar widget by sending the following IPC message: + +```sh +noctalia msg plugin martasskv5/ssh-agent:ssh-agent-service all refresh +``` + +Stop the SSH agent and remove the socket by sending: + +```sh +noctalia msg plugin martasskv5/ssh-agent:ssh-agent-service all stop-agent +``` + +Start a new SSH agent and connect to it by sending: + +```sh +noctalia msg plugin martasskv5/ssh-agent:ssh-agent-service all start-agent +``` + +## Notes + +The plugin creates and manages the SSH agent socket and stores saved connection data in a JSON file under the configured `sessions_file` path. It may spawn `ssh-agent`, `ssh-add`, and the configured askpass helper to prompt for passphrases or manage keys, and it writes temporary files in `/tmp` while creating and switching SSH agent sockets. + +If your environment does not export `SSH_AUTH_SOCK`, applications outside the plugin may not see the agent even when it is running, so it is often helpful to set it in both the shell and the window manager or compositor config. \ No newline at end of file diff --git a/ssh-agent/panel.luau b/ssh-agent/panel.luau new file mode 100644 index 00000000..e4a6a82d --- /dev/null +++ b/ssh-agent/panel.luau @@ -0,0 +1,315 @@ +--!nonstrict +-- Floating panel built from the declarative ui.* vocabulary. Replaces +-- Panel.qml. There's no native file picker in the plugin API, so "Add Key" +-- is a path input instead of NFilePicker — paste or type the key path. +-- +-- Everything here is a thin client: it reads noctalia.state("status") +-- (published by service.luau) and requests actions by writing to the +-- "command" channel. It never touches ssh-agent/ssh-add directly. + +local status = noctalia.state.get("status") or {} + +local editing = false +local selectedSessionName = "" +local editName, editHost, editUser, editPort, editKeyPath, editExtraArgs = "", "", "", "22", "", "" +local addKeyPath = "" + +local function sendCommand(action, extra) + local cmd = extra or {} + cmd.action = action + cmd.ts = noctalia.nowMs() + noctalia.state.set("command", cmd) +end + +local function clearForm() + editing = false + selectedSessionName = "" + editName, editHost, editUser, editPort, editKeyPath, editExtraArgs = "", "", "", "22", "", "" +end + +local function loadIntoForm(session) + editing = true + selectedSessionName = session.name or "" + editName = session.name or "" + editHost = session.host or "" + editUser = session.user or "" + editPort = tostring(session.port or "22") + editKeyPath = session.key_path or "" + editExtraArgs = session.extra_args or "" +end + +local function render() + local keyRows = {} + for _, key in ipairs(status.loaded_keys or {}) do + table.insert( + keyRows, + ui.row({ gap = 8, align = "center" }, { + ui.label({ text = key.name or "(unknown)", flexGrow = 1 }), + ui.label({ text = key.type or "", color = "on_surface_variant", fontSize = 11 }), + ui.button({ + glyph = "trash", + onClick = function() + sendCommand("remove-key", { name = key.name }) + end, + }), + }) + ) + end + if #keyRows == 0 then + table.insert(keyRows, ui.label({ text = "No keys loaded", color = "on_surface_variant" })) + end + + local children = { + -- Header row + ui.row({ align = "center", justify = "space_between", gap = 8 }, { + ui.label({ text = "SSH Agent", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }), + ui.label({ + text = status.agent_running and "Running" or "Stopped", + color = status.agent_running and "primary" or "error", + }), + ui.button({ glyph = "close", onClick = "onCloseClicked" }), + }), + + -- Loaded Keys Section + ui.column({ gap = 8 }, { + ui.row({ align = "center", justify = "space_between", gap = 8 }, { + ui.label({ text = "Loaded Keys", fontWeight = "medium", color = "on_surface_variant", flexGrow = 1 }), + ui.button({ glyph = "refresh", onClick = "onRefreshClicked" }), + }), + ui.column({ gap = 4 }, keyRows), + ui.row({ gap = 8, align = "center" }, { + ui.input({ + key = "add_key_path", + placeholder = "Path to private key…", + value = addKeyPath, + onChange = "onAddKeyPathChange", + onSubmit = "onAddKeySubmit", + flexGrow = 1 + }), + ui.button({ + text = (not addKeyPath or addKeyPath == "") and "Browse…" or "Add Key", + onClick = "onAddKeyClicked", + }), + }), + }), + } + + -- Conditionally evaluate and load Saved Sessions + local showSessionsConfig = noctalia.getConfig("show_saved_sessions") + if showSessionsConfig == true or showSessionsConfig == "true" then + local sessionRows = {} + for _, s in ipairs(status.sessions or {}) do + table.insert( + sessionRows, + ui.row({ gap = 8, align = "center" }, { + ui.label({ text = s.name or "", fontWeight = "medium", flexGrow = 1 }), + ui.label({ text = s.host or "", color = "on_surface_variant" }), + ui.button({ + glyph = "terminal", + onClick = function() + sendCommand("launch-session", { session = s }) + end, + }), + ui.button({ + glyph = "edit", + onClick = function() + loadIntoForm(s) + render() + end, + }), + ui.button({ + glyph = "trash", + onClick = function() + sendCommand("delete-session", { name = s.name }) + end, + }), + }) + ) + end + if #sessionRows == 0 then + table.insert(sessionRows, ui.label({ text = "No saved sessions", color = "on_surface_variant" })) + end + + table.insert(children, ui.column({ gap = 8 }, { + ui.row({ align = "center", justify = "space_between", gap = 8 }, { + ui.label({ text = "Saved Sessions", fontWeight = "medium", color = "on_surface_variant", flexGrow = 1 }), + ui.button({ text = "New Session", glyph = "plus", onClick = "onNewSessionClicked" }), + }), + ui.column({ gap = 4 }, sessionRows), + })) + end + + -- Editor Form Section + if editing then + table.insert( + children, + ui.column({ gap = 8 }, { + ui.label({ text = selectedSessionName ~= "" and "Edit Session" or "New Session", fontWeight = "medium" }), + ui.input({ key = "name", placeholder = "Session name", value = editName, onChange = "onNameChange" }), + ui.input({ key = "host", placeholder = "Host", value = editHost, onChange = "onHostChange" }), + ui.input({ key = "user", placeholder = "User", value = editUser, onChange = "onUserChange" }), + ui.input({ key = "port", placeholder = "Port", value = editPort, onChange = "onPortChange" }), + ui.input({ key = "key_path", placeholder = "Key path", value = editKeyPath, onChange = "onKeyPathChange" }), + ui.input({ + key = "extra_args", + placeholder = "Extra SSH args", + value = editExtraArgs, + onChange = "onExtraArgsChange", + }), + ui.row({ gap = 8 }, { + ui.button({ + text = (selectedSessionName ~= "" and "Update Session" or "Save Session"), + glyph = "device-floppy", + onClick = "onSaveSession", + }), + ui.button({ text = "Cancel", glyph = "x", onClick = "onCancelEdit" }), + }), + }) + ) + end + + -- Using a structural column with padding rather than an aggressive scrolling viewport tracker. + -- This enables Noctalia's window framework engine to dynamically compress boundaries. + panel.render(ui.scroll({ + -- Caps the physical view boundary so it doesn't bleed out of the display edge + maxHeight = 500, + -- Instructs the scroll panel to occupy the full width of the parent panel frame + width = "stretch" + }, { + -- Your main structural layout block nesting inside the view tracker + ui.column({ + gap = 16, + padding = 16, + align = "stretch", + -- Tightly binds the column height around the generated Luau node tree array + height = "shrink" + }, children) + })) +end + + +function onOpen(_context) + status = noctalia.state.get("status") or {} + render() +end + +noctalia.state.watch("status", function(value) + status = value or {} + render() +end) + +function onNameChange(value) + editName = value +end +function onHostChange(value) + editHost = value +end +function onUserChange(value) + editUser = value +end +function onPortChange(value) + editPort = value +end +function onKeyPathChange(value) + editKeyPath = value +end +function onExtraArgsChange(value) + editExtraArgs = value +end +function onAddKeyPathChange(value) + addKeyPath = value +end + +function onNewSessionClicked() + clearForm() + editing = true + render() +end + +function onCancelEdit() + clearForm() + render() +end + +function onSaveSession() + sendCommand("upsert-session", { + session = { + name = editName, + host = editHost, + user = editUser, + port = editPort, + key_path = editKeyPath, + extra_args = editExtraArgs, + }, + }) + clearForm() + render() +end + +-- function onAddKeyClicked() +-- if addKeyPath ~= "" then +-- sendCommand("add-key", { path = addKeyPath }) +-- addKeyPath = "" +-- render() +-- end +-- end + +function onAddKeySubmit(value) + addKeyPath = value + if addKeyPath ~= "" then + sendCommand("add-key", { path = addKeyPath }) + addKeyPath = "" + end + render() +end + +function onRefreshClicked() + sendCommand("refresh") +end + +function onCloseClicked() + panel.close() +end + +local function openFileBrowser(callback) + print(noctalia.getConfig("default_key_browse_path")) + local defaultPath = noctalia.getConfig("default_key_browse_path") or "~/.ssh" + local filenameArg = "" + + -- 1. Ensure the path is explicitly recognized as a directory by Zenity using a trailing slash + -- 2. Construct the exact shell execution string bypassing Lua's sandboxed environment restrictions + if defaultPath:sub(1, 1) == "~" then + -- Appends a slash after stripping '~', resulting in: --filename=$HOME"/.ssh/" + filenameArg = '--filename=$HOME"' .. defaultPath:sub(2) .. '/"' + else + -- Forces a trailing slash if it doesn't have one + if defaultPath:sub(-1) ~= "/" then + defaultPath = defaultPath .. "/" + end + filenameArg = '--filename="' .. defaultPath .. '"' + end + + -- Run Zenity safely with double quotes wrapping the command arguments + noctalia.runAsync('zenity --file-selection --title="Select SSH Private Key" ' .. filenameArg .. ' 2>/dev/null', function(res) + local output = (res and res.stdout) or "" + if res and res.exitCode == 0 and output ~= "" then + callback((output:gsub("%s+$", ""))) + else + callback(nil) + end + end, 60000) +end + +function onAddKeyClicked() + if not addKeyPath or addKeyPath:match("^%s*$") then + openFileBrowser(function(selectedPath) + if selectedPath then + addKeyPath = selectedPath + onAddKeySubmit(selectedPath) + end + end) + else + onAddKeySubmit(addKeyPath) + end +end + diff --git a/ssh-agent/plugin.toml b/ssh-agent/plugin.toml new file mode 100644 index 00000000..28136afa --- /dev/null +++ b/ssh-agent/plugin.toml @@ -0,0 +1,107 @@ +# Noctalia v5 plugin manifest for SSH Agent. +# v5 plugins run as Luau scripts in isolated VMs (no more QML). Each [[entry]] +# below is a separate script; they only talk to each other through +# noctalia.state (shared key/value) and onIpc — there is no shared +# "mainInstance" object like v4's pluginApi.mainInstance. +# +# Docs: https://docs.noctalia.dev/v5/plugins/ + +id = "martasskv5/ssh-agent" +name = "SSH Agent" +version = "1.0.0" +plugin_api = 9 +author = "martasskv5" +license = "MIT" +dependencies = ["zenity", "ssh-add", "ssh-agent", "pkill", "mkdir", "ksshaskpass", "ssh-askpass", "lxqt-openssh-askpass", "ssh"] +tags = ["utility", "network"] +icon = "key" +description = "Manage your SSH agent, loaded keys, and saved SSH sessions from the bar." + +# ── Bar widget ──────────────────────────────────────────────────────────── +# Settings are declared once here but shared across every entry in the +# plugin via noctalia.getConfig(key) — the service and panel read the same +# values. This replaces v4's Settings.qml + manifest.metadata.defaultSettings. +[[widget]] +id = "ssh-agent-widget" +entry = "widget.luau" + +[[setting]] +key = "socket_path" +type = "string" +label_key = "settings.socket_path.label" +description_key = "settings.socket_path.description" +default = "" + +[[setting]] +key = "sessions_file" +type = "string" +label_key = "settings.sessions_file.label" +description_key = "settings.sessions_file.description" +default = "~/.ssh/sessions.json" + +[[setting]] +key = "default_key_browse_path" +type = "string" +label_key = "settings.default_key_browse_path.label" +description_key = "settings.default_key_browse_path.description" +default = "~/.ssh/" + +[[setting]] +key = "terminal_command" +type = "string" +label_key = "settings.terminal_command.label" +description_key = "settings.terminal_command.description" +default = "" + +[[setting]] +key = "show_saved_sessions" +type = "bool" +label_key = "settings.show_saved_sessions.label" +description_key = "settings.show_saved_sessions.description" +default = true + +[[setting]] +key = "show_notifications" +type = "bool" +label_key = "settings.show_notifications.label" +description_key = "settings.show_notifications.description" +default = true + +[[setting]] +key = "auto_start_mode" +type = "select" +label_key = "settings.auto_start_mode.label" +description_key = "settings.auto_start_mode.description" +default = "connect_existing" + +[[setting.options]] +value = "create_new" +label_key = "settings.auto_start_mode.options.create_new" + +[[setting.options]] +value = "connect_existing" +label_key = "settings.auto_start_mode.options.connect_existing" + +[[setting.options]] +value = "ask_each_time" +label_key = "settings.auto_start_mode.options.ask_each_time" + +# ── Headless service ────────────────────────────────────────────────────── +# Owns the ssh-agent process, ssh-add calls, and the sessions file. Runs for +# the whole session, independent of whether the widget/panel are open — +# this replaces Main.qml + its Process{} blocks. +[[service]] +id = "ssh-agent-service" +entry = "service.luau" + +# ── Panel ───────────────────────────────────────────────────────────────── +# Declarative UI (ui.* tree), replaces Panel.qml. Size is host-owned and +# declared here so the surface is right on first open. +# Open manually with: noctalia msg panel-toggle martasskv5/ssh-agent:panel +[[panel]] +id = "ssh-agent-panel" +entry = "panel.luau" +# width = 480 +# height = 340 +placement = "attached" +position = "near-trigger" diff --git a/ssh-agent/service.luau b/ssh-agent/service.luau new file mode 100644 index 00000000..07d88194 --- /dev/null +++ b/ssh-agent/service.luau @@ -0,0 +1,478 @@ +--!nonstrict +-- Headless [[service]] entry. Owns everything Main.qml used to own: the +-- ssh-agent process, ssh-add calls, and the sessions file. Runs for the +-- whole session. Other entries (widget.luau, panel.luau) never call these +-- functions directly — Luau VMs are isolated per entry — they instead: +-- * read published data via noctalia.state.watch("status", ...) +-- * request actions by writing noctalia.state.set("command", {...}) +-- * or drive it externally via: noctalia msg plugin martasskv5/ssh-agent:agent + +local agentRunning = false +local loadingKeys = false +local loadedKeys = {} +local sessions = {} +local sshVersion = "Loading..." +local isStartupPhase = true + +local function trimmed(s) + return (tostring(s or ""):match("^%s*(.-)%s*$")) +end + +local function shellQuote(text) + return "'" .. tostring(text or ""):gsub("'", "'\\''") .. "'" +end + +-- ── Settings (shared across all of this plugin's entries via getConfig) ──── + +local function defaultSocketPath() + return "/tmp/ssh-agent-" .. (noctalia.getenv("USER") or "default") .. ".sock" +end + +local function agentSocketPath() + local v = trimmed(noctalia.getConfig("socket_path")) + if v == "" then + return defaultSocketPath() + end + return v +end + +local function sessionsFilePath() + local v = trimmed(noctalia.getConfig("sessions_file")) + if v == "" then + return "~/.ssh/sessions.json" + end + return v +end + +local function terminalCommand() + return trimmed(noctalia.getConfig("terminal_command")) +end + +local function showNotifications() + local v = noctalia.getConfig("show_notifications") + if v == nil then + return true + end + return v == true +end + +local function autoStartMode() + local v = trimmed(noctalia.getConfig("auto_start_mode")) + if v == "" then + return "connect_existing" + end + return v +end + +local function notifyInfo(title, msg) + if showNotifications() then + noctalia.notify(title, msg or "") + end +end + +local function notifyError(title, msg) + noctalia.notifyError(title, msg or "") +end + +-- ── Publish state for widget.luau / panel.luau ───────────────────────────── + +local function publishStatus() + noctalia.state.set("status", { + agent_running = agentRunning, + loading_keys = loadingKeys, + loaded_keys = loadedKeys, + sessions = sessions, + ssh_version = sshVersion, + }) +end + +-- ── ssh-agent lifecycle ───────────────────────────────────────────────────── + +local function extractVersionLine(output) + for line in tostring(output or ""):gmatch("[^\r\n]+") do + local l = trimmed(line) + if l ~= "" then + local lower = l:lower() + if lower:sub(1, 6) ~= "usage:" and not lower:find("^unknown option") and not lower:find("^option requires an argument") then + return l + end + end + end + return "Unknown" +end + +local function getSshVersion() + noctalia.runAsync( + "if command -v ssh >/dev/null 2>&1; then ssh -V 2>&1; else echo 'Not installed'; fi", + function(res) + sshVersion = extractVersionLine(res.stdout ~= "" and res.stdout or res.stderr) + publishStatus() + end, + 5000 + ) +end + +local function checkAgentRunning(onDone) + noctalia.runAsync("test -S " .. shellQuote(agentSocketPath()), function(res) + agentRunning = res.exitCode == 0 + publishStatus() + if onDone then + onDone() + end + end, 5000) +end + +local function parseLoadedKeys(text, exitCode) + text = tostring(text or "") + if exitCode ~= 0 then + if text:find("The agent has no identities") or text:find("The agent has no keys") then + loadedKeys = {} + else + loadedKeys = {} + if not isStartupPhase and trimmed(text) ~= "" then + notifyError("SSH Agent", trimmed(text)) + end + end + return + end + + local keys = {} + for line in text:gmatch("[^\r\n]+") do + local l = trimmed(line) + if l ~= "" then + local bits, fingerprint, rest = l:match("^(%d+)%s+(%S+)%s+(.*)$") + if bits then + local name, ktype = rest:match("^(.-)%s+%(([^%)]+)%)$") + if not name then + name = rest + ktype = "" + end + table.insert(keys, { bits = bits, fingerprint = fingerprint, name = name, type = ktype }) + end + end + end + loadedKeys = keys +end + +local function refreshLoadedKeys() + loadingKeys = true + publishStatus() + local cmd = "SSH_AUTH_SOCK=" .. shellQuote(agentSocketPath()) .. " ssh-add -l 2>&1" + noctalia.runAsync(cmd, function(res) + loadingKeys = false + parseLoadedKeys(res.stdout, res.exitCode) + publishStatus() + end, 8000) +end + +local function refreshState() + checkAgentRunning() + refreshLoadedKeys() +end + +local function startAgent(killPrevious) + local sock = agentSocketPath() + local dir = sock:match("^(.*)/[^/]*$") or "/tmp" + local script = "" + if killPrevious then + script = script .. "pkill -f " .. shellQuote("ssh-agent -a " .. sock) .. " 2>/dev/null || true\n" + end + script = script .. "mkdir -p " .. shellQuote(dir) .. "\n" + script = script .. "ssh-agent -a " .. shellQuote(sock) .. " >/dev/null" + + noctalia.runAsync(script, function(res) + if res.exitCode == 0 then + if not isStartupPhase then + notifyInfo("SSH Agent", "Agent started") + end + elseif not isStartupPhase then + local msg = trimmed(res.stderr ~= "" and res.stderr or res.stdout) + if msg ~= "" then + notifyError("SSH Agent", msg) + end + end + refreshState() + end, 8000) +end + +local function stopAgent() + local sock = agentSocketPath() + noctalia.runAsync("pkill -f " .. shellQuote("ssh-agent -a " .. sock), function(res) + if res.exitCode == 0 and not isStartupPhase then + notifyInfo("SSH Agent", "Agent stopped") + end + refreshState() + end, 5000) +end + +-- ── Keys ───────────────────────────────────────────────────────────────── + +local function addKey(keyPath) + local path = noctalia.expandPath(trimmed(keyPath)) + if path == "" then + return + end + local script = table.concat({ + "ASKPASS=${SSH_ASKPASS:-}", + "if [ -z \"$ASKPASS\" ] || [ ! -x \"$ASKPASS\" ]; then", + " for p in /usr/bin/ksshaskpass /usr/bin/ssh-askpass /usr/lib/ssh/ssh-askpass /usr/bin/lxqt-openssh-askpass; do", + " if [ -x \"$p\" ]; then ASKPASS=\"$p\"; break; fi", + " done", + "fi", + "if [ -n \"$ASKPASS\" ]; then", + " export SSH_ASKPASS=\"$ASKPASS\"", + " export SSH_ASKPASS_REQUIRE=prefer", + "fi", + "SSH_AUTH_SOCK=" .. shellQuote(agentSocketPath()) .. " ssh-add " .. shellQuote(path) .. " 2>&1", + }, "\n") + + -- Long timeout: an askpass GUI prompt can sit open for a while. + noctalia.runAsync(script, function(res) + if res.exitCode == 0 then + notifyInfo("SSH Key Added", path) + else + local msg = trimmed(res.stderr ~= "" and res.stderr or res.stdout) + notifyError("Failed to Add Key", msg ~= "" and msg or path) + end + refreshLoadedKeys() + end, 60000) +end + +local function removeKey(keyName) + if not keyName or keyName == "" then + return + end + local cmd = "SSH_AUTH_SOCK=" .. shellQuote(agentSocketPath()) .. " ssh-add -d " .. shellQuote(keyName) .. " 2>&1" + noctalia.runAsync(cmd, function(res) + if res.exitCode == 0 then + notifyInfo("SSH Key Unloaded", keyName) + else + local msg = trimmed(res.stderr ~= "" and res.stderr or res.stdout) + notifyError("Failed to Unload Key", msg ~= "" and msg or keyName) + end + refreshLoadedKeys() + end, 8000) +end + +local function removeAllKeys() + local cmd = "SSH_AUTH_SOCK=" .. shellQuote(agentSocketPath()) .. " ssh-add -D 2>&1" + noctalia.runAsync(cmd, function(res) + if res.exitCode == 0 then + notifyInfo("SSH Agent", "All keys removed") + else + notifyError("Failed to Remove Keys", trimmed(res.stderr ~= "" and res.stderr or res.stdout)) + end + refreshLoadedKeys() + end, 8000) +end + +-- ── Sessions ───────────────────────────────────────────────────────────── + +local function launchSession(session) + if not session or trimmed(session.host or "") == "" then + return + end + local user = trimmed(session.user or "") + local host = trimmed(session.host or "") + local destination = user ~= "" and (user .. "@" .. host) or host + local port = trimmed(tostring(session.port or "22")) + if port == "" then + port = "22" + end + local keyPath = noctalia.expandPath(trimmed(session.key_path or "")) + local extraArgs = trimmed(session.extra_args or "") + + local sshCmd = "ssh -p " .. shellQuote(port) + if keyPath ~= "" then + sshCmd = sshCmd .. " -i " .. shellQuote(keyPath) + end + if extraArgs ~= "" then + sshCmd = sshCmd .. " " .. extraArgs + end + sshCmd = sshCmd .. " " .. shellQuote(destination) + + local wrapped = "SSH_AUTH_SOCK=" .. shellQuote(agentSocketPath()) .. " " .. sshCmd + local term = terminalCommand() + + if term ~= "" then + noctalia.runAsync(term .. " -e sh -lc " .. shellQuote(wrapped)) + elseif not noctalia.runInTerminal(wrapped) then + notifyError("Launch Failed", "No terminal available to launch the session") + end +end + +local function normalizeSession(raw) + local port = trimmed(tostring((raw and raw.port) or "22")) + if port == "" then + port = "22" + end + return { + name = trimmed(raw and raw.name), + host = trimmed(raw and raw.host), + user = trimmed(raw and raw.user), + port = port, + key_path = trimmed(raw and raw.key_path), + extra_args = trimmed(raw and raw.extra_args), + } +end + +local function writeSessions() + local path = sessionsFilePath() + local encoded = noctalia.json.encode(sessions, true) + local expanded = noctalia.expandPath(path) + local dir = expanded:match("^(.*)/[^/]*$") + if dir and dir ~= "" then + noctalia.mkdirAll(dir) + end + local ok, err = noctalia.writeFile(path, (encoded or "[]") .. "\n") + if not ok then + notifyError("Saved Sessions", err or "Failed to save sessions") + end +end + +local function upsertSession(raw) + local session = normalizeSession(raw) + if session.name == "" or session.host == "" then + notifyError("Saved Sessions", "Session name and host are required") + return + end + + local updated = {} + local replaced = false + for _, s in ipairs(sessions) do + if s.name == session.name then + table.insert(updated, session) + replaced = true + else + table.insert(updated, s) + end + end + if not replaced then + table.insert(updated, session) + end + + sessions = updated + writeSessions() + notifyInfo("Saved Sessions", "Stored '" .. session.name .. "'") + publishStatus() +end + +local function deleteSession(name) + local n = trimmed(name) + if n == "" then + return + end + local updated = {} + for _, s in ipairs(sessions) do + if s.name ~= n then + table.insert(updated, s) + end + end + sessions = updated + writeSessions() + notifyInfo("Saved Sessions", "Deleted '" .. n .. "'") + publishStatus() +end + +local function loadSessionsFromDisk() + local path = sessionsFilePath() + local contents = noctalia.readFile(path) + if not contents or trimmed(contents) == "" then + sessions = {} + publishStatus() + return + end + + local decoded, err = noctalia.json.decode(trimmed(contents)) + if err or decoded == nil then + sessions = {} + if not isStartupPhase then + notifyError("Saved Sessions", "Invalid JSON in sessions file") + end + publishStatus() + return + end + + if decoded.entries then + sessions = decoded.entries + else + sessions = decoded + end + publishStatus() +end + +-- ── Cross-entry command channel ───────────────────────────────────────────── +-- widget.luau / panel.luau write here; each write includes a fresh `ts` so +-- repeat commands (e.g. clicking "refresh" twice) still trigger the watcher. + +noctalia.state.watch("command", function(cmd) + if type(cmd) ~= "table" or not cmd.action then + return + end + local action = cmd.action + if action == "refresh" then + refreshState() + elseif action == "start-agent" then + startAgent(cmd.kill == true) + elseif action == "stop-agent" then + stopAgent() + elseif action == "add-key" then + addKey(cmd.path) + elseif action == "remove-key" then + removeKey(cmd.name) + elseif action == "remove-all-keys" then + removeAllKeys() + elseif action == "launch-session" then + launchSession(cmd.session) + elseif action == "upsert-session" then + upsertSession(cmd.session) + elseif action == "delete-session" then + deleteSession(cmd.name) + end +end) + +-- ── External IPC ───────────────────────────────────────────────────────── + +function onIpc(event, _payload) + if event == "refresh" then + refreshState() + elseif event == "start-agent" then + startAgent(false) + elseif event == "stop-agent" then + stopAgent() + end +end + +-- Plugin-level settings changed (e.g. socket path edited in Settings). +function onConfigChanged() + refreshState() + loadSessionsFromDisk() +end + +-- ── Startup ────────────────────────────────────────────────────────────── +-- Runs once when the service loads. There's no QML Timer here, so a short +-- update() tick stands in for the old 500ms one-shot startup delay; after +-- that the service settles into a slow background sync. + +refreshState() +getSshVersion() +loadSessionsFromDisk() + +local ticks = 0 +noctalia.setUpdateInterval(500) + +function update() + ticks += 1 + if ticks == 1 then + local mode = autoStartMode() + if mode == "create_new" then + startAgent(true) + elseif mode == "connect_existing" and not agentRunning then + startAgent(false) + end + isStartupPhase = false + noctalia.setUpdateInterval(60000) -- background sync every minute thereafter + else + refreshState() + end +end diff --git a/ssh-agent/thumbnail.webp b/ssh-agent/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..41f3eff811e7a4e598a96637f0b6670e76ea325c GIT binary patch literal 48734 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 literal 0 HcmV?d00001 diff --git a/ssh-agent/translations/en.json b/ssh-agent/translations/en.json new file mode 100644 index 00000000..3cee7baa --- /dev/null +++ b/ssh-agent/translations/en.json @@ -0,0 +1,37 @@ +{ + "settings": { + "socket_path": { + "label": "Agent Socket Path", + "description": "Path to the ssh-agent UNIX socket. Leave empty to use /tmp/ssh-agent-$USER.sock" + }, + "sessions_file": { + "label": "Saved Sessions File", + "description": "JSON file used to store saved SSH sessions" + }, + "default_key_browse_path": { + "label": "Default Key Browse Path", + "description": "Default path used when browsing for SSH keys to add" + }, + "terminal_command": { + "label": "Preferred Terminal", + "description": "Optional terminal command used to launch SSH sessions (e.g. kitty). Leave empty to use the system default terminal" + }, + "show_saved_sessions": { + "label": "Show Saved Sessions", + "description": "Display saved SSH sessions in the main window" + }, + "show_notifications": { + "label": "Show Notifications", + "description": "Display success and failure toasts for SSH operations" + }, + "auto_start_mode": { + "label": "Startup Behavior", + "description": "What to do when Noctalia starts", + "options": { + "create_new": "Create New", + "connect_existing": "Connect Existing", + "ask_each_time": "Ask Each Time" + } + } + } +} diff --git a/ssh-agent/widget.luau b/ssh-agent/widget.luau new file mode 100644 index 00000000..dc1f0320 --- /dev/null +++ b/ssh-agent/widget.luau @@ -0,0 +1,47 @@ +--!nonstrict +-- Bar widget — a thin client of the [[service]] entry (agent.luau owns all +-- the process/state logic). This replaces BarWidget.qml. +-- +-- v5 bar widgets don't support a custom multi-item right-click context menu +-- (there's no ui.* menu component). The old "Refresh / Start Agent / +-- Settings" menu maps onto gestures instead: +-- left click -> toggle the panel (has Refresh / Start / Stop buttons) +-- right click -> quick refresh +-- middle click -> opens this plugin's settings (built-in host default) + +local PLUGIN_PANEL = "martasskv5/ssh-agent:ssh-agent-panel" + +local agentRunning = false +local loadedKeys = {} + +local function render() + barWidget.setGlyph(agentRunning and "key" or "key-off") + barWidget.setGlyphColor(agentRunning and "primary" or "on_surface_variant") + + local count = #loadedKeys + local statusText = agentRunning and "Running" or "Stopped" + local tooltip = statusText + if count > 0 then + tooltip = tooltip .. " — Keys: " .. count + end + barWidget.setTooltip(tooltip) +end + +local function applyStatus(status) + if type(status) == "table" then + agentRunning = status.agent_running == true + loadedKeys = status.loaded_keys or {} + end + render() +end + +applyStatus(noctalia.state.get("status")) +noctalia.state.watch("status", applyStatus) + +function onClick() + noctalia.togglePanel(PLUGIN_PANEL) +end + +function onRightClick() + noctalia.state.set("command", { action = "refresh", ts = noctalia.nowMs() }) +end From 36b4b6afaa00b37c6ca630c3cd9025b1aa7174a5 Mon Sep 17 00:00:00 2001 From: martinko-nb995-btw Date: Thu, 20 Aug 2026 15:34:25 +0200 Subject: [PATCH 2/3] docs: add missing requirement for ssh in README --- ssh-agent/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ssh-agent/README.md b/ssh-agent/README.md index 6e89c3de..2c8fd0da 100644 --- a/ssh-agent/README.md +++ b/ssh-agent/README.md @@ -13,6 +13,7 @@ Manage your SSH keys and saved sessions directly from the Noctalia bar. This plu Install the following on `PATH` for the plugin to work correctly: +- `ssh` - `ssh-agent` - `ssh-add` - `zenity` From 180e61ac0c1adb936759b4bde058925613bbd47e Mon Sep 17 00:00:00 2001 From: Martin Michalec <145648867+martasskv5@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:10:00 +0200 Subject: [PATCH 3/3] Change position setting to 'auto' in plugin.toml --- ssh-agent/plugin.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssh-agent/plugin.toml b/ssh-agent/plugin.toml index 28136afa..618de515 100644 --- a/ssh-agent/plugin.toml +++ b/ssh-agent/plugin.toml @@ -104,4 +104,4 @@ entry = "panel.luau" # width = 480 # height = 340 placement = "attached" -position = "near-trigger" +position = "auto"