Install lucid
This commit is contained in:
38 files changed
+6439
-70
No files matched your search
+9
-1
@@ -1,6 +1,14 @@
|
||||
{ pkgs, pkgs-pinned, host, ... }: {
|
||||
{ pkgs, pkgs-pinned, lib, osConfig, ... }: {
|
||||
imports = [
|
||||
../../modules/user/packages/cs_utils.nix
|
||||
../../modules/user/lucid
|
||||
./modules/lucid
|
||||
] ++ lib.optionals (osConfig.networking.hostName == "Impreza") [
|
||||
../../host/Impreza/modules/lucid
|
||||
] ++ lib.optionals (osConfig.networking.hostName == "Rias") [
|
||||
../../host/Rias/modules/lucid
|
||||
] ++ lib.optionals (osConfig.networking.hostName == "Eclipse") [
|
||||
../../host/Eclipse/modules/lucid
|
||||
];
|
||||
disabledModules = [
|
||||
../../modules/user/packages/art.nix
|
||||
|
||||
@@ -71,9 +71,9 @@ in {
|
||||
{ leaf = "fade"; enabled = true; speed = 7; bezier = "default"; }
|
||||
{ leaf = "workspaces"; enabled = true; speed = 6; bezier = "default"; }
|
||||
];
|
||||
|
||||
bind = [
|
||||
(hyprLua.bind "SUPER + SHIFT + S" (hyprLua.exec ''${lib.getExe pkgs.grim} -g "$(${lib.getExe pkgs.slurp})" - | ${lib.getExe pkgs.swappy} -f -''))
|
||||
];
|
||||
};
|
||||
|
||||
lucidHyprlandBinds = [
|
||||
(hyprLua.bind "SUPER + SHIFT + S" (hyprLua.exec ''${lib.getExe pkgs.grim} -g "$(${lib.getExe pkgs.slurp})" - | ${lib.getExe pkgs.swappy} -f -''))
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
source = config.lucid.upstreamSource;
|
||||
specialBindIds = [ "scratchpad" "scratchpad-stash" "special-music" "special-comms" "special-todo" "special-sysmon" "daemons-workspace" ];
|
||||
specialSeed = pkgs.writeText "lucid-special-bindings.json" (builtins.toJSON {
|
||||
version = 1;
|
||||
binds = builtins.filter (entry: builtins.elem entry.id specialBindIds)
|
||||
(builtins.fromJSON (builtins.readFile ../../../../modules/user/lucid/lucid-keybind-catalog.json)).binds;
|
||||
});
|
||||
chestBindingsScript = pkgs.writeText "lucid-chest-bindings.py" ''
|
||||
import hashlib, json, re, sys
|
||||
from collections import Counter
|
||||
source, reference, special, output = sys.argv[1:]
|
||||
binds = json.loads(source)
|
||||
refs = json.load(open(reference))["binds"]
|
||||
specials = json.load(open(special))["binds"]
|
||||
counts, result = Counter(), []
|
||||
for entry in binds:
|
||||
args = entry["_args"]
|
||||
if not 2 <= len(args) <= 3 or args[1].get("_type") != "lua-inline":
|
||||
raise ValueError(f"Unsupported Hyprland binding: {entry}")
|
||||
keys, expr = args[0], args[1]["expr"]
|
||||
opts = args[2] if len(args) == 3 else {}
|
||||
signature = json.dumps([keys.upper().replace(" ", ""), opts], sort_keys=True)
|
||||
counts[signature] += 1
|
||||
identifier = hashlib.sha256(signature.encode()).hexdigest()[:12]
|
||||
b = {"id": f"chest-{identifier}-{counts[signature]}", "keys": keys}
|
||||
match = next((r for r in refs if r.get("keys", "").upper().replace(" ", "") == keys.upper().replace(" ", "") and r.get("opts", {}) == opts), None)
|
||||
b["desc"] = match.get("desc", keys) if match else keys
|
||||
b["category"] = match.get("category", "Other") if match else "Other"
|
||||
workspace = re.search(r"workspace = (\d+)", expr)
|
||||
if workspace and expr.startswith("hl.dsp.focus("):
|
||||
b.update(desc=f"Focus workspace {workspace.group(1)}", category="Workspaces")
|
||||
elif workspace and expr.startswith("hl.dsp.window.move("):
|
||||
b.update(desc=f"Move window to workspace {workspace.group(1)}", category="Workspaces")
|
||||
if keys.upper().replace(" ", "") == "SUPER+SHIFT+S": b.update(desc="Region screenshot and edit", category="Screenshots")
|
||||
m = re.fullmatch(r'hl\.dsp\.exec_cmd\(("(?:\\.|[^"\\])*")\)\s*', expr, re.S)
|
||||
if m:
|
||||
cmd = json.loads(m.group(1))
|
||||
cmd = re.sub(r'/nix/store/[0-9a-z]{32}-[^/\s"\\]+/bin/([A-Za-z0-9_.+-]+)', r'\1', cmd)
|
||||
b.update(type="exec", cmd=cmd)
|
||||
else: b.update(type="lua", lua=expr)
|
||||
if opts: b["opts"] = opts
|
||||
result.append(b)
|
||||
json.dump({"version": 1, "binds": result + specials}, open(output, "w"), indent=2)
|
||||
'';
|
||||
chestSeed = pkgs.runCommand "lucid-chest-bindings.json" { nativeBuildInputs = [ pkgs.python3 ]; } ''
|
||||
python3 ${chestBindingsScript} ${lib.escapeShellArg (builtins.toJSON config.lucidHyprlandBinds)} \
|
||||
${../../../../modules/user/lucid/lucid-keybind-catalog.json} ${specialSeed} "$out"
|
||||
'';
|
||||
chestBinds = pkgs.runCommand "lucid-chest-loader.lua" { nativeBuildInputs = [ pkgs.python3 ]; } ''
|
||||
python3 - ${source}/support/hypr/modules/binds.lua "$out" <<'PY'
|
||||
import pathlib, sys
|
||||
source = pathlib.Path(sys.argv[1]).read_text()
|
||||
old = '{ id = "terminal", keys = "F9", type = "exec", cmd = "kitty" }'
|
||||
new = '{ id = "terminal", keys = "SUPER + RETURN", type = "exec", cmd = "ghostty" }'
|
||||
if source.count(old) != 1: raise SystemExit("Lucid emergency bindings changed")
|
||||
pathlib.Path(sys.argv[2]).write_text(source.replace(old, new, 1))
|
||||
PY
|
||||
'';
|
||||
chestMonitors = pkgs.runCommand "lucid-chest-monitors.lua" { nativeBuildInputs = [ pkgs.python3 ]; } ''
|
||||
python3 - ${source}/support/hypr/modules/monitors.lua "$out" <<'PY'
|
||||
import pathlib, sys
|
||||
source = pathlib.Path(sys.argv[1]).read_text()
|
||||
rule = """hl.monitor({
|
||||
output = "",
|
||||
mode = M.defaults.mode,
|
||||
position = M.defaults.position,
|
||||
scale = M.defaults.scale,
|
||||
})
|
||||
"""
|
||||
if source.count(rule) != 1: raise SystemExit("Lucid monitor defaults changed")
|
||||
pathlib.Path(sys.argv[2]).write_text(source.replace(rule, "", 1))
|
||||
PY
|
||||
'';
|
||||
chestBorders = pkgs.runCommand "lucid-chest-decorations.lua" { nativeBuildInputs = [ pkgs.python3 ]; } ''
|
||||
python3 - ${source}/support/hypr/modules/decorations.lua "$out" <<'PY'
|
||||
import pathlib, sys
|
||||
source = pathlib.Path(sys.argv[1]).read_text()
|
||||
marker = "local active_border, inactive_border = borders()"
|
||||
if source.count(marker) != 1: raise SystemExit("Lucid border module changed")
|
||||
pathlib.Path(sys.argv[2]).write_text(source.split(marker, 1)[0])
|
||||
PY
|
||||
'';
|
||||
chestPrefs = pkgs.writeText "lucid-chest-prefs.json" (builtins.toJSON (
|
||||
(builtins.fromJSON (builtins.readFile "${source}/defaults/prefs.json")) // {
|
||||
specialCustom = builtins.toJSON [ { key = "daemons"; label = "Daemons"; glyph = "build"; on = true; apps = ""; } ];
|
||||
}
|
||||
));
|
||||
in
|
||||
{
|
||||
lucid.keybindSeed = chestSeed;
|
||||
lucid.prefsSeed = chestPrefs;
|
||||
lucid.bindsModule = chestBinds;
|
||||
lucid.luaModules = {
|
||||
"modules/decorations.lua" = lib.mkForce chestBorders;
|
||||
"modules/monitors.lua" = lib.mkForce { content = chestMonitors; autoLoad = false; };
|
||||
"modules/glass.lua" = { content = "${source}/support/hypr/modules/glass.lua"; autoLoad = false; };
|
||||
"modules/settings.lua" = { content = "${source}/support/hypr/modules/settings.lua"; autoLoad = false; };
|
||||
"modules/startup.lua" = { content = "${source}/support/hypr/modules/startup.lua"; autoLoad = false; };
|
||||
};
|
||||
lucid.extraHyprlandConfig = ''
|
||||
require("modules.monitors")
|
||||
require("modules.settings")
|
||||
require("modules.glass")
|
||||
require("modules.startup")
|
||||
'';
|
||||
lucid.shellFile = pkgs.writeText "lucid-chest-shell.qml" (builtins.replaceStrings [
|
||||
''import "./lucidlock"''
|
||||
" Lock {\n }\n"
|
||||
" void Clip.probed;"
|
||||
] [
|
||||
""
|
||||
""
|
||||
" void Notifs.count;\n void KdeConnect.installed;\n void HyprConfig.moduleProbed;\n void Monitors.moduleProbed;\n void Glass.moduleProbed;\n void Startup.moduleProbed;\n void Clip.probed;"
|
||||
] (builtins.readFile ../../../../modules/user/lucid/lucid-shell.qml));
|
||||
|
||||
lucid.prefsFilter = ''.barEnabled = true | .widgetOnTop = false | .widgetHideFullscreen = true | .fontFamily = $font | .liveWallpapers = false | .idleLock = false | .idleLockBeforeSleep = false | .launcherPowerButtons = "suspend,reboot,shutdown" | .powerModuleActions = "suspend,hibernate,logout,reboot,shutdown"'';
|
||||
|
||||
home.packages = with pkgs; [ brightnessctl playerctl slurp swappy ];
|
||||
|
||||
wayland.windowManager.hyprland.settings = {
|
||||
bind = lib.mkForce [];
|
||||
window_rule = [
|
||||
{
|
||||
name = "float-lucid-settings";
|
||||
match = { class = "org.quickshell"; title = "Lucid Settings"; };
|
||||
float = true;
|
||||
size = [ 1180 800 ];
|
||||
center = true;
|
||||
}
|
||||
{
|
||||
name = "float-lucid-choosers";
|
||||
match = {
|
||||
class = "^(org\\.gnome\\.Nautilus|xdg-desktop-portal-gtk|xdg-desktop-portal-gnome|zenity|org\\.kde\\.kdialog|kdialog)$";
|
||||
title = "^(Choose an account picture|Choose a template|Choose a colour scheme|Export the palette|Save received files to|Send files|Send to .+)$";
|
||||
};
|
||||
float = true;
|
||||
center = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
home.activation.installChestLucidFixes = lib.hm.dag.entryAfter [ "installLucid" ] ''
|
||||
target="${config.home.homeDirectory}/.config/quickshell"
|
||||
lock_qml="$target/Lockscreen.qml"
|
||||
if ! grep -Fq 'return; // Lucid lock disabled for chest' "$lock_qml"; then
|
||||
grep -Fq 'function lock(settle) {' "$lock_qml" && \
|
||||
grep -Fq 'function pushLockedHint() {' "$lock_qml" && \
|
||||
grep -Fq 'id: logindWatch' "$lock_qml" || {
|
||||
echo "Lucid lock integration changed; refusing to leave it active for chest" >&2
|
||||
exit 1
|
||||
}
|
||||
$DRY_RUN_CMD sed -i \
|
||||
-e '/function lock(settle) {/a\ return; // Lucid lock disabled for chest' \
|
||||
-e '/function pushLockedHint() {/a\ return; // Lucid lock disabled for chest' \
|
||||
-e '/id: logindWatch/{n;n;s/running: true/running: false/;}' "$lock_qml"
|
||||
fi
|
||||
if ! grep -Fq 'id: chestBrightnessPoll' "$target/lucidosd/Osd.qml"; then
|
||||
$DRY_RUN_CMD ${pkgs.patch}/bin/patch --batch --forward -p1 -d "$target" < ${pkgs.writeText "lucid-chest-osd.patch" ''
|
||||
--- a/lucidosd/Osd.qml
|
||||
+++ b/lucidosd/Osd.qml
|
||||
@@ -26 +26 @@
|
||||
- readonly property int brightnessPercent: osdWindow.maxBrightness > 0 ? Math.round((parseInt(brightnessFile.text()) / osdWindow.maxBrightness) * 100) : 0
|
||||
+ property int brightnessPercent: 0
|
||||
@@ -378,2 +378,15 @@
|
||||
onFileChanged: reload()
|
||||
+ onLoaded: {
|
||||
+ const value = parseInt(brightnessFile.text());
|
||||
+ if (osdWindow.maxBrightness > 0 && !isNaN(value))
|
||||
+ osdWindow.brightnessPercent = Math.round(value / osdWindow.maxBrightness * 100);
|
||||
+ }
|
||||
}
|
||||
+
|
||||
+ Timer {
|
||||
+ id: chestBrightnessPoll
|
||||
+ interval: 500
|
||||
+ repeat: true
|
||||
+ running: osdWindow.backlightDevice !== ""
|
||||
+ onTriggered: brightnessFile.reload()
|
||||
+ }
|
||||
''}
|
||||
fi
|
||||
'';
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{ pkgs, config, lib, osConfig, inputs, ... }: let
|
||||
mikuFrost = osConfig.networking.hostName == "Eclipse";
|
||||
mikuFrost = builtins.elem osConfig.networking.hostName [ "Eclipse" "Impreza" ];
|
||||
colors = config.lib.stylix.colors.withHashtag;
|
||||
accent = colors.${config.stylix.accent};
|
||||
# Render the same Stylix templates, changing only native translucency and accent.
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
];
|
||||
|
||||
programs.zsh.enable = true;
|
||||
programs.kdeconnect.enable = true;
|
||||
home-manager.users.${name}.services.kdeconnect.enable = true;
|
||||
|
||||
users = {
|
||||
defaultUserShell = pkgs.zsh;
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
{ pkgs, ... }: {
|
||||
{ pkgs, lib, osConfig, ... }: {
|
||||
imports = [
|
||||
../../modules/user/lucid
|
||||
./modules/lucid
|
||||
] ++ lib.optionals (osConfig.networking.hostName == "Rias") [
|
||||
../../host/Rias/modules/lucid
|
||||
./modules/lucid/rias.nix
|
||||
];
|
||||
|
||||
programs.hyprlock.enable = true;
|
||||
home.packages = with pkgs; [
|
||||
android-studio
|
||||
|
||||
@@ -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}"))
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"}))
|
||||
Reference in new issue
Block a user