Add pass launcher plugin (#89)
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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 |
@@ -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."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user