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

+11 -17
View File
@@ -1,16 +1,14 @@
{ pkgs, lib, config, osConfig, inputs, ... }: {
wayland.windowManager.hyprland = let
{ pkgs, lib, config, osConfig, ... }:
let
colors = config.lib.stylix.colors;
accent = colors.${config.stylix.accent};
hyprLua = import ../../../modules/user/hyprland/lua.nix { inherit lib; };
wallpaper_changer = pkgs.writers.writePython3Bin "wallpaper_changer" {
libraries = [ pkgs.python3Packages.requests ];
flakeIgnore = [ "E111" "E121" "E241" "E501" "E701" "E731" ];
} /*py*/ ''
import requests as requests
from random import choice
from os import listdir, makedirs, remove, replace, system
from os import listdir, makedirs, remove, replace
from os.path import exists
from subprocess import run
@@ -123,9 +121,15 @@
finally:
if filename is not None:
notify("Applying wallpaper", 100)
system(f"awww img {folder}/{filename} --transition-type center")
run([
"${config.home.homeDirectory}/.config/hypr/scripts/wallpaper/set-wallpaper.sh",
f"{folder}/{filename}",
], check=False)
'';
in {
in {
home.packages = [ wallpaper_changer ];
wayland.windowManager.hyprland = {
settings = {
config = {
general = {
@@ -188,10 +192,6 @@
{ leaf = "workspaces"; enabled = true; speed = 6; bezier = "default"; }
];
on = [
(hyprLua.onStart [ "${lib.getExe wallpaper_changer}" ])
];
workspace_rule = [
{
workspace = "2";
@@ -199,12 +199,6 @@
}
];
bind = [
(hyprLua.bind "Print" (hyprLua.exec "${lib.getExe pkgs.hyprshot} -z -o ~/Screenshots -m active -m output"))
(hyprLua.bind "CTRL + Print" (hyprLua.exec "${lib.getExe pkgs.hyprshot} -z -o ~/Screenshots -m region"))
(hyprLua.bind "ALT + Print" (hyprLua.exec "${lib.getExe pkgs.hyprshot} -z -o ~/Screenshots -m active -m window"))
(hyprLua.bind "SUPER + W" (hyprLua.exec "${lib.getExe wallpaper_changer}"))
];
};
};
}
+310
View File
@@ -0,0 +1,310 @@
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
// reminders: what the clock rings for, what the calendar marks, and what the
// widgets and the lock screen list as coming up
Singleton {
id: root
// [{ id, year, month, day, hour, minute, name, repeat }]; repeat is
// "" | "daily" | "weekdays" | "weekly", counted from the stored date
property alias items: store.items
property int tick: 0
// reminders due and not yet dealt with, oldest first
property var ringing: []
readonly property var firing: root.ringing.length > 0 ? root.ringing[0] : null
property var snoozedUntil: ({})
property var dismissedAt: ({})
property string checkedDay: ""
// when this copy of the shell started, so a reload does not ring out loud
readonly property real bornAt: Date.now()
// changes once a minute; what the countdown text needs, instead of every tick
property string minuteKey: ""
readonly property var next: {
root.tick;
CalendarSource.tick;
return root.upcoming(1)[0] || null;
}
readonly property bool soon: {
var n = root.next;
return n !== null && n.at.getTime() - Loc.nowMs() <= 7 * 86400000;
}
readonly property var repeatOptions: [
{ "key": "", "label": "Once" },
{ "key": "daily", "label": "Daily" },
{ "key": "weekdays", "label": "Weekdays" },
{ "key": "weekly", "label": "Weekly" }
]
function dayStart(y, m, d) {
return new Date(y, m, d, 0, 0, 0, 0);
}
// does this reminder happen on the given day?
function occursOn(r, y, m, d) {
var day = root.dayStart(y, m, d);
var start = root.dayStart(r.year, r.month, r.day);
if (day < start)
return false;
switch (r.repeat || "") {
case "daily":
return true;
case "weekdays":
return day.getDay() !== 0 && day.getDay() !== 6;
case "weekly":
return day.getDay() === start.getDay();
}
return day.getTime() === start.getTime();
}
// the next time a reminder rings at or after `from`
// `next` re-reads this every second for every reminder, so it must not walk
// the calendar: a one-off happens on exactly its own date, and daily,
// weekdays and weekly all come round inside a week of whichever is later,
// today or the date the repeat starts from
function nextAt(r, from) {
if (!r.repeat) {
var at = new Date(r.year, r.month, r.day, r.hour, r.minute, 0, 0);
return at >= from ? at : null;
}
var fromDay = root.dayStart(from.getFullYear(), from.getMonth(), from.getDate());
var startDay = root.dayStart(r.year, r.month, r.day);
var probe = startDay > fromDay ? startDay : fromDay;
for (var i = 0; i < 8; i++) {
if (root.occursOn(r, probe.getFullYear(), probe.getMonth(), probe.getDate())) {
var a = new Date(probe.getFullYear(), probe.getMonth(), probe.getDate(), r.hour, r.minute, 0, 0);
if (a >= from)
return a;
}
probe.setDate(probe.getDate() + 1);
}
return null;
}
function upcoming(limit) {
var now = Loc.now();
var out = [];
for (var i = 0; i < root.items.length; i++) {
var at = root.nextAt(root.items[i], now);
if (at)
out.push({ "item": root.items[i], "at": at });
}
out = out.concat(CalendarSource.upcoming());
out.sort((a, b) => {
return a.at - b.at;
});
return out.slice(0, limit || out.length);
}
function forDay(y, m, d) {
var local = root.items.filter((r) => {
return root.occursOn(r, y, m, d);
});
var fromThunderbird = CalendarSource.forDay(y, m, d).map((event) => CalendarSource.asAgendaItem(event));
return local.concat(fromThunderbird).sort((a, b) => {
return (a.hour * 60 + a.minute) - (b.hour * 60 + b.minute);
});
}
function hasOn(y, m, d) {
if (CalendarSource.hasOn(y, m, d))
return true;
for (var i = 0; i < root.items.length; i++) {
if (root.occursOn(root.items[i], y, m, d))
return true;
}
return false;
}
function add(y, m, d, hour, minute, name, repeat) {
root.items = root.items.concat([{
"id": Date.now() + "-" + Math.random().toString(36).slice(2, 7),
"year": y,
"month": m,
"day": d,
"hour": hour,
"minute": minute,
"name": name,
"repeat": repeat || ""
}]);
root.tick++;
}
function remove(id) {
root.items = root.items.filter((r) => {
return r.id !== id;
});
root.ringing = root.ringing.filter((r) => {
return r.id !== id;
});
root.tick++;
}
function timeText(r) {
if (!r)
return "";
if (Prefs.clock24h)
return String(r.hour).padStart(2, "0") + ":" + String(r.minute).padStart(2, "0");
var h = r.hour % 12 === 0 ? 12 : r.hour % 12;
return h + ":" + String(r.minute).padStart(2, "0") + " " + (r.hour < 12 ? "AM" : "PM");
}
function untilText(at) {
if (!at)
return "";
var mins = Math.round((at - Loc.now()) / 60000);
if (mins < 1)
return "now";
if (mins < 60)
return "in " + mins + " min";
var hrs = Math.round(mins / 60);
if (hrs < 24)
return "in " + hrs + (hrs === 1 ? " hour" : " hours");
var days = Math.round(hrs / 24);
return days === 1 ? "tomorrow" : "in " + days + " days";
}
function repeatText(r) {
switch (r.repeat || "") {
case "daily":
return "Every day";
case "weekdays":
return "Weekdays";
case "weekly":
return "Every " + root.dayStart(r.year, r.month, r.day).toLocaleDateString(Qt.locale(), "dddd");
}
return "";
}
function snooze(id, minutes) {
var m = Object.assign({}, root.snoozedUntil);
m[id] = Loc.nowMs() + minutes * 60000;
root.snoozedUntil = m;
root.ringing = root.ringing.filter((q) => {
return q.id !== id;
});
}
// a one-off goes once it has rung; a repeating one waits for its next turn
function dismiss(id) {
var m = Object.assign({}, root.dismissedAt);
m[id] = Loc.nowMs();
root.dismissedAt = m;
root.ringing = root.ringing.filter((q) => {
return q.id !== id;
});
var r = root.items.find((x) => {
return x.id === id;
});
if (r && !r.repeat)
root.remove(id);
}
function prune(now) {
var today = root.dayStart(now.getFullYear(), now.getMonth(), now.getDate());
var kept = root.items.filter((r) => {
return r.repeat || root.dayStart(r.year, r.month, r.day) >= today;
});
if (kept.length !== root.items.length)
root.items = kept;
}
function check() {
var now = Loc.now();
var key = now.getFullYear() + "-" + now.getMonth() + "-" + now.getDate();
if (root.checkedDay !== key) {
root.prune(now);
root.checkedDay = key;
}
var mkey = key + "-" + now.getHours() + "-" + now.getMinutes();
if (root.minuteKey !== mkey)
root.minuteKey = mkey;
var fresh = false;
for (var i = 0; i < root.items.length; i++) {
var r = root.items[i];
if (root.ringing.some((q) => {
return q.id === r.id;
}))
continue;
var until = root.snoozedUntil[r.id];
if (until && now.getTime() < until)
continue;
// today's occurrence, if there is one and it has come round
if (!root.occursOn(r, now.getFullYear(), now.getMonth(), now.getDate()))
continue;
var due = new Date(now.getFullYear(), now.getMonth(), now.getDate(), r.hour, r.minute, 0, 0);
if (now < due)
continue;
var last = root.dismissedAt[r.id];
if (last && last >= due.getTime())
continue;
// an hour late is too late to ring
if (!until && now.getTime() - due.getTime() > 3600000)
continue;
root.ringing = root.ringing.concat([r]);
// a reload forgets dismissals, so one already overdue as the shell
// starts rings without a sound; after that, one that came due while
// the machine slept still gets it
if (until || Date.now() - root.bornAt > 5000 || now.getTime() - due.getTime() < 60000)
fresh = true;
}
if (fresh && Prefs.reminderSound)
Sounds.play("reminder", 1);
root.tick++;
}
Timer {
interval: 1000
repeat: true
running: true
onTriggered: root.check()
}
Timer {
id: saveDebounce
interval: 300
onTriggered: file.writeAdapter()
}
FileView {
id: file
path: Quickshell.env("HOME") + "/.config/quickshell/lucidbar/clock_reminders.json"
blockLoading: true
onAdapterUpdated: saveDebounce.restart()
adapter: JsonAdapter {
id: store
property var items: []
}
}
}
@@ -0,0 +1,98 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs
pragma Singleton
// Read-only events already synchronized by Thunderbird. This deliberately does
// not connect to DAV or store credentials; Thunderbird remains the DAV client.
Singleton {
id: source
property var events: []
property int tick: 0
property string error: ""
property real updatedAt: 0
function refresh() {
if (reader.running)
return;
reader.command = ["python3", Quickshell.env("HOME") + "/.config/quickshell/thunderbird-calendar.py"];
reader.running = true;
}
function forDay(year, month, day) {
source.tick;
return source.events.filter((event) => {
const date = new Date(event.startMs);
return date.getFullYear() === year && date.getMonth() === month && date.getDate() === day;
});
}
function upcoming(limit) {
source.tick;
const now = Loc.nowMs();
return source.events.filter((event) => event.startMs >= now).slice(0, limit || source.events.length).map((event) => ({
"item": source.asAgendaItem(event),
"at": new Date(event.startMs)
}));
}
function hasOn(year, month, day) {
return source.forDay(year, month, day).length > 0;
}
function asAgendaItem(event) {
const date = new Date(event.startMs);
return {
"id": "thunderbird:" + event.id,
"name": event.name,
"calendar": event.calendar,
"source": "thunderbird",
"startMs": event.startMs,
"endMs": event.endMs,
"hour": date.getHours(),
"minute": date.getMinutes(),
"repeat": ""
};
}
Process {
id: reader
command: []
stdout: StdioCollector {
onStreamFinished: {
if (this.text.trim() === "")
return;
try {
const result = JSON.parse(this.text);
if (!result.error) {
source.events = result.events || [];
source.error = "";
source.updatedAt = Date.now();
source.tick++;
} else {
source.error = result.error;
}
} catch (e) {
source.error = "Could not read Thunderbird calendar cache";
}
}
}
onExited: (code) => {
if (code !== 0 && source.error === "")
source.error = "Could not read Thunderbird calendar cache";
}
}
Timer {
interval: 300000
repeat: true
running: true
triggeredOnStart: true
onTriggered: source.refresh()
}
}
@@ -0,0 +1,536 @@
import QtQuick
import qs
import qs.lucidui
// the month on the left, the picked day's reminders and a way to add one on the right
Item {
id: page
property var host: null
property var selected: {
const n = Loc.now();
return { "year": n.getFullYear(), "month": n.getMonth(), "day": n.getDate() };
}
readonly property date selectedDate: new Date(page.selected.year, page.selected.month, page.selected.day)
readonly property var dayItems: {
Agenda.checkedDay;
return Agenda.forDay(page.selected.year, page.selected.month, page.selected.day);
}
property int newHour: 9
property int newMinute: 0
// the hour is freshTime's next-hour guess, not one anybody chose
property bool hourGuessed: false
// am/pm was set by hand, so a typed hour keeps to it
property bool halfPicked: false
property string newRepeat: ""
readonly property bool pm: page.newHour >= 12
readonly property int shownHour: Prefs.clock24h ? page.newHour : (page.newHour % 12 === 0 ? 12 : page.newHour % 12)
// a one-off set before now would never ring
readonly property bool past: {
Agenda.minuteKey;
return page.newRepeat === "" && page.gone(page.newHour, page.newMinute);
}
readonly property bool shown: page.visible && (!page.host || page.host.expanded)
onShownChanged: if (page.shown)
page.freshTime()
function selectToday() {
const n = Loc.now();
page.selected = { "year": n.getFullYear(), "month": n.getMonth(), "day": n.getDate() };
month.goToday();
page.freshTime();
}
// earlier than the minute we are in, on the picked day
function gone(hour, minute) {
const n = Loc.now();
return new Date(page.selected.year, page.selected.month, page.selected.day, hour, minute) < new Date(n.getFullYear(), n.getMonth(), n.getDate(), n.getHours(), n.getMinutes());
}
function isToday() {
return page.gone(0, 0) && !page.gone(23, 59);
}
// every opening of the clock starts on today, unless a reminder is half written
function startVisit() {
if (nameField.text !== "")
return ;
const n = Loc.now();
page.selected = { "year": n.getFullYear(), "month": n.getMonth(), "day": n.getDate() };
if (!month.isCurrent)
month.goToday();
page.freshTime();
}
// 9:00, or the next hour once 9:00 has gone today; a time already set for a name stays
function freshTime() {
if (nameField.text !== "")
return ;
page.newHour = 9;
page.newMinute = 0;
page.hourGuessed = false;
page.halfPicked = false;
const next = Loc.now().getHours() + 1;
if (page.gone(9, 0) && next < 24 && !page.gone(next, 0)) {
page.newHour = next;
page.hourGuessed = true;
}
}
// a typed 12-hour time means the half still to come: 12:04 at noon is pm
function preferUpcoming() {
if (Prefs.clock24h || !page.past)
return ;
const flipped = (page.newHour + 12) % 24;
if (!page.gone(flipped, page.newMinute))
page.newHour = flipped;
}
function focusName() {
nameField.input.forceActiveFocus();
}
function submit() {
const name = nameField.text.trim();
if (name === "" || page.past)
return ;
Agenda.add(page.selected.year, page.selected.month, page.selected.day, page.newHour, page.newMinute, name, page.newRepeat);
nameField.text = "";
page.newRepeat = "";
}
function step(minutes) {
page.hourGuessed = false;
let t = (page.newHour * 60 + page.newMinute + minutes + 1440) % 1440;
page.newHour = Math.floor(t / 60);
page.newMinute = t % 60;
}
// typed hours are 0-23, or 1-12 next to the am/pm toggle; on today a
// 12-hour one is its next turn, not the half the guessed hour left behind
function typeHour(h) {
page.hourGuessed = false;
if (Prefs.clock24h) {
page.newHour = Math.min(23, h);
} else if (page.isToday() && !page.halfPicked) {
// the minutes are still to come, so only an hour wholly gone is past
const h12 = Math.min(12, h) % 12;
page.newHour = page.gone(h12, 59) ? h12 + 12 : h12;
} else {
page.newHour = Math.min(12, h) % 12 + (page.pm ? 12 : 0);
page.preferUpcoming();
}
}
// minutes typed under the guessed hour mean the next time the clock shows
// them: at 12:19, 20 is 12:20 rather than 1:20
function typeMinute(m) {
page.newMinute = Math.min(59, m);
if (page.hourGuessed && page.isToday()) {
const h = Loc.now().getHours();
if (!page.gone(h, page.newMinute))
page.newHour = h;
else if (h + 1 < 24)
page.newHour = h + 1;
}
page.preferUpcoming();
}
function setPm(wantPm) {
page.hourGuessed = false;
page.halfPicked = true;
page.newHour = page.newHour % 12 + (wantPm ? 12 : 0);
}
// two digits you type into, each holding its own value until focus leaves
component TimeDigit: Item {
id: digit
property int value: 0
property int span: 60
property Item nextDigit: null
readonly property bool typing: field.activeFocus
signal typed(int v)
signal meridiem(bool wantPm)
signal accepted()
function show() {
field.text = String(digit.value).padStart(2, "0");
}
function take() {
field.forceActiveFocus();
field.selectAll();
}
function nudge(by) {
digit.typed((digit.value + by + digit.span) % digit.span);
digit.show();
}
width: Theme.dp(30)
height: Theme.dp(30)
onValueChanged: if (!field.activeFocus)
digit.show()
Component.onCompleted: digit.show()
TextInput {
id: field
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
maximumLength: 2
inputMethodHints: Qt.ImhDigitsOnly
selectByMouse: true
color: Theme.text
selectionColor: Theme.alpha(Theme.primary, 0.35)
selectedTextColor: Theme.text
font.family: Theme.fontFamily
font.pixelSize: Theme.typeSize("labelLarge")
font.variableAxes: Theme.axes(Theme.typeSize("labelLarge"), 640, 0)
font.features: ({
"tnum": 1
})
onTextEdited: {
if (field.text !== "")
digit.typed(parseInt(field.text, 10));
if (field.text.length === 2 && digit.nextDigit)
digit.nextDigit.take();
}
onActiveFocusChanged: {
if (field.activeFocus)
Qt.callLater(field.selectAll);
else
digit.show();
}
onAccepted: digit.accepted()
Keys.onUpPressed: digit.nudge(1)
Keys.onDownPressed: digit.nudge(-1)
Keys.onTabPressed: digit.nextDigit ? digit.nextDigit.take() : digit.show()
Keys.onPressed: (e) => {
const c = e.text.toLowerCase();
if (c === "a" || c === "p")
digit.meridiem(c === "p");
}
validator: RegularExpressionValidator {
regularExpression: /[0-9]{0,2}/
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - Theme.dp(6)
height: Theme.dp(2)
radius: 1
color: digit.typing ? Theme.primary : Theme.alpha(Theme.subtext, 0.4)
Behavior on color {
ColorAnimation {
duration: Theme.durFastEffects
}
}
}
}
implicitHeight: Theme.dp(404)
Connections {
function onExpandedChanged() {
if (page.host.expanded)
page.startVisit();
}
target: page.host
}
Rectangle {
id: monthCard
width: Theme.dp(312)
height: parent.height
radius: Theme.shapeXl
color: Theme.withBlur(Theme.surfaceHigh)
MonthView {
id: month
anchors.centerIn: parent
cell: Theme.dp(40)
selected: page.selected
onPicked: (y, m, d) => {
page.selected = { "year": y, "month": m, "day": d };
if (m !== month.monthIndex)
month.page(y * 12 + m > month.year * 12 + month.monthIndex ? 1 : -1);
}
}
}
Rectangle {
anchors.left: monthCard.right
anchors.leftMargin: Theme.dp(12)
anchors.right: parent.right
height: parent.height
radius: Theme.shapeXl
color: Theme.withBlur(Theme.surfaceHigh)
Item {
anchors.fill: parent
anchors.margins: Theme.dp(18)
Column {
id: dayHead
width: parent.width
spacing: 0
LText {
role: "titleLarge"
weight: 560
text: page.selectedDate.toLocaleDateString(Qt.locale(), "dddd")
}
LText {
role: "bodyMedium"
color: Theme.subtext
text: page.selectedDate.toLocaleDateString(Qt.locale(), "d MMMM yyyy") + (page.dayItems.length ? " · " + page.dayItems.length + (page.dayItems.length === 1 ? " reminder" : " reminders") : "")
}
}
Flickable {
id: list
anchors.top: dayHead.bottom
anchors.topMargin: Theme.dp(12)
anchors.bottom: form.top
anchors.bottomMargin: Theme.dp(12)
width: parent.width
contentHeight: items.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
Column {
id: items
width: list.width
spacing: Theme.dp(2)
Repeater {
model: page.dayItems
ListItem {
id: row
required property var modelData
required property int index
width: items.width
first: row.index === 0
last: row.index === page.dayItems.length - 1
containerColor: Theme.withBlur(Theme.surfaceHighest)
clickable: false
headline: row.modelData.name
supporting: Agenda.timeText(row.modelData) + (row.modelData.repeat ? " · " + Agenda.repeatText(row.modelData) : "") + (row.modelData.source === "thunderbird" ? " · " + row.modelData.calendar : "")
icon: row.modelData.source === "thunderbird" ? "event" : (row.modelData.repeat ? "repeat" : "notifications")
iconColor: Theme.primary
trailing: [
IconButton {
visible: row.modelData.source !== "thunderbird"
icon: "delete"
size: "xs"
onClicked: Agenda.remove(row.modelData.id)
}
]
}
}
}
Column {
visible: page.dayItems.length === 0
anchors.horizontalCenter: parent.horizontalCenter
y: Theme.dp(24)
spacing: Theme.dp(8)
Icon {
anchors.horizontalCenter: parent.horizontalCenter
name: "event_available"
size: Theme.dp(32)
color: Theme.subtextDim
}
LText {
anchors.horizontalCenter: parent.horizontalCenter
role: "bodyMedium"
color: Theme.subtext
text: "A free day"
}
}
}
Column {
id: form
anchors.bottom: parent.bottom
width: parent.width
spacing: Theme.dp(10)
TextField {
id: nameField
width: parent.width
label: "New reminder"
placeholder: "What should it say?"
supporting: page.past && nameField.text.trim() !== "" ? (page.gone(0, 0) && !page.gone(23, 59) ? Agenda.timeText({ "hour": page.newHour, "minute": page.newMinute }) + " has already gone by today" : "That day has already gone by") : ""
containerColor: Theme.withBlur(Theme.surfaceHighest)
onAccepted: page.submit()
}
ButtonGroup {
width: parent.width
size: "xs"
options: Agenda.repeatOptions
current: page.newRepeat
onPicked: (k) => {
return page.newRepeat = k;
}
}
Row {
width: parent.width
spacing: Theme.dp(8)
Rectangle {
id: timeBox
width: timeRow.implicitWidth + Theme.dp(20)
height: Theme.dp(36)
radius: Theme.dp(18)
color: Theme.withBlur(Theme.surfaceHighest)
Row {
id: timeRow
anchors.centerIn: parent
spacing: Theme.dp(2)
TimeDigit {
id: hourDigit
anchors.verticalCenter: parent.verticalCenter
value: page.shownHour
span: Prefs.clock24h ? 24 : 12
nextDigit: minuteDigit
onTyped: (v) => {
return page.typeHour(v);
}
onMeridiem: (wantPm) => {
return page.setPm(wantPm);
}
onAccepted: page.submit()
}
LText {
anchors.verticalCenter: parent.verticalCenter
role: "labelLarge"
weight: 640
color: Theme.subtext
text: ":"
}
TimeDigit {
id: minuteDigit
anchors.verticalCenter: parent.verticalCenter
value: page.newMinute
span: 60
onTyped: (v) => {
return page.typeMinute(v);
}
onMeridiem: (wantPm) => {
return page.setPm(wantPm);
}
onAccepted: page.submit()
}
Item {
visible: !Prefs.clock24h
anchors.verticalCenter: parent.verticalCenter
width: Theme.dp(32)
height: Theme.dp(30)
LText {
anchors.centerIn: parent
role: "labelMedium"
weight: 640
color: Theme.primary
text: page.pm ? "PM" : "AM"
}
StateLayer {
radius: Theme.rad(8)
onClicked: page.setPm(!page.pm)
}
}
}
// the wheel moves a minute at a time
WheelHandler {
onWheel: (e) => {
return page.step(e.angleDelta.y > 0 ? 1 : -1);
}
}
}
Item {
width: parent.width - timeBox.width - addBtn.width - Theme.dp(16)
height: 1
}
Button {
id: addBtn
variant: "filled"
size: "xs"
icon: "add"
text: "Add"
disabled: nameField.text.trim() === "" || page.past
onClicked: page.submit()
}
}
}
}
}
}
+28
View File
@@ -0,0 +1,28 @@
{ config, lib, pkgs, ... }:
let
specialCustom = builtins.toJSON [ {
key = "daemons";
label = "Daemons";
glyph = "build";
on = true;
apps = "";
} ];
in {
lucid.keybindSeed = ./lucid-keybinds.json;
lucid.prefsSeed = pkgs.writeText "sweetbread-lucid-prefs.json" (builtins.toJSON (
(builtins.fromJSON (builtins.readFile ./lucid-prefs.json)) // {
specialCustom = specialCustom;
}
));
lucid.startWallpaper = true;
lucid.installWallpaperSupport = true;
lucid.managedFiles = {
"${config.home.homeDirectory}/.config/quickshell/Agenda.qml" = ./Agenda.qml;
"${config.home.homeDirectory}/.config/quickshell/CalendarSource.qml" = ./CalendarSource.qml;
"${config.home.homeDirectory}/.config/quickshell/lucidbar/ClockCalendar.qml" = ./ClockCalendar.qml;
"${config.home.homeDirectory}/.config/quickshell/thunderbird-calendar.py" = ./thunderbird-calendar.py;
};
wayland.windowManager.hyprland.settings.bind = lib.mkForce [];
}
@@ -0,0 +1,76 @@
{
"version": 1,
"binds": [
{ "id": "wallpaper-picker", "keys": "SUPER + W", "desc": "Wallpaper picker", "category": "Lucid", "type": "exec", "cmd": "hyprctl eval 'hl.config({ input = { follow_mouse = 2 } })' && qs ipc call launcher wallpaper" },
{ "id": "wallpaper-random", "keys": "SUPER + SHIFT + W", "desc": "Set a random wallpaper", "category": "Desktop", "type": "exec", "cmd": "wallpaper_changer" },
{ "id": "calculator-volume", "keys": "XF86Calculator", "desc": "Open volume mixer", "category": "Apps", "type": "exec", "cmd": "ghostty --title=pulsemixer -e pulsemixer" },
{ "id": "zoom-reset-scroll-up", "keys": "SUPER + SHIFT + mouse_up", "desc": "Reset cursor zoom", "category": "Accessibility", "type": "lua", "lua": "function() hl.config({ cursor = { zoom_factor = 1 } }) end" },
{ "id": "zoom-reset-scroll-down", "keys": "SUPER + SHIFT + mouse_down", "desc": "Reset cursor zoom", "category": "Accessibility", "type": "lua", "lua": "function() hl.config({ cursor = { zoom_factor = 1 } }) end" },
{ "id": "zoom-reset-minus", "keys": "SUPER + SHIFT + minus", "desc": "Reset cursor zoom", "category": "Accessibility", "type": "lua", "lua": "function() hl.config({ cursor = { zoom_factor = 1 } }) end" },
{ "id": "zoom-reset-kp-subtract", "keys": "SUPER + SHIFT + KP_SUBTRACT", "desc": "Reset cursor zoom", "category": "Accessibility", "type": "lua", "lua": "function() hl.config({ cursor = { zoom_factor = 1 } }) end" },
{ "id": "zoom-reset-zero", "keys": "SUPER + SHIFT + 0", "desc": "Reset cursor zoom", "category": "Accessibility", "type": "lua", "lua": "function() hl.config({ cursor = { zoom_factor = 1 } }) end" },
{ "id": "zoom-in-equal", "keys": "SUPER + equal", "desc": "Zoom in", "category": "Accessibility", "type": "lua", "lua": "function() local current = hl.get_config('cursor.zoom_factor'); hl.config({ cursor = { zoom_factor = current * 1.1 } }) end", "opts": { "repeating": true } },
{ "id": "zoom-out-minus", "keys": "SUPER + minus", "desc": "Zoom out", "category": "Accessibility", "type": "lua", "lua": "function() local current = hl.get_config('cursor.zoom_factor'); hl.config({ cursor = { zoom_factor = math.max(1, current * 0.9) } }) end", "opts": { "repeating": true } },
{ "id": "zoom-in-kp-add", "keys": "SUPER + KP_ADD", "desc": "Zoom in", "category": "Accessibility", "type": "lua", "lua": "function() local current = hl.get_config('cursor.zoom_factor'); hl.config({ cursor = { zoom_factor = current * 1.1 } }) end", "opts": { "repeating": true } },
{ "id": "zoom-out-kp-subtract", "keys": "SUPER + KP_SUBTRACT", "desc": "Zoom out", "category": "Accessibility", "type": "lua", "lua": "function() local current = hl.get_config('cursor.zoom_factor'); hl.config({ cursor = { zoom_factor = math.max(1, current * 0.9) } }) end", "opts": { "repeating": true } },
{ "id": "widgets-panel", "keys": "SUPER + SHIFT + G", "desc": "Toggle widgets panel", "category": "Lucid", "type": "exec", "cmd": "qs ipc call widgets panel" },
{ "id": "launcher", "keys": "SUPER + D", "desc": "App launcher", "category": "Lucid", "type": "exec", "cmd": "hyprctl eval 'hl.config({ input = { follow_mouse = 2 } })' && qs ipc call launcher toggle" },
{ "id": "vaultwarden-password", "keys": "SUPER + X", "desc": "Type a password from Vaultwarden", "category": "Apps", "type": "exec", "cmd": "qs ipc call -- launcher search '>pass '" },
{ "id": "clipse", "keys": "SUPER + V", "desc": "Clipboard manager", "category": "Apps", "type": "exec", "cmd": "ghostty --title=clipse -e clipse" },
{ "id": "clipboard", "keys": "SUPER + SHIFT + V", "desc": "Clipboard history", "category": "Lucid", "type": "exec", "cmd": "hyprctl eval 'hl.config({ input = { follow_mouse = 2 } })' && qs ipc call launcher clipboard" },
{ "id": "screenshot", "keys": "Print", "desc": "Full screenshot", "category": "Screenshots", "type": "exec", "cmd": "qs ipc call screenshot full" },
{ "id": "screenshot-full", "keys": "SUPER + Print", "desc": "Full screenshot", "category": "Screenshots", "type": "exec", "cmd": "qs ipc call screenshot full" },
{ "id": "screenshot-region", "keys": "CTRL + Print", "desc": "Region screenshot", "category": "Screenshots", "type": "exec", "cmd": "qs ipc call snap region" },
{ "id": "screenshot-window", "keys": "ALT + Print", "desc": "Window screenshot", "category": "Screenshots", "type": "exec", "cmd": "qs ipc call snap window" },
{ "id": "terminal", "keys": "SUPER + RETURN", "desc": "Terminal", "category": "Apps", "type": "exec", "cmd": "ghostty" },
{ "id": "close-window", "keys": "SUPER + Q", "desc": "Close window", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.close()" },
{ "id": "exit-hyprland", "keys": "SUPER + M", "desc": "Exit Hyprland", "category": "Session", "type": "lua", "lua": "hl.dsp.exit()" },
{ "id": "file-manager", "keys": "SUPER + E", "desc": "File manager", "category": "Apps", "type": "exec", "cmd": "ghostty -e sh -c yazi" },
{ "id": "toggle-float", "keys": "SUPER + F", "desc": "Toggle floating window", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.float({ action = 'toggle' })" },
{ "id": "toggle-pseudo", "keys": "SUPER + P", "desc": "Toggle pseudo-tile", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.pseudo({ action = 'toggle' })" },
{ "id": "toggle-split", "keys": "SUPER + J", "desc": "Toggle split direction", "category": "Windows", "type": "lua", "lua": "hl.dsp.layout('togglesplit')" },
{ "id": "swap-split", "keys": "SUPER + K", "desc": "Swap split direction", "category": "Windows", "type": "lua", "lua": "hl.dsp.layout('swapsplit')" },
{ "id": "focus-left", "keys": "SUPER + LEFT", "desc": "Focus left", "category": "Windows", "type": "lua", "lua": "hl.dsp.focus({ direction = 'l' })" },
{ "id": "focus-right", "keys": "SUPER + RIGHT", "desc": "Focus right", "category": "Windows", "type": "lua", "lua": "hl.dsp.focus({ direction = 'r' })" },
{ "id": "focus-up", "keys": "SUPER + UP", "desc": "Focus up", "category": "Windows", "type": "lua", "lua": "hl.dsp.focus({ direction = 'u' })" },
{ "id": "focus-down", "keys": "SUPER + DOWN", "desc": "Focus down", "category": "Windows", "type": "lua", "lua": "hl.dsp.focus({ direction = 'd' })" },
{ "id": "swap-left", "keys": "SUPER + SHIFT + LEFT", "desc": "Move window left", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.swap({ direction = 'l' })" },
{ "id": "swap-right", "keys": "SUPER + SHIFT + RIGHT", "desc": "Move window right", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.swap({ direction = 'r' })" },
{ "id": "swap-up", "keys": "SUPER + SHIFT + UP", "desc": "Move window up", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.swap({ direction = 'u' })" },
{ "id": "swap-down", "keys": "SUPER + SHIFT + DOWN", "desc": "Move window down", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.swap({ direction = 'd' })" },
{ "id": "resize-left", "keys": "SUPER + CTRL + LEFT", "desc": "Resize window left", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.resize({ x = -60, y = 0, relative = true })" },
{ "id": "resize-right", "keys": "SUPER + CTRL + RIGHT", "desc": "Resize window right", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.resize({ x = 60, y = 0, relative = true })" },
{ "id": "resize-up", "keys": "SUPER + CTRL + UP", "desc": "Resize window up", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.resize({ x = 0, y = -60, relative = true })" },
{ "id": "resize-down", "keys": "SUPER + CTRL + DOWN", "desc": "Resize window down", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.resize({ x = 0, y = 60, relative = true })" },
{ "id": "workspace-focus", "keys": "SUPER + {key}", "desc": "Focus workspace {n}", "category": "Workspaces", "type": "lua", "lua": "hl.dsp.focus({ workspace = {n} })", "each": "workspace" },
{ "id": "workspace-move", "keys": "SUPER + SHIFT + {key}", "desc": "Move window to workspace {n}", "category": "Workspaces", "type": "lua", "lua": "hl.dsp.window.move({ workspace = {n}, follow = false })", "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", "type": "lua", "lua": "hl.dsp.window.fullscreen({ action = 'toggle', mode = 'fullscreen' })" },
{ "id": "workspace-next-scroll", "keys": "SUPER + mouse_down", "desc": "Next workspace", "category": "Workspaces", "type": "lua", "lua": "hl.dsp.focus({ workspace = 'e-1' })" },
{ "id": "workspace-previous-scroll", "keys": "SUPER + mouse_up", "desc": "Previous workspace", "category": "Workspaces", "type": "lua", "lua": "hl.dsp.focus({ workspace = 'e+1' })" },
{ "id": "keyboard-brightness-up", "keys": "SUPER + F3", "desc": "Keyboard backlight up", "category": "Displays", "type": "exec", "cmd": "brightnessctl -d *::kbd_backlight set +33%" },
{ "id": "keyboard-brightness-down", "keys": "SUPER + F2", "desc": "Keyboard backlight down", "category": "Displays", "type": "exec", "cmd": "brightnessctl -d *::kbd_backlight set 33%-" },
{ "id": "audio-mute", "keys": "XF86AudioMute", "desc": "Toggle audio mute", "category": "Media", "type": "exec", "cmd": "pamixer -t" },
{ "id": "microphone-mute", "keys": "XF86AudioMicMute", "desc": "Toggle microphone mute", "category": "Media", "type": "exec", "cmd": "pamixer --default-source -t" },
{ "id": "play-pause", "keys": "XF86AudioPlay", "desc": "Play or pause media", "category": "Media", "type": "exec", "cmd": "playerctl play-pause" },
{ "id": "file-manager-media", "keys": "XF86Explorer", "desc": "File manager", "category": "Apps", "type": "exec", "cmd": "ghostty -e sh -c yazi" },
{ "id": "email", "keys": "XF86Mail", "desc": "Email", "category": "Apps", "type": "exec", "cmd": "thunderbird" },
{ "id": "browser", "keys": "XF86WWW", "desc": "Web browser", "category": "Apps", "type": "exec", "cmd": "google-chrome-stable" },
{ "id": "display-brightness-down", "keys": "XF86MonBrightnessDown", "desc": "Brightness down", "category": "Displays", "type": "exec", "cmd": "brightnessctl set 5%-" },
{ "id": "display-brightness-up", "keys": "XF86MonBrightnessUp", "desc": "Brightness up", "category": "Displays", "type": "exec", "cmd": "brightnessctl set +5%" },
{ "id": "volume-up", "keys": "XF86AudioRaiseVolume", "desc": "Volume up", "category": "Media", "type": "exec", "cmd": "pamixer -i 5", "opts": { "repeating": true } },
{ "id": "volume-down", "keys": "XF86AudioLowerVolume", "desc": "Volume down", "category": "Media", "type": "exec", "cmd": "pamixer -d 5", "opts": { "repeating": true } },
{ "id": "previous-seek", "keys": "XF86AudioPrev", "desc": "Seek backward", "category": "Media", "type": "exec", "cmd": "playerctl position 5-", "opts": { "click": true } },
{ "id": "next-seek", "keys": "XF86AudioNext", "desc": "Seek forward", "category": "Media", "type": "exec", "cmd": "playerctl position 5+", "opts": { "click": true } },
{ "id": "previous-track", "keys": "XF86AudioPrev", "desc": "Previous track", "category": "Media", "type": "exec", "cmd": "playerctl previous", "opts": { "long_press": true } },
{ "id": "next-track", "keys": "XF86AudioNext", "desc": "Next track", "category": "Media", "type": "exec", "cmd": "playerctl next", "opts": { "long_press": true } },
{ "id": "move-window-drag", "keys": "SUPER + mouse:272", "desc": "Move window with mouse", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.drag()", "opts": { "mouse": true } },
{ "id": "resize-window-drag", "keys": "SUPER + mouse:273", "desc": "Resize window with mouse", "category": "Windows", "type": "lua", "lua": "hl.dsp.window.resize()", "opts": { "mouse": true } }
]
}
@@ -0,0 +1,347 @@
{
"accentPunch": 1,
"audioCombine": "",
"audioMeters": true,
"audioMoveStreams": true,
"autoColorMode": false,
"barAutoHide": false,
"barEnabled": true,
"barFullCorner": 18,
"barHeight": 35,
"barHoverGrow": 3,
"barHoverOpen": true,
"barJoinDividers": false,
"barJoins": "[]",
"barLayout": "{\"left\":[\"start\",\"workspaces\",\"media\",\"tray\"],\"center\":[\"clock\"],\"right\":[\"window\",\"notifications\",\"system\",\"privacy\",\"power\"]}",
"barMotionScale": 1.35,
"barNotchFlare": 14,
"barPopupGap": 10,
"barPopupMode": true,
"barPopupNotched": false,
"barSideMargin": 17,
"barSpacing": 8,
"barStyle": "full",
"barTopMargin": 5,
"barZoneGap": 26,
"btAgent": true,
"btReceive": true,
"btReceiveAutoPaired": false,
"btReceiveFolder": "",
"btScanOnOpen": true,
"btShowUnnamed": false,
"clipboardEnabled": true,
"clock24h": true,
"clockDateFormat": "long",
"clockShowDate": true,
"clockShowSeconds": true,
"clockShowTimer": true,
"clockShowWeather": true,
"clockStyle": "accent",
"clockWorldZones": "Europe/Berlin,Europe/Kiev,America/Chicago,Asia/Tokyo",
"cornerScale": 1,
"desktopIconSize": "medium",
"desktopIconSpacing": "normal",
"desktopIcons": false,
"desktopIconsAutoArrange": false,
"desktopIconsCorner": "left",
"desktopIconsDrives": true,
"desktopIconsHidden": false,
"desktopIconsHideExt": false,
"desktopIconsHome": false,
"desktopIconsOpen": "double",
"desktopIconsShown": true,
"desktopIconsSort": "name",
"desktopIconsSortDesc": false,
"desktopIconsSpring": true,
"desktopIconsThumbs": true,
"desktopIconsTrash": true,
"desktopKeys": "{}",
"desktopMenu": true,
"desktopSelection": true,
"dndAllowCritical": true,
"dndFullscreen": false,
"doNotDisturb": false,
"dockAutoHide": true,
"dockBottomMargin": 13,
"dockDynamicHide": true,
"dockEnabled": true,
"dockHoverEffect": 1,
"dockIconPadding": 0,
"dockIconSize": 41,
"dockIconTiles": true,
"dockMagnify": true,
"dockNotchFlare": 14,
"dockRadius": 40,
"dockShowIndicators": true,
"dockShowRunning": true,
"dockShowTooltips": true,
"dockSpacing": 12,
"dockSplitGap": 8,
"dockStyle": "notch",
"envAdopted": true,
"envAppFont": "Pixeloid Sans",
"envAppFontSize": 13,
"envApplyGtk": true,
"envApplyHypr": true,
"envApplyQt": true,
"envColorScheme": "dark",
"envCursorShadow": true,
"envCursorSize": 32,
"envCursorTheme": "Kurumi",
"envDocumentFont": "Pixeloid Sans",
"envDocumentFontSize": 13,
"envFontSync": false,
"envGtkTheme": "Adwaita",
"envIconSet": "gravity",
"envIconTheme": "Pop-Dark",
"envLegacyIcons": false,
"envMonoFont": "JetBrains Mono",
"envMonoFontSize": 13,
"envQtPlatformTheme": "",
"envQtStyle": "kvantum",
"fontFamily": "Pixeloid Sans",
"fontScale": 1,
"gameModeOffCmd": "",
"gameModeOnCmd": "",
"gameModeStatusCmd": "",
"glassApps": "vscodium",
"glassValues": "vscodium=0.9",
"gpsEnabled": false,
"hiddenThemes": "",
"hyprOptions": "{\"general.layout\":\"scrolling\"}",
"idleAdopted": true,
"idleAutostart": true,
"idleDim": true,
"idleDimAfter": 120,
"idleDimKeyboard": true,
"idleDimLevel": 10,
"idleEnabled": false,
"idleKeepAwake": false,
"idleLock": true,
"idleLockAfter": 600,
"idleLockBeforeSleep": true,
"idleRespectInhibitors": true,
"idleScreenOff": true,
"idleScreenOffAfter": 900,
"idleSuspend": false,
"idleSuspendAfter": 1800,
"idleSuspendOnAc": false,
"idleWakeAfterSleep": true,
"idleWhileMedia": true,
"jellyAmount": 1,
"jellyMotion": true,
"kdeConnectEnabled": true,
"launcherActionHints": true,
"launcherAddressFirst": true,
"launcherAppActions": true,
"launcherAppDescriptions": true,
"launcherCalculator": true,
"launcherContentScale": 0.8,
"launcherDensity": "compact",
"launcherFrequentFirst": true,
"launcherHiddenApps": "",
"launcherModeBar": true,
"launcherPowerButtons": "lock,suspend,reboot,shutdown",
"launcherPowerChips": false,
"launcherPowerSearch": true,
"launcherSearchEngine": "lair",
"launcherSearchPosition": "bottom",
"launcherSettingsResults": true,
"launcherWebRow": true,
"launcherWidth": "wide",
"launcherWindows": true,
"liveDecoder": "cuda",
"liveGameMode": "pause",
"livePauseUnder": "any",
"liveWallpapers": true,
"locationLabel": "Nizhny Novgorod, Nizhny Novgorod Oblast, Russia",
"locationLat": 56.32867,
"locationLon": 44.00205,
"locationName": "Nizhniy Novgorod",
"locationTz": "Europe/Moscow",
"matugenContrast": 0,
"matugenScheme": "scheme-tonal-spot",
"matugenSourceImage": "",
"matugenSourceIndex": 0,
"mediaArtist": true,
"mediaHideIdle": true,
"mediaPanelStyle": "side",
"mediaPlayButton": true,
"mediaStyle": "disc",
"mediaTitleWidth": 300,
"mediaWheelVolume": true,
"monitorBarScreen": "*",
"monitorDockScreen": "",
"monitorSetups": "{\"desc:Huawei Technologies Co. Inc. ZQE-CAA 0xC080F622\":{\"x\":1920,\"y\":0,\"mode\":\"3440x1440@120.00\"},\"desc:Samsung Electric Company SAMSUNG 0x01000E00\":{\"x\":0,\"y\":0,\"mode\":\"1920x1080@60.00\"}}",
"monitorShellScreen": "desc:Huawei Technologies Co. Inc. ZQE-CAA 0xC080F622",
"monitorWorkspaces": "{\"mode\":\"shared\",\"assign\":{}}",
"motionScale": 1,
"nightLight": false,
"nightLightFrom": 1260,
"nightLightSchedule": "off",
"nightLightTemp": 4000,
"nightLightTo": 420,
"notifGrouping": true,
"notifInlineReply": true,
"notifMaxHistory": 50,
"notifMutedApps": "",
"notifProgress": true,
"notifSeenApps": "AyuGram Desktop,Clock,Hyprshot,Lucid test,starship,vesktop,Vivaldi,wallpaper_changer",
"notifShowIcons": true,
"notifSound": true,
"notifSoundName": "pulse",
"notifSoundUrgentOnly": false,
"notifSoundVolume": 0.6,
"notifTimestamps": true,
"notificationsStyle": "badge",
"osdCapsLock": true,
"osdNumLock": true,
"pinBadge": "top-right",
"pinBorder": "tertiary",
"pinPulse": true,
"pomodoroAutoFocus": true,
"pomodoroAutoStart": true,
"pomodoroFocus": 25,
"pomodoroGoal": 0,
"pomodoroLong": 15,
"pomodoroRepeat": true,
"pomodoroRounds": 4,
"pomodoroShort": 5,
"pomodoroSilence": false,
"powerModuleActions": "lock,suspend,hibernate,logout,reboot,shutdown",
"powerModuleConfirm": true,
"powerModulePanelStyle": "list",
"powerModuleStyle": "icon",
"powerModuleUptime": true,
"privacyAlwaysShown": false,
"privacyStyle": "marks",
"privacyToast": true,
"privacyWatch": "mic,camera,screen",
"quietFrom": 1320,
"quietHours": false,
"quietTo": 420,
"recordCountdown": 3,
"recordEncoder": "auto",
"recordFolder": "",
"recordFps": 0,
"reminderSound": true,
"shellGap": 0,
"shotFolder": "",
"shotLoupe": true,
"shotOcrLang": "eng+rus",
"shotPreview": "preview",
"shotPreviewSeconds": 6,
"shotRoundWindows": true,
"shotSaveFile": true,
"showClock": true,
"showKbLayout": true,
"showMedia": true,
"showNotifications": true,
"showPower": true,
"showPrivacy": true,
"showStart": true,
"showSystem": true,
"showTray": true,
"showWindow": true,
"showWorkspaces": true,
"soundOnBattery": true,
"soundOnBluetooth": true,
"soundOnBrightness": true,
"soundOnCamera": true,
"soundOnCaps": true,
"soundOnCapture": true,
"soundOnCharger": true,
"soundOnMic": true,
"soundOnUsb": true,
"soundOnVolume": true,
"specialBlur": false,
"specialComms": true,
"specialCommsApps": "auto",
"specialCustom": "[]",
"specialDim": 0.2,
"specialGaps": 0,
"specialHideOnSwitch": false,
"specialKeepApps": true,
"specialMusic": true,
"specialMusicApps": "auto",
"specialScratchpad": true,
"specialSysmon": true,
"specialSysmonApps": "auto",
"specialTodo": true,
"specialTodoApps": "auto",
"startModuleStyle": "distro",
"startupApps": "[]",
"startupEnv": "[]",
"startupXdg": false,
"storageLowPercent": 10,
"storageLowWarn": true,
"storageTrashDays": 0,
"surfaceDarkness": -1,
"surfaceTint": -1,
"sysSoundVolume": 0.6,
"sysSounds": true,
"systemTiles": "dnd,awake,dark,airplane,location,mic,capture,record,picker,keyboard,timer,session,gamemode",
"themeColour": "#6750a4",
"themeOrder": "",
"timeZoneAuto": true,
"timerSound": true,
"toastBodyLines": 4,
"toastCriticalSticky": true,
"toastEnabled": true,
"toastMaxVisible": 3,
"toastOnAudio": true,
"toastOnBattery": true,
"toastOnBluetooth": true,
"toastOnCamera": true,
"toastOnDisplays": true,
"toastOnGameMode": true,
"toastOnLayout": true,
"toastOnPower": true,
"toastOnUsb": true,
"toastOnWifi": true,
"toastShowActions": true,
"toastShowBody": true,
"toastTimeout": 5,
"toastUseAppTimeout": true,
"trayHidden": "",
"trayIconColor": "original",
"trayStyle": "collapsed",
"uiScale": 1.1,
"units": "metric",
"updateBeta": false,
"updateCheck": true,
"urgentBadge": "top-left",
"urgentPulse": true,
"wallTransAngle": 45,
"wallTransBezier": ".54,0,.34,.99",
"wallTransCustomPresets": "",
"wallTransDuration": 1,
"wallTransOrigin": "center",
"wallTransPreset": "soft",
"wallTransType": "fade",
"wallTransWave": "20,20",
"wallpaperBentoHero": "medium",
"wallpaperBentoPrev": true,
"wallpaperFolder": "/home/sweetbread/Wallpapers",
"wallpaperPickerKeys": "sides",
"wallpaperPickerStyle": "strip",
"wallpaperPillsShape": "uniform",
"wallpaperStripSides": 2,
"wallpaperStripSteps": "normal",
"wallpaperTilesAspect": "square",
"wallpaperTilesRows": 2,
"weekStartMonday": true,
"widgetHideFullscreen": true,
"widgetLockAll": false,
"widgetOnTop": false,
"widgetSnap": true,
"widgetsEnabled": true,
"windowModuleMiddleClose": false,
"windowModuleScroll": true,
"windowModuleText": "title",
"windowModuleWidth": 260,
"workspacesByDisplay": true,
"workspacesShown": 1,
"workspacesStyle": "numbers",
"workspacesWheel": true
}
+13
View File
@@ -0,0 +1,13 @@
{ config, lib, ... }:
{
lucid.prefsSeed = lib.mkForce (builtins.toFile "sweetbread-rias-lucid-prefs.json" (builtins.toJSON (
(builtins.fromJSON (builtins.readFile ./lucid-prefs.json)) // {
specialCustom = "[]";
}
)));
lucid.extraHyprlandConfig = ''
require("modules.settings")
require("modules.glass")
require("modules.startup")
'';
}
@@ -0,0 +1,278 @@
#!/usr/bin/env python3
"""Read-only bridge from Thunderbird's synchronized CalDAV event cache."""
import configparser
import datetime as dt
import json
import re
import shutil
import sqlite3
import tempfile
import time
from pathlib import Path
HOME = Path.home()
DB_NAME = "cache.sqlite"
WINDOW_START = dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=30)
WINDOW_END = dt.datetime.now(dt.timezone.utc) + dt.timedelta(days=400)
def active_profile():
ini = configparser.ConfigParser(interpolation=None)
ini.read(HOME / ".thunderbird" / "profiles.ini", encoding="utf-8")
for section in ini.sections():
if not section.startswith("Profile") or ini.get(section, "Default", fallback="0") != "1":
continue
profile = Path(ini.get(section, "Path"))
if ini.get(section, "IsRelative", fallback="1") == "1":
profile = HOME / ".thunderbird" / profile
return profile
raise FileNotFoundError("No default Thunderbird profile")
def calendar_registry(profile):
prefs = profile / "prefs.js"
pattern = re.compile(r'^user_pref\("calendar\.registry\.([^.]+)\.([^\"]+)",\s*(.*)\);$')
registry = {}
if not prefs.exists():
return registry
with prefs.open(encoding="utf-8", errors="replace") as source:
for line in source:
match = pattern.match(line.strip())
if not match:
continue
calendar_id, key, raw = match.groups()
try:
value = json.loads(raw)
except json.JSONDecodeError:
continue
registry.setdefault(calendar_id, {})[key] = value
return {
calendar_id: data
for calendar_id, data in registry.items()
if data.get("type") == "caldav" and not data.get("disabled", False)
}
def stable_snapshot(database, target_dir):
"""Copy DB and WAL together; retry if Thunderbird writes during the copy."""
target = target_dir / DB_NAME
sidecars = [Path(str(database) + suffix) for suffix in ("", "-wal", "-shm")]
for _ in range(4):
before = []
for path in sidecars:
try:
stat = path.stat()
before.append((stat.st_size, stat.st_mtime_ns))
except FileNotFoundError:
before.append(None)
for path, state in zip(sidecars, before):
dest = Path(str(target) + str(path)[len(str(database)):])
if state is None:
dest.unlink(missing_ok=True)
else:
shutil.copy2(path, dest)
after = []
for path in sidecars:
try:
stat = path.stat()
after.append((stat.st_size, stat.st_mtime_ns))
except FileNotFoundError:
after.append(None)
if before == after:
return target
time.sleep(0.05)
raise RuntimeError("Thunderbird calendar cache changed during snapshot")
def parse_until(value):
if not value:
return None
value = value.strip()
try:
if re.fullmatch(r"\d{8}", value):
return dt.datetime.strptime(value, "%Y%m%d").replace(tzinfo=dt.timezone.utc)
return dt.datetime.strptime(value.rstrip("Z"), "%Y%m%dT%H%M%S").replace(tzinfo=dt.timezone.utc)
except ValueError:
return None
def parse_recurrence(text):
rules = []
excluded = set()
for line in text.replace("\r", "").split("\n"):
if line.startswith("RRULE:"):
rules.append(dict(part.split("=", 1) for part in line[6:].split(";") if "=" in part))
elif line.startswith("EXDATE") and ":" in line:
values = line.split(":", 1)[1].split(",")
for value in values:
parsed = parse_until(value)
if parsed:
excluded.add(int(parsed.timestamp() * 1000))
return rules, excluded
def month_dates(year, month, base_day, rule):
import calendar
month_days = int(calendar.monthrange(year, month)[1])
if "BYMONTHDAY" in rule:
days = []
for raw in rule["BYMONTHDAY"].split(","):
value = int(raw)
day = value if value > 0 else month_days + value + 1
if 1 <= day <= month_days:
days.append(day)
return sorted(set(days))
if "BYDAY" not in rule:
return [base_day] if base_day <= month_days else []
weekdays = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, "FR": 4, "SA": 5, "SU": 6}
result = []
for entry in rule["BYDAY"].split(","):
match = re.fullmatch(r"([+-]?\d+)?(MO|TU|WE|TH|FR|SA|SU)", entry)
if not match:
continue
ordinal, name = match.groups()
weekday = weekdays[name]
matching = [day for day in range(1, month_days + 1) if dt.date(year, month, day).weekday() == weekday]
if ordinal:
index = int(ordinal)
if index < 0:
index = len(matching) + index + 1
if 1 <= index <= len(matching):
result.append(matching[index - 1])
else:
result.extend(matching)
return sorted(set(result))
def occurrences(start, rule, excluded):
freq = rule.get("FREQ", "").upper()
interval = max(1, int(rule.get("INTERVAL", "1")))
count_limit = int(rule["COUNT"]) if rule.get("COUNT", "").isdigit() else None
until = parse_until(rule.get("UNTIL"))
candidates = []
base_date = start.date()
if freq == "DAILY":
day = base_date
while day <= WINDOW_END.date():
delta = (day - base_date).days
if delta % interval == 0:
candidates.append(dt.datetime.combine(day, start.timetz()))
day += dt.timedelta(days=1)
elif freq == "WEEKLY":
days = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, "FR": 4, "SA": 5, "SU": 6}
weekdays = sorted({days[x] for x in rule.get("BYDAY", "").split(",") if x in days}) or [base_date.weekday()]
week = base_date - dt.timedelta(days=base_date.weekday())
while week <= WINDOW_END.date():
weeks = (week - (base_date - dt.timedelta(days=base_date.weekday()))).days // 7
if weeks % interval == 0:
for weekday in weekdays:
day = week + dt.timedelta(days=weekday)
if day >= base_date:
candidates.append(dt.datetime.combine(day, start.timetz()))
week += dt.timedelta(days=7)
elif freq == "MONTHLY":
month_index = base_date.year * 12 + base_date.month - 1
last_index = WINDOW_END.year * 12 + WINDOW_END.month - 1
for index in range(month_index, last_index + 1, interval):
year, month0 = divmod(index, 12)
for day in month_dates(year, month0 + 1, base_date.day, rule):
date = dt.date(year, month0 + 1, day)
if date >= base_date:
candidates.append(dt.datetime.combine(date, start.timetz()))
elif freq == "YEARLY":
for year in range(base_date.year, WINDOW_END.year + 1, interval):
months = [int(x) for x in rule.get("BYMONTH", str(base_date.month)).split(",")]
for month in months:
for day in month_dates(year, month, base_date.day, rule):
date = dt.date(year, month, day)
if date >= base_date:
candidates.append(dt.datetime.combine(date, start.timetz()))
candidates.sort()
result = []
for ordinal, occurrence in enumerate(candidates):
if count_limit is not None and ordinal >= count_limit:
break
if until and occurrence > until:
break
stamp = int(occurrence.timestamp() * 1000)
if WINDOW_START.timestamp() * 1000 <= stamp <= WINDOW_END.timestamp() * 1000 and stamp not in excluded:
result.append(stamp)
return result
def main():
profile = active_profile()
registry = calendar_registry(profile)
database = profile / "calendar-data" / DB_NAME
events = []
if registry and database.exists():
with tempfile.TemporaryDirectory(prefix="lucid-tb-calendar-") as td:
snapshot = stable_snapshot(database, Path(td))
db = sqlite3.connect(snapshot)
db.row_factory = sqlite3.Row
recurrence = {}
for row in db.execute("SELECT cal_id, item_id, icalString FROM cal_recurrence"):
recurrence[(row["cal_id"], row["item_id"])] = parse_recurrence(row["icalString"] or "")
rows = list(db.execute(
"SELECT cal_id, id, title, event_start, event_end, recurrence_id, ical_status "
"FROM cal_events WHERE event_start IS NOT NULL"
))
overrides = set()
for row in rows:
if row["recurrence_id"] is not None:
try:
overrides.add((row["cal_id"], row["id"], int(row["recurrence_id"])))
except (TypeError, ValueError):
pass
for row in rows:
cal_id = row["cal_id"]
if cal_id not in registry or (row["ical_status"] or "").upper() == "CANCELLED":
continue
try:
start_us = int(row["event_start"])
except (TypeError, ValueError):
continue
start_ms = start_us // 1000
duration_ms = max(0, int(row["event_end"] or row["event_start"]) // 1000 - start_ms)
title = (row["title"] or "(untitled event)").strip()
cal_name = registry[cal_id].get("name") or "CalDAV"
rule_data = recurrence.get((cal_id, row["id"]))
starts = [start_ms]
if rule_data:
rules, excluded = rule_data
if rules:
base = dt.datetime.fromtimestamp(start_ms / 1000, dt.timezone.utc)
starts = occurrences(base, rules[0], excluded)
for occurrence_ms in starts:
if (cal_id, row["id"], occurrence_ms * 1000) in overrides:
continue
if not (WINDOW_START.timestamp() * 1000 <= occurrence_ms <= WINDOW_END.timestamp() * 1000):
continue
events.append({
"id": f"{cal_id}:{row['id']}:{occurrence_ms}",
"name": title,
"calendar": cal_name,
"startMs": occurrence_ms,
"endMs": occurrence_ms + duration_ms,
})
db.close()
events.sort(key=lambda event: (event["startMs"], event["name"].casefold()))
print(json.dumps({"events": events}, ensure_ascii=False, separators=(",", ":")))
if __name__ == "__main__":
try:
main()
except Exception:
# Keep refresh failures out of Lucid's console: current calendar events
# remain visible until the next successful sync.
print(json.dumps({"events": [], "error": "Thunderbird calendar cache unavailable"}))