180 lines
5 KiB
TypeScript
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;
|
|
}
|