Install lucid

This commit is contained in:
Sweetbread committed 2026-11-13 00:06:31 +03:00
1 parent 72c85c59ff
commit dccab8e54f
38 files changed
+6439 -70

No files matched your search

-1
View File
@@ -13,7 +13,6 @@
./user/sops.nix
./user/ssh.nix
./user/walker.nix
./user/wob.nix
./user/yazi.nix
./user/zsh.nix
+26 -4
View File
@@ -28,13 +28,23 @@ in {
optImport ../../host/${hostname}/modules/hyprland.nix ++
optImport ../../user/${username}/modules/hyprland.nix;
options.lucidHyprlandBinds = lib.mkOption {
type = lib.types.listOf lib.types.attrs;
default = [];
internal = true;
};
config = {
home.packages = with pkgs; [
ghostty
pamixer
clipse
cliphist
wl-clipboard
wl-clip-persist
xclip
rbw
wtype
awww
wifitui
];
@@ -104,7 +114,10 @@ in {
{ _args = [ "XDG_SCREENSHOTS_DIR" "~/screens" ]; }
];
gesture = [
# Lucid's Rias integration loads support/hypr/modules/gestures.lua,
# which defines this workspace swipe (and a four-finger variant).
# Avoid registering the same 3-finger gesture twice in Hyprland.
gesture = lib.optionals (hostname != "Rias") [
{
fingers = 3;
direction = "horizontal";
@@ -115,6 +128,7 @@ in {
workspace_rule = [
{
workspace = "w[t1]";
gaps_in = 0;
gaps_out = 0;
}
];
@@ -150,8 +164,18 @@ in {
])
];
bind = [
bind = lib.mkIf (username != "chest") config.lucidHyprlandBinds;
};
};
lucidHyprlandBinds = [
(bind "SUPER + V" (exec "ghostty --title=clipse -e clipse"))
(bind "SUPER + X" (exec "qs ipc call -- launcher search '>vault'"))
(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"))
(bind "CTRL + Print" (exec "qs ipc call snap region"))
(bind "ALT + Print" (exec "qs ipc call snap window"))
(bind "SUPER + RETURN" (exec "ghostty"))
(bind "SUPER + Q" "hl.dsp.window.close()")
(bind "SUPER + M" "hl.dsp.exit()")
@@ -204,7 +228,5 @@ in {
(bindWithFlags "SUPER + mouse:272" "hl.dsp.window.drag()" { mouse = true; })
(bindWithFlags "SUPER + mouse:273" "hl.dsp.window.resize()" { mouse = true; })
];
};
};
}
+6
View File
@@ -7,6 +7,12 @@ in rec {
exec = command: "hl.dsp.exec_cmd(${quote command})";
# Hyprland 0.57 can leave exclusive layer-shell launchers unfocused when
# follow_mouse = 1 and the pointer is already over a regular window.
execLauncher = command: ''
hl.dsp.exec_cmd(${quote ("hyprctl eval 'hl.config({ input = { follow_mouse = 2 } })' && " + command)})
'';
bind = keys: dispatcher: {
_args = [
keys
+1 -1
View File
@@ -17,7 +17,7 @@ let
hl.config({ cursor = { zoom_factor = math.max(1, current * 0.9) } })
end'';
in {
wayland.windowManager.hyprland.settings.bind = [
lucidHyprlandBinds = [
(bind "SUPER + SHIFT + mouse_up" resetZoom)
(bind "SUPER + SHIFT + mouse_down" resetZoom)
(bind "SUPER + SHIFT + minus" resetZoom)
File diff suppressed because it is too large. Load diff
+129
View File
@@ -0,0 +1,129 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
pragma Singleton
Singleton {
id: root
property var entries: []
property bool loading: false
property string error: ""
property string targetAddress: ""
property string pendingPassword: ""
property var passwordProcess: null
signal passwordReady(string password, string targetAddress)
function refresh() {
if (listReader.running)
return;
root.loading = true;
root.error = "";
listReader.command = ["rbw", "list", "--fields", "id,name,user,folder"];
listReader.running = true;
}
function getPassword(id, target) {
if (root.passwordProcess)
return;
root.targetAddress = target || "";
root.error = "";
root.passwordProcess = passwordProcessComponent.createObject(root, { entryId: id });
if (!root.passwordProcess) {
root.error = "Could not start rbw.";
return;
}
root.passwordProcess.running = true;
}
function typePassword(password, target) {
root.pendingPassword = password;
root.targetAddress = target || "";
typeDelay.restart();
}
Process {
id: listReader
command: []
stdout: StdioCollector {
id: listOutput
}
onExited: (code) => {
root.loading = false;
if (code !== 0) {
root.error = "Could not read the vault. Unlock it with rbw, then retry.";
root.entries = [];
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.error = "";
}
}
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.error = "Could not read that password.";
} else {
root.passwordReady(passwordOutput.text.replace(/\r?\n$/, ""), root.targetAddress);
}
passwordJob.destroy();
}
}
}
Process {
id: typer
command: ["wtype", "-"]
stdinEnabled: true
onStarted: {
write(root.pendingPassword);
stdinEnabled = false;
root.pendingPassword = "";
root.targetAddress = "";
}
}
Timer {
id: typeDelay
interval: 260
onTriggered: {
if (root.targetAddress !== "")
Hyprland.dispatch("focuswindow address:" + root.targetAddress);
typer.running = false;
typer.stdinEnabled = true;
typer.running = true;
}
}
}
+617
View File
@@ -0,0 +1,617 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs
Variants {
id: variants
model: Quickshell.screens
Scope {
id: unit
required property var modelData
PanelWindow {
id: layer
PaletteFade {
active: layer.visible
}
readonly property bool suppressed: Prefs.widgetHideFullscreen && layer.windowUp
readonly property var workspaceMonitor: Hyprland.monitorFor(layer.screen)
readonly property var activeWorkspace: workspaceMonitor ? workspaceMonitor.activeWorkspace : null
readonly property bool windowUp: {
const workspace = layer.activeWorkspace;
if (!workspace || !workspace.toplevels)
return false;
return workspace.toplevels.values.some((t) => {
const o = t.lastIpcObject || {};
if (o.mapped === false || o.hidden === true)
return false;
if (o.size && (o.size[0] <= 0 || o.size[1] <= 0))
return false;
return (o.fullscreen || 0) > 0 || o.floating === false;
});
}
screen: unit.modelData
visible: Prefs.loaded && Widgets.loaded && (deck.panelLive || ((Prefs.widgetsEnabled || deck.stowing) && Widgets.count > 0 && !layer.suppressed))
color: "transparent"
// reserves nothing and refuses to be shrunk into the bar and dock's strips,
// so a card can be dragged to a true screen edge and sit under the dock.
// setting exclusiveZone at all would silently undo this
exclusionMode: ExclusionMode.Ignore
// the widget panel lifts the whole board over the windows until it has slid away
WlrLayershell.layer: deck.panelLive ? WlrLayer.Overlay : (Prefs.widgetOnTop ? WlrLayer.Top : WlrLayer.Bottom)
WlrLayershell.keyboardFocus: deck.panelUp ? WlrKeyboardFocus.Exclusive : (deck.wantsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None)
anchors {
top: true
bottom: true
left: true
right: true
}
// lastIpcObject only moves when something asks it to, so nudge it on
// the events that can change whether a tiled or fullscreen window exists.
Connections {
function onRawEvent(event) {
if (event.name === "fullscreen" || event.name === "activewindowv2" || event.name === "closewindow" || event.name === "openwindow" || event.name === "workspace" || event.name === "focusedmon" || event.name === "movewindow" || event.name === "changefloatingmode")
windowPoll.restart();
}
enabled: Prefs.widgetHideFullscreen
target: Hyprland
}
Timer {
id: stowEnd
onTriggered: deck.stowing = false
}
Timer {
id: windowPoll
interval: 120
onTriggered: Hyprland.refreshToplevels()
}
Item {
id: deck
// bumped whenever the repeater's children change, so frameAt() bindings re-run
property int rev: 0
property Item dragFrame: null
readonly property string screenName: layer.screen ? layer.screen.name : ""
readonly property bool isPrimary: layer.screen !== null && layer.screen === Monitors.mainScreen
// the widget panel is open on this screen, and still on its way out
readonly property bool panelUp: Widgets.panelScreen !== "" && Widgets.panelScreen === deck.screenName
readonly property bool panelLive: deck.panelUp || panel.shown
// the cards are out: on, or the panel is up to place them
readonly property bool widgetsOn: Prefs.widgetsEnabled || deck.panelLive
// put away a moment ago: the board stays up while the cards go
property bool stowing: false
readonly property int stowStep: Math.round(30 * Theme.motionScale)
readonly property int wakeStep: Math.round(55 * Theme.motionScale)
// the order the cards go and come back in: down the screen, then across
function orderOf(f) {
var n = 0;
for (var i = 0; i < rep.count; i++) {
var o = rep.itemAt(i);
if (o && o !== f && o.onThisScreen && (o.y < f.y - 1 || (Math.abs(o.y - f.y) <= 1 && o.x < f.x)))
n++;
}
return n;
}
onWidgetsOnChanged: {
if (deck.widgetsOn) {
stowEnd.stop();
deck.stowing = false;
} else {
stowEnd.interval = rep.count * deck.stowStep + Theme.durMedium + 80;
deck.stowing = true;
stowEnd.restart();
}
}
readonly property int frameCount: rep.count
readonly property bool editing: Widgets.editUid !== "" && deck.editFrame !== null
// only ask for keys at all once something you can type into is placed;
// on-demand hands focus back the moment a window is clicked, which
// exclusive would not
readonly property bool wantsKeyboard: {
for (var i = 0; i < Widgets.model.count; i++) {
var e = Widgets.model.get(i);
if (!e.closing && (e.wtype === "notes" || e.wtype === "todo" || e.wtype === "github"))
return true;
}
return false;
}
readonly property bool grabbing: Widgets.dragUid !== "" || Widgets.menuUid !== "" || deck.editing
readonly property Item menuFrame: {
var _ = deck.rev;
for (var i = 0; i < rep.count; i++) {
var f = rep.itemAt(i);
if (f && f.uid === Widgets.menuUid && f.visible)
return f;
}
return null;
}
readonly property Item editFrame: {
var _ = deck.rev;
for (var i = 0; i < rep.count; i++) {
var f = rep.itemAt(i);
if (f && f.uid === Widgets.editUid && f.visible)
return f;
}
return null;
}
function frameAt(i) {
var _ = deck.rev;
return (i >= 0 && i < rep.count) ? rep.itemAt(i) : null;
}
function frameFor(uid) {
for (var i = 0; i < rep.count; i++) {
var f = rep.itemAt(i);
if (f && f.uid === uid)
return f;
}
return null;
}
// a board point over the open panel, where a dragged card is let go of
function binHit(px, py) {
return deck.panelUp && panel.sheetHit(px, py);
}
// the topmost card under a board point, for the menu overlay: it
// covers the screen while it is up, so the cards never see the press
function frameUnder(px, py) {
var best = null;
for (var i = 0; i < rep.count; i++) {
var f = rep.itemAt(i);
if (!f || !f.visible || f.closing)
continue;
if (px < f.x || px > f.x + f.width || py < f.y || py > f.y + f.height)
continue;
if (best === null || f.z >= best.z)
best = f;
}
return best;
}
anchors.fill: parent
focus: true
Keys.onEscapePressed: {
var busy = Widgets.editUid !== "" || Widgets.menuUid !== "" || Object.keys(Widgets.selection).length > 0;
Widgets.editUid = "";
Widgets.menuUid = "";
Widgets.clearSelection();
if (!busy && deck.panelUp)
Widgets.closePanel();
}
onWidthChanged: deck.report()
onHeightChanged: deck.report()
Component.onCompleted: deck.report()
function report() {
if (deck.isPrimary && deck.width > 0) {
Widgets.canvasW = deck.width;
Widgets.canvasH = deck.height;
}
}
// the dim behind the cards while the widget panel is open, and the way out of it
Rectangle {
anchors.fill: parent
color: Theme.alpha(Theme.cScrim, Theme.blurAmount > 0 ? 0.3 : 0.5)
opacity: panel.reveal
visible: opacity > 0.01
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
enabled: deck.panelUp
onWheel: (event) => {
return event.accepted = true;
}
}
}
// click-through everywhere except the cards, so this only fires for the
// open menu, a note being typed in, or the widget panel
MouseArea {
anchors.fill: parent
enabled: deck.grabbing || deck.panelUp
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: {
var busy = Widgets.menuUid !== "" || Widgets.editUid !== "";
Widgets.menuUid = "";
Widgets.editUid = "";
if (!busy && deck.panelUp) {
Widgets.clearSelection();
Widgets.closePanel();
}
}
}
// the cards stack among themselves, under the guides and the panel
Item {
anchors.fill: parent
Repeater {
id: rep
model: Widgets.model
onItemAdded: deck.rev++
onItemRemoved: deck.rev++
WidgetFrame {
board: deck
updatesEnabled: layer.visible
}
}
}
Rectangle {
width: 1
height: deck.height
x: deck.dragFrame ? deck.dragFrame.guideX : -1
color: Theme.accent
opacity: (deck.dragFrame && deck.dragFrame.guideX >= 0) ? 0.65 : 0
visible: opacity > 0.01
z: 900
Behavior on opacity {
NumberAnimation {
duration: Theme.durQuick
}
}
}
Rectangle {
height: 1
width: deck.width
y: deck.dragFrame ? deck.dragFrame.guideY : -1
color: Theme.accent
opacity: (deck.dragFrame && deck.dragFrame.guideY >= 0) ? 0.65 : 0
visible: opacity > 0.01
z: 900
Behavior on opacity {
NumberAnimation {
duration: Theme.durQuick
}
}
}
WidgetPanel {
id: panel
anchors.fill: parent
z: 1000
board: deck
}
}
mask: Region {
x: 0
y: 0
width: (deck.grabbing || deck.panelUp) ? layer.width : 0
height: (deck.grabbing || deck.panelUp) ? layer.height : 0
WidgetRegion {
frame: deck.frameAt(0)
}
WidgetRegion {
frame: deck.frameAt(1)
}
WidgetRegion {
frame: deck.frameAt(2)
}
WidgetRegion {
frame: deck.frameAt(3)
}
WidgetRegion {
frame: deck.frameAt(4)
}
WidgetRegion {
frame: deck.frameAt(5)
}
WidgetRegion {
frame: deck.frameAt(6)
}
WidgetRegion {
frame: deck.frameAt(7)
}
WidgetRegion {
frame: deck.frameAt(8)
}
WidgetRegion {
frame: deck.frameAt(9)
}
WidgetRegion {
frame: deck.frameAt(10)
}
WidgetRegion {
frame: deck.frameAt(11)
}
WidgetRegion {
frame: deck.frameAt(12)
}
WidgetRegion {
frame: deck.frameAt(13)
}
WidgetRegion {
frame: deck.frameAt(14)
}
WidgetRegion {
frame: deck.frameAt(15)
}
WidgetRegion {
frame: deck.frameAt(16)
}
WidgetRegion {
frame: deck.frameAt(17)
}
WidgetRegion {
frame: deck.frameAt(18)
}
WidgetRegion {
frame: deck.frameAt(19)
}
}
// the open panel frosts the whole screen behind its dim, cards and sheet alike
BackgroundEffect.blurRegion: Theme.blurAmount > 0 ? (deck.panelUp ? wholeBlur : widgetBlur) : null
Region {
id: wholeBlur
width: layer.width
height: layer.height
}
Region {
id: widgetBlur
WidgetRegion {
blur: true
frame: deck.frameAt(0)
}
WidgetRegion {
blur: true
frame: deck.frameAt(1)
}
WidgetRegion {
blur: true
frame: deck.frameAt(2)
}
WidgetRegion {
blur: true
frame: deck.frameAt(3)
}
WidgetRegion {
blur: true
frame: deck.frameAt(4)
}
WidgetRegion {
blur: true
frame: deck.frameAt(5)
}
WidgetRegion {
blur: true
frame: deck.frameAt(6)
}
WidgetRegion {
blur: true
frame: deck.frameAt(7)
}
WidgetRegion {
blur: true
frame: deck.frameAt(8)
}
WidgetRegion {
blur: true
frame: deck.frameAt(9)
}
WidgetRegion {
blur: true
frame: deck.frameAt(10)
}
WidgetRegion {
blur: true
frame: deck.frameAt(11)
}
WidgetRegion {
blur: true
frame: deck.frameAt(12)
}
WidgetRegion {
blur: true
frame: deck.frameAt(13)
}
WidgetRegion {
blur: true
frame: deck.frameAt(14)
}
WidgetRegion {
blur: true
frame: deck.frameAt(15)
}
WidgetRegion {
blur: true
frame: deck.frameAt(16)
}
WidgetRegion {
blur: true
frame: deck.frameAt(17)
}
WidgetRegion {
blur: true
frame: deck.frameAt(18)
}
WidgetRegion {
blur: true
frame: deck.frameAt(19)
}
}
}
// an options panel on the widget layer would sit under every window, so it
// gets an overlay of its own that only exists while the panel is up
PanelWindow {
id: menuLayer
// the frame the panel belongs to, held past the close so the fade-out
// keeps its geometry and its contents
property Item held: null
readonly property Item target: deck.menuFrame
screen: unit.modelData
// up the moment the menu opens, so the fade-in plays inside a mapped
// surface, and stays up until the fade-out finishes
visible: menuLayer.target !== null || menu.visible
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: menuLayer.target !== null ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
onTargetChanged: if (menuLayer.target)
menuLayer.held = menuLayer.target
onVisibleChanged: if (!menuLayer.visible)
menuLayer.held = null
anchors {
top: true
bottom: true
left: true
right: true
}
Item {
anchors.fill: parent
focus: true
Keys.onEscapePressed: Widgets.menuUid = ""
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: (mouse) => {
var f = mouse.button === Qt.RightButton ? deck.frameUnder(mouse.x, mouse.y) : null;
var was = Widgets.menuUid;
Widgets.menuUid = "";
// a right press on another card carries the panel over to it,
// the way it did when the panel still sat on the widget layer
if (f && f.uid !== was)
f.toggleMenu(mouse.x - f.x, mouse.y - f.y);
}
}
WidgetMenu {
id: menu
frame: menuLayer.held
fieldW: menuLayer.width
fieldH: menuLayer.height
}
}
// no input once the panel starts closing, so the fade-out is never a
// dead region over the screen
mask: Region {
width: menuLayer.target ? menuLayer.width : 0
height: menuLayer.target ? menuLayer.height : 0
}
BackgroundEffect.blurRegion: Theme.blurAmount > 0 ? menuBlur : null
Region {
id: menuBlur
WidgetRegion {
blur: true
part: "menu"
panel: menu
}
}
}
}
}
+136
View File
@@ -0,0 +1,136 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs
Column {
id: page
// the map is drawn over this
property string wallpaper: ""
spacing: Theme.dp(26)
// the header switch turns the whole layer off, so the page has nothing left to set
enabled: Prefs.widgetsEnabled
opacity: Prefs.widgetsEnabled ? 1 : 0.38
Behavior on opacity {
NumberAnimation {
duration: Theme.durShort
}
}
FileView {
path: Quickshell.env("HOME") + "/.cache/current_wallpaper"
blockLoading: true
printErrors: false
watchChanges: true
onFileChanged: reload()
onLoaded: page.wallpaper = text().trim()
}
WidgetsMap {
width: parent.width
wallpaper: page.wallpaper
}
SettingCard {
title: "ON THE DESKTOP"
SettingRow {
title: "Widget panel"
description: "Every widget there is, to drag out onto the desktop or click in, the ones already out there, and layouts to start from or save. It opens from the desktop's right-click menu too."
showDivider: false
M3Button {
text: "Open the panel"
variant: "tonal"
iconPath: "widgets"
onClicked: {
Prefs.settingsHideRequested();
Widgets.openPanel(Monitors.mainScreen ? Monitors.mainScreen.name : "");
}
}
}
SettingRow {
title: "Edit mode"
description: "The desktop shades over and everything on it can be moved: widgets, the bar's modules and the dock. Right-click anything for its options."
showDivider: false
M3Button {
text: "Edit the desktop"
variant: "tonal"
iconPath: "edit"
onClicked: Edit.enterFromSettings("widgets", "")
}
}
}
SettingCard {
title: "BEHAVIOUR"
SettingRow {
title: "Snap while dragging"
resetKey: "widgetSnap"
description: "Widgets catch on the screen's edges and centre lines, and line up with each other, so a hand-placed arrangement still looks deliberate."
M3Switch {
checked: Prefs.widgetSnap
onToggled: (v) => {
return Prefs.widgetSnap = v;
}
}
}
SettingRow {
title: "Pin everything"
resetKey: "widgetLockAll"
description: "Locks every widget where it stands, including the ones that are not individually pinned. Useful once the layout is settled."
M3Switch {
checked: Prefs.widgetLockAll
onToggled: (v) => {
return Prefs.widgetLockAll = v;
}
}
}
SettingRow {
title: "Keep above windows"
resetKey: "widgetOnTop"
description: "Off, widgets live on the desktop and windows cover them. On, they float over everything - handy for a clock or a timer you always want in sight."
M3Switch {
checked: Prefs.widgetOnTop
onToggled: (v) => {
return Prefs.widgetOnTop = v;
}
}
}
SettingRow {
title: "Hide when tiled or fullscreen windows are open"
resetKey: "widgetHideFullscreen"
description: "Widgets step aside while a tiled or fullscreen window is on the workspace; they return on empty workspaces and stay over floating windows."
showDivider: false
M3Switch {
checked: Prefs.widgetHideFullscreen
onToggled: (v) => {
return Prefs.widgetHideFullscreen = v;
}
}
}
}
}
+743
View File
@@ -0,0 +1,743 @@
{ config, lib, pkgs, ... }:
let
staticBindKeys = map (entry: builtins.elemAt entry._args 0) config.wayland.windowManager.hyprland.settings.bind;
nonRiasBinds = pkgs.runCommand "lucid-nix-binds.lua" { nativeBuildInputs = [ pkgs.python3 ]; } ''
python3 - ${source}/support/hypr/modules/binds.lua "$out" ${lib.escapeShellArg (builtins.toJSON staticBindKeys)} <<'PY'
import json
import pathlib
import sys
source = pathlib.Path(sys.argv[1]).read_text()
keys = json.loads(sys.argv[3])
lua_keys = "{ " + ", ".join(json.dumps(key, ensure_ascii=True) for key in keys) + " }"
guard = r"""
local function normalize_nix_bind(keys)
local mods, key = {}, ""
for part in tostring(keys or ""):gmatch("[^+]+") do
local token = (part:match("^%s*(.-)%s*$") or ""):upper()
if token == "CONTROL" then token = "CTRL" end
if token == "WIN" or token == "LOGO" or token == "MOD4" then token = "SUPER" end
if token == "SUPER" or token == "CTRL" or token == "ALT" or token == "SHIFT" then
mods[token] = true
else
key = token
end
end
local out = {}
for _, mod in ipairs({"ALT", "CTRL", "SHIFT", "SUPER"}) do
if mods[mod] then out[#out + 1] = mod end
end
return table.concat(out, "+") .. "+" .. key
end
local nix_binds = {}
for _, key in ipairs(NIX_BIND_KEYS) do nix_binds[normalize_nix_bind(key)] = true end
""".replace("NIX_BIND_KEYS", lua_keys)
marker = "-- several dispatchers on one key, in order"
binding = ' if type(keys) ~= "string" or keys == "" then error("no keys", 0) end'
fallback = ' for _, entry in ipairs(EMERGENCY) do apply(entry) end'
fallback_source = ' status.source = "emergency"'
fallback_text = 'Emergency binds are active - SUPER+S opens Settings.'
for needle in (marker, binding, fallback, fallback_source, fallback_text):
if source.count(needle) != 1:
raise SystemExit("Lucid bind loader changed; refusing to skip conflict protection")
source = source.replace(marker, guard + "\n" + marker, 1)
source = source.replace(binding, binding + '\n if nix_binds[normalize_nix_bind(keys)] then error("Already bound by Nix: " .. keys, 0) end', 1)
source = source.replace(fallback, ' -- Nix supplies the fallback keys on these hosts.', 1)
source = source.replace(fallback_source, ' status.source = "nix"', 1)
source = source.replace(fallback_text, 'Nix bindings remain active.', 1)
pathlib.Path(sys.argv[2]).write_text(source)
PY
'';
vaultwardenLauncherPatch = pkgs.writeText "lucid-vaultwarden-launcher.py" ''
import pathlib
import sys
face_path, dock_path = map(pathlib.Path, sys.argv[1:])
face = face_path.read_text()
dock = dock_path.read_text()
def replace_once(source, old, new, label):
count = source.count(old)
if count != 1:
raise SystemExit(f"Lucid {label} anchor changed (found {count}); refusing an unsafe launcher patch")
return source.replace(old, new, 1)
face = replace_once(face,
' "clipboard": "Search clipboard history",\n',
' "clipboard": "Search clipboard history",\n "vaultwarden": "Search Vaultwarden",\n', "launcher placeholder")
face = replace_once(face,
' "clipboard": "content_paste",\n',
' "clipboard": "content_paste",\n "vaultwarden": "key",\n', "launcher icon")
face = replace_once(face,
' "icon": "content_paste"\n }, {\n "key": "wallpaper",',
' "icon": "content_paste"\n }, {\n "key": "vaultwarden",\n "label": "Vaultwarden",\n "icon": "key"\n }, {\n "key": "wallpaper",', "launcher mode")
face = replace_once(face,
' emptyLabel: {\n',
' emptyLabel: {\n if (face.displayMode === "vaultwarden")\n return "No matching vault entries";\n', "launcher empty label")
face = replace_once(face,
' emptyHint: {\n',
' emptyHint: {\n if (face.displayMode === "vaultwarden")\n return "Unlock the vault with rbw, then search by name, username, or folder.";\n', "launcher empty hint")
dock = replace_once(dock,
' if (q.indexOf(">clip") === 0)\n return "clipboard";\n',
' if (q.indexOf(">clip") === 0)\n return "clipboard";\n\n if (q.indexOf(">pass") === 0)\n return "vaultwarden";\n', "launcher prefix")
dock = replace_once(dock,
' if (mode === "clipboard")\n return ">clip";\n',
' if (mode === "clipboard")\n return ">clip ";\n\n if (mode === "vaultwarden")\n return ">pass ";\n', "launcher mode prefix")
dock = replace_once(dock,
' if (mode === "wallpaper")\n return ">wallpaper";\n',
' if (mode === "wallpaper")\n return ">wallpaper ";\n', "wallpaper prefix spacing")
dock = replace_once(dock,
' if (mode === "theme")\n return ">theme";\n',
' if (mode === "theme")\n return ">theme ";\n', "theme prefix spacing")
dock = replace_once(dock,
' if (mode === "live")\n return ">live";\n',
' if (mode === "live")\n return ">live ";\n', "live prefix spacing")
dock = replace_once(dock,
' if (mode === "commands")\n return ">";\n',
' if (mode === "commands")\n return "> ";\n', "command prefix spacing")
dock = replace_once(dock,
' if (text === ">wallpaper" && !exact && Prefs.liveWallpapers && LiveWall.isVideo(dockWindow.appliedWallpaper))\n text = ">live";\n',
' if (text.trim() === ">wallpaper" && !exact && Prefs.liveWallpapers && LiveWall.isVideo(dockWindow.appliedWallpaper))\n text = ">live ";\n', "wallpaper live prefix spacing")
dock = replace_once(dock,
' dockWindow.setSearchText(">wallpaper");\n } else if (id === "theme") {\n dockWindow.setSearchText(">theme");\n',
' dockWindow.setSearchText(">wallpaper ");\n } else if (id === "theme") {\n dockWindow.setSearchText(">theme ");\n', "menu prefix spacing")
dock = replace_once(dock,
' } else if (id === "clipboard") {\n dockWindow.setSearchText(">clip");\n',
' } else if (id === "clipboard") {\n dockWindow.setSearchText(">clip ");\n', "clipboard prefix spacing")
dock = replace_once(dock,
' dockWindow.setSearchText(">wallpaper", true);',
' dockWindow.setSearchText(">wallpaper ", true);', "wallpaper picker prefix spacing")
dock = replace_once(dock,
'onPickerKindRequested: (kind) => dockWindow.setSearchText(kind === "live" ? ">live" : ">wallpaper", true)',
'onPickerKindRequested: (kind) => dockWindow.setSearchText(kind === "live" ? ">live " : ">wallpaper ", true)', "picker tab prefix spacing")
dock = replace_once(dock,
' dockWindow.openLauncher(">wallpaper");\n }\n\n function live(): void {\n dockWindow.openLauncher(">live");',
' dockWindow.openLauncher(">wallpaper ");\n }\n\n function live(): void {\n dockWindow.openLauncher(">live ");', "wallpaper ipc prefix spacing")
dock = replace_once(dock,
'onBackRequested: dockWindow.setSearchText(dockWindow.mode === "commands" ? "" : ">")',
'onBackRequested: dockWindow.setSearchText(dockWindow.mode === "commands" ? "" : "> ")', "back prefix spacing")
dock = replace_once(dock,
' if (dockWindow.mode === "clipboard")\n Clip.refresh();\n',
' if (dockWindow.mode === "clipboard")\n Clip.refresh();\n if (dockWindow.mode === "vaultwarden")\n Vaultwarden.refresh();\n', "launcher mode activation")
dock = replace_once(dock,
' readonly property var allCommands: [{',
""" Connections {
target: Vaultwarden
function onEntriesChanged() { dockWindow.rebuildResults(); }
function onLoadingChanged() { dockWindow.rebuildResults(); }
function onErrorChanged() { dockWindow.rebuildResults(); }
function onPasswordReady(password, targetAddress) {
dockWindow.menuOpen = false;
Vaultwarden.typePassword(password, targetAddress);
}
}
readonly property var allCommands: [{""", "vaultwarden updates")
dock = replace_once(dock,
' function rebuildResults() {',
""" function vaultRows(query) {
if (Vaultwarden.loading)
return [dockWindow.headerRow("Loading Vaultwarden…")];
if (Vaultwarden.error !== "")
return [dockWindow.headerRow(Vaultwarden.error)];
var rows = [];
var entries = Vaultwarden.entries || [];
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var haystack = (entry.name + " " + entry.user + " " + entry.folder).toLowerCase();
if (query !== "" && haystack.indexOf(query) === -1)
continue;
var detail = [entry.user, entry.folder].filter((part) => part !== "").join(" · ");
rows.push(dockWindow.makeRow("vaultwarden", "vault-" + entry.id, entry.name, detail, {
"glyph": "key",
"payload": entry.id
}));
}
return rows;
}
function rebuildResults() {""", "vaultwarden search")
dock = replace_once(dock,
' } else if (mode === "clipboard") {\n var ents = Clip.entries;',
' } else if (mode === "vaultwarden") {\n rows = dockWindow.vaultRows(q);\n } else if (mode === "clipboard") {\n var ents = Clip.entries;', "vaultwarden results")
dock = replace_once(dock,
' } else if (row.kind === "clip") {\n Clip.copy(row.payload);\n dockWindow.menuOpen = false;\n',
' } else if (row.kind === "clip") {\n Clip.copy(row.payload);\n dockWindow.menuOpen = false;\n } else if (row.kind === "vaultwarden") {\n Vaultwarden.getPassword(row.payload, Hyprland.activeToplevel ? Hyprland.activeToplevel.address : "");\n', "vaultwarden selection")
face_path.write_text(face)
dock_path.write_text(dock)
'';
baseSource = pkgs.applyPatches {
src = pkgs.fetchgit {
url = "https://github.com/Sn3akyy1/lucid.git";
rev = "bdd7f79da18efd837f79dcecbe5d2ec178abc10c";
hash = "sha256-b6qylKjCGYy2Yo64i1w9g4oGrYgcXf502jY9qNNWhSI=";
sparseCheckout = [
"/*"
"!/wallpapers/*"
"!/assets/*.webp"
];
nonConeMode = true;
};
patches = [
(pkgs.writeText "lucid-scratchpad-target.patch" ''
diff --git a/support/hypr/modules/specials.lua b/support/hypr/modules/specials.lua
--- a/support/hypr/modules/specials.lua
+++ b/support/hypr/modules/specials.lua
@@ -176,8 +176,11 @@
function M.scratchpad()
return function()
if space(config(), "special").enabled == false then return end
- local shown = hl.get_active_special_workspace()
- local name = shown and (shown.name:gsub("^special:", "")) or "special"
- hl.dispatch(hl.dsp.workspace.toggle_special(name))
+ local shown = hl.get_active_special_workspace()
+ if shown and shown.name == "special:special" then
+ hl.dispatch(hl.dsp.workspace.toggle_special("special"))
+ else
+ hl.dispatch(hl.dsp.focus({ workspace = "special:special" }))
+ end
end
end
'')
(pkgs.writeText "lucid-nonpersistent-workspaces.patch" ''
diff --git a/support/hypr/modules/gestures.lua b/support/hypr/modules/gestures.lua
--- a/support/hypr/modules/gestures.lua
+++ b/support/hypr/modules/gestures.lua
@@ -6,9 +6,5 @@
hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
hl.gesture({ fingers = 4, direction = "horizontal", action = "workspace" })
--- Pre-create workspaces 1-5 so gesture swipes can always live-track to them
-for i = 1, 6 do
- hl.workspace_rule({ workspace = tostring(i), persistent = true })
-end
-- local function minimize_all()
'')
(pkgs.writeText "lucid-lair-search.patch" ''
--- a/luciddocks/Dock.qml
+++ b/luciddocks/Dock.qml
@@ -683,4 +683,8 @@
readonly property var searchEngines: ({
+ "lair": {
+ "name": "Lair",
+ "url": "https://s.lair.moe/web?s="
+ },
"duckduckgo": {
"name": "DuckDuckGo",
"url": "https://duckduckgo.com/?q="
--- a/lucidprefs/LauncherPage.qml
+++ b/lucidprefs/LauncherPage.qml
@@ -386,5 +386,8 @@
current: Prefs.launcherSearchEngine
options: [{
+ "key": "lair",
+ "label": "Lair"
+ }, {
"key": "duckduckgo",
"label": "DuckDuckGo"
}, {
'')
(pkgs.writeText "lucid-shot-special-workspace-name.patch" ''
diff --git a/lucidshot/ShotOverlay.qml b/lucidshot/ShotOverlay.qml
--- a/lucidshot/ShotOverlay.qml
+++ b/lucidshot/ShotOverlay.qml
@@ -228,1 +228,1 @@
- const ws = mon.activeWorkspace ? mon.activeWorkspace.id : null;
+ const ws = mon.activeWorkspace ? mon.activeWorkspace.name : null;
@@ -229,1 +229,1 @@
- const special = mon.specialWorkspace ? mon.specialWorkspace.id : 0;
+ const special = mon.specialWorkspace ? mon.specialWorkspace.name : "";
@@ -234,1 +234,1 @@
- return c.workspace.id === ws || (special !== 0 && c.workspace.id === special) || (c.pinned && c.monitor === mon.id);
+ return c.workspace.name === ws || (special && c.workspace.name === special) || (c.pinned && c.monitor === mon.id);
@@ -246,1 +246,1 @@
- "special": special !== 0 && c.workspace.id === special,
+ "special": !!special && c.workspace.name === special,
'')
];
};
source = pkgs.runCommand "lucid-vaultwarden-launcher-source" {
nativeBuildInputs = [ pkgs.python3 ];
} ''
cp -r ${baseSource} "$out"
chmod -R u+w "$out"
python3 ${vaultwardenLauncherPatch} "$out/luciddocks/LauncherFace.qml" "$out/luciddocks/Dock.qml"
'';
wallpaperDir = "${config.home.homeDirectory}/Wallpapers";
wallpaperScript = "${config.home.homeDirectory}/.config/hypr/scripts/wallpaper/set-wallpaper.sh";
defaultWallpaper = "${wallpaperDir}/bluehour.jpg";
hyprLua = import ../hyprland/lua.nix { inherit lib; };
colors = config.lib.stylix.colors.withHashtag;
paletteFile = pkgs.runCommand "lucid-stylix-palette.json" {
nativeBuildInputs = [ pkgs.python3 ];
} ''
python3 - "$out" ${lib.escapeShellArg config.stylix.base16Scheme} \
${lib.escapeShellArg "${source}/support/lucid"} \
${lib.escapeShellArg config.stylix.accent} <<'PY'
import json
import runpy
import sys
from pathlib import Path
output, scheme_path, scripts, accent_key = sys.argv[1:]
sys.path.insert(0, scripts)
from lucid_palette import at_tone
importer = runpy.run_path(str(Path(scripts) / "add-theme.py"))
scheme = importer["parse_base16"](scheme_path, Path(scheme_path).read_text())
if scheme is None:
raise SystemExit("Stylix scheme is not valid Base16")
bg, fg, accents, hints = importer["to_palette"](scheme)
accent_key = accent_key.lower()
hints["primary"] = scheme["colors"][accent_key]
hints["secondary"] = scheme["colors"]["base0d" if accent_key != "base0d" else "base0e"]
hints["tertiary"] = scheme["colors"]["base0c" if accent_key == "base0b" else "base0b"]
palette = importer["build_palette"](
bg, fg, accents, hints, mode="dark" if importer["is_dark"](bg) else "light"
)
if importer["is_dark"](bg):
colors = scheme["colors"]
# Keep the Base16 dark surface ladder. Lucid's built-in Mocha uses base00
# for its main panels, with base01/base02 as the raised surfaces; the
# generic Material builder starts at base02 and makes the whole shell pale.
background_tone = importer["tone"](bg)
darkest = importer["hx"](at_tone(bg, background_tone - 4))
near_background = importer["hx"](at_tone(bg, background_tone - 2))
palette.update({
"surface": colors["base00"],
"surface_dim": darkest,
"surface_bright": importer["hx"](at_tone(bg, background_tone + 8)),
"surface_container_lowest": darkest,
"surface_container_low": near_background,
"surface_container": colors["base00"],
"surface_container_high": colors["base01"],
"surface_container_highest": colors["base02"],
"surface_variant": colors["base01"],
"on_surface_variant": colors["base04"],
"outline": colors["base03"],
"outline_variant": colors["base01"],
})
Path(output).write_text(json.dumps(palette, indent=2) + "\n")
PY
'';
themeMeta = pkgs.writeText "lucid-stylix-meta.json" (builtins.toJSON {
id = "stylix";
name = "Stylix";
desc = "Active Stylix palette";
swatchBg = colors.base00;
swatchAccent = colors.${config.stylix.accent};
source = "Home Manager";
detected = "base16";
user = true;
});
themeName = pkgs.writeText "lucid-current-theme" "stylix\n";
themeAutoPatch = pkgs.writeText "lucid-theme-auto.patch" ''
--- a/Prefs.qml
+++ b/Prefs.qml
@@ -455,5 +455,12 @@
function setColorMode(mode) {
+ if (mode === "auto") {
+ root.setAutoColorMode(true);
+ return;
+ }
- if ((mode !== "dark" && mode !== "light") || mode === root.colorMode)
+ if (mode !== "dark" && mode !== "light")
return;
+ root.setAutoColorMode(false);
+ if (mode === root.colorMode)
+ return;
// set straight away so the control answers the click; the cache file is
@@ -484,2 +491,4 @@
function setAutoColorMode(on) {
+ if (on && root.currentTheme !== "matugen" && root.currentTheme !== "pywal")
+ return;
root.autoColorMode = on;
@@ -1669,1 +1678,5 @@
- onLoaded: root.currentTheme = text().trim() || "matugen"
+ onLoaded: {
+ root.currentTheme = text().trim() || "matugen";
+ if (root.currentTheme !== "matugen" && root.currentTheme !== "pywal" && root.autoColorMode)
+ root.setAutoColorMode(false);
+ }
--- a/lucidprefs/ThemePage.qml
+++ b/lucidprefs/ThemePage.qml
@@ -11,9 +11,14 @@
readonly property string home: Quickshell.env("HOME")
readonly property string currentTheme: Prefs.currentTheme
- readonly property var modeOptions: [{
+ readonly property bool wallpaperDerivedTheme: page.currentTheme === "matugen" || page.currentTheme === "pywal"
+ readonly property var modeOptions: [{
"key": "dark",
"label": "Dark"
}, {
"key": "light",
"label": "Light"
}]
+ readonly property var autoModeOptions: page.modeOptions.concat([{
+ "key": "auto",
+ "label": "Auto"
+ }])
@@ -22,1 +27,1 @@
- readonly property string modeHint: page.currentTheme === "matugen" || page.currentTheme === "pywal" ? "Re-derives the palette from your wallpaper in the mode you pick. Applications are asked to match." : (page.currentTheme === "colour" ? "Builds the palette from your colour again in the mode you pick. Applications are asked to match." : "Builds a light palette from this theme's own colours. Applications are asked to match.")
+ readonly property string modeHint: page.wallpaperDerivedTheme ? "Auto follows the wallpaper brightness whenever you change the wallpaper. Dark and Light stay fixed until you choose another mode." : (page.currentTheme === "colour" ? "Builds the palette from your colour again in the mode you pick. Applications are asked to match." : "Builds a light palette from this theme's own colours. Applications are asked to match.")
@@ -273,6 +278,6 @@
M3Segmented {
- width: Theme.dp(200)
- current: Prefs.colorMode
- options: page.modeOptions
+ width: Theme.dp(page.wallpaperDerivedTheme ? 300 : 200)
+ current: Prefs.autoColorMode && page.wallpaperDerivedTheme ? "auto" : Prefs.colorMode
+ options: page.wallpaperDerivedTheme ? page.autoModeOptions : page.modeOptions
onChosen: (key) => {
return Prefs.setColorMode(key);
@@ -284,13 +289,0 @@
- SettingRow {
- title: "Follow the wallpaper"
- description: "Picks light for a bright wallpaper and dark for a dark one, every time the wallpaper changes. A hand-picked mode holds until the wallpaper changes next."
-
- M3Switch {
- checked: Prefs.autoColorMode
- onToggled: (v) => {
- return Prefs.setAutoColorMode(v);
- }
- }
-
- }
-
'';
settingsScreenPatch = pkgs.writeText "lucid-settings-screen.patch" ''
--- a/lucidprefs/Settings.qml
+++ b/lucidprefs/Settings.qml
@@ -9,2 +9,4 @@
FloatingWindow {
id: win
+ property var preferredScreen: null
+ screen: win.preferredScreen || Monitors.mainScreen
'';
wallpaperAutoPatch = pkgs.writeText "lucid-wallpaper-auto.patch" ''
--- a/set-wallpaper.sh
+++ b/set-wallpaper.sh
@@ -32,4 +32,5 @@
MODE="''${2:-}"
if [[ -z "$MODE" ]]; then
- if grep -qE '"autoColorMode"[[:space:]]*:[[:space:]]*true' "$HOME/.config/quickshell/lucidprefs/prefs.json" 2>/dev/null; then
+ if [[ "$CURRENT_THEME" == matugen || "$CURRENT_THEME" == pywal ]] && \
+ grep -qE '"autoColorMode"[[:space:]]*:[[:space:]]*true' "$HOME/.config/quickshell/lucidprefs/prefs.json" 2>/dev/null; then
MODE=auto
'';
kdeConnectBridgePython = pkgs.python3.withPackages (ps: [ ps.pygobject3 ]);
applyTheme = pkgs.writeShellScript "lucid-apply-theme" ''
case "''${1:-}" in
--apps-only) exit 0 ;;
--shell-only) shift ;;
esac
exec "$HOME/.config/lucid/apply-theme-upstream.sh" --shell-only "$@"
'';
qmlPackages = with pkgs.qt6; [ qt5compat qtmultimedia ];
tesseract = pkgs.tesseract4.override { enableLanguages = [ "eng" "rus" ]; };
lucid = pkgs.writeShellScript "lucid" ''
export NIXPKGS_QT6_QML_IMPORT_PATH="${lib.makeSearchPath "lib/qt-6/qml" qmlPackages}:''${NIXPKGS_QT6_QML_IMPORT_PATH:-}"
export QT_PLUGIN_PATH="${lib.makeSearchPath "lib/qt-6/plugins" qmlPackages}:''${QT_PLUGIN_PATH:-}"
export PATH="${lib.makeBinPath (with pkgs; [ bash brightnessctl cliphist coreutils curl ffmpeg grim imagemagick jq libnotify pulseaudio kdeConnectBridgePython pywal16 wf-recorder wl-clipboard xdg-utils ])}:${tesseract}/bin:$PATH"
exec ${pkgs.quickshell}/bin/qs --no-duplicate --path "${config.home.homeDirectory}/.config/quickshell"
'';
startWallpaper = pkgs.writeShellScript "lucid-wallpaper-start" ''
export PATH="${lib.makeBinPath (with pkgs; [ awww bash coreutils ffmpeg imagemagick jq python3 pywal16 util-linux ])}:$PATH"
selected="${defaultWallpaper}"
if [ -s "${config.home.homeDirectory}/.cache/current_live_wallpaper" ]; then
saved=$(${pkgs.coreutils}/bin/head -n1 "${config.home.homeDirectory}/.cache/current_live_wallpaper")
if [ -f "$saved" ]; then
selected="$saved"
fi
fi
if [ -s "${config.home.homeDirectory}/.cache/current_wallpaper" ]; then
saved=$(${pkgs.coreutils}/bin/cat "${config.home.homeDirectory}/.cache/current_wallpaper")
if [ "$selected" = "${defaultWallpaper}" ] && [ -f "$saved" ]; then
selected="$saved"
fi
fi
if [ ! -f "$selected" ]; then
echo "Lucid startup wallpaper not found; leaving the current wallpaper unchanged" >&2
exit 0
fi
exec "${wallpaperScript}" "$selected"
'';
in {
options.lucid = {
upstreamSource = lib.mkOption {
type = lib.types.path;
default = source;
description = "Patched Lucid source tree used by profile modules.";
};
keybindSeed = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Optional initial Lucid keybinding file.";
};
prefsSeed = lib.mkOption {
type = lib.types.path;
default = "${source}/defaults/prefs.json";
description = "Initial Lucid preferences file.";
};
widgetSeed = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Optional initial Lucid widget layout.";
};
initialFiles = lib.mkOption {
type = lib.types.attrsOf lib.types.path;
default = {};
description = "Files to seed once at their absolute destinations.";
};
managedFiles = lib.mkOption {
type = lib.types.attrsOf lib.types.path;
default = {};
description = "Lucid files installed at their absolute destinations.";
};
prefsFilter = lib.mkOption {
type = lib.types.lines;
default = ''.barEnabled = true | .widgetOnTop = false | .widgetHideFullscreen = true | .fontFamily = $font | .wallpaperFolder = $wallpaperDir | .liveDecoder = $decoder'';
description = "Declarative jq updates applied to Lucid preferences.";
};
liveDecoder = lib.mkOption {
type = lib.types.str;
default = "vaapi";
description = "Hardware decoder used by Lucid wallpaper playback.";
};
bindsModule = lib.mkOption {
type = lib.types.path;
default = nonRiasBinds;
description = "Lua module that loads this profile's Hyprland bindings.";
};
luaModules = lib.mkOption {
type = lib.types.attrs;
default = {};
description = "Additional Lucid Hyprland Lua modules for this profile.";
};
extraHyprlandConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Profile-specific Hyprland Lua configuration loaded after the shared Lucid setup.";
};
startWallpaper = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Apply the configured wallpaper before Lucid starts.";
};
installWallpaperSupport = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Install Lucid wallpaper integration scripts.";
};
shellFile = lib.mkOption {
type = lib.types.path;
default = ./lucid-shell.qml;
description = "Lucid Quickshell entry point for this user profile.";
};
};
config = {
programs.waybar.enable = lib.mkForce false;
services.mako.enable = lib.mkForce false;
home.packages = with pkgs; [ cliphist ffmpeg grim imagemagick libnotify pywal16 wf-recorder tesseract ];
home.activation.installLucid = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
target="${config.home.homeDirectory}/.config/quickshell"
$DRY_RUN_CMD mkdir -p "$target"
$DRY_RUN_CMD ${pkgs.rsync}/bin/rsync -rltp --chmod=Du+rwx,Fu+rw \
--exclude='/shell.qml' \
--exclude='/LucidBar.qml' \
--exclude='/lucidprefs/prefs.json' \
--exclude='/lucidbar/blur.json' \
--exclude='/lucidbar/clock_reminders.json' \
--exclude='/lucidbar/mpris_shazam.json' \
--exclude='/luciddocks/pinned.json' \
--exclude='/luciddocks/usage.json' \
--exclude='/luciddocks/wallpaper.json' \
--exclude='/lucidmoji/config.json' \
--exclude='/lucidmoji/state.json' \
--exclude='/lucidkeys/state.json' \
--exclude='/lucidwidgets/widgets.json' \
"${source}/" "$target/"
$DRY_RUN_CMD ${pkgs.patch}/bin/patch --batch -p1 -d "$target" < ${./idle-work.patch}
if ! grep -Fq 'mode === "auto"' "$target/Prefs.qml"; then
$DRY_RUN_CMD ${pkgs.patch}/bin/patch --batch --forward -p1 -d "$target" < ${themeAutoPatch}
fi
if ! grep -Fq 'property var preferredScreen: null' "$target/lucidprefs/Settings.qml"; then
$DRY_RUN_CMD ${pkgs.patch}/bin/patch --batch --forward -p1 -d "$target" < ${settingsScreenPatch}
fi
settings_qml="$target/lucidprefs/Settings.qml"
if ! grep -Fq 'win.preferredScreen = Monitors.focusedScreen || Monitors.mainScreen;' "$settings_qml"; then
$DRY_RUN_CMD sed -i '/if (win.visible) {/a\ win.preferredScreen = Monitors.focusedScreen || Monitors.mainScreen;' "$settings_qml"
fi
dock_qml="$target/luciddocks/Dock.qml"
if grep -Fq '/var/lib/snapd/desktop/applications; do' "$dock_qml"; then
$DRY_RUN_CMD sed -i \
's#/var/lib/snapd/desktop/applications; do#/var/lib/snapd/desktop/applications $HOME/.nix-profile/share/applications $HOME/.local/state/nix/profile/share/applications /etc/profiles/per-user/$USER/share/applications /nix/profile/share/applications /nix/var/nix/profiles/default/share/applications /run/current-system/sw/share/applications; do#' \
"$dock_qml"
elif ! grep -Fq '/etc/profiles/per-user/$USER/share/applications; do' "$dock_qml"; then
echo "Lucid launcher application scan pattern changed; refusing to skip Nix profiles" >&2
exit 1
fi
keyboard_focus='WlrLayershell.keyboardFocus: dockWindow.menuOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None'
keyboard_focus_fixed='WlrLayershell.keyboardFocus: dockWindow.menuOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None'
if grep -Fq "$keyboard_focus" "$dock_qml"; then
$DRY_RUN_CMD sed -i \
's/WlrLayershell.keyboardFocus: dockWindow.menuOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None/WlrLayershell.keyboardFocus: dockWindow.menuOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None/' \
"$dock_qml"
elif ! grep -Fq "$keyboard_focus_fixed" "$dock_qml"; then
echo "Lucid launcher keyboard focus pattern changed; refusing to skip focus fix" >&2
exit 1
fi
if ! grep -Fq 'Nix focus workaround for Hyprland 0.57' "$dock_qml"; then
if [ "$(grep -Fxc ' onMenuOpenChanged: {' "$dock_qml")" -ne 1 ]; then
echo "Lucid menu-open handler changed; refusing to skip focus workaround" >&2
exit 1
fi
$DRY_RUN_CMD sed -i '/ onMenuOpenChanged: {/a\
// Nix focus workaround for Hyprland 0.57: detach pointer and keyboard focus while menus are open.\
Quickshell.execDetached(["hyprctl", "eval", "hl.config({ input = { follow_mouse = " + (dockWindow.menuOpen ? 2 : 1) + " } })"]);' "$dock_qml"
fi
$DRY_RUN_CMD install -m644 ${config.lucid.shellFile} "$target/shell.qml"
$DRY_RUN_CMD install -m644 ${./LucidBar.qml} "$target/LucidBar.qml"
$DRY_RUN_CMD install -m644 ${./Vaultwarden.qml} "$target/Vaultwarden.qml"
$DRY_RUN_CMD install -m644 ${./WidgetLayer.qml} "$target/lucidwidgets/WidgetLayer.qml"
$DRY_RUN_CMD install -m644 ${./WidgetsPage.qml} "$target/lucidprefs/WidgetsPage.qml"
${lib.optionalString config.lucid.installWallpaperSupport ''
$DRY_RUN_CMD mkdir -p "${wallpaperDir}" \
"${config.home.homeDirectory}/.config/hypr/scripts/wallpaper"
$DRY_RUN_CMD install -m755 "${source}/support/wallpaper/set-wallpaper.sh" \
"${wallpaperScript}"
if ! grep -Fq 'CURRENT_THEME" == matugen' "${wallpaperScript}"; then
$DRY_RUN_CMD ${pkgs.patch}/bin/patch --batch --forward -p1 -d "$(dirname ${lib.escapeShellArg wallpaperScript})" < ${wallpaperAutoPatch}
fi
''}
lucid_dir="${config.home.homeDirectory}/.config/lucid"
$DRY_RUN_CMD mkdir -p "$lucid_dir/themes"
if [ ! -s "$lucid_dir/keybinds.json" ]; then
${lib.optionalString (config.lucid.keybindSeed != null) ''$DRY_RUN_CMD install -m644 ${config.lucid.keybindSeed} "$lucid_dir/keybinds.json"''}
fi
$DRY_RUN_CMD ${pkgs.rsync}/bin/rsync -rltp --chmod=Du+rwx,Fu+rw --ignore-existing \
"${source}/support/lucid/themes/" "$lucid_dir/themes/"
for script in palette-edit.py lucid_palette.py add-theme.py scheme-gallery.py gen-light-palette.py; do
$DRY_RUN_CMD install -m644 "${source}/support/lucid/$script" "$lucid_dir/$script"
done
for script in gen-pywal-palette.py set-mode.sh wallpaper-mode.sh; do
$DRY_RUN_CMD install -m755 "${source}/support/lucid/$script" "$lucid_dir/$script"
done
$DRY_RUN_CMD install -m755 "${source}/support/lucid/apply-theme.sh" \
"$lucid_dir/apply-theme-upstream.sh"
$DRY_RUN_CMD install -m755 ${applyTheme} \
"$lucid_dir/apply-theme.sh"
if [ ! -e "$target/lucidprefs/prefs.json" ]; then
$DRY_RUN_CMD install -m644 ${config.lucid.prefsSeed} "$target/lucidprefs/prefs.json"
fi
if [ -z "''${DRY_RUN_CMD:-}" ] && [ -f "$target/lucidprefs/prefs.json" ]; then
prefs_tmp=$(${pkgs.coreutils}/bin/mktemp "$target/lucidprefs/prefs.json.XXXXXX")
if ${pkgs.jq}/bin/jq --arg font ${lib.escapeShellArg config.stylix.fonts.sansSerif.name} \
--arg wallpaperDir ${lib.escapeShellArg wallpaperDir} \
--arg decoder ${lib.escapeShellArg config.lucid.liveDecoder} \
${lib.escapeShellArg config.lucid.prefsFilter} \
"$target/lucidprefs/prefs.json" > "$prefs_tmp"; then
${pkgs.coreutils}/bin/chmod 644 "$prefs_tmp"
${pkgs.coreutils}/bin/mv "$prefs_tmp" "$target/lucidprefs/prefs.json"
else
${pkgs.coreutils}/bin/rm -f "$prefs_tmp"
exit 1
fi
fi
${lib.optionalString (config.lucid.widgetSeed != null) ''
if [ ! -e "$target/lucidwidgets/widgets.json" ]; then
$DRY_RUN_CMD install -m644 ${config.lucid.widgetSeed} "$target/lucidwidgets/widgets.json"
fi
''}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (destination: sourceFile: ''
if [ ! -e ${lib.escapeShellArg destination} ]; then
$DRY_RUN_CMD mkdir -p ${lib.escapeShellArg (builtins.dirOf destination)}
$DRY_RUN_CMD install -m644 ${sourceFile} ${lib.escapeShellArg destination}
fi
'') config.lucid.initialFiles)}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (destination: sourceFile: ''
$DRY_RUN_CMD mkdir -p ${lib.escapeShellArg (builtins.dirOf destination)}
$DRY_RUN_CMD install -m644 ${sourceFile} ${lib.escapeShellArg destination}
'') config.lucid.managedFiles)}
for pair in \
'blur.json lucidbar/blur.json' \
'clock_reminders.json lucidbar/clock_reminders.json' \
'mpris_shazam.json lucidbar/mpris_shazam.json' \
'usage.json luciddocks/usage.json' \
'wallpaper.json luciddocks/wallpaper.json' \
'moji-config.json lucidmoji/config.json' \
'moji-state.json lucidmoji/state.json' \
'keys-state.json lucidkeys/state.json'; do
set -- $pair
if [ ! -e "$target/$2" ]; then
$DRY_RUN_CMD install -m644 "${source}/defaults/$1" "$target/$2"
fi
done
$DRY_RUN_CMD mkdir -p "${config.home.homeDirectory}/.config/lucid/themes/stylix" \
"${config.home.homeDirectory}/.cache/quickshell"
$DRY_RUN_CMD install -m644 ${paletteFile} \
"${config.home.homeDirectory}/.config/lucid/themes/stylix/quickshell.json"
$DRY_RUN_CMD install -m644 ${themeMeta} \
"${config.home.homeDirectory}/.config/lucid/themes/stylix/meta.json"
if [ ! -e "${config.home.homeDirectory}/.cache/current_theme" ]; then
$DRY_RUN_CMD install -m644 ${themeName} \
"${config.home.homeDirectory}/.cache/current_theme"
fi
if [ ! -e "${config.home.homeDirectory}/.cache/current_theme" ] || \
[ "$(${pkgs.coreutils}/bin/cat "${config.home.homeDirectory}/.cache/current_theme")" = stylix ]; then
$DRY_RUN_CMD install -m644 ${paletteFile} \
"${config.home.homeDirectory}/.cache/quickshell/matugen.json"
fi
'';
systemd.user.services.lucid = {
Unit = {
Description = "Lucid bar, dock and desktop widgets";
After = [ "graphical-session.target" ];
PartOf = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${lucid}";
Restart = "on-failure";
RestartSec = 2;
} // lib.optionalAttrs config.lucid.startWallpaper { ExecStartPre = "${startWallpaper}"; };
Install.WantedBy = [ "hyprland-session.target" ];
};
wayland.windowManager.hyprland = {
settings = {
on = [ (hyprLua.onStart [ "systemctl --user start lucid.service" ]) ];
};
# Home Manager autoloads these before its generated settings. Lucid's
# stateful UI modules are installed too, then required after the Nix base
# config below so saved UI choices take effect without losing that base.
extraLuaFiles = {
"modules/binds.lua" = config.lucid.bindsModule;
"modules/json.lua" = builtins.toPath "${source}/support/hypr/modules/json.lua";
"modules/specials.lua" = {
content = builtins.toPath "${source}/support/hypr/modules/specials.lua";
autoLoad = false;
};
} // config.lucid.luaModules;
# Nix owns session startup and environment exports; don't load Lucid's
# generic autostart/env defaults on top of the existing host setup.
extraConfig = ''
local config_home = os.getenv("XDG_CONFIG_HOME") or ${builtins.toJSON config.xdg.configHome}
package.path = config_home .. "/hypr/?.lua;" .. config_home .. "/hypr/?/init.lua;" .. package.path
require("modules.specials")
LucidSpecials.apply()
'' + config.lucid.extraHyprlandConfig;
};
};
}
+253
View File
@@ -0,0 +1,253 @@
--- a/lucidbar/Clock.qml
+++ b/lucidbar/Clock.qml
@@ -94,7 +94,7 @@
Timer {
interval: 1000
repeat: true
- running: true
+ running: root.visible && root.hostWindow !== null && root.hostWindow.visible
triggeredOnStart: true
onTriggered: root.now = Loc.now()
}
@@ -477,36 +477,7 @@
color: Theme.accent
transformOrigin: Item.Top
- SequentialAnimation on rotation {
- loops: Animation.Infinite
- running: bell.visible
-
- NumberAnimation {
- from: 0
- to: -14
- duration: Theme.barMs(100)
- easing.type: Easing.OutQuad
- }
-
- NumberAnimation {
- from: -14
- to: 14
- duration: Theme.barMs(160)
- easing.type: Easing.InOutQuad
- }
-
- NumberAnimation {
- from: 14
- to: 0
- duration: Theme.barMs(140)
- easing.type: Easing.OutQuad
- }
-
- PauseAnimation {
- duration: Theme.barMs(2800)
- }
-
- }
+
}
--- a/lucidbar/Mpris.qml
+++ b/lucidbar/Mpris.qml
@@ -649,7 +649,7 @@
// media card, both closed most of the time. a per-frame interpolation behind
// a closed panel measured ~18% of a core, so it waits until something shows it
FrameAnimation {
- running: root.isPlaying && !trackHitArea.dragging && (root.expanded || root.posWanted)
+ running: root.visible && root.hostWindow !== null && root.hostWindow.visible && root.isPlaying && !trackHitArea.dragging && (root.expanded || root.posWanted)
onTriggered: root.livePosSec = Math.min(root.lenSec, root.posBase + (Date.now() - root.posTimestamp) / 1000)
}
@@ -731,7 +731,7 @@
// silence costs the same as sound to capture and parse, so only listen
// while the pill's bars can dance or the panel is open on the waveform
- running: (root.isPlaying && compactBars.visible) || root.expanded
+ running: root.visible && root.hostWindow !== null && root.hostWindow.visible && ((root.isPlaying && compactBars.visible) || root.expanded)
command: ["cava", "-p", Quickshell.env('HOME') + "/.config/cava/quickshell.conf"]
onRunningChanged: {
if (!cavaProc.running && root.bars.length > 0)
@@ -948,7 +948,7 @@
to: 360
duration: 14000
loops: Animation.Infinite
- running: root.isPlaying && compactArt.visible
+ running: root.isPlaying && compactArt.visible && root.hostWindow !== null && root.hostWindow.visible
}
}
@@ -2906,7 +2906,7 @@
readonly property real segmentWidth: measure.implicitWidth
readonly property real naturalWidth: bare.implicitWidth
readonly property bool overflowing: mq.naturalWidth > mq.width
- readonly property bool animating: mq.scrolling && mq.overflowing
+ readonly property bool animating: mq.scrolling && mq.overflowing && mq.visible && mq.opacity > 0
property real currentX: 0
function updateScroll() {
--- a/lucidwidgets/WidgetFrame.qml
+++ b/lucidwidgets/WidgetFrame.qml
@@ -4,6 +4,8 @@
Item {
id: frame
+
+ property bool updatesEnabled: true
required property string uid
required property string wtype
--- a/lucidwidgets/WidgetBody.qml
+++ b/lucidwidgets/WidgetBody.qml
@@ -6,6 +6,7 @@
// set by the frame once the loader is ready
property var host: null
+ readonly property bool updatesEnabled: !body.host || (body.host.updatesEnabled !== false && body.host.onThisScreen !== false)
// a variant that draws straight onto the wallpaper with no container behind it
property bool bare: false
// a body with nothing worth showing right now: the frame fades the whole card
--- a/lucidwidgets/ClockWidget.qml
+++ b/lucidwidgets/ClockWidget.qml
@@ -43,7 +43,7 @@
Timer {
interval: 1000
repeat: true
- running: true
+ running: w.updatesEnabled && w.visible
triggeredOnStart: true
onTriggered: w.now = Loc.now()
}
--- a/lucidwidgets/SystemWidget.qml
+++ b/lucidwidgets/SystemWidget.qml
@@ -127,17 +127,23 @@
return w.inkAccent;
}
- Component.onCompleted: {
- if (!w.preview)
- Sys.hold(w.uid || "w", true, w.pollMs);
-
- }
- Component.onDestruction: Sys.hold(w.uid || "w", false)
- onPollMsChanged: {
- if (!w.preview)
- Sys.hold(w.uid || "w", true, w.pollMs);
-
- }
+ property string pollingTag: ""
+
+ function syncPolling() {
+ const tag = w.uid;
+ if (w.pollingTag !== "" && w.pollingTag !== tag)
+ Sys.hold(w.pollingTag, false);
+ w.pollingTag = tag;
+ if (tag !== "")
+ Sys.hold(tag, !w.preview && w.updatesEnabled, w.pollMs);
+ }
+
+ Component.onCompleted: w.syncPolling()
+ Component.onDestruction: Sys.hold(w.pollingTag, false)
+ onUidChanged: w.syncPolling()
+ onPollMsChanged: w.syncPolling()
+ onUpdatesEnabledChanged: w.syncPolling()
+ onPreviewChanged: w.syncPolling()
// rings: one thick m3 ring per reading, the icon inside, the number under it
Row {
--- a/lucidwidgets/FetchWidget.qml
+++ b/lucidwidgets/FetchWidget.qml
@@ -49,12 +49,22 @@
readonly property var swatches: [Theme.primary, Theme.secondary, Theme.tertiary, Theme.error, Theme.primaryContainer, Theme.secondaryContainer, Theme.tertiaryContainer, Theme.surfaceHighest]
defaultTone: "surface"
- Component.onCompleted: {
- if (!w.preview)
- Sys.hold("fetch-" + w.uid, true, 5000);
-
- }
- Component.onDestruction: Sys.hold("fetch-" + w.uid, false)
+ property string pollingTag: ""
+
+ function syncPolling() {
+ const tag = w.uid === "" ? "" : "fetch-" + w.uid;
+ if (w.pollingTag !== "" && w.pollingTag !== tag)
+ Sys.hold(w.pollingTag, false);
+ w.pollingTag = tag;
+ if (tag !== "")
+ Sys.hold(tag, !w.preview && w.updatesEnabled, 5000);
+ }
+
+ Component.onCompleted: w.syncPolling()
+ Component.onDestruction: Sys.hold(w.pollingTag, false)
+ onUidChanged: w.syncPolling()
+ onUpdatesEnabledChanged: w.syncPolling()
+ onPreviewChanged: w.syncPolling()
// card: the mark and who you are, then a labelled list
Item {
--- a/lucidwidgets/VisualiserWidget.qml
+++ b/lucidwidgets/VisualiserWidget.qml
@@ -50,6 +50,11 @@
if (w.preview || w.uid === "")
return ;
+ if (!w.updatesEnabled) {
+ Cava.drop(w.filed);
+ w.filed = "";
+ return;
+ }
Cava.want(w.uid, w.barCount);
w.filed = w.uid;
}
@@ -124,6 +129,7 @@
onWantCountChanged: w.refreshCount()
onResizingChanged: w.refreshCount()
onBarCountChanged: w.file()
+ onUpdatesEnabledChanged: w.file()
onUidChanged: {
if (w.filed !== "" && w.filed !== w.uid)
Cava.drop(w.filed);
@@ -158,7 +164,7 @@
Timer {
interval: 55
repeat: true
- running: w.preview
+ running: w.preview && w.updatesEnabled && w.visible
onTriggered: w.phase += 0.055
}
--- a/lucidprefs/ThemePage.qml
+++ b/lucidprefs/ThemePage.qml
@@ -770,6 +770,9 @@
"key": "any",
"label": "Any window"
}, {
+ "key": "non-float",
+ "label": "Non-float"
+ }, {
"key": "fullscreen",
"label": "Fullscreen"
}, {
--- a/LiveWall.qml
+++ b/LiveWall.qml
@@ -42,13 +42,18 @@
const out = {};
const mons = Hyprland.monitors.values;
for (let i = 0; i < mons.length; i++) {
const ws = mons[i].activeWorkspace;
- const windows = ws ? ws.toplevels.values.length : 0;
+ const windows = ws ? ws.toplevels.values : [];
let still = Lockscreen.locked || (root.gameMode && Prefs.liveGameMode === "pause");
if (Prefs.livePauseUnder === "any")
- still = still || windows > 0;
+ still = still || windows.length > 0;
+ else if (Prefs.livePauseUnder === "non-float")
+ still = still || windows.some((t) => {
+ const o = t && t.lastIpcObject;
+ return o && o.mapped !== false && o.hidden !== true && o.floating !== true;
+ });
else if (Prefs.livePauseUnder === "fullscreen")
still = still || (ws !== null && ws.hasFullscreen);
out[mons[i].name] = !still;
}
@@ -0,0 +1,483 @@
{
"version": 1,
"binds": [
{
"id": "wallpaper-picker",
"keys": "SUPER + W",
"desc": "Wallpaper picker",
"category": "Lucid"
},
{
"id": "wallpaper-random",
"keys": "SUPER + SHIFT + W",
"desc": "Set a random wallpaper",
"category": "Desktop"
},
{
"id": "calculator-volume",
"keys": "XF86Calculator",
"desc": "Open volume mixer",
"category": "Apps"
},
{
"id": "zoom-reset-scroll-up",
"keys": "SUPER + SHIFT + mouse_up",
"desc": "Reset cursor zoom",
"category": "Accessibility"
},
{
"id": "zoom-reset-scroll-down",
"keys": "SUPER + SHIFT + mouse_down",
"desc": "Reset cursor zoom",
"category": "Accessibility"
},
{
"id": "zoom-reset-minus",
"keys": "SUPER + SHIFT + minus",
"desc": "Reset cursor zoom",
"category": "Accessibility"
},
{
"id": "zoom-reset-kp-subtract",
"keys": "SUPER + SHIFT + KP_SUBTRACT",
"desc": "Reset cursor zoom",
"category": "Accessibility"
},
{
"id": "zoom-reset-zero",
"keys": "SUPER + SHIFT + 0",
"desc": "Reset cursor zoom",
"category": "Accessibility"
},
{
"id": "zoom-in-equal",
"keys": "SUPER + equal",
"desc": "Zoom in",
"category": "Accessibility",
"opts": {
"repeating": true
}
},
{
"id": "zoom-out-minus",
"keys": "SUPER + minus",
"desc": "Zoom out",
"category": "Accessibility",
"opts": {
"repeating": true
}
},
{
"id": "zoom-in-kp-add",
"keys": "SUPER + KP_ADD",
"desc": "Zoom in",
"category": "Accessibility",
"opts": {
"repeating": true
}
},
{
"id": "zoom-out-kp-subtract",
"keys": "SUPER + KP_SUBTRACT",
"desc": "Zoom out",
"category": "Accessibility",
"opts": {
"repeating": true
}
},
{
"id": "widgets-panel",
"keys": "SUPER + SHIFT + G",
"desc": "Toggle widgets panel",
"category": "Lucid"
},
{
"id": "launcher",
"keys": "SUPER + D",
"desc": "App launcher",
"category": "Lucid"
},
{
"id": "vaultwarden-password",
"keys": "SUPER + X",
"desc": "Type a password from Vaultwarden",
"category": "Apps"
},
{
"id": "clipse",
"keys": "SUPER + V",
"desc": "Clipboard manager",
"category": "Apps"
},
{
"id": "clipboard",
"keys": "SUPER + SHIFT + V",
"desc": "Clipboard history",
"category": "Lucid"
},
{
"id": "screenshot",
"keys": "Print",
"desc": "Full screenshot",
"category": "Screenshots"
},
{
"id": "screenshot-full",
"keys": "SUPER + Print",
"desc": "Full screenshot",
"category": "Screenshots"
},
{
"id": "screenshot-region",
"keys": "CTRL + Print",
"desc": "Region screenshot",
"category": "Screenshots"
},
{
"id": "screenshot-window",
"keys": "ALT + Print",
"desc": "Window screenshot",
"category": "Screenshots"
},
{
"id": "terminal",
"keys": "SUPER + RETURN",
"desc": "Terminal",
"category": "Apps"
},
{
"id": "close-window",
"keys": "SUPER + Q",
"desc": "Close window",
"category": "Windows"
},
{
"id": "exit-hyprland",
"keys": "SUPER + M",
"desc": "Exit Hyprland",
"category": "Session"
},
{
"id": "file-manager",
"keys": "SUPER + E",
"desc": "File manager",
"category": "Apps"
},
{
"id": "toggle-float",
"keys": "SUPER + F",
"desc": "Toggle floating window",
"category": "Windows"
},
{
"id": "toggle-pseudo",
"keys": "SUPER + P",
"desc": "Toggle pseudo-tile",
"category": "Windows"
},
{
"id": "toggle-split",
"keys": "SUPER + J",
"desc": "Toggle split direction",
"category": "Windows"
},
{
"id": "swap-split",
"keys": "SUPER + K",
"desc": "Swap split direction",
"category": "Windows"
},
{
"id": "focus-left",
"keys": "SUPER + LEFT",
"desc": "Focus left",
"category": "Windows"
},
{
"id": "focus-right",
"keys": "SUPER + RIGHT",
"desc": "Focus right",
"category": "Windows"
},
{
"id": "focus-up",
"keys": "SUPER + UP",
"desc": "Focus up",
"category": "Windows"
},
{
"id": "focus-down",
"keys": "SUPER + DOWN",
"desc": "Focus down",
"category": "Windows"
},
{
"id": "swap-left",
"keys": "SUPER + SHIFT + LEFT",
"desc": "Move window left",
"category": "Windows"
},
{
"id": "swap-right",
"keys": "SUPER + SHIFT + RIGHT",
"desc": "Move window right",
"category": "Windows"
},
{
"id": "swap-up",
"keys": "SUPER + SHIFT + UP",
"desc": "Move window up",
"category": "Windows"
},
{
"id": "swap-down",
"keys": "SUPER + SHIFT + DOWN",
"desc": "Move window down",
"category": "Windows"
},
{
"id": "resize-left",
"keys": "SUPER + CTRL + LEFT",
"desc": "Resize window left",
"category": "Windows"
},
{
"id": "resize-right",
"keys": "SUPER + CTRL + RIGHT",
"desc": "Resize window right",
"category": "Windows"
},
{
"id": "resize-up",
"keys": "SUPER + CTRL + UP",
"desc": "Resize window up",
"category": "Windows"
},
{
"id": "resize-down",
"keys": "SUPER + CTRL + DOWN",
"desc": "Resize window down",
"category": "Windows"
},
{
"id": "workspace-focus",
"keys": "SUPER + {key}",
"desc": "Focus workspace {n}",
"category": "Workspaces",
"each": "workspace"
},
{
"id": "workspace-move",
"keys": "SUPER + SHIFT + {key}",
"desc": "Move window to workspace {n}",
"category": "Workspaces",
"each": "workspace"
},
{
"id": "scratchpad",
"keys": "SUPER + ALT + P",
"desc": "Toggle scratchpad",
"category": "Workspaces",
"type": "lua",
"lua": "specials.scratchpad()"
},
{
"id": "scratchpad-stash",
"keys": "SUPER + ALT + S",
"desc": "Stash or restore the focused window",
"category": "Workspaces",
"type": "lua",
"lua": "specials.stash()"
},
{
"id": "special-music",
"keys": "SUPER + ALT + M",
"desc": "Toggle Music workspace",
"category": "Workspaces",
"type": "lua",
"lua": "specials.toggle('music')"
},
{
"id": "special-comms",
"keys": "SUPER + ALT + C",
"desc": "Toggle Comms workspace",
"category": "Workspaces",
"type": "lua",
"lua": "specials.toggle('comms')"
},
{
"id": "special-todo",
"keys": "SUPER + ALT + T",
"desc": "Toggle To-do workspace",
"category": "Workspaces",
"type": "lua",
"lua": "specials.toggle('todo')"
},
{
"id": "special-sysmon",
"keys": "SUPER + ALT + B",
"desc": "Toggle system monitor workspace",
"category": "Workspaces",
"type": "lua",
"lua": "specials.toggle('sysmon')"
},
{
"id": "daemons-workspace",
"keys": "SUPER + ALT + D",
"desc": "Daemons workspace",
"category": "Workspaces",
"type": "lua",
"lua": "specials.toggle('daemons')"
},
{
"id": "fullscreen",
"keys": "SUPER + SHIFT + F",
"desc": "Toggle fullscreen",
"category": "Windows"
},
{
"id": "workspace-next-scroll",
"keys": "SUPER + mouse_down",
"desc": "Next workspace",
"category": "Workspaces"
},
{
"id": "workspace-previous-scroll",
"keys": "SUPER + mouse_up",
"desc": "Previous workspace",
"category": "Workspaces"
},
{
"id": "keyboard-brightness-up",
"keys": "SUPER + F3",
"desc": "Keyboard backlight up",
"category": "Displays"
},
{
"id": "keyboard-brightness-down",
"keys": "SUPER + F2",
"desc": "Keyboard backlight down",
"category": "Displays"
},
{
"id": "audio-mute",
"keys": "XF86AudioMute",
"desc": "Toggle audio mute",
"category": "Media"
},
{
"id": "microphone-mute",
"keys": "XF86AudioMicMute",
"desc": "Toggle microphone mute",
"category": "Media"
},
{
"id": "play-pause",
"keys": "XF86AudioPlay",
"desc": "Play or pause media",
"category": "Media"
},
{
"id": "file-manager-media",
"keys": "XF86Explorer",
"desc": "File manager",
"category": "Apps"
},
{
"id": "email",
"keys": "XF86Mail",
"desc": "Email",
"category": "Apps"
},
{
"id": "browser",
"keys": "XF86WWW",
"desc": "Web browser",
"category": "Apps"
},
{
"id": "display-brightness-down",
"keys": "XF86MonBrightnessDown",
"desc": "Brightness down",
"category": "Displays"
},
{
"id": "display-brightness-up",
"keys": "XF86MonBrightnessUp",
"desc": "Brightness up",
"category": "Displays"
},
{
"id": "volume-up",
"keys": "XF86AudioRaiseVolume",
"desc": "Volume up",
"category": "Media",
"opts": {
"repeating": true
}
},
{
"id": "volume-down",
"keys": "XF86AudioLowerVolume",
"desc": "Volume down",
"category": "Media",
"opts": {
"repeating": true
}
},
{
"id": "previous-seek",
"keys": "XF86AudioPrev",
"desc": "Seek backward",
"category": "Media",
"opts": {
"click": true
}
},
{
"id": "next-seek",
"keys": "XF86AudioNext",
"desc": "Seek forward",
"category": "Media",
"opts": {
"click": true
}
},
{
"id": "previous-track",
"keys": "XF86AudioPrev",
"desc": "Previous track",
"category": "Media",
"opts": {
"long_press": true
}
},
{
"id": "next-track",
"keys": "XF86AudioNext",
"desc": "Next track",
"category": "Media",
"opts": {
"long_press": true
}
},
{
"id": "move-window-drag",
"keys": "SUPER + mouse:272",
"desc": "Move window with mouse",
"category": "Windows",
"opts": {
"mouse": true
}
},
{
"id": "resize-window-drag",
"keys": "SUPER + mouse:273",
"desc": "Resize window with mouse",
"category": "Windows",
"opts": {
"mouse": true
}
}
]
}
+149
View File
@@ -0,0 +1,149 @@
import "./luciddocks"
import "./lucidedit"
import "./lucidlock"
import "./lucidosd"
import "./lucidprefs"
import "./lucidshot"
import "./lucidwidgets"
import QtQuick
import Quickshell
import qs
ShellRoot {
id: root
Component.onCompleted: {
void Specials.moduleProbed;
void Clip.probed;
void Capture.state;
}
function announceShot(entry) {
if (Prefs.shotPreview === "preview") {
shotCards.show(entry);
return;
}
if (Prefs.shotPreview !== "notify")
return;
const where = String(entry.file).replace(Quickshell.env("HOME"), "~");
if (entry.kind === "video")
Quickshell.execDetached(["notify-send", "-a", "LucidShot", "-i", "media-record", "Recording saved", where]);
else
Quickshell.execDetached(["sh", "-c", "a=$(notify-send -a LucidShot -i \"$1\" -A 'edit=Mark up' -A 'open=Open' --wait \"$2\" \"$3\"); case \"$a\" in edit) qs ipc call snap edit \"$1\" ;; open) xdg-open \"$1\" ;; esac; true", "sh", entry.file, entry.temp ? "Screenshot copied" : "Screenshot saved", entry.temp ? "It is on the clipboard only" : where]);
}
ShotEngine {
id: shotEngine
cards: shotCards
onReady: (entry) => {
if (entry.owner === "overlay") {
shotCards.update(entry);
return;
}
if (entry.owner === "pin")
return;
root.announceShot(entry);
}
onFailed: (id, why) => toastMod.popup("warning", why, true)
onTextRead: (r) => {
if (!shotEngine.claim(r.id))
return;
if (r.status === "ok") {
if (Prefs.shotPreview === "preview")
shotCards.show({ "id": r.id, "kind": "text", "text": r.text, "codes": r.codes, "screen": Monitors.focusedScreen ? Monitors.focusedScreen.name : "" });
else
toastMod.popup("copy", "Text copied", false);
} else if (r.status === "notool") {
toastMod.popup("alert", "Install tesseract to copy text", true);
} else {
toastMod.popup("alert", "No text found", true);
}
}
}
ShotOverlay {
id: shotOverlay
engine: shotEngine
cards: shotCards
pins: shotPins
onNotify: (icon, text, warn) => toastMod.popup(icon, text, warn)
onSwatch: (hex, text) => toastMod.popupSwatch(hex, text)
onFreezeFailed: (why) => toastMod.popup("alert", "Couldn't capture the screen", true)
}
ShotCards {
id: shotCards
engine: shotEngine
pins: shotPins
overlay: shotOverlay
}
PinBoard {
id: shotPins
engine: shotEngine
}
RecordHud {}
Connections {
target: Capture
function onSaved(file, seconds) {
root.announceShot({ "id": "rec-" + Date.now(), "kind": "video", "file": file, "duration": seconds, "screen": Capture.output });
}
function onFailed(why) {
toastMod.popup("warning", why, true);
}
}
LucidBar {
id: barRoot
dock: dockRoot
toastMod: toastMod
}
Dock {
id: dockRoot
}
Toast {
id: toastMod
}
Osd {
}
ToastEvents {
toast: toastMod
}
Binding {
target: toastMod
property: "screen"
value: Monitors.shellPlacement
when: Monitors.shellPlacement !== null
}
WidgetLayer {
}
WidgetIpc {
}
Lock {
}
EditLayer {
}
SettingsHost {
}
DisplayTrial {
}
Binding {
target: Monitors
property: "barWindows"
value: barRoot.instances
}
}
+2 -2
View File
@@ -188,7 +188,7 @@ in {
'';
};
wayland.windowManager.hyprland.settings.bind = [
(hyprLua.bind "SUPER + D" (hyprLua.exec "walker"))
lucidHyprlandBinds = [
(hyprLua.bind "SUPER + D" (hyprLua.execLauncher "qs ipc call launcher toggle"))
];
}