From 9ce9fefaa7b6ce17d9dac4d6e612e0f907ae9f9b Mon Sep 17 00:00:00 2001 From: Sweetbread Date: Sun, 8 Nov 2026 17:18:22 +0300 Subject: [PATCH] fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Install lucid --- modules/user/hyprland.nix | 3 + modules/user/lucid/Vaultwarden.qml | 363 +++++++++++++++++++++++++++++ modules/user/lucid/default.nix | 42 +++- modules/user/lucid/lucid-shell.qml | 3 + 4 files changed, 409 insertions(+), 2 deletions(-) create mode 100644 modules/user/lucid/Vaultwarden.qml diff --git a/modules/user/hyprland.nix b/modules/user/hyprland.nix index 5267183..81012d7 100644 --- a/modules/user/hyprland.nix +++ b/modules/user/hyprland.nix @@ -43,6 +43,8 @@ in { wl-clipboard wl-clip-persist xclip + rbw + wtype awww wifitui ]; @@ -168,6 +170,7 @@ in { lucidHyprlandBinds = [ (bind "SUPER + V" (exec "ghostty --title=clipse -e clipse")) + (bind "SUPER + X" (exec "qs ipc call -- vaultwarden open")) (hyprLua.bind "SUPER + SHIFT + V" (hyprLua.execLauncher "qs ipc call launcher clipboard")) (bind "Print" (exec "qs ipc call screenshot full")) (bind "SUPER + Print" (exec "qs ipc call screenshot full")) diff --git a/modules/user/lucid/Vaultwarden.qml b/modules/user/lucid/Vaultwarden.qml new file mode 100644 index 0000000..5ab6bb7 --- /dev/null +++ b/modules/user/lucid/Vaultwarden.qml @@ -0,0 +1,363 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import Quickshell.Io +import Quickshell.Wayland +import qs + +Item { + id: root + + property bool shown: false + property bool busy: false + property string status: "" + property string targetAddress: "" + property string pendingPassword: "" + property var passwordProcess: null + property var entries: [] + property int selectedIndex: 0 + + readonly property var filteredEntries: { + const query = search.text.trim().toLocaleLowerCase(); + if (query === "") + return root.entries; + + return root.entries.filter((entry) => + entry.name.toLocaleLowerCase().includes(query) + || entry.user.toLocaleLowerCase().includes(query) + || entry.folder.toLocaleLowerCase().includes(query)); + } + + function openPicker() { + if (typeDelay.running) + return; + if (root.shown) { + root.closePicker(); + return; + } + + const active = Hyprland.activeToplevel; + root.targetAddress = active ? active.address : ""; + root.shown = true; + root.busy = true; + root.status = "Loading vault…"; + root.entries = []; + search.text = ""; + root.selectedIndex = 0; + listReader.command = ["rbw", "list", "--fields", "id,name,user,folder"]; + listReader.running = true; + Qt.callLater(() => search.forceActiveFocus()); + } + + function closePicker() { + typeDelay.stop(); + if (listReader.running) + listReader.running = false; + if (root.passwordProcess) { + root.passwordProcess.running = false; + root.passwordProcess.destroy(); + root.passwordProcess = null; + } + root.shown = false; + root.busy = false; + root.status = ""; + root.pendingPassword = ""; + } + + function choose(entry) { + if (!entry || root.busy) + return; + + root.busy = true; + root.status = "Reading password…"; + root.passwordProcess = passwordProcessComponent.createObject(root, { entryId: entry.id }); + if (!root.passwordProcess) { + root.busy = false; + root.status = "Could not start rbw."; + return; + } + root.passwordProcess.running = true; + } + + function deliverPassword(password) { + if (password === "") { + root.busy = false; + root.status = "This entry has an empty password."; + return; + } + + root.pendingPassword = password; + root.shown = false; + root.status = ""; + typeDelay.restart(); + } + + IpcHandler { + target: "vaultwarden" + + function open(): void { + root.openPicker(); + } + + function close(): void { + root.closePicker(); + } + } + + Process { + id: listReader + + command: [] + stdout: StdioCollector { + id: listOutput + } + + onExited: (code) => { + root.busy = false; + if (code !== 0) { + root.status = "Could not read the vault. Unlock it with rbw, then try again."; + return; + } + + root.entries = listOutput.text.split(/\r?\n/).filter((line) => line !== "").map((line) => { + const fields = line.split("\t"); + return { + id: fields[0] || "", + name: fields[1] || "Untitled", + user: fields[2] || "", + folder: fields[3] || "" + }; + }).filter((entry) => entry.id !== ""); + root.status = root.entries.length === 0 ? "No entries in the vault." : ""; + } + } + + Component { + id: passwordProcessComponent + + Process { + id: passwordJob + + property string entryId: "" + command: ["rbw", "get", entryId] + stdout: StdioCollector { + id: passwordOutput + } + + onExited: (code) => { + if (root.passwordProcess === passwordJob) + root.passwordProcess = null; + if (code !== 0) { + root.busy = false; + root.status = "Could not read that password."; + } else { + root.deliverPassword(passwordOutput.text.replace(/\r?\n$/, "")); + } + passwordJob.destroy(); + } + } + } + + Process { + id: typer + + command: ["wtype", "-"] + stdinEnabled: true + onStarted: { + write(root.pendingPassword); + stdinEnabled = false; + root.pendingPassword = ""; + root.busy = false; + } + } + + Timer { + id: typeDelay + + interval: 180 + onTriggered: { + if (root.targetAddress !== "") + Hyprland.dispatch("focuswindow address:" + root.targetAddress); + root.targetAddress = ""; + + typer.running = false; + typer.stdinEnabled = true; + typer.running = true; + } + } + + PanelWindow { + id: window + + visible: root.shown + anchors { + top: true + bottom: true + left: true + right: true + } + color: "transparent" + exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: root.shown ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + + Rectangle { + anchors.fill: parent + color: "#99000000" + + MouseArea { + anchors.fill: parent + onClicked: root.closePicker() + } + } + + Rectangle { + id: card + + anchors.centerIn: parent + width: Math.min(parent.width - Theme.dp(40), Theme.dp(620)) + height: Math.min(parent.height - Theme.dp(40), Theme.dp(560)) + radius: Theme.shapeXl + color: Theme.surfaceContainer + border.width: 1 + border.color: Theme.outline + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: (mouse) => mouse.accepted = true + } + + Column { + anchors.fill: parent + anchors.margins: Theme.dp(24) + spacing: Theme.dp(14) + + Text { + text: "Vaultwarden" + color: Theme.text + font.pixelSize: Theme.fontHeadlineLg + font.weight: Font.DemiBold + } + + Rectangle { + width: parent.width + height: Theme.dp(52) + radius: Theme.shapeMd + color: Theme.surfaceHigh + border.width: search.activeFocus ? 2 : 1 + border.color: search.activeFocus ? Theme.accent : Theme.outline + + TextInput { + id: search + + anchors.fill: parent + anchors.leftMargin: Theme.dp(16) + anchors.rightMargin: Theme.dp(16) + verticalAlignment: TextInput.AlignVCenter + color: Theme.text + selectionColor: Theme.accent + selectedTextColor: Theme.fgAccent + font.pixelSize: Theme.fontTitle + clip: true + focus: root.shown + selectByMouse: true + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + text: "Search entries…" + color: Theme.subtext + font: search.font + visible: search.text === "" + } + + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + root.closePicker(); + event.accepted = true; + } else if (event.key === Qt.Key_Down) { + root.selectedIndex = Math.max(0, Math.min(root.filteredEntries.length - 1, root.selectedIndex + 1)); + if (root.filteredEntries.length > 0) + list.positionViewAtIndex(root.selectedIndex, ListView.Contain); + event.accepted = true; + } else if (event.key === Qt.Key_Up) { + root.selectedIndex = Math.max(0, root.selectedIndex - 1); + if (root.filteredEntries.length > 0) + list.positionViewAtIndex(root.selectedIndex, ListView.Contain); + event.accepted = true; + } + } + + onTextChanged: root.selectedIndex = 0 + onAccepted: { + if (root.filteredEntries.length > 0) + root.choose(root.filteredEntries[root.selectedIndex] || root.filteredEntries[0]); + } + } + } + + ListView { + id: list + + width: parent.width + height: Math.max(0, parent.height - Theme.dp(122)) + clip: true + spacing: Theme.dp(4) + model: root.filteredEntries + currentIndex: root.selectedIndex + + delegate: Rectangle { + required property var modelData + required property int index + + width: list.width + height: Theme.dp(60) + radius: Theme.shapeMd + color: index === root.selectedIndex ? Theme.bgHover : "transparent" + + Column { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.dp(14) + anchors.rightMargin: Theme.dp(14) + spacing: Theme.dp(3) + + Text { + width: parent.width + text: modelData.name + color: Theme.text + font.pixelSize: Theme.fontTitle + elide: Text.ElideRight + } + + Text { + width: parent.width + text: [modelData.user, modelData.folder].filter((part) => part !== "").join(" · ") + color: Theme.subtext + font.pixelSize: Theme.fontLabel + elide: Text.ElideRight + visible: text !== "" + } + } + + MouseArea { + anchors.fill: parent + onEntered: root.selectedIndex = index + onClicked: root.choose(modelData) + } + } + } + + Text { + width: parent.width + text: root.busy || root.status !== "" ? root.status : "↑ ↓ Select Enter Type password Esc Close" + color: root.status !== "" && !root.busy ? Theme.warning : Theme.subtext + font.pixelSize: Theme.fontLabel + elide: Text.ElideRight + } + } + } + } +} diff --git a/modules/user/lucid/default.nix b/modules/user/lucid/default.nix index 40ff2ac..69af926 100644 --- a/modules/user/lucid/default.nix +++ b/modules/user/lucid/default.nix @@ -9,6 +9,31 @@ let binds = builtins.filter (entry: builtins.elem entry.id specialBindIds) (builtins.fromJSON (builtins.readFile ./lucid-keybinds-rias.json)).binds; }); + vaultwardenBindSeed = pkgs.writeText "lucid-vaultwarden-binding.json" (builtins.toJSON { + binds = [ { + id = "vaultwarden-password"; + keys = "SUPER + X"; + desc = "Type a password from Vaultwarden"; + category = "Apps"; + type = "exec"; + cmd = "qs ipc call -- vaultwarden open"; + } ]; + }); + mergeVaultwardenBind = pkgs.writeText "lucid-merge-vaultwarden-bind.jq" '' + def normkeys: (. // "") | ascii_upcase | gsub("\\s+"; "") | split("+") + | map(if . == "WIN" or . == "LOGO" or . == "MOD4" then "SUPER" + elif . == "CONTROL" then "CTRL" else . end) | sort | join("+"); + if (.binds | type) != "array" then error("keybinds.json has no binds array") else . end + | . as $old + | ($seed[0].binds[0]) as $seed_bind + | ($old.binds | map(select(.id == $seed_bind.id)) | first) as $existing + | ($old.binds | map(select(.id != $seed_bind.id))) as $others + | ($others | any(.enabled != false and ((.keys | normkeys) == ($seed_bind.keys | normkeys)))) as $conflict + | .binds = $others + [ + $seed_bind + (if $existing.enabled == false or ($existing == null and $conflict) + then {enabled: false} else {} end) + ] + ''; chestBindingsScript = pkgs.writeText "lucid-chest-bindings.py" '' import hashlib import json @@ -585,6 +610,7 @@ in { fi $DRY_RUN_CMD install -m644 ${shellFile} "$target/shell.qml" $DRY_RUN_CMD install -m644 ${./LucidBar.qml} "$target/LucidBar.qml" + $DRY_RUN_CMD install -m644 ${./Vaultwarden.qml} "$target/Vaultwarden.qml" ${lib.optionalString (!isChest) '' $DRY_RUN_CMD install -m644 ${./Agenda.qml} "$target/Agenda.qml" $DRY_RUN_CMD install -m644 ${./CalendarSource.qml} "$target/CalendarSource.qml" @@ -605,8 +631,20 @@ in { lucid_dir="${config.home.homeDirectory}/.config/lucid" $DRY_RUN_CMD mkdir -p "$lucid_dir/themes" - if [ "${host}" = Rias ] && [ ! -s "$lucid_dir/keybinds.json" ]; then - $DRY_RUN_CMD install -m644 ${./lucid-keybinds-rias.json} "$lucid_dir/keybinds.json" + if [ "${host}" = Rias ] && [ -z "''${DRY_RUN_CMD:-}" ]; then + if [ ! -s "$lucid_dir/keybinds.json" ]; then + install -m644 ${./lucid-keybinds-rias.json} "$lucid_dir/keybinds.json" + fi + binds_tmp=$(${pkgs.coreutils}/bin/mktemp "$lucid_dir/keybinds.json.XXXXXX") + if ${pkgs.jq}/bin/jq --slurpfile seed ${vaultwardenBindSeed} -f ${mergeVaultwardenBind} \ + "$lucid_dir/keybinds.json" > "$binds_tmp"; then + chmod 644 "$binds_tmp" + mv "$binds_tmp" "$lucid_dir/keybinds.json" + else + rm -f "$binds_tmp" + echo "Could not merge the Vaultwarden Lucid binding" >&2 + exit 1 + fi fi ${lib.optionalString (!isRias) '' special_migration="$lucid_dir/special-workspaces-v1" diff --git a/modules/user/lucid/lucid-shell.qml b/modules/user/lucid/lucid-shell.qml index aed1e70..a5189ed 100644 --- a/modules/user/lucid/lucid-shell.qml +++ b/modules/user/lucid/lucid-shell.qml @@ -126,6 +126,9 @@ ShellRoot { WidgetLayer { } + Vaultwarden { + } + WidgetIpc { }