From d4d1d659b4fefa81c5c05b9370f69ef7bc93d5ce Mon Sep 17 00:00:00 2001 From: Bojan Jovanovic Date: Sat, 16 May 2026 16:43:12 +0200 Subject: [PATCH 1/3] Adds a launcher intercept that integrates with linux passwordstore --- pass/Main.qml | 98 ++++++++++++++++++++++++++++++++++++++++++++++ pass/README.md | 93 +++++++++++++++++++++++++++++++++++++++++++ pass/manifest.json | 20 ++++++++++ registry.json | 16 ++++++++ 4 files changed, 227 insertions(+) create mode 100644 pass/Main.qml create mode 100644 pass/README.md create mode 100644 pass/manifest.json diff --git a/pass/Main.qml b/pass/Main.qml new file mode 100644 index 000000000..0d9c752b5 --- /dev/null +++ b/pass/Main.qml @@ -0,0 +1,98 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI + +QtObject { + id: root + property var pluginApi: null + + property Component _providerComponent: Component { + Item { + id: provider + property var launcher: null + property var pluginApi: null + property string name: "Password Store" + property bool handleSearch: true + property var passwords: [] + + function init() { + listProcess.running = true; + } + + function onOpened() { + if (passwords.length === 0) + listProcess.running = true; + } + + function getResults(query) { + const isPass = query === "pass" || query.startsWith("pass "); + const isOtp = query === "po" || query.startsWith("po "); + if (!isPass && !isOtp) + return []; + + const search = isPass + ? (query.startsWith("pass ") ? query.substring(5).trim() : "") + : (query.startsWith("po ") ? query.substring(3).trim() : ""); + + const otp = isOtp; + + let matches; + if (!search) { + matches = passwords.slice(0, 15).map(p => ({ target: p, score: 1.0 })); + } else { + const results = FuzzySort.go(search, passwords, { limit: 15 }); + matches = Array.from({ length: results.length }, (_, i) => results[i]); + } + + return matches.map(function(match) { + const passName = match.target; + return { + "name": passName, + "description": otp ? "Copy OTP to clipboard" : "Copy password to clipboard", + "icon": otp ? "clock-shield" : "lock", + "isTablerIcon": true, + "isImage": false, + "_score": match.score, + "onActivate": function() { + if (provider.launcher) + provider.launcher.closeImmediately(); + Qt.callLater(function() { + Quickshell.execDetached(otp + ? ["pass", "otp", "-c", passName] + : ["pass", "-c", passName]); + }); + } + }; + }); + } + + Process { + id: listProcess + command: [ + "sh", "-c", + "store=\"${PASSWORD_STORE_DIR:-$HOME/.password-store}\"; " + + "find \"$store\" -name '*.gpg' | sed \"s|$store/||;s|\\.gpg$||\" | sort" + ] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text && text.trim()) + provider.passwords = text.trim().split("\n").filter(p => p.length > 0); + } + } + stderr: StdioCollector {} + } + } + } + + Component.onCompleted: { + LauncherProviderRegistry.registerPluginProvider(pluginApi.pluginId, _providerComponent, {}); + } + + Component.onDestruction: { + LauncherProviderRegistry.unregisterPluginProvider(pluginApi.pluginId); + } +} diff --git a/pass/README.md b/pass/README.md new file mode 100644 index 000000000..7518029e1 --- /dev/null +++ b/pass/README.md @@ -0,0 +1,93 @@ +# Password Store + +Search and copy passwords and OTP codes from [pass](https://www.passwordstore.org/) directly in the Noctalia launcher. + +## Features + +- **Password search** — type `pass ` to fuzzy search your password store and copy a password to clipboard +- **OTP search** — type `po ` to fuzzy search your password store and copy a TOTP code to clipboard +- **Fuzzy matching** — uses Noctalia's built-in FuzzySort engine, consistent with the app launcher +- **Clipboard safety** — passwords and OTP codes are cleared from clipboard after 45 seconds (pass default) +- **XDG-aware** — respects `$PASSWORD_STORE_DIR` with fallback to `~/.password-store` + +## Requirements + +- **System packages:** `pass`, `pass-otp` +- **Wayland clipboard:** `wl-clipboard` (for `wl-copy` support — recommended on Wayland) + +### Installing dependencies + +```bash +# Arch Linux / Manjaro +sudo pacman -S pass pass-otp wl-clipboard + +# Debian / Ubuntu +sudo apt install pass pass-extension-otp wl-clipboard + +# Fedora +sudo dnf install pass pass-otp wl-clipboard +``` + +## Installation + +### Manual Installation + +```bash +git clone https://github.com/noctalia-dev/noctalia-plugins ~/.config/noctalia/plugins/pass +``` + +Then restart Noctalia: + +```bash +qs kill -c noctalia-shell && qs -c noctalia-shell -d +``` + +## Usage + +Open the launcher (`Alt+Space` by default) and type: + +| Prefix | Action | +|--------|--------| +| `pass ` | Fuzzy search all pass entries — press Enter to copy the password | +| `po ` | Fuzzy search all pass entries — press Enter to copy the TOTP code | + +### Examples + +``` +pass kortechs/aws → finds kortechs/aws/bojan@kortechs.io +pass aws bojan → finds any entry matching both "aws" and "bojan" +po github → finds OTP entry for github, copies the current TOTP code +``` + +Selecting an entry runs `pass -c ` or `pass otp -c ` respectively. If your GPG key requires a passphrase, a pinentry dialog will appear. + +## File Structure + +``` +pass/ +├── manifest.json # Plugin metadata +├── Main.qml # Launcher provider — search and activation logic +└── README.md # This file +``` + +## Troubleshooting + +**No results appear** +- Make sure `pass` is installed and `~/.password-store` exists (or `$PASSWORD_STORE_DIR` is set) +- Restart Noctalia after installing the plugin + +**OTP copy fails silently** +- Verify `pass-otp` is installed: `pass otp --help` +- Test manually in a terminal: `pass otp -c ` + +**Password not copied on Wayland** +- Install `wl-clipboard`: `sudo pacman -S wl-clipboard` +- `pass` detects Wayland via `$WAYLAND_DISPLAY` and uses `wl-copy` automatically + +## License + +MIT + +## Author + +**Bojan Jovanovic** - [github.com/virogenesis](https://github.com/virogenesis) diff --git a/pass/manifest.json b/pass/manifest.json new file mode 100644 index 000000000..af551b177 --- /dev/null +++ b/pass/manifest.json @@ -0,0 +1,20 @@ +{ + "id": "pass", + "name": "Password Store", + "version": "1.0.0", + "minNoctaliaVersion": "3.6.0", + "author": "Bojan Jovanovic", + "license": "MIT", + "repository": "https://github.com/noctalia-dev/noctalia-plugins", + "description": "Search pass (password-store) from the launcher. Type 'pass ' to copy a password or 'po ' to copy a TOTP code to clipboard.", + "tags": ["Launcher", "Utility"], + "entryPoints": { + "main": "Main.qml" + }, + "dependencies": { + "plugins": [] + }, + "metadata": { + "defaultSettings": {} + } +} diff --git a/registry.json b/registry.json index 7b3ae8f54..17ae1c13e 100644 --- a/registry.json +++ b/registry.json @@ -1299,6 +1299,22 @@ ], "lastUpdated": "2026-04-21T17:26:58+02:00" }, + { + "id": "pass", + "name": "Password Store", + "version": "1.0.0", + "official": false, + "author": "Bojan Jovanovic", + "description": "Search pass (password-store) from the launcher. Type 'pass ' to copy a password or 'po ' to copy a TOTP code to clipboard.", + "repository": "https://github.com/virogenesis/noctalia-pass-plugin", + "minNoctaliaVersion": "3.6.0", + "license": "MIT", + "tags": [ + "Launcher", + "Utility" + ], + "lastUpdated": "2026-05-16T16:25:57+02:00" + }, { "id": "plugin-manager", "name": "Plugin Manager", From 20cd7b272f374c4edf0595b94d01eca1daabc1b4 Mon Sep 17 00:00:00 2001 From: Bojan Arch Jovanovic Date: Sat, 16 May 2026 23:46:01 +0200 Subject: [PATCH 2/3] (H) Line 1804: Do not add the 'official' property. It's automatically false and is NOT needed! (H) Line 1309: The repository field needs to point to the "https://github.com/noctalia-dev/noctalia-plugins" github repository! Signed-off-by: Bojan Arch Jovanovic --- registry.json | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/registry.json b/registry.json index 17ae1c13e..7b3ae8f54 100644 --- a/registry.json +++ b/registry.json @@ -1299,22 +1299,6 @@ ], "lastUpdated": "2026-04-21T17:26:58+02:00" }, - { - "id": "pass", - "name": "Password Store", - "version": "1.0.0", - "official": false, - "author": "Bojan Jovanovic", - "description": "Search pass (password-store) from the launcher. Type 'pass ' to copy a password or 'po ' to copy a TOTP code to clipboard.", - "repository": "https://github.com/virogenesis/noctalia-pass-plugin", - "minNoctaliaVersion": "3.6.0", - "license": "MIT", - "tags": [ - "Launcher", - "Utility" - ], - "lastUpdated": "2026-05-16T16:25:57+02:00" - }, { "id": "plugin-manager", "name": "Plugin Manager", From 6bb515701a2da199f71939b7256f03de84d67727 Mon Sep 17 00:00:00 2001 From: Bojan Arch Jovanovic Date: Tue, 2 Jun 2026 19:20:44 +0200 Subject: [PATCH 3/3] Per @spiros132 suggestions I created an object called LauncherProvider and provided that obj in the manifest itself. Also changed to a more meaningful id, not just pass. Also let claude fix the readme :) --- pass/Main.qml | 98 -------------------------- password-store/LauncherProvider.qml | 82 +++++++++++++++++++++ {pass => password-store}/README.md | 30 +++++--- {pass => password-store}/manifest.json | 4 +- 4 files changed, 105 insertions(+), 109 deletions(-) delete mode 100644 pass/Main.qml create mode 100644 password-store/LauncherProvider.qml rename {pass => password-store}/README.md (73%) rename {pass => password-store}/manifest.json (87%) diff --git a/pass/Main.qml b/pass/Main.qml deleted file mode 100644 index 0d9c752b5..000000000 --- a/pass/Main.qml +++ /dev/null @@ -1,98 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Commons -import qs.Services.UI - -QtObject { - id: root - property var pluginApi: null - - property Component _providerComponent: Component { - Item { - id: provider - property var launcher: null - property var pluginApi: null - property string name: "Password Store" - property bool handleSearch: true - property var passwords: [] - - function init() { - listProcess.running = true; - } - - function onOpened() { - if (passwords.length === 0) - listProcess.running = true; - } - - function getResults(query) { - const isPass = query === "pass" || query.startsWith("pass "); - const isOtp = query === "po" || query.startsWith("po "); - if (!isPass && !isOtp) - return []; - - const search = isPass - ? (query.startsWith("pass ") ? query.substring(5).trim() : "") - : (query.startsWith("po ") ? query.substring(3).trim() : ""); - - const otp = isOtp; - - let matches; - if (!search) { - matches = passwords.slice(0, 15).map(p => ({ target: p, score: 1.0 })); - } else { - const results = FuzzySort.go(search, passwords, { limit: 15 }); - matches = Array.from({ length: results.length }, (_, i) => results[i]); - } - - return matches.map(function(match) { - const passName = match.target; - return { - "name": passName, - "description": otp ? "Copy OTP to clipboard" : "Copy password to clipboard", - "icon": otp ? "clock-shield" : "lock", - "isTablerIcon": true, - "isImage": false, - "_score": match.score, - "onActivate": function() { - if (provider.launcher) - provider.launcher.closeImmediately(); - Qt.callLater(function() { - Quickshell.execDetached(otp - ? ["pass", "otp", "-c", passName] - : ["pass", "-c", passName]); - }); - } - }; - }); - } - - Process { - id: listProcess - command: [ - "sh", "-c", - "store=\"${PASSWORD_STORE_DIR:-$HOME/.password-store}\"; " + - "find \"$store\" -name '*.gpg' | sed \"s|$store/||;s|\\.gpg$||\" | sort" - ] - running: false - - stdout: StdioCollector { - onStreamFinished: { - if (text && text.trim()) - provider.passwords = text.trim().split("\n").filter(p => p.length > 0); - } - } - stderr: StdioCollector {} - } - } - } - - Component.onCompleted: { - LauncherProviderRegistry.registerPluginProvider(pluginApi.pluginId, _providerComponent, {}); - } - - Component.onDestruction: { - LauncherProviderRegistry.unregisterPluginProvider(pluginApi.pluginId); - } -} diff --git a/password-store/LauncherProvider.qml b/password-store/LauncherProvider.qml new file mode 100644 index 000000000..a82152a68 --- /dev/null +++ b/password-store/LauncherProvider.qml @@ -0,0 +1,82 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Item { + id: provider + property var launcher: null + property var pluginApi: null + property string name: "Password Store" + property bool handleSearch: true + property var passwords: [] + + function init() { + listProcess.running = true; + } + + function onOpened() { + if (passwords.length === 0) + listProcess.running = true; + } + + function getResults(query) { + const isPass = query === "pass" || query.startsWith("pass "); + const isOtp = query === "po" || query.startsWith("po "); + if (!isPass && !isOtp) + return []; + + const search = isPass + ? (query.startsWith("pass ") ? query.substring(5).trim() : "") + : (query.startsWith("po ") ? query.substring(3).trim() : ""); + + const otp = isOtp; + + let matches; + if (!search) { + matches = passwords.slice(0, 15).map(p => ({ target: p, score: 1.0 })); + } else { + const results = FuzzySort.go(search, passwords, { limit: 15 }); + matches = Array.from({ length: results.length }, (_, i) => results[i]); + } + + return matches.map(function(match) { + const passName = match.target; + return { + "name": passName, + "description": otp ? "Copy OTP to clipboard" : "Copy password to clipboard", + "icon": otp ? "clock-shield" : "lock", + "isTablerIcon": true, + "isImage": false, + "_score": match.score, + "onActivate": function() { + if (provider.launcher) + provider.launcher.closeImmediately(); + Qt.callLater(function() { + Quickshell.execDetached(otp + ? ["pass", "otp", "-c", passName] + : ["pass", "-c", passName]); + }); + } + }; + }); + } + + Process { + id: listProcess + command: [ + "sh", "-c", + "store=\"${PASSWORD_STORE_DIR:-$HOME/.password-store}\"; " + + "find \"$store\" -name '*.gpg' | sed \"s|$store/||;s|\\.gpg$||\" | sort" + ] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text && text.trim()) + provider.passwords = text.trim().split("\n").filter(p => p.length > 0); + } + } + stderr: StdioCollector {} + } +} diff --git a/pass/README.md b/password-store/README.md similarity index 73% rename from pass/README.md rename to password-store/README.md index 7518029e1..35afcf759 100644 --- a/pass/README.md +++ b/password-store/README.md @@ -28,18 +28,30 @@ sudo apt install pass pass-extension-otp wl-clipboard sudo dnf install pass pass-otp wl-clipboard ``` -## Installation +## gopass -### Manual Installation +If you use [gopass](https://github.com/gopasspw/gopass) instead of pass, you can make this plugin work by symlinking `gopass` as `pass`: ```bash -git clone https://github.com/noctalia-dev/noctalia-plugins ~/.config/noctalia/plugins/pass +ln -s $(which gopass) ~/.local/bin/pass ``` -Then restart Noctalia: +The plugin calls `pass -c ` and `pass otp -c `, which gopass understands. Make sure `~/.local/bin` is on your `$PATH`. + +You also need to point the plugin at gopass's store by setting `$PASSWORD_STORE_DIR`: + +```bash +export PASSWORD_STORE_DIR="$HOME/.local/share/gopass/stores/root" +``` + +Add this to your shell profile (`.bashrc`, `.zshrc`, etc.) so it persists. + +## Installation + +Install directly from the Noctalia plugin manager in Settings, or clone manually: ```bash -qs kill -c noctalia-shell && qs -c noctalia-shell -d +git clone https://github.com/noctalia-dev/noctalia-plugins ~/.config/noctalia/plugins/password-store ``` ## Usage @@ -64,10 +76,10 @@ Selecting an entry runs `pass -c ` or `pass otp -c ` respectively. ## File Structure ``` -pass/ -├── manifest.json # Plugin metadata -├── Main.qml # Launcher provider — search and activation logic -└── README.md # This file +password-store/ +├── manifest.json # Plugin metadata +├── LauncherProvider.qml # Launcher provider — search and activation logic +└── README.md # This file ``` ## Troubleshooting diff --git a/pass/manifest.json b/password-store/manifest.json similarity index 87% rename from pass/manifest.json rename to password-store/manifest.json index af551b177..da0cd0fe7 100644 --- a/pass/manifest.json +++ b/password-store/manifest.json @@ -1,5 +1,5 @@ { - "id": "pass", + "id": "password-store", "name": "Password Store", "version": "1.0.0", "minNoctaliaVersion": "3.6.0", @@ -9,7 +9,7 @@ "description": "Search pass (password-store) from the launcher. Type 'pass ' to copy a password or 'po ' to copy a TOTP code to clipboard.", "tags": ["Launcher", "Utility"], "entryPoints": { - "main": "Main.qml" + "launcherProvider": "LauncherProvider.qml" }, "dependencies": { "plugins": []