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:
4 files changed
+409
-2
No files matched your search
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -126,6 +126,9 @@ ShellRoot {
|
||||
WidgetLayer {
|
||||
}
|
||||
|
||||
Vaultwarden {
|
||||
}
|
||||
|
||||
WidgetIpc {
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user