From 8b7101e5331e17d121fa790b7469747b853c976f Mon Sep 17 00:00:00 2001 From: Sweetbread Date: Mon, 2 Nov 2026 20:29:19 +0300 Subject: [PATCH] Install lucid --- modules/user/LucidBar.qml | 1091 +++++++++++++++++++++++++++++ modules/user/lucid-pinned.json | 3 + modules/user/lucid-prefs.json | 11 + modules/user/lucid-shell.qml | 36 + modules/user/lucid-widgets.json | 20 + user/sweetbread/home.nix | 2 + user/sweetbread/modules/lucid.nix | 159 +++++ 7 files changed, 1322 insertions(+) create mode 100644 modules/user/LucidBar.qml create mode 100644 modules/user/lucid-pinned.json create mode 100644 modules/user/lucid-prefs.json create mode 100644 modules/user/lucid-shell.qml create mode 100644 modules/user/lucid-widgets.json create mode 100644 user/sweetbread/modules/lucid.nix diff --git a/modules/user/LucidBar.qml b/modules/user/LucidBar.qml new file mode 100644 index 0000000..37ed856 --- /dev/null +++ b/modules/user/LucidBar.qml @@ -0,0 +1,1091 @@ +// Lucid bar from Sn3akyy1/lucid at bdd7f79da18efd837f79dcecbe5d2ec178abc10c (MIT). +// Kept as a separate component so the other Lucid surfaces stay optional. +import "./lucidbar" +import "./lucidedit" +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import qs + + Variants { + id: bars + property var dock + property var toastMod: null + + model: Monitors.barEverywhere ? Quickshell.screens : [""] + + PanelWindow { + id: bar + + // a display from Quickshell.screens with a bar on every one, or "" for the one bar + required property var modelData + // the bar that speaks for the shell: it alone turns into notification + // popups, raises the privacy toast and answers the modules' ipc + readonly property bool primary: !Monitors.barEverywhere || bar.screen === Monitors.popupScreen + // what the shell's ipc targets reach on the main bar + readonly property var clockModule: clockMod + readonly property var mprisModule: mprisMod + readonly property var workspacesModule: workspacesMod + + PaletteFade { + active: bar.visible + } + + // the display Settings > Displays picks, or this one's own. gated, + // because unset must leave the choice to hyprland, which null would not + Binding { + target: bar + property: "screen" + value: bar.modelData !== "" ? bar.modelData : Monitors.barPlacement + when: bar.modelData !== "" || Monitors.barPlacement !== null + } + + visible: Prefs.loaded && Prefs.barEnabled && Monitors.surfacesUp + property bool laidOut: false + // stand-ins take room in edit mode, so this asks after the modules themselves + readonly property bool anyModuleShown: Prefs.barZones.some((zone) => { + return bar.zoneKeys(zone).some((key) => { + return bar.realWidthOf(key) > 0.5; + }); + }) + // edit mode: the module in hand, how far it sits off its slot, and the + // arrangement the bar shows while it is held + property real editT: Edit.active ? 1 : 0 + // in edit mode the modules part a little, so each gap has room for its clasp + readonly property real editGap: Theme.dp(16) * bar.editT + property string dragKey: "" + property real dragShift: 0 + property real dragLift: 0 + property var dragGroups: null + property bool dragHeld: false + + Behavior on editT { + NumberAnimation { + duration: Theme.barMs(320) + easing.type: Easing.OutCubic + } + + } + + function xOf(key) { + return bar.placeFor(key) + (key === bar.dragKey ? bar.dragShift : 0); + } + + function liftOf(key) { + return key === bar.dragKey ? bar.dragLift : 0; + } + + // a right click on a module: its sheet, on the side of the screen away from it + function editModule(key) { + const mod = bar.pillFor[key]; + const cx = bar.xOf(key) + (mod ? mod.width : 0) / 2; + Edit.openSheet("module", key, Edit.sideFor(cx, bar.width), bar.screen ? bar.screen.name : ""); + } + + // the sheet's open button: this bar's module opens as a click would open it + Connections { + function onModuleOpenRequested(id, screen) { + if (screen !== "" ? (!bar.screen || bar.screen.name !== screen) : !bar.primary) + return ; + + const mod = bar.pillFor[id]; + if (!mod) + return ; + + if (id === "start") { + dock.openLauncher(""); + } else if (id === "media") { + mod.openPanel("player"); + } else { + if (mod.compactClicked) + mod.compactClicked(); + + mod.expanded = true; + } + } + + target: Edit + } + + // keys, when given, lets modules joined to the next one sit flush against it + function placeGroup(widths, originX, keys) { + const gap = Theme.dp(Prefs.barSpacing) + bar.editGap; + const out = []; + let x = originX; + let any = false; + let trailing = 0; + for (let i = 0; i < widths.length; i++) { + out.push(x); + const w = widths[i]; + if (w > 0.5) { + trailing = keys && bar.joinedAfter(keys, widths, i) ? 0 : (gap > 0 ? gap * Math.min(1, w / gap) : 0); + x += w + trailing; + any = true; + } + } + out.push(any ? Math.max(originX, x - trailing) : originX); + return out; + } + + // whether the module at i is joined to the one right after it. both + // have to be in the bar this moment: one that has emptied out (a tray + // with no icons, workspaces off in the overview) parts its group there + function joinedAfter(keys, widths, i) { + return i + 1 < keys.length && widths[i] > 0.5 && widths[i + 1] > 0.5 && bar.realWidthOf(keys[i]) > 0.5 && bar.realWidthOf(keys[i + 1]) > 0.5 && Prefs.barJoinedNow(keys[i], keys[i + 1]); + } + + // what a module's pill reads to square off the corners it shares + function joinedLeftOf(key) { + const at = bar.placeMap[key]; + if (!at || at.at === 0) + return false; + + const keys = bar.zoneKeys(at.zone); + return bar.realWidthOf(key) > 0.5 && bar.realWidthOf(keys[at.at - 1]) > 0.5 && Prefs.barJoinedNow(keys[at.at - 1], key); + } + + function joinedRightOf(key) { + const at = bar.placeMap[key]; + if (!at) + return false; + + const keys = bar.zoneKeys(at.zone); + return at.at + 1 < keys.length && bar.realWidthOf(key) > 0.5 && bar.realWidthOf(keys[at.at + 1]) > 0.5 && Prefs.barJoinedNow(key, keys[at.at + 1]); + } + + // per module key, radius caps where a hanging panel meets the islands: {corners, tops, sides, fillets} + readonly property var hangPlan: bar.planHangs() + + function surfaceSpan(mod) { + const l = mod.x + (mod.surfaceX !== undefined ? mod.surfaceX : 0); + return [l, l + (mod.surfaceWidth !== undefined ? mod.surfaceWidth : mod.width)]; + } + + function planHangs() { + const plan = ({ + "corners": ({}), + "tops": ({}), + "sides": ({}), + "fillets": [] + }); + const popups = Prefs.barPopupMode; + if (popups && !Prefs.barPopupNotched) + return plan; + + const most = Theme.dp(Prefs.barNotchFlare); + const bite = 0.5; + const cap = (key, corner, v) => { + const c = plan.corners[key] || (plan.corners[key] = ({})); + c[corner] = Math.min(c[corner] === undefined ? 1e6 : c[corner], Math.max(0, v)); + }; + const fillet = (x, y, size, mirrored, flipped) => { + if (size >= 1) + plan.fillets.push({ "x": x, "y": y, "size": size, "mirrored": mirrored, "flipped": flipped }); + + }; + // islands: runs of joined modules as they stand + const islands = []; + const shelves = []; + for (const zone of Prefs.barZones) { + const keys = bar.zoneKeys(zone); + let run = null; + for (let i = 0; i < keys.length; i++) { + const mod = bar.pillFor[keys[i]]; + if (!mod || bar.widthOf(keys[i]) <= 0.5) { + run = null; + continue; + } + if (run && Prefs.barJoinedNow(keys[i - 1], keys[i])) { + run.keys.push(keys[i]); + run.r = bar.surfaceSpan(mod)[1]; + } else { + run = ({ "keys": [keys[i]], "l": bar.surfaceSpan(mod)[0], "r": bar.surfaceSpan(mod)[1], "top": mod.y, "bottom": mod.y + mod.compactHeight }); + islands.push(run); + } + } + } + // what hangs: a notched pop-up, or a joined module opened in place + for (const isle of islands) { + for (let j = 0; j < isle.keys.length; j++) { + const m = bar.pillFor[isle.keys[j]]; + if (popups) { + const pop = m.notchedPopup === true && m.popupOpen ? m.popupItem : null; + if (pop && pop.height > 0.5) + shelves.push({ "key": isle.keys[j], "l": m.x + pop.x, "r": m.x + pop.x + pop.width, "y": m.y + pop.y, "depth": pop.height }); + + } else if (isle.keys.length > 1 && m !== workspacesMod && m.height - m.compactHeight > 0.5) { + const depth = m.height - m.compactHeight; + const side = ({}); + if (j > 0) { + const run = m.x - isle.l; + const f = Math.floor(Math.min(most, run / 2, depth / 2)); + fillet(m.x - f + bite, isle.bottom - bite, f, false, false); + cap(isle.keys[0], "bl", run - f); + side.bl = depth - f; + } + if (j < isle.keys.length - 1) { + const run = isle.r - (m.x + m.width); + const f = Math.floor(Math.min(most, run / 2, depth / 2)); + fillet(m.x + m.width - bite, isle.bottom - bite, f, true, false); + cap(isle.keys[isle.keys.length - 1], "br", run - f); + side.br = depth - f; + } + plan.sides[isle.keys[j]] = side; + } + } + } + // a pop-up's top is the screen edge upside down: islands stand on it or it hangs from them + for (const shelf of shelves) { + const over = islands.filter((isle) => { + return isle.l < shelf.r - 0.5 && isle.r > shelf.l + 0.5 && Math.abs(isle.bottom - shelf.y) < 1; + }); + const top = ({}); + plan.tops[shelf.key] = top; + if (over.length === 0) + continue; + + for (let k = 0; k < over.length; k++) { + const isle = over[k]; + const first = isle.keys[0]; + const last = isle.keys[isle.keys.length - 1]; + const tall = isle.bottom - isle.top; + const dl = isle.l - shelf.l; + if (k > 0) { + const f = Math.floor(Math.min(most, (isle.l - over[k - 1].r) / 2, tall)); + fillet(isle.l - f + bite, shelf.y - f + bite, f, false, true); + cap(first, "bl", 0); + } else if (dl > 0.5) { + const f = Math.floor(Math.min(most, dl / 2, tall)); + fillet(isle.l - f + bite, shelf.y - f + bite, f, false, true); + cap(first, "bl", 0); + top.tl = dl - f; + } else if (dl < -0.5) { + const f = Math.floor(Math.min(most, -dl / 2, shelf.depth / 2)); + fillet(shelf.l - f + bite, shelf.y - bite, f, false, false); + cap(first, "bl", -dl - f); + top.tl = 0; + } else { + cap(first, "bl", 0); + top.tl = 0; + } + const dr = shelf.r - isle.r; + if (k < over.length - 1) { + const f = Math.floor(Math.min(most, (over[k + 1].l - isle.r) / 2, tall)); + fillet(isle.r - bite, shelf.y - f + bite, f, true, true); + cap(last, "br", 0); + } else if (dr > 0.5) { + const f = Math.floor(Math.min(most, dr / 2, tall)); + fillet(isle.r - bite, shelf.y - f + bite, f, true, true); + cap(last, "br", 0); + top.tr = dr - f; + } else if (dr < -0.5) { + const f = Math.floor(Math.min(most, -dr / 2, shelf.depth / 2)); + fillet(shelf.r - bite, shelf.y - bite, f, true, false); + cap(last, "br", -dr - f); + top.tr = 0; + } else { + cap(last, "br", 0); + top.tr = 0; + } + } + } + return plan; + } + + property real wsCollapse: (workspacesMod.expanded && !Prefs.barPopupMode) ? 0 : 1 + + // the modules are addressed by their id in Prefs.barModules; the + // groups in Prefs.barLayout decide which slot each one lands in + readonly property var pillFor: ({ + "workspaces": workspacesMod, + "media": mprisMod, + "tray": sysTrayMod, + "clock": clockMod, + "notifications": notifMod, + "system": systemMod, + "privacy": privacyMod, + "power": powerMod, + "window": windowMod, + "start": startMod + }) + // what BarPill looks itself up in, for a right click to its card + readonly property var moduleById: bar.pillFor + readonly property var modules: [workspacesMod, mprisMod, sysTrayMod, clockMod, notifMod, systemMod, privacyMod, powerMod, windowMod, startMod] + + // which modules switched on have left the bar for now, for the Bar + // page's arrangement + Binding { + target: Prefs + property: "barModulesAway" + when: bar.primary + value: Prefs.barModules.filter((m) => { + const mod = bar.pillFor[m.id]; + return Prefs[m.key] === true && mod && !mod.shown; + }).map((m) => { + return m.id; + }) + } + + // a module on its way out of the bar keeps the slot it had until its + // pill has finished emptying, so its neighbours close the gap at the + // rate it shrinks rather than dropping into it + function zoneKeys(zone) { + const live = bar.dragGroups ? bar.dragGroups[zone === "centre" ? "center" : zone].filter((id) => { + return Prefs.barShown(id); + }) : Prefs.barKeysOf(zone); + const r = Prefs.barRetiring; + if (!r || r.zone !== zone || Prefs.barHas(r.key)) + return live; + + const out = live.slice(); + out.splice(Math.min(r.at, out.length), 0, r.key); + return out; + } + + readonly property var retiring: Prefs.barRetiring + + onRetiringChanged: { + if (bar.retiring) + retireTimer.restart(); + + } + + Timer { + id: retireTimer + + interval: Theme.barMs(560) + onTriggered: Prefs.barClearRetiring() + } + + // key -> which zone it sits in and how far along + readonly property var placeMap: { + const out = ({}); + for (var z = 0; z < Prefs.barZones.length; z++) { + const zone = Prefs.barZones[z]; + const keys = bar.zoneKeys(zone); + for (var i = 0; i < keys.length; i++) out[keys[i]] = ({ "zone": zone, "at": i }) + } + return out; + } + + // workspaces empties its slot as it flies to the screen centre, so the + // rest of its zone closes up behind it + function realWidthOf(key) { + const it = bar.pillFor[key]; + if (!it) + return 0; + + return it === workspacesMod ? it.width * bar.wsCollapse : it.width; + } + + // in edit mode a module with nothing to show keeps a stand-in's room + function widthOf(key) { + return Math.max(bar.realWidthOf(key), barEdit.ghostWidthOf(key)); + } + + function widthsOf(zone) { + return bar.zoneKeys(zone).map(bar.widthOf); + } + + function placesOf(zone) { + return zone === "left" ? bar.leftPlaces : (zone === "centre" ? bar.centrePlaces : bar.rightPlaces); + } + + function alignFor(zone) { + return zone === "centre" ? "center" : (zone === "right" ? "right" : "left"); + } + + function placeFor(key) { + const at = bar.placeMap[key]; + if (!at) + return 0; + + const places = bar.placesOf(at.zone); + return at.at < places.length ? places[at.at] : 0; + } + + function alignOf(key) { + const at = bar.placeMap[key]; + return bar.alignFor(at ? at.zone : "left"); + } + + readonly property var leftWidths: bar.widthsOf("left") + readonly property var centreWidths: bar.widthsOf("centre") + readonly property var rightWidths: bar.widthsOf("right") + readonly property real leftGroupWidth: bar.placeGroup(bar.leftWidths, 0, bar.zoneKeys("left"))[bar.leftWidths.length] + readonly property real centreGroupWidth: bar.placeGroup(bar.centreWidths, 0, bar.zoneKeys("centre"))[bar.centreWidths.length] + readonly property real rightGroupWidth: bar.placeGroup(bar.rightWidths, 0, bar.zoneKeys("right"))[bar.rightWidths.length] + readonly property int zoneGap: Theme.dp(Prefs.barZoneGap) + + // pills ease to their new slots only while the arrangement is changing. + // a Behavior left on all the time would sit on top of the width + // animations a panel opening or a tray icon arriving already drive, and + // one that restarts every frame never arrives + readonly property int reorderTick: Prefs.barReorderTick + property bool shuffling: false + + onReorderTickChanged: { + bar.shuffling = true; + shuffleTimer.restart(); + } + + Timer { + id: shuffleTimer + + interval: Theme.barMs(520) + onTriggered: bar.shuffling = false + } + + readonly property real leftOriginX: bar.sideMargin + readonly property real rightOriginX: bar.width - bar.rightGroupWidth - bar.sideMargin + readonly property real centreOriginX: Math.min(Math.max((bar.width - bar.centreGroupWidth) / 2, bar.leftOriginX + bar.leftGroupWidth + bar.zoneGap), bar.rightOriginX - bar.centreGroupWidth - bar.zoneGap) + readonly property var leftPlaces: bar.placeGroup(bar.leftWidths, bar.leftOriginX, bar.zoneKeys("left")) + readonly property var centrePlaces: bar.placeGroup(bar.centreWidths, bar.centreOriginX, bar.zoneKeys("centre")) + readonly property var rightPlaces: bar.placeGroup(bar.rightWidths, bar.rightOriginX, bar.zoneKeys("right")) + + Behavior on wsCollapse { + NumberAnimation { + duration: Theme.barMs(380) + easing.type: Easing.OutCubic + } + + } + readonly property real sideMargin: Theme.dp(Prefs.barSideMargin) + + // busy: a panel is open somewhere on the bar, so it stays out + // regardless of the pointer. held: the pointer is on the reveal + // strip or already on a module - either keeps it from hiding + readonly property bool barBusy: bar.modules.some((m) => { + return m.expanded === true || m.anyOpen === true; + }) + property bool slidingAway: false + readonly property bool heldByPointer: Prefs.barAutoHide && (revealArea.containsMouse || (!bar.slidingAway && bar.modules.some((m) => { + return m.visible && (m.surfaceHovered === true || m.compactHovered === true); + }))) + readonly property bool barRevealed: !Prefs.barAutoHide || bar.barBusy || bar.heldByPointer || Edit.active + // how far the compact content sits above its resting y=0 while + // hidden - past the window's own top edge, so it clips away for free + property real hiddenOffset: bar.barRevealed ? 0 : -(Theme.dp(Prefs.barHeight) + Prefs.effectiveBarTopMargin + Theme.dp(4)) + + Behavior on hiddenOffset { + NumberAnimation { + duration: Theme.durLong + easing.type: Easing.Bezier + easing.bezierCurve: Theme.easeEmphasizedDecel + } + + } + + onBarRevealedChanged: { + if (bar.barRevealed) { + bar.slidingAway = false; + slideAwayTimer.stop(); + } else { + bar.slidingAway = true; + slideAwayTimer.restart(); + } + } + + Timer { + id: slideAwayTimer + + interval: Theme.durLong + onTriggered: bar.slidingAway = false + } + + Component.onCompleted: laidOutTimer.start() + + Timer { + id: laidOutTimer + + interval: 120 + onTriggered: bar.laidOut = true + } + + color: "transparent" + implicitHeight: bar.screen ? bar.screen.height - Prefs.effectiveBarTopMargin : Theme.dp(800) + exclusiveZone: (Prefs.barEnabled && bar.anyModuleShown && !Prefs.barAutoHide) ? Theme.dp(Prefs.barHeight) + Prefs.shellGap : 0 + + anchors { + top: true + bottom: false + left: true + right: true + } + + margins { + top: Prefs.effectiveBarTopMargin + } + + // full bar: one strip across the whole top edge, under every module + Rectangle { + id: fullStrip + + visible: Prefs.barFull && bar.anyModuleShown + x: 0 + y: bar.hiddenOffset + z: -2 + width: bar.width + height: Theme.dp(Prefs.barHeight) + color: Theme.bg + + Behavior on color { + enabled: bar.laidOut + + ColorAnimation { + duration: Theme.barMs(260) + easing.type: Easing.OutCubic + } + + } + + } + + // rounded corners hanging off the strip where it meets the screen sides + BarFlare { + visible: fullStrip.visible && size > 0 + mirrored: true + size: Theme.dp(Prefs.barFullCorner) + x: 0 + y: Theme.dp(Prefs.barHeight) + bar.hiddenOffset + z: -2 + } + + BarFlare { + visible: fullStrip.visible && size > 0 + size: Theme.dp(Prefs.barFullCorner) + x: bar.width - width + y: Theme.dp(Prefs.barHeight) + bar.hiddenOffset + z: -2 + } + + // full bar: an open panel hangs off the strip, so round the two concave + // corners where its sides meet the strip's lower edge. They grow with + // the panel's height, so a folding panel takes them with it + Repeater { + model: (Prefs.barFull && !Prefs.barPopupMode) ? bar.modules.filter((m) => { + return m !== workspacesMod; + }) : [] + + Item { + id: panelFlares + + required property var modelData + + // surfaceOpen, not the height alone: a pill that is only + // resizing its compact face is see-through on the full bar + readonly property real reach: (panelFlares.modelData.visible && panelFlares.modelData.surfaceOpen === true) ? panelFlares.modelData.height - Theme.dp(Prefs.barHeight) : 0 + + // never past the screen edge, and never into the screen corner + // that already rounds that side + function sizeFor(toTheLeft) { + var m = panelFlares.modelData; + var corner = Theme.dp(Prefs.barFullCorner); + var toEdge = toTheLeft ? m.x : bar.width - m.x - m.width; + var room = toEdge - (toEdge < corner * 2 ? corner : 0); + return Math.max(0, Math.floor(Math.min(corner, panelFlares.reach, room))); + } + + anchors.fill: parent + z: -1 + + BarFlare { + size: panelFlares.sizeFor(true) + x: panelFlares.modelData.x - width + 0.5 + y: Theme.dp(Prefs.barHeight) + } + + BarFlare { + mirrored: true + size: panelFlares.sizeFor(false) + x: panelFlares.modelData.x + panelFlares.modelData.width - 0.5 + y: Theme.dp(Prefs.barHeight) + } + + } + + } + + Mpris { + id: mprisMod + + popupAlign: bar.alignOf("media") + + hostWindow: bar + x: bar.xOf("media") + editLifted: bar.dragKey === "media" + anchors.top: parent.top + anchors.topMargin: bar.hiddenOffset + bar.liftOf("media") + + Behavior on x { + enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "media" + + NumberAnimation { + duration: Theme.barDurEnter + easing.type: Easing.Bezier + easing.bezierCurve: Theme.easeEmphasizedDecel + } + + } + + } + + SysTray { + id: sysTrayMod + + popupAlign: bar.alignOf("tray") + + hostWindow: bar + x: bar.xOf("tray") + editLifted: bar.dragKey === "tray" + anchors.top: parent.top + anchors.topMargin: bar.hiddenOffset + bar.liftOf("tray") + + Behavior on x { + enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "tray" + + NumberAnimation { + duration: Theme.barDurEnter + easing.type: Easing.Bezier + easing.bezierCurve: Theme.easeEmphasizedDecel + } + + } + + } + + Clock { + id: clockMod + + popupAlign: bar.alignOf("clock") + + hostWindow: bar + anchors.top: parent.top + anchors.topMargin: bar.hiddenOffset + bar.liftOf("clock") + x: bar.xOf("clock") + editLifted: bar.dragKey === "clock" + + Behavior on x { + enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "clock" + + NumberAnimation { + duration: Theme.barDurEnter + easing.type: Easing.Bezier + easing.bezierCurve: Theme.easeEmphasizedDecel + } + + } + + } + + Notifications { + id: notifMod + + showsPopups: bar.primary + popupAlign: bar.alignOf("notifications") + + hostWindow: bar + x: bar.xOf("notifications") + editLifted: bar.dragKey === "notifications" + anchors.top: parent.top + anchors.topMargin: bar.hiddenOffset + bar.liftOf("notifications") + + Behavior on x { + enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "notifications" + + NumberAnimation { + duration: Theme.barDurEnter + easing.type: Easing.Bezier + easing.bezierCurve: Theme.easeEmphasizedDecel + } + + } + + } + + Privacy { + id: privacyMod + + popupAlign: bar.alignOf("privacy") + toast: bar.primary ? toastMod : null + + hostWindow: bar + x: bar.xOf("privacy") + editLifted: bar.dragKey === "privacy" + anchors.top: parent.top + anchors.topMargin: bar.hiddenOffset + bar.liftOf("privacy") + + Behavior on x { + enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "privacy" + + NumberAnimation { + duration: Theme.barDurEnter + easing.type: Easing.Bezier + easing.bezierCurve: Theme.easeEmphasizedDecel + } + + } + + } + + SessionMenu { + id: powerMod + + popupAlign: bar.alignOf("power") + + hostWindow: bar + x: bar.xOf("power") + editLifted: bar.dragKey === "power" + anchors.top: parent.top + anchors.topMargin: bar.hiddenOffset + bar.liftOf("power") + + Behavior on x { + enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "power" + + NumberAnimation { + duration: Theme.barDurEnter + easing.type: Easing.Bezier + easing.bezierCurve: Theme.easeEmphasizedDecel + } + + } + + } + + StartButton { + id: startMod + + popupAlign: bar.alignOf("start") + + hostWindow: bar + dockMod: dock + x: bar.xOf("start") + editLifted: bar.dragKey === "start" + anchors.top: parent.top + anchors.topMargin: bar.hiddenOffset + bar.liftOf("start") + + Behavior on x { + enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "start" + + NumberAnimation { + duration: Theme.barDurEnter + easing.type: Easing.Bezier + easing.bezierCurve: Theme.easeEmphasizedDecel + } + + } + + } + + ActiveWindow { + id: windowMod + + popupAlign: bar.alignOf("window") + + hostWindow: bar + x: bar.xOf("window") + editLifted: bar.dragKey === "window" + anchors.top: parent.top + anchors.topMargin: bar.hiddenOffset + bar.liftOf("window") + + Behavior on x { + enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "window" + + NumberAnimation { + duration: Theme.barDurEnter + easing.type: Easing.Bezier + easing.bezierCurve: Theme.easeEmphasizedDecel + } + + } + + } + + System { + id: systemMod + + popupAlign: bar.alignOf("system") + + hostWindow: bar + mprisMod: mprisMod + x: bar.xOf("system") + editLifted: bar.dragKey === "system" + anchors.top: parent.top + anchors.topMargin: bar.hiddenOffset + bar.liftOf("system") + + Behavior on x { + enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "system" + + NumberAnimation { + duration: Theme.barDurEnter + easing.type: Easing.Bezier + easing.bezierCurve: Theme.easeEmphasizedDecel + } + + } + + } + + Repeater { + model: Prefs.barNotch ? bar.modules : [] + + Item { + id: flares + + required property var modelData + + readonly property bool present: flares.modelData && flares.modelData.width > 0.5 && flares.modelData.visible && !(bar.dragLift > 0.5 && bar.pillFor[bar.dragKey] === flares.modelData) + readonly property bool modHovered: flares.modelData ? (flares.modelData.compactHovered === true && flares.modelData !== workspacesMod) : false + + function flareFor(toTheLeft) { + if (!flares.present) + return 0; + + var mods = bar.modules; + var edge = toTheLeft ? flares.modelData.x : flares.modelData.x + flares.modelData.width; + var toEdge = toTheLeft ? edge : bar.width - edge; + var toNeighbour = 100000; + for (var i = 0; i < mods.length; i++) { + var o = mods[i]; + if (!o || o === flares.modelData || o.width <= 0.5 || !o.visible) + continue; + + var d = toTheLeft ? edge - (o.x + o.width) : o.x - edge; + if (d >= 0) + toNeighbour = Math.min(toNeighbour, d); + + } + return Math.max(0, Math.min(Theme.dp(Prefs.barNotchFlare), Math.floor(toNeighbour / 2), Math.floor(toEdge))); + } + + anchors.fill: parent + z: -1 + visible: Prefs.barNotch && flares.present + + readonly property real bite: 0.5 + + BarFlare { + hovered: flares.modHovered + size: flares.flareFor(true) + x: flares.modelData ? flares.modelData.x - width + flares.bite : 0 + y: bar.hiddenOffset + } + + BarFlare { + hovered: flares.modHovered + mirrored: true + size: flares.flareFor(false) + x: flares.modelData ? flares.modelData.x + flares.modelData.width - flares.bite : 0 + y: bar.hiddenOffset + } + + } + + } + + // fillets where a hanging panel meets the islands above it + Repeater { + model: 16 + + BarFlare { + required property int index + readonly property var spec: bar.hangPlan.fillets.length > index ? bar.hangPlan.fillets[index] : null + + z: 0 + size: spec ? spec.size : 0 + mirrored: spec ? spec.mirrored : false + flipped: spec ? spec.flipped : false + x: spec ? spec.x : 0 + y: spec ? spec.y : 0 + } + + } + + Workspaces { + id: workspacesMod + + hostWindow: bar + dockMod: dock + restX: bar.xOf("workspaces") + restY: bar.hiddenOffset + bar.liftOf("workspaces") + z: bar.dragKey === "workspaces" ? 120 : (workspacesMod.popupOpen ? 100 : 1) + + Behavior on restX { + enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "workspaces" + + NumberAnimation { + duration: Theme.barDurEnter + easing.type: Easing.Bezier + easing.bezierCurve: Theme.easeEmphasizedDecel + } + + } + + } + + mask: Region { + + ModuleRegion { + mod: workspacesMod + } + + ModuleRegion { + mod: mprisMod + } + + ModuleRegion { + mod: sysTrayMod + } + + ModuleRegion { + mod: clockMod + } + + ModuleRegion { + mod: notifMod + } + + ModuleRegion { + mod: systemMod + } + + ModuleRegion { + mod: privacyMod + } + + ModuleRegion { + mod: powerMod + } + + ModuleRegion { + mod: windowMod + } + + ModuleRegion { + mod: startMod + } + + Region { + item: Prefs.barAutoHide ? revealArea : null + } + + Region { + width: bar.dragHeld || Edit.active ? bar.width : 0 + height: bar.dragHeld ? bar.height : (Edit.active ? Theme.dp(Prefs.barHeight) + Theme.dp(4) : 0) + } + + } + + BarEdit { + id: barEdit + + host: bar + } + + // touching this strip reveals the bar + MouseArea { + id: revealArea + + x: 0 + y: 0 + width: bar.width + height: Theme.dp(4) + hoverEnabled: true + acceptedButtons: Qt.NoButton + enabled: Prefs.barAutoHide + visible: Prefs.barAutoHide + } + + // the overview and the dock menu shut each other + Connections { + function onExpandedChanged() { + if (workspacesMod.expanded) + dock.menuOpen = false; + + } + + target: workspacesMod + } + + Connections { + function onMenuOpenChanged() { + if (dock.menuOpen) + workspacesMod.expanded = false; + + } + + target: dock + } + + BackgroundEffect.blurRegion: (Theme.blurAmount > 0 && bar.laidOut) ? barBlurRegion : null + + // clipped to the surface: hyprland frosts the whole layer for a region + // wholly outside it, which is where a hidden bar's pills are + Region { + id: barBlurRegion + + intersection: Intersection.Intersect + + Region { + x: 0 + y: fullStrip.visible ? bar.hiddenOffset : 0 + width: fullStrip.visible ? Math.round(bar.width) : 0 + height: fullStrip.visible ? Theme.dp(Prefs.barHeight) : 0 + } + + ModuleRegion { + blur: true + mod: workspacesMod + } + + ModuleRegion { + blur: true + mod: mprisMod + } + + ModuleRegion { + blur: true + mod: sysTrayMod + } + + ModuleRegion { + blur: true + mod: clockMod + } + + ModuleRegion { + blur: true + mod: notifMod + } + + ModuleRegion { + blur: true + mod: systemMod + } + + ModuleRegion { + blur: true + mod: privacyMod + } + + ModuleRegion { + blur: true + mod: powerMod + } + + ModuleRegion { + blur: true + mod: windowMod + } + + ModuleRegion { + blur: true + mod: startMod + } + + } + + } + + } diff --git a/modules/user/lucid-pinned.json b/modules/user/lucid-pinned.json new file mode 100644 index 0000000..163100b --- /dev/null +++ b/modules/user/lucid-pinned.json @@ -0,0 +1,3 @@ +{ + "pinnedApps": [] +} diff --git a/modules/user/lucid-prefs.json b/modules/user/lucid-prefs.json new file mode 100644 index 0000000..fc48c6c --- /dev/null +++ b/modules/user/lucid-prefs.json @@ -0,0 +1,11 @@ +{ + "barEnabled": true, + "dockEnabled": true, + "widgetsEnabled": true, + "widgetOnTop": false, + "widgetHideFullscreen": true, + "desktopIcons": false, + "toastEnabled": true, + "liveWallpapers": true, + "fontFamily": "Pixeloid Sans" +} diff --git a/modules/user/lucid-shell.qml b/modules/user/lucid-shell.qml new file mode 100644 index 0000000..dbce623 --- /dev/null +++ b/modules/user/lucid-shell.qml @@ -0,0 +1,36 @@ +import "./luciddocks" +import "./lucidedit" +import "./lucidprefs" +import "./lucidwidgets" +import QtQuick +import Quickshell +import qs + +ShellRoot { + LucidBar { + id: barRoot + dock: dockRoot + } + + Dock { + id: dockRoot + } + + WidgetLayer { + } + + WidgetIpc { + } + + EditLayer { + } + + SettingsHost { + } + + Binding { + target: Monitors + property: "barWindows" + value: barRoot.instances + } +} diff --git a/modules/user/lucid-widgets.json b/modules/user/lucid-widgets.json new file mode 100644 index 0000000..70d2290 --- /dev/null +++ b/modules/user/lucid-widgets.json @@ -0,0 +1,20 @@ +{ + "nextId": 3, + "boardFull": true, + "instances": [ + { + "uid": "w1", + "type": "clock", + "variant": "digital", + "wx": 40, + "wy": 70 + }, + { + "uid": "w2", + "type": "system", + "variant": "rings", + "wx": 40, + "wy": 250 + } + ] +} diff --git a/user/sweetbread/home.nix b/user/sweetbread/home.nix index bcc1eb4..d789863 100644 --- a/user/sweetbread/home.nix +++ b/user/sweetbread/home.nix @@ -1,4 +1,6 @@ { pkgs, ... }: { + imports = [ ./modules/lucid.nix ]; + programs.hyprlock.enable = true; home.packages = with pkgs; [ android-studio diff --git a/user/sweetbread/modules/lucid.nix b/user/sweetbread/modules/lucid.nix new file mode 100644 index 0000000..9d4ba01 --- /dev/null +++ b/user/sweetbread/modules/lucid.nix @@ -0,0 +1,159 @@ +{ config, lib, pkgs, ... }: +let + source = pkgs.fetchgit { + url = "https://github.com/Sn3akyy1/lucid.git"; + rev = "bdd7f79da18efd837f79dcecbe5d2ec178abc10c"; + hash = "sha256-b6qylKjCGYy2Yo64i1w9g4oGrYgcXf502jY9qNNWhSI="; + sparseCheckout = [ + "/*" + "!/wallpapers/*" + "!/assets/*.webp" + ]; + nonConeMode = true; + }; + hyprLua = import ../../../modules/user/hyprland/lua.nix { inherit lib; }; + colors = config.lib.stylix.colors.withHashtag; + accent = colors.${config.stylix.accent}; + palette = { + primary = accent; + on_primary = colors.base00; + primary_container = accent; + on_primary_container = colors.base05; + secondary = colors.base0D; + secondary_container = colors.base02; + tertiary = colors.base0E; + on_tertiary = colors.base00; + error = colors.base08; + on_error = colors.base00; + error_container = colors.base03; + surface = colors.base00; + on_surface = colors.base05; + surface_variant = colors.base02; + on_surface_variant = colors.base07; + surface_dim = colors.base01; + surface_container_lowest = colors.base01; + surface_container_low = colors.base00; + surface_container = colors.base00; + surface_container_high = colors.base02; + surface_container_highest = colors.base03; + outline = colors.base04; + outline_variant = colors.base02; + inverse_surface = colors.base05; + shadow = "#000000"; + scrim = "#000000"; + source_color = accent; + }; + paletteFile = pkgs.writeText "lucid-stylix-palette.json" (builtins.toJSON palette); + themeMeta = pkgs.writeText "lucid-stylix-meta.json" (builtins.toJSON { + id = "stylix"; + name = "Stylix"; + desc = "Active Stylix palette"; + swatchBg = colors.base00; + swatchAccent = accent; + source = "Home Manager"; + detected = "base16"; + user = true; + }); + themeName = pkgs.writeText "lucid-current-theme" "stylix\n"; + qmlPackages = with pkgs.qt6; [ qt5compat qtmultimedia ]; + 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 coreutils curl jq pulseaudio python3 xdg-utils ])}:$PATH" + exec ${pkgs.quickshell}/bin/qs --no-duplicate --path "${config.home.homeDirectory}/.config/quickshell" + ''; +in { + programs.waybar.enable = lib.mkForce false; + services.mako.enable = lib.mkForce false; + + 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 install -m644 ${../../../modules/user/lucid-shell.qml} "$target/shell.qml" + $DRY_RUN_CMD install -m644 ${../../../modules/user/LucidBar.qml} "$target/LucidBar.qml" + + if [ ! -e "$target/lucidprefs/prefs.json" ]; then + $DRY_RUN_CMD install -m644 ${../../../modules/user/lucid-prefs.json} "$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 '${config.stylix.fonts.sansSerif.name}' \ + '.barEnabled = true | .widgetOnTop = false | .widgetHideFullscreen = true | .fontFamily = $font' \ + "$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 + if [ ! -e "$target/luciddocks/pinned.json" ]; then + $DRY_RUN_CMD install -m644 ${../../../modules/user/lucid-pinned.json} "$target/luciddocks/pinned.json" + fi + if [ ! -e "$target/lucidwidgets/widgets.json" ]; then + $DRY_RUN_CMD install -m644 ${../../../modules/user/lucid-widgets.json} "$target/lucidwidgets/widgets.json" + fi + + 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" + $DRY_RUN_CMD install -m644 ${paletteFile} \ + "${config.home.homeDirectory}/.cache/quickshell/matugen.json" + $DRY_RUN_CMD install -m644 ${themeName} \ + "${config.home.homeDirectory}/.cache/current_theme" + ''; + + 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; + }; + Install.WantedBy = [ "hyprland-session.target" ]; + }; + + wayland.windowManager.hyprland.settings = { + on = [ (hyprLua.onStart [ "systemctl --user start lucid.service" ]) ]; + bind = [ + (hyprLua.bind "SUPER + SHIFT + G" (hyprLua.exec "qs ipc call widgets panel")) + ]; + }; +}