jroshell/widget/Bar/DateTime.tsx
2026-06-08 18:22:05 +02:00

180 lines
5 KiB
TypeScript

import { Gtk } from "ags/gtk4";
import GLib from "gi://GLib";
import Gio from "gi://Gio";
import { watchFile } from "../../lib/fileMonitor";
// ---------------------------------------------------------------------------
// Thunderbird calendar database
// ---------------------------------------------------------------------------
const THUNDERBIRD_PROFILE = "tjle5dz0.default-release";
const DB_PATH = GLib.build_filenamev([
GLib.get_home_dir(),
".thunderbird",
THUNDERBIRD_PROFILE,
"calendar-data",
"local.sqlite",
]);
/**
* Query the Thunderbird local.sqlite for distinct days (1-31) that have at
* least one event in the given month.
*
* Thunderbird stores `event_start` / `event_end` as Unix timestamps in
* **microseconds**, so we multiply JS millisecond timestamps by 1000.
*/
function queryEventDays(year: number, month: number): number[] {
// month is 0-based (GTK Calendar convention)
const monthStart = Date.UTC(year, month, 1) * 1000; // ms → μs
const monthEnd = Date.UTC(year, month + 1, 1) * 1000;
const sql = [
"SELECT DISTINCT",
" CAST(strftime('%d', event_start / 1000000, 'unixepoch') AS INTEGER) AS day",
"FROM cal_events",
`WHERE event_start >= ${monthStart}`,
` AND event_start < ${monthEnd}`,
"ORDER BY day;",
].join(" ");
try {
const proc = Gio.Subprocess.new(
["sqlite3", DB_PATH, sql],
Gio.SubprocessFlags.STDOUT_PIPE |
Gio.SubprocessFlags.STDERR_PIPE,
);
const [, stdout] = proc.communicate(null, null);
if (!proc.get_successful()) return [];
const output = new TextDecoder().decode(stdout);
return output
.trim()
.split("\n")
.filter((l) => l !== "")
.map(Number);
} catch (e) {
console.error("Failed to query calendar events:", e);
return [];
}
}
// ---------------------------------------------------------------------------
// Clock helper
// ---------------------------------------------------------------------------
function formatTime(dt: GLib.DateTime): string {
return dt.format("%H:%M:%S");
}
function formatDate(dt: GLib.DateTime): string {
return dt.format("%d.%m. %a");
}
// ---------------------------------------------------------------------------
// Widget
// ---------------------------------------------------------------------------
export default function DateTime() {
const root = new Gtk.Box({ cssClasses: ["datetime-bar"] });
// -- Clock labels --
const dateLabel = new Gtk.Label({
cssClasses: ["clock-date"],
halign: Gtk.Align.CENTER,
});
const timeLabel = new Gtk.Label({
cssClasses: ["clock-time"],
halign: Gtk.Align.CENTER,
});
const clockBox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
halign: Gtk.Align.CENTER,
valign: Gtk.Align.CENTER,
});
clockBox.append(dateLabel);
clockBox.append(timeLabel);
// -- Button that contains the clock --
const button = new Gtk.Button({ cssClasses: ["datetime-btn"] });
button.set_child(clockBox);
root.append(button);
// -- Calendar (inside popover) --
const calendar = new Gtk.Calendar({
vexpand: true,
hexpand: true,
});
calendar.add_css_class("calendar-widget");
function refreshMarks() {
const y = calendar.get_year();
const m = calendar.get_month(); // 0-11
calendar.clear_marks();
const days = queryEventDays(y, m);
for (const d of days) {
calendar.mark_day(d);
}
}
// Re-fetch marks whenever the user navigates to a different month
calendar.connect("next-month", refreshMarks);
calendar.connect("prev-month", refreshMarks);
calendar.connect("next-year", refreshMarks);
calendar.connect("prev-year", refreshMarks);
// -- Popover --
const popover = new Gtk.Popover({
autohide: true,
has_arrow: true,
cssClasses: ["calendar-popover"],
});
popover.set_parent(button);
popover.set_child(calendar);
button.connect("clicked", () => {
if (popover.is_visible()) {
popover.popdown();
} else {
refreshMarks(); // ensure fresh data on open
popover.popup();
}
});
// -- Clock timer (1 s) --
let clockTimer = 0;
function updateClock(): boolean {
const now = GLib.DateTime.new_now_local();
timeLabel.label = formatTime(now);
dateLabel.label = formatDate(now);
return GLib.SOURCE_CONTINUE;
}
updateClock(); // initial paint
clockTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, updateClock);
// -- Watch the SQLite DB for external changes (Thunderbird writes) --
watchFile(DB_PATH, refreshMarks, root);
// -- Initial event marks --
refreshMarks();
// -- Cleanup --
root.connect("destroy", () => {
if (clockTimer) GLib.Source.remove(clockTimer);
});
return root;
}