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; }