feat(pass): add new async and improved cache service (#258)
This commit is contained in:
+2
-2
@@ -12,7 +12,7 @@ Pass adds password-store search to the Noctalia launcher so you can copy passwor
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
Install `pass`, `pass-otp`, `gpg`, and `wl-copy` on `PATH`.
|
Install `pass`, `pass-otp`, `gpg`, `wl-copy`, and `find` 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`.
|
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`.
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ This plugin does not expose custom IPC actions. It provides launcher provider `s
|
|||||||
## Notes
|
## 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.
|
- 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.
|
- Spawned processes: the cache service runs `find` asynchronously to index entry paths without blocking Noctalia's plugin runtime. 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
id = "emrtnn/pass"
|
id = "emrtnn/pass"
|
||||||
name = "Pass"
|
name = "Pass"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
plugin_api = 3
|
plugin_api = 3
|
||||||
author = "emrtnn"
|
author = "emrtnn"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -8,7 +8,7 @@ deprecated = false
|
|||||||
icon = "key"
|
icon = "key"
|
||||||
description = "Search and copy password-store entries from the Noctalia launcher"
|
description = "Search and copy password-store entries from the Noctalia launcher"
|
||||||
tags = ["launcher", "privacy", "productivity", "utility"]
|
tags = ["launcher", "privacy", "productivity", "utility"]
|
||||||
dependencies = ["pass", "pass-otp", "gpg", "wl-copy"]
|
dependencies = ["pass", "pass-otp", "gpg", "wl-copy", "find"]
|
||||||
|
|
||||||
[[setting]]
|
[[setting]]
|
||||||
key = "refresh_interval"
|
key = "refresh_interval"
|
||||||
|
|||||||
+58
-49
@@ -1,11 +1,16 @@
|
|||||||
--!nonstrict
|
--!nonstrict
|
||||||
|
|
||||||
local entries = {}
|
local entries = {}
|
||||||
|
local scanInFlight = false
|
||||||
|
|
||||||
local function publish()
|
local function publish()
|
||||||
noctalia.state.set("entries", entries)
|
noctalia.state.set("entries", entries)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function shellEscape(value)
|
||||||
|
return "'" .. value:gsub("'", "'\\''") .. "'"
|
||||||
|
end
|
||||||
|
|
||||||
local function passwordStoreDir()
|
local function passwordStoreDir()
|
||||||
local dir = noctalia.getenv("PASSWORD_STORE_DIR")
|
local dir = noctalia.getenv("PASSWORD_STORE_DIR")
|
||||||
|
|
||||||
@@ -16,56 +21,11 @@ local function passwordStoreDir()
|
|||||||
return noctalia.expandPath("~/.password-store")
|
return noctalia.expandPath("~/.password-store")
|
||||||
end
|
end
|
||||||
|
|
||||||
local function scan(dir, prefix)
|
local function rebuild()
|
||||||
prefix = prefix or ""
|
if scanInFlight then
|
||||||
|
|
||||||
local names, err = noctalia.listDir(dir)
|
|
||||||
|
|
||||||
if not names then
|
|
||||||
noctalia.log("Failed to list " .. dir .. ": " .. tostring(err))
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, name in ipairs(names) do
|
|
||||||
-- Ignore hidden files/directories (.git, .extensions, .gpg-id, ...)
|
|
||||||
if name:sub(1, 1) ~= "." then
|
|
||||||
-- listDir already proved the name exists, so an entry needs no stat.
|
|
||||||
-- Only non-entry names get one, to decide whether to recurse.
|
|
||||||
if 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,
|
|
||||||
})
|
|
||||||
|
|
||||||
else
|
|
||||||
local full = dir .. "/" .. name
|
|
||||||
local info = noctalia.fileInfo(full)
|
|
||||||
|
|
||||||
if info and info.isDir then
|
|
||||||
scan(full, prefix .. name .. "/")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function rebuild()
|
|
||||||
entries = {}
|
|
||||||
|
|
||||||
local root = passwordStoreDir()
|
local root = passwordStoreDir()
|
||||||
|
|
||||||
if not noctalia.fileExists(root) then
|
if not noctalia.fileExists(root) then
|
||||||
@@ -73,13 +33,62 @@ local function rebuild()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
scan(root)
|
-- Directory traversal can exceed Noctalia's callback CPU budget on large stores.
|
||||||
|
-- Run it in a subprocess and only build the small launcher cache in Luau.
|
||||||
|
local command = "cd " .. shellEscape(root)
|
||||||
|
.. " && find . -mindepth 1"
|
||||||
|
.. " \\( -type d -name '.*' -prune \\)"
|
||||||
|
.. " -o \\( -name '*.gpg' ! -name '.*' -print0 \\)"
|
||||||
|
|
||||||
table.sort(entries, function(a, b)
|
scanInFlight = true
|
||||||
|
|
||||||
|
local accepted = noctalia.runAsync(command, function(result)
|
||||||
|
scanInFlight = false
|
||||||
|
|
||||||
|
if result.exitCode ~= 0 then
|
||||||
|
noctalia.log("Failed to scan " .. root .. ": " .. (result.stderr or "unknown error"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if result.stdoutTruncated then
|
||||||
|
noctalia.log("Failed to scan " .. root .. ": command output was truncated")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local nextEntries = {}
|
||||||
|
|
||||||
|
for file in (result.stdout or ""):gmatch("([^%z]+)%z") do
|
||||||
|
-- find returns paths as ./folder/entry.gpg.
|
||||||
|
local path = file:sub(3, -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(nextEntries, {
|
||||||
|
id = path,
|
||||||
|
path = path,
|
||||||
|
title = title,
|
||||||
|
subtitle = subtitle,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(nextEntries, function(a, b)
|
||||||
return a.title:lower() < b.title:lower()
|
return a.title:lower() < b.title:lower()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
entries = nextEntries
|
||||||
publish()
|
publish()
|
||||||
|
end, 60000)
|
||||||
|
|
||||||
|
if not accepted then
|
||||||
|
scanInFlight = false
|
||||||
|
noctalia.log("Failed to start password-store scan")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function update()
|
function update()
|
||||||
|
|||||||
Reference in New Issue
Block a user