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": {} + } +}