2 * Universal Calendar Applet Core
3 * ==============================
5 * This is the main entry point for the Cinnamon Calendar Applet.
6 * It orchestrates all components and manages the complete UI lifecycle.
8 * IMPORTANT: This file is COMPLETELY DIFFERENT from the simplified
9 * documentation version previously created. This is the ACTUAL production code.
11 * ------------------------------------------------------------------
12 * ARCHITECTURE OVERVIEW:
13 * ------------------------------------------------------------------
14 * This applet follows a composite MVC architecture:
17 * - EventManager: Fetches calendar data from Evolution Data Server (EDS)
18 * - CalendarLogic: Calculates dates, holidays, and business logic
21 * - CalendarView: Main calendar grid (month/year/day views)
22 * - EventListView: Sidebar event list with scrollable agenda
23 * - Header/Footer: Additional UI components for navigation
25 * 3. CONTROLLER LAYER:
26 * - This class (UniversalCalendarApplet): Coordinates everything
27 * - Settings system, hotkeys, UI layout, signal routing
29 * ------------------------------------------------------------------
30 * VISUAL LAYOUT STRUCTURE:
31 * ------------------------------------------------------------------
32 * The applet uses a sophisticated two-column layout:
34 * ┌─────────────────────────────────────────────────────────────┐
35 * │ [Event List View] │ [Calendar View + Header + Footer] │
36 * │ (Left Column) │ (Right Column) │
37 * │ • Scrollable event list│ • Date header │
38 * │ • Clickable events │ • Month grid │
39 * │ • Date navigation │ • Year view │
41 * │ │ • Footer buttons │
42 * └─────────────────────────────────────────────────────────────┘
44 * @author Arnold Schiller <calendar@projektit.de>
45 * @link https://github.com/ArnoldSchiller/calendar
46 * @link https://projektit.de/kalender
47 * @license GPL-3.0-or-later
51 * @brief Main entry point for the Cinnamon Calendar Applet
53 * @details This file implements the UniversalCalendarApplet class which acts as the
54 * central controller in the MVC architecture. It orchestrates all components and
55 * manages the complete UI lifecycle.
57 * @author Arnold Schiller <calendar@projektit.de>
59 * @copyright GPL-3.0-or-later
67/* ================================================================
68 * CINNAMON / GJS IMPORTS
69 * ================================================================
71 * GJS (GNOME JavaScript) provides bindings to Cinnamon's native APIs.
72 * These are NOT npm packages - they're loaded at runtime by Cinnamon.
75const GLib = imports.gi.GLib; // Low-level GLib utilities (timers, file ops)
76const St = imports.gi.St; // Shell Toolkit (UI widgets)
77const Applet = imports.ui.applet; // Base applet classes
78const PopupMenu = imports.ui.popupMenu; // Popup menu system
79const Settings = imports.ui.settings; // User settings persistence
80const Main = imports.ui.main; // Main Cinnamon UI manager
81const Util = imports.misc.util; // Utility functions (spawn commands)
82const FileUtils = imports.misc.fileUtils; // File system utilities
83const Gettext = imports.gettext; // Internationalization (i18n)
84const Gtk = imports.gi.Gtk; // GTK for file dialogs (ICS import)
85const Gio = imports.gi.Gio; // GIO for file operations
87/* ================================================================
88 * MODULE IMPORTS (TypeScript/ES6 style)
89 * ================================================================
91 * These are local project modules. During TypeScript compilation,
92 * they're bundled together. At runtime, they're available globally.
95import { EventManager } from './EventManager';
96import { EventListView } from './EventListView';
97import { CalendarLogic } from './CalendarLogic';
99/* ================================================================
100 * INTERNATIONALIZATION (i18n) SETUP
101 * ================================================================
103 * Cinnamon applets use Gettext for translations. This system:
104 * 1. Looks for translations in the applet's locale/ directory
105 * 2. Falls back to Cinnamon's system translations
106 * 3. Falls back to GNOME Calendar translations
108 * This maximizes translation coverage with minimal effort.
112 * Global translation function.
113 * Must be initialized by setupLocalization() before use.
115let _: (str: string) => string;
118 * Initializes the translation system for this applet instance.
120 * @param uuid - Unique identifier of the applet (e.g., "calendar@projektit.de")
121 * @param path - Filesystem path to the applet directory
123function setupLocalization(uuid: string, path: string) {
124 // Bind the applet's translation domain
125 Gettext.bindtextdomain(uuid, path + "/locale");
127 // Create translation function with fallback chain
128 _ = function(str: string) {
129 // 1. Try applet-specific translations
130 let custom = Gettext.dgettext(uuid, str);
131 if (custom !== str) return custom;
133 // 2. Try Cinnamon core translations
134 let cinnamon = Gettext.dgettext("cinnamon", str);
135 if (cinnamon !== str) return cinnamon;
137 // 3. Fall back to GNOME Calendar translations
138 return Gettext.dgettext("gnome-calendar", str);
142/* ================================================================
144 * ================================================================
146 * This is the central controller class that Cinnamon instantiates.
147 * One instance exists for each panel placement of the applet.
149 * Extends TextIconApplet which supports both text label and icon
150 * in the Cinnamon panel.
153 * @class UniversalCalendarApplet
154 * @extends Applet.TextIconApplet
155 * @brief Main applet controller class
157 * @details This class is instantiated by Cinnamon when the applet is loaded.
159 * - Component initialization and wiring
160 * - Settings management
161 * - UI layout assembly
162 * - Signal routing between components
163 * - Hotkey and panel integration
166class UniversalCalendarApplet extends Applet.TextIconApplet {
167 /* ============================================================
168 * PUBLIC PROPERTIES (Accessed by other components)
169 * ============================================================
173 * Reference to the main calendar grid UI component.
174 * CalendarView is loaded dynamically at runtime.
176 public CalendarView: any;
179 * Manages all calendar event data (fetching, filtering, caching).
180 * Connected to Evolution Data Server (EDS) via DBus.
182 public eventManager: EventManager;
185 * Displays events in a list/agenda format (left sidebar).
187 public eventListView: EventListView;
190 * Pure business logic for date calculations and holiday detection.
191 * No UI dependencies, no I/O operations.
193 public CalendarLogic: CalendarLogic;
196 * The popup menu that contains the entire calendar UI.
197 * TypeScript declaration - actual initialization is in constructor.
199 declare public menu: any;
201 /* ============================================================
202 * PRIVATE PROPERTIES (Internal implementation)
203 * ============================================================
207 * Manages the popup menu lifecycle and state.
209 private menuManager: any;
212 * Handles persistence of user settings (panel icon, formats, etc.).
214 private settings: any;
217 * ID of the periodic update timer. Used for cleanup.
219 private _updateId: number = 0;
222 * Unique identifier for this applet instance.
223 * Used for settings keys and hotkey registration.
225 private uuid: string;
227 /* ============================================================
228 * UI ELEMENTS (Header Section)
229 * ============================================================
231 * The header displays current date information and acts as a
232 * "home" button to return to today's date.
236 * Main vertical container for the entire popup UI.
238 private _mainBox: any;
241 * Horizontal layout bridge that holds left (events) and right (calendar) columns.
243 private _contentLayout: any;
246 * Day of week label (e.g., "Monday").
248 private _dayLabel: any;
251 * Date label (e.g., "1. January 2026").
253 private _dateLabel: any;
256 * Holiday label (shows holiday names when applicable).
258 private _holidayLabel: any;
260 /* ============================================================
261 * SETTINGS PROPERTIES (Bound to UI settings)
262 * ============================================================
264 * These properties are automatically synchronized with the
265 * Cinnamon settings system via bind() calls.
269 * Whether to show the calendar icon in the panel.
271 public showIcon: boolean = false;
274 * Whether to show the event list sidebar.
276 public showEvents: boolean = true;
279 * Whether to display ISO week numbers in the month grid.
281 public showWeekNumbers: boolean = false;
284 * Whether to use custom date/time formats.
286 public useCustomFormat: boolean = false;
289 * Custom format string for panel label (uses GLib.DateTime format).
291 public customFormat: string = "";
294 * Custom format string for panel tooltip.
296 public customTooltipFormat: string = "";
299 * Global hotkey to open the calendar popup.
301 public keyOpen: string = "";
303 /* ============================================================
305 * ============================================================
307 * Called by Cinnamon when the applet is loaded into the panel.
308 * Initializes ALL components and builds the complete UI hierarchy.
310 * The constructor is organized in clear phases:
311 * 1. Backend initialization (settings, managers, logic)
312 * 2. UI construction (layout, components, wiring)
313 * 3. Signal connections and final setup
316 * @brief Constructs the UniversalCalendarApplet
318 * @param metadata Applet metadata from Cinnamon
319 * @param orientation Panel orientation (horizontal/vertical)
320 * @param panelHeight Height of the panel in pixels
321 * @param instanceId Unique instance identifier
323 * @note Called automatically by Cinnamon when the applet is loaded.
324 * The constructor is organized in clear phases:
325 * 1. Backend initialization
327 * 3. Signal connections
330 constructor(metadata: any, orientation: any, panel_height: number, instance_id: number) {
331 // Call parent constructor (TextIconApplet)
332 super(orientation, panel_height, instance_id);
334 // Store applet identifier for settings and hotkeys
335 this.uuid = metadata.uuid;
337 // Initialize translation system
338 setupLocalization(this.uuid, metadata.path);
341 /* ====================================================
342 * PHASE 1: BACKEND INITIALIZATION
343 * ==================================================== */
345 // 1.1 Settings system - persists user preferences
346 this.settings = new Settings.AppletSettings(this, this.uuid, instance_id);
348 // 1.2 Menu manager - handles popup menu lifecycle
349 this.menuManager = new PopupMenu.PopupMenuManager(this);
351 // 1.3 Core business components
352 this.eventManager = new EventManager();
353 this.eventListView = new EventListView();
354 this.CalendarLogic = new CalendarLogic(metadata.path);
356 // 1.4 Dynamic component loading
357 // CalendarView is loaded from a separate file at runtime
358 const CalendarModule = FileUtils.requireModule(metadata.path + '/CalendarView');
360 /* ====================================================
361 * PHASE 2: SETTINGS BINDING
362 * ====================================================
364 * Connect settings UI to internal properties.
365 * When a setting changes, the corresponding callback is triggered.
368 this.settings.bind("show-icon", "showIcon", this.on_settings_changed);
369 this.settings.bind("show-events", "showEvents", this.on_settings_changed);
370 this.settings.bind("show-week-numbers", "showWeekNumbers", this.on_settings_changed);
371 this.settings.bind("use-custom-format", "useCustomFormat", this.on_settings_changed);
372 this.settings.bind("custom-format", "customFormat", this.on_settings_changed);
373 this.settings.bind("custom-tooltip-format", "customTooltipFormat", this.on_settings_changed);
374 this.settings.bind("keyOpen", "keyOpen", this.on_hotkey_changed);
376 /* ====================================================
377 * PHASE 3: POPUP MENU CONSTRUCTION
378 * ==================================================== */
380 // Create the popup menu that will host our calendar UI
381 this.menu = new Applet.AppletPopupMenu(this, orientation);
382 this.menuManager.addMenu(this.menu);
384 /* ====================================================
385 * PHASE 4: UI CONSTRUCTION
386 * ====================================================
388 * The UI is built as a hierarchical tree of St widgets.
389 * This is the most complex part of the constructor.
392 // 4.1 Main vertical container (root of our UI)
393 this._mainBox = new St.BoxLayout({
395 style_class: 'calendar-main-box'
401 * Displays current day/date and acts as a "Home" button.
402 * Clicking it returns to today's date in the calendar.
405 * ┌─────────────────────────────┐
406 * │ Monday │ ← Day label
407 * │ 1. January 2026 │ ← Date label
408 * │ New Year's Day │ ← Holiday (optional)
409 * └─────────────────────────────┘
412 let headerBox = new St.BoxLayout({
414 style_class: 'calendar-today-home-button',
415 reactive: true // Makes it clickable
418 // Click handler: Return to today's date
419 headerBox.connect("button-release-event", () => {
420 this.CalendarView.resetToToday();
421 this.setHeaderDate(new Date());
424 // Create header labels
425 this._dayLabel = new St.Label({ style_class: 'calendar-today-day-label' });
426 this._dateLabel = new St.Label({ style_class: 'calendar-today-date-label' });
427 this._holidayLabel = new St.Label({ style_class: 'calendar-today-holiday' });
429 // Add labels to header
430 headerBox.add_actor(this._dayLabel);
431 headerBox.add_actor(this._dateLabel);
432 headerBox.add_actor(this._holidayLabel);
437 * The main calendar component (month/year/day views).
438 * Loaded dynamically from CalendarView module.
440 this.CalendarView = new CalendarModule.CalendarView(this);
443 * SIGNAL CONNECTION: Event List → Calendar Navigation
444 * ----------------------------------------------------
445 * When a user clicks an event in the list view,
446 * the calendar should jump to that event's date.
448 * This creates navigation flow between components.
450 this.eventListView.connect('event-clicked', (actor: any, ev: any) => {
451 if (ev && ev.start) {
452 // 1. Jump calendar to the event's date
453 this.CalendarView.jumpToDate(ev.start);
455 // 2. Update header to show the event's date
456 this.setHeaderDate(ev.start);
463 * System management buttons at the bottom.
465 let footerBox = new St.BoxLayout({ style_class: 'calendar-footer' });
467 // Button: Open Cinnamon's date/time settings
468 let settingsBtn = new St.Button({
469 label: _("Date and Time Settings"),
470 style_class: 'calendar-footer-button',
473 settingsBtn.connect("clicked", () => {
475 Util.spawnCommandLine("cinnamon-settings calendar");
478 // Button: Open calendar management
479 let calendarBtn = new St.Button({
480 label: _("Manage Calendars"),
481 style_class: 'calendar-footer-button',
484 calendarBtn.connect("clicked", () => {
487 const currentDate = this.CalendarView.getCurrentlyDisplayedDate();
488 const epoch = Math.floor(currentDate.getTime() / 1000);
490 // Try to open via calendar:// URI (XDG standard)
492 Util.spawnCommandLine(`xdg-open calendar:///?startdate=${epoch}`);
494 // Fallback to GNOME Calendar if URI fails
495 Util.spawnCommandLine(`gnome-calendar --date=${epoch}`);
499 // Add buttons to footer
500 footerBox.add_actor(settingsBtn);
501 footerBox.add_actor(calendarBtn);
506 * Assemble the two-column layout:
508 * ┌─────────────────────────────────────────────┐
509 * │ EventListView │ Header + Calendar + Footer │
510 * │ (Left Column) │ (Right Column) │
511 * └─────────────────────────────────────────────┘
514 // 1. Create right column (traditional calendar view)
515 let rightColumn = new St.BoxLayout({
517 style_class: 'calendar-right-column'
519 rightColumn.add_actor(headerBox);
520 rightColumn.add_actor(this.CalendarView.actor);
521 rightColumn.add_actor(footerBox);
523 // 2. Create horizontal bridge container
524 this._contentLayout = new St.BoxLayout({
525 vertical: false, // Side-by-side layout
526 style_class: 'calendar-content-layout'
529 // 3. Add left wing (events) and right column (calendar)
530 this._contentLayout.add_actor(this.eventListView.actor);
531 this._contentLayout.add_actor(rightColumn);
533 // 4. Final assembly: Add everything to main container
534 this._mainBox.add_actor(this._contentLayout);
536 // 5. Add main container to popup menu
537 this.menu.addActor(this._mainBox);
539 /* ====================================================
540 * PHASE 5: INITIALIZATION AND SIGNAL SETUP
541 * ==================================================== */
543 // Apply initial settings
544 this.on_settings_changed();
545 this.on_hotkey_changed();
548 * MENU OPEN/CLOSE HANDLING
549 * -------------------------
551 * 1. Refresh calendar display
552 * 2. Update header to current date
553 * 3. Focus calendar for keyboard navigation
555 this.menu.connect("open-state-changed", (menu: any, isOpen: boolean) => {
557 this.CalendarView.render();
558 this.setHeaderDate(new Date());
560 // Small delay to ensure UI is ready before focusing
561 GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => {
562 this.CalendarView.actor.grab_key_focus();
568 // Initial panel label/tooltip update
569 this.update_label_and_tooltip();
571 // Start periodic updates (every 10 seconds)
572 this._updateId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 10, () => {
573 this.update_label_and_tooltip();
574 return true; // Continue timer
578 // Log critical initialization failures
579 global.log(`[${this.uuid}] CRITICAL: Initialization failed: ${e}`);
583 /* ============================================================
584 * SETTINGS CHANGE HANDLER
585 * ============================================================
587 * Called automatically when ANY bound setting changes.
588 * Updates UI elements to reflect new settings.
591 on_settings_changed() {
592 // Toggle panel icon visibility
594 this.set_applet_icon_name("office-calendar");
595 if (this._applet_icon_box) this._applet_icon_box.show();
600 // Toggle event list visibility (left sidebar)
601 if (this.eventListView) {
602 if (this.showEvents) {
603 this.eventListView.actor.show();
605 this.eventListView.actor.hide();
609 // Update panel label and tooltip
610 this.update_label_and_tooltip();
612 // If menu is open, re-render to reflect format changes
613 if (this.menu && this.menu.isOpen) {
614 this.CalendarView.render();
618 /* ============================================================
619 * HELPER: HIDE PANEL ICON
620 * ============================================================
622 * Cleanly hides the icon from the panel.
623 * Different Cinnamon versions handle empty icons differently.
627 this.set_applet_icon_name("");
628 if (this._applet_icon_box) {
629 this._applet_icon_box.hide();
633 /* ============================================================
634 * PANEL CLICK HANDLER
635 * ============================================================
637 * Called by Cinnamon when user clicks the applet in the panel.
640 * @brief Handles panel icon clicks
642 * @param event The click event from Cinnamon
644 * @note Called automatically by Cinnamon when user clicks the panel icon.
645 * Toggles the popup menu and refreshes events if opening.
648 on_applet_clicked(event: any): void {
649 // Refresh events if opening the menu
650 if (!this.menu.isOpen) {
651 this.eventManager.refresh();
653 // Toggle menu open/close
657 /* ============================================================
658 * HOTKEY CHANGE HANDLER
659 * ============================================================
661 * Updates global keyboard shortcut when setting changes.
664 on_hotkey_changed() {
666 Main.keybindingManager.removeHotKey(`${this.uuid}-open`);
668 // Register new hotkey if set
670 Main.keybindingManager.addHotKey(`${this.uuid}-open`, this.keyOpen, () => {
671 this.on_applet_clicked(null);
676 /* ============================================================
677 * PANEL LABEL AND TOOLTIP UPDATER
678 * ============================================================
680 * Updates the text shown in the Cinnamon panel and its tooltip.
681 * Runs periodically (every 10 seconds) to keep time accurate.
684 update_label_and_tooltip() {
685 const now = new Date();
686 const gNow = GLib.DateTime.new_now_local();
688 // Panel label (time display)
689 let timeLabel = this.useCustomFormat
690 ? gNow.format(this.customFormat)
691 : now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
693 // Tooltip (date display)
694 let dateTooltip = this.useCustomFormat
695 ? gNow.format(this.customTooltipFormat)
696 : now.toLocaleDateString([], {
703 this.set_applet_label(timeLabel || "");
704 this.set_applet_tooltip(dateTooltip || "");
707 /* ============================================================
708 * HEADER DATE UPDATER
709 * ============================================================
711 * Updates the header section in the popup menu.
712 * Shows day, date, and holiday information.
714 * @param date - The date to display in the header
717 public setHeaderDate(date: Date) {
718 if (!this._dayLabel || !this.CalendarView) return;
720 const gDate = GLib.DateTime.new_from_unix_local(date.getTime() / 1000);
723 this._dayLabel.set_text(gDate.format("%A"));
725 // Format: "1. January 2026"
726 this._dateLabel.set_text(gDate.format("%e. %B %Y"));
728 // Check for holidays
729 const tagInfo = this.CalendarView.getHolidayForDate(date);
730 if (tagInfo && tagInfo.beschreibung) {
731 this._holidayLabel.set_text(tagInfo.beschreibung);
732 this._holidayLabel.show();
734 this._holidayLabel.hide();
738 /* ============================================================
740 * ============================================================
742 * Called when applet is removed from panel or Cinnamon restarts.
743 * Essential to prevent memory leaks and dangling resources.
746 on_applet_removed_from_panel() {
747 // Remove global hotkey
748 Main.keybindingManager.removeHotKey(`${this.uuid}-open`);
750 // Stop periodic update timer
751 if (this._updateId > 0) {
752 GLib.source_remove(this._updateId);
755 // Destroy menu and all UI elements
759 /* ============================================================
760 * ICS FILE IMPORT DIALOG
761 * ============================================================
763 * Opens a GTK file chooser to import .ics calendar files.
764 * Currently not connected in UI (commented out in CalendarView).
766 * TODO: This feature is disabled due to EDS import limitations.
769 private _openICSFileChooser(): void {
770 const dialog = new Gtk.FileChooserDialog({
771 title: _("Import Calendar (.ics)"),
772 action: Gtk.FileChooserAction.OPEN,
776 dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL);
777 dialog.add_button(_("Import"), Gtk.ResponseType.OK);
779 const filter = new Gtk.FileFilter();
780 filter.set_name("iCalendar (*.ics)");
781 filter.add_pattern("*.ics");
782 dialog.add_filter(filter);
784 dialog.connect("response", (_dlg: any, response: number) => {
785 if (response === Gtk.ResponseType.OK) {
786 const file = dialog.get_file();
788 const path = file.get_path();
790 this.eventManager.importICSFile(path)
793 `${this.uuid}: ICS import failed: ${e}`
806/* ================================================================
807 * CINNAMON ENTRY POINT
808 * ================================================================
810 * Cinnamon calls this function to create the applet instance.
811 * Must be named 'main' exactly.
814function main(metadata: any, orientation: any, panel_height: number, instance_id: number) {
816 return new UniversalCalendarApplet(metadata, orientation, panel_height, instance_id);
818 // Log initialization errors to Cinnamon's global log
819 if (typeof global !== 'undefined') {
820 global.log(metadata.uuid + " CRITICAL: Initialization error: " + e);
826/* ================================================================
827 * GLOBAL EXPORT (CINNAMON RUNTIME REQUIREMENT)
828 * ================================================================
830 * CRITICAL: Cinnamon loads applets by evaluating JS files.
831 * There is NO module system at runtime - everything must be global.
833 * This dual export pattern supports:
834 * 1. TypeScript/development environment (exports)
835 * 2. Cinnamon production runtime (global assignment)
838if (typeof global !== 'undefined') {
840 (global as any).main = main;
841 if (typeof Applet !== 'undefined') {
842 global.Applet = Applet;
843 (global as any).Applet = Applet;
847/* ================================================================
848 * TODOs (DOCUMENTATION ONLY - NO CODE CHANGES)
849 * ================================================================
851 * TODO: Add lazy loading for CalendarView to improve startup performance.
853 * TODO: Implement proper error boundaries for component initialization failures.
855 * TODO: Add comprehensive keyboard navigation between EventListView and CalendarView.
857 * TODO: Consider extracting the two-column layout into a reusable LayoutManager class.
859 * TODO: Add support for calendar color theme synchronization with system theme.
861 * TODO: Implement drag-and-drop event creation in month view.
863 * TODO: Add export functionality (current month events to .ics).