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

This commit is contained in:
Sweetbread committed 2026-11-08 17:18:22 +03:00
1 parent 422bf29318
commit 9ce9fefaa7
4 files changed
+409 -2

No files matched your search

+3
View File
@@ -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"))
+363
View File
@@ -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
}
}
}
}
}
+40 -2
View File
@@ -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"
+3
View File
@@ -126,6 +126,9 @@ ShellRoot {
WidgetLayer {
}
Vaultwarden {
}
WidgetIpc {
}