Add pass launcher plugin (#89)

This commit is contained in:
emrtnn
2026-07-24 08:26:28 -04:00
committed by GitHub
parent efe8a1a2c9
commit 7a3a922e73
6 changed files with 277 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
# Pass
Pass adds password-store search to the Noctalia launcher so you can copy passwords and OTP codes from `pass`.
## Plugin
| Field | Value |
| --- | --- |
| ID | `emrtnn/pass` |
| Entries | Launcher provider: `search`; service: `cache` |
| Launcher Prefix | `/pass` |
## Requirements
Install `pass`, `pass-otp`, `gpg`, and `wl-copy` on `PATH`.
A working password store is expected in the same location `pass` uses: `$PASSWORD_STORE_DIR` when that variable is set, otherwise the default `~/.password-store`. OTP copying requires entries that are configured for `pass-otp`.
## Usage
Open the Noctalia launcher and type `/pass` to search password-store entries indexed by the `cache` service. Activate a result from launcher provider `search` to copy the password with `pass -c <entry>`.
To copy an OTP code instead, type `/pass otp <query>` and activate the matching result. For example:
```text
/pass otp github
```
If GPG needs an unlock passphrase, the plugin opens the configured terminal and runs the same copy command there so you can unlock the key interactively.
## Settings
| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `refresh_interval` | `int` | `30` | Seconds between password-store rescans. Minimum `5`, maximum `3600`. |
## IPC
This plugin does not expose custom IPC actions. It provides launcher provider `search` and background service `cache` entries only.
## Notes
- Filesystem reads: the service recursively scans `$PASSWORD_STORE_DIR` when set, otherwise `~/.password-store`, and indexes non-hidden `*.gpg` file names. It does not read decrypted password contents.
- Spawned processes: activating a password result runs `pass -c <entry>`; activating an OTP result runs `pass otp -c <entry>`. If GPG reports an unlock failure, the plugin opens a terminal and runs the same command interactively.
- Clipboard/privacy: copied secrets are handled by `pass`/`pass-otp` and the system clipboard tooling, typically including `gpg` and `wl-copy` on Wayland. The plugin stores only entry paths/titles in Noctalia state, not decrypted secrets.
- Network: the plugin makes no network calls.
- Writes: the plugin does not write files directly. `pass`, `pass-otp`, `gpg`, or clipboard tools may update their own runtime files such as agent or clipboard state.
+95
View File
@@ -0,0 +1,95 @@
--!nonstrict
local entries = {}
local function shellEscape(str)
return "'" .. str:gsub("'", "'\\''") .. "'"
end
noctalia.state.watch("entries", function(value)
entries = value or {}
end)
function onQuery(query)
local results = {}
local action = "password"
local search = query
if query:match("^otp%s+") then
action = "otp"
search = query:gsub("^otp%s+", "")
end
for _, entry in ipairs(entries) do
local score
if search == "" then
score = 1
else
score = noctalia.fuzzyScore(search, entry.path)
end
if score then
local subtitle = entry.subtitle ~= "" and entry.subtitle or nil
if action == "otp" then
table.insert(results, {
id = "otp:" .. entry.id,
title = "Copy OTP: " .. entry.title,
subtitle = subtitle,
glyph = "123",
score = score,
})
else
table.insert(results, {
id = "password:" .. entry.id,
title = entry.title,
subtitle = subtitle,
glyph = "key",
score = score,
})
end
end
end
launcher.setResults(query, results)
end
local function copyWithPass(command, path, successMessage)
local escaped = shellEscape(path)
noctalia.runAsync(command .. " " .. escaped, function(result)
if result.exitCode == 0 then
noctalia.notify("Pass", successMessage)
return
end
local stderr = result.stderr or ""
local err = stderr:lower()
if err:find("gpg") then
noctalia.notify("Pass", noctalia.tr("notification.unlocking"))
noctalia.runInTerminal(command .. " " .. escaped)
return
end
if err == "" then
err = noctalia.tr("notification.copy_failed")
else
err = stderr
end
noctalia.notifyError("Pass", err)
end)
end
function onActivate(id)
local action, path = id:match("^([^:]+):(.+)$")
if action == "otp" then
copyWithPass("pass otp -c", path, "OTP code copied")
else
copyWithPass("pass -c", path or id, noctalia.tr("notification.copied"))
end
end
+30
View File
@@ -0,0 +1,30 @@
id = "emrtnn/pass"
name = "Pass"
version = "0.0.1"
plugin_api = 3
author = "emrtnn"
license = "MIT"
deprecated = false
icon = "key"
description = "Search and copy password-store entries from the Noctalia launcher"
tags = ["launcher", "privacy", "productivity", "utility"]
dependencies = ["pass", "pass-otp", "gpg", "wl-copy"]
[[setting]]
key = "refresh_interval"
type = "int"
label_key = "settings.refresh_interval.label"
description_key = "settings.refresh_interval.description"
default = 30
min = 5
max = 3600
[[launcher_provider]]
id = "search"
entry = "launcher.luau"
prefix = "pass"
glyph = "key"
[[service]]
id = "cache"
entry = "service.luau"
+91
View File
@@ -0,0 +1,91 @@
--!nonstrict
local entries = {}
local function publish()
noctalia.state.set("entries", entries)
end
local function passwordStoreDir()
local dir = noctalia.getenv("PASSWORD_STORE_DIR")
if dir and dir ~= "" then
return noctalia.expandPath(dir)
end
return noctalia.expandPath("~/.password-store")
end
local function scan(dir, prefix)
prefix = prefix or ""
local names, err = noctalia.listDir(dir)
if not names then
noctalia.log("Failed to list " .. dir .. ": " .. tostring(err))
return
end
for _, name in ipairs(names) do
-- Ignore hidden files/directories (.git, .extensions, .gpg-id, ...)
if name:sub(1, 1) ~= "." then
local full = dir .. "/" .. name
local info = noctalia.fileInfo(full)
if info then
if info.isDir then
scan(full, prefix .. name .. "/")
elseif name:sub(-4) == ".gpg" then
local path = prefix .. name:sub(1, -5)
local title = path
local subtitle = ""
local lastSlash = path:match("^.*()/")
if lastSlash then
subtitle = path:sub(1, lastSlash - 1)
title = path:sub(lastSlash + 1)
end
table.insert(entries, {
id = path,
path = path,
title = title,
subtitle = subtitle,
})
end
end
end
end
end
local function rebuild()
entries = {}
local root = passwordStoreDir()
if not noctalia.fileExists(root) then
noctalia.notifyError("Pass", noctalia.tr("notification.store_not_found"))
return
end
scan(root)
table.sort(entries, function(a, b)
return a.title:lower() < b.title:lower()
end)
publish()
end
function update()
rebuild()
end
local refresh = noctalia.getConfig("refresh_interval") or 30
noctalia.setUpdateInterval(refresh * 1000)
rebuild()
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+14
View File
@@ -0,0 +1,14 @@
{
"settings": {
"refresh_interval": {
"label": "Refresh interval (seconds)",
"description": "How often to rescan the password store, in seconds."
}
},
"notification": {
"copied": "Password copied.",
"unlocking": "Unlocking GPG key…",
"copy_failed": "Failed to copy password.",
"store_not_found": "Password store not found."
}
}