2 * Project IT Calendar – CalendarView
3 * =================================
5 * This file implements the main visual calendar component of the applet.
6 * It is responsible for rendering and managing all calendar-related UI
7 * views (Month, Year, Day) using Cinnamon’s St toolkit.
9 * IMPORTANT DOCUMENTATION NOTE
10 * ----------------------------
11 * This file is intentionally *not* refactored or modified functionally.
12 * No logic, structure, or behavior has been changed.
14 * The purpose of this pass is **documentation only**:
15 * - Translate remaining German comments to English
16 * - Add explanatory comments for readers unfamiliar with:
18 * - GJS (GNOME JavaScript)
19 * - Cinnamon applet development
20 * - Preserve commented-out code exactly as-is
21 * - Add TODO comments where improvement opportunities are visible
23 * This ensures the file doubles as:
25 * - Architectural documentation
27 * License is preserved as requested.
29 * ------------------------------------------------------------------
31 * ------------------------------------------------------------------
32 * This documentation assumes the reader:
33 * - May never have seen TypeScript before
34 * - Does not know Cinnamon, GNOME, or GJS
35 * - Wants to understand *why* this code exists and how it fits together
37 * ------------------------------------------------------------------
38 * @author Arnold Schiller
39 * @license GPL-3.0-or-later
42 * @file CalendarView.ts
43 * @brief Main calendar UI component
45 * @details Implements the visual calendar grid with month/year/day views.
46 * Uses Cinnamon's St toolkit for rendering and Clutter for input handling.
48 * @author Arnold Schiller <calendar@projektit.de>
50 * @copyright GPL-3.0-or-later
55 * @brief State-driven UI component for calendar display
57 * @details This class manages all calendar UI rendering including:
58 * - Month grid view with navigation
61 * - Event highlighting and tooltips
63 * @note Does NOT store event data itself. Relies on EventManager for data
64 * and CalendarLogic for date calculations.
68/* ================================================================
69 * GJS / CINNAMON IMPORTS
70 * ================================================================
72 * GJS uses a dynamic import system provided by GNOME.
73 * `imports.gi` exposes GObject Introspection bindings.
74 * `St` is Cinnamon’s UI toolkit (Shell Toolkit).
77declare const imports: any;
78declare const global: any;
79declare const __meta: any;
81const { St, Clutter, Gio } = imports.gi;
82const { fileUtils: FileUtils } = imports.misc;
83const Gettext = imports.gettext;
84const Tooltips = imports.ui.tooltips;
86/* ================================================================
87 * APPLET METADATA RESOLUTION
88 * ================================================================
90 * Cinnamon applets can run in different environments:
91 * - Normal applet runtime
92 * - Development / test environment
94 * This logic ensures translation and path resolution works in both.
98 typeof __meta !== "undefined"
100 : "calendar@projektit.de";
103 typeof __meta !== "undefined"
105 : imports.ui.appletManager.appletMeta[UUID].path;
107Gettext.bindtextdomain(UUID, AppletDir + "/locale");
110 * Translation helper.
113 * 1. Applet translation domain
114 * 2. Cinnamon system translations
115 * 3. GNOME Calendar translations (fallback)
117 * This allows reuse of existing translations where possible.
119function _(str: string): string {
120 let translated = Gettext.dgettext(UUID, str);
121 if (translated !== str) return translated;
123 translated = Gettext.dgettext("cinnamon", str);
124 if (translated !== str) return translated;
126 return Gettext.dgettext("gnome-calendar", str);
129/* ================================================================
130 * CALENDAR VIEW CLASS
131 * ================================================================
133 * CalendarView is a *state-driven* UI component.
135 * It does NOT store event data itself.
137 * - EventManager (data source)
138 * - CalendarLogic (date / holiday calculations)
140 * Any state change triggers a full re-render.
144 * @class CalendarView
145 * @brief Main calendar view class
147 * @details For detailed documentation see the main class documentation.
150 * @class CalendarView
151 * @brief Main calendar view class
153 * @details For detailed documentation see the main class documentation.
155export class CalendarView {
159 private _uuid: string;
161 private contentBox: any;
164 * Currently displayed year/month in the UI.
165 * These define the navigation context.
167 private displayedYear: number;
168 private displayedMonth: number;
172 * - MONTH: default grid view
173 * - YEAR: year overview
174 * - DAY: single-day detail view
178 * @brief Available view modes
180 private currentView: "MONTH" | "YEAR" | "DAY" = "MONTH";
183 * Selected day within the current month.
184 * `null` means no specific day is selected.
186 private selectedDay: number | null = null;
191 * ADD = show add-event form
192 * EDIT = show edit-event form
194 private dayMode: "VIEW" | "ADD" | "EDIT" = "VIEW";
195 private dayModeDate: Date | null = null;
199 * Event currently being edited (if any).
201 private editingEvent: any | null = null;
204 * Locale used for date formatting.
205 * Undefined means: use system locale.
207 private readonly LOCALE = undefined;
210 * Optional callback triggered from the Year View
211 * when the user requests an ICS import.
213 * The actual import logic lives elsewhere.
215 public onImportRequested?: () => void;
220 * Creates the root actor, sets up input handlers,
221 * initializes state, and performs the first render.
223 constructor(applet: any, uuid: string = "calendar@projektit.de") {
224 this.applet = applet;
227 const today = new Date();
228 this.displayedYear = today.getFullYear();
229 this.displayedMonth = today.getMonth();
231 /* --------------------------------------------------------
233 * --------------------------------------------------------
235 * St.BoxLayout is a vertical container.
236 * This is the main entry point added to the popup menu.
239 this.actor = new St.BoxLayout({
241 style_class: "calendar-main-box",
247 // Allow children (e.g. tooltips) to overflow their bounds
248 this.actor.set_clip_to_allocation(false);
250 /* --------------------------------------------------------
252 * --------------------------------------------------------
254 * Mouse wheel and keyboard navigation are handled here.
255 * This keeps navigation logic centralized.
258 // Mouse wheel: scroll months
259 this.actor.connect("scroll-event", (_: any, event: any) => {
260 const dir = event.get_scroll_direction();
261 if (dir === Clutter.ScrollDirection.UP) this.scrollMonth(-1);
262 if (dir === Clutter.ScrollDirection.DOWN) this.scrollMonth(1);
263 return Clutter.EVENT_STOP;
266 // Keyboard navigation
267 this.actor.connect("key-press-event", (_: any, event: any) => {
268 switch (event.get_key_symbol()) {
269 case Clutter.KEY_Left:
270 this.scrollMonth(-1);
271 return Clutter.EVENT_STOP;
272 case Clutter.KEY_Right:
274 return Clutter.EVENT_STOP;
277 return Clutter.EVENT_STOP;
278 case Clutter.KEY_Down:
280 return Clutter.EVENT_STOP;
282 return Clutter.EVENT_PROPAGATE;
285 /* --------------------------------------------------------
287 * --------------------------------------------------------
289 * navBox = month/year navigation
290 * contentBox = active view (month/year/day)
293 this.navBox = new St.BoxLayout({ style_class: "calendar-nav-box" });
294 this.contentBox = new St.BoxLayout({ vertical: true });
296 this.actor.add_actor(this.navBox);
297 this.actor.add_actor(this.contentBox);
304 * Reset calendar state to today and switch to month view.
306 * Used by external controls (e.g. “Today” button).
308 public resetToToday(): void {
309 const today = new Date();
310 this.displayedYear = today.getFullYear();
311 this.displayedMonth = today.getMonth();
312 this.currentView = "MONTH";
314 const todayEvents = this.applet.eventManager.getEventsForDate(today);
315 this.applet.eventListView.updateForDate(today, todayEvents);
321 * Returns the date currently represented by the navigation state.
323 * If no specific day is selected, the first day of the month is used.
325 public getCurrentlyDisplayedDate(): Date {
329 this.selectedDay || 1
334 * Helper used by the applet to retrieve holiday information.
336 * CalendarView itself does not calculate holidays.
338 public getHolidayForDate(date: Date): { beschreibung: string } | null {
339 if (!this.applet.CalendarLogic) return null;
342 this.applet.CalendarLogic.getHolidaysForDate(date, "de");
344 return holidays.length > 0
345 ? { beschreibung: holidays.join(", ") }
349 /* ============================================================
351 * ============================================================
353 * The remainder of this file contains:
354 * - Navigation rendering
355 * - Month / Year / Day view rendering
356 * - Event list synchronization
357 * - Date helper utilities
359 * All logic below is unchanged.
360 * Only comments were translated and clarified.
362 * TODO (Documentation):
363 * - Extract view modes into dedicated sub-classes
364 * - Add explicit state diagram to project documentation
370 /* ============================================================
371 * NAVIGATION BAR – MONTH / YEAR SELECTOR
372 * ============================================================
374 * This section renders the top navigation bar of the calendar.
376 * ────────────────────────────────────────────────────────────
377 * CONTEXT / PRECONDITIONS
378 * ────────────────────────────────────────────────────────────
380 * At this point in execution, the following is already true:
382 * 1. This class is a Cinnamon applet view written in TypeScript,
383 * compiled to GJS-compatible JavaScript.
385 * 2. `this.navBox` is a St.BoxLayout that already exists and is
386 * dedicated exclusively to holding the navigation bar UI.
388 * 3. The following state variables are already initialized and valid:
389 * - this.displayedYear (number)
390 * - this.displayedMonth (0–11, JavaScript Date convention)
391 * - this.selectedDay (number | null)
392 * - this.currentView ("MONTH" | "DAY" | "YEAR")
394 * 4. The following helper methods already exist and work:
395 * - scrollMonth(delta)
396 * - scrollYear(delta)
399 * 5. Cinnamon's St (Shell Toolkit) namespace is available and imported,
400 * providing BoxLayout, Button, Label, alignment constants, etc.
402 * The navigation bar itself is stateless UI: it does NOT store state,
403 * it only manipulates the existing calendar state and triggers re-rendering.
405 * ────────────────────────────────────────────────────────────
407 * ────────────────────────────────────────────────────────────
409 * [ < ] [ Month Name ] [ > ] [ spacer ] [ < ] [ Year ] [ > ]
411 * - Left side: Month navigation
412 * - Center: Reserved space (future messages / indicators)
413 * - Right side: Year navigation
415 * ────────────────────────────────────────────────────────────
418 private renderNav(): void {
419 // Remove all previously rendered navigation elements.
420 // This ensures a clean rebuild on every render() call.
421 this.navBox.destroy_children();
423 // Root container for the navigation bar.
424 // All sub-components (month, spacer, year) are placed inside.
425 const navContainer = new St.BoxLayout({
426 style_class: "calendar",
427 x_align: St.Align.MIDDLE,
430 /* ========================================================
431 * MONTH SELECTOR (LEFT SIDE)
432 * ========================================================
434 * Allows navigating backward / forward by one month.
435 * The month name itself is clickable and switches to
436 * MONTH view when clicked.
439 const monthBox = new St.BoxLayout({ style: "margin-right: 5px;" });
441 // Button: previous month
442 const btnPrevM = new St.Button({
444 style_class: "calendar-change-month-back",
447 // Decrease month by one and re-render
448 btnPrevM.connect("clicked", () => this.scrollMonth(-1));
450 // Button displaying the current month name
451 const monthBtn = new St.Button({
455 ).toLocaleString(this.LOCALE, { month: "long" }),
457 style_class: "calendar-month-label",
462 // We explicitly force transparency and remove default
463 // button padding to visually behave like a label.
466 "background-color: transparent; " +
468 "min-width: 140px; " +
469 "text-align: center;",
472 // Clicking the month name switches explicitly to MONTH view.
473 // This is useful when coming from DAY or YEAR view.
474 monthBtn.connect("clicked", () => {
475 this.currentView = "MONTH";
479 // Button: next month
480 const btnNextM = new St.Button({
482 style_class: "calendar-change-month-forward",
485 // Increase month by one and re-render
486 btnNextM.connect("clicked", () => this.scrollMonth(1));
488 // Assemble month selector
489 monthBox.add_actor(btnPrevM);
490 monthBox.add_actor(monthBtn);
491 monthBox.add_actor(btnNextM);
493 /* ========================================================
494 * MIDDLE SPACER (CENTER)
495 * ========================================================
498 * This spacer keeps the layout visually balanced and
499 * allows future extensions (messages, sync status, etc.)
500 * without redesigning the navigation bar.
502 * TODO: Replace with meaningful status indicators
505 const middleBox = new St.BoxLayout({
509 // Non-breaking spaces used to enforce minimum width.
510 const middleLabel = new St.Label({
512 "\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0" +
513 "\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0" +
514 "\u00A0\u00A0\u00A0\u00A0",
515 style_class: "calendar-month-label",
516 style: "min-width: 50px; text-align: center;",
519 middleBox.add_actor(middleLabel);
521 /* ========================================================
522 * YEAR SELECTOR (RIGHT SIDE)
523 * ========================================================
525 * Allows navigating backward / forward by one year.
526 * Clicking the year switches to YEAR overview.
529 const yearBox = new St.BoxLayout({
530 style: "margin-left: 5px;",
533 // Button: previous year
534 const btnPrevY = new St.Button({
536 style_class: "calendar-change-month-back",
539 btnPrevY.connect("clicked", () => this.scrollYear(-1));
541 // Button displaying the current year
542 const yearBtn = new St.Button({
543 label: this.displayedYear.toString(),
544 style_class: "calendar-month-label",
549 // Switch to YEAR view
550 yearBtn.connect("clicked", () => {
551 this.currentView = "YEAR";
556 const btnNextY = new St.Button({
558 style_class: "calendar-change-month-forward",
561 btnNextY.connect("clicked", () => this.scrollYear(1));
563 // Assemble year selector
564 yearBox.add_actor(btnPrevY);
565 yearBox.add_actor(yearBtn);
566 yearBox.add_actor(btnNextY);
568 // Assemble full navigation bar
569 navContainer.add_actor(monthBox);
570 navContainer.add_actor(middleBox);
571 navContainer.add_actor(yearBox);
573 this.navBox.add_actor(navContainer);
576 /* ============================================================
577 * YEAR / MONTH SCROLL HELPERS
578 * ============================================================
580 * These helpers modify the calendar's temporal context
581 * and immediately trigger a re-render.
583 * They also reset selectedDay to avoid invalid state
584 * when switching months or years.
587 private scrollYear(delta: number): void {
588 this.displayedYear += delta;
589 this.selectedDay = null;
593 private scrollMonth(delta: number): void {
596 this.displayedMonth + delta,
600 this.selectedDay = null;
601 this.displayedYear = d.getFullYear();
602 this.displayedMonth = d.getMonth();
606 /* ============================================================
607 * EXTERNAL VIEW SYNCHRONIZATION
608 * ============================================================
610 * Keeps the EventListView (if enabled) in sync with the
611 * currently displayed calendar context.
613 * This method acts as a bridge between:
614 * - CalendarView (date navigation)
615 * - EventListView (list-based representation)
618 private _updateExternalViews() {
619 if (!this.applet.showEvents || !this.applet.eventListView) return;
621 const elv = this.applet.eventListView;
623 if (this.currentView === "DAY" || this.selectedDay !== null) {
624 // A specific day is selected → show day details
625 const targetDate = new Date(
628 this.selectedDay || 1
632 this.applet.eventManager.getEventsForDate(targetDate);
634 elv.updateForDate(targetDate, events);
636 // No specific day → show month overview
638 this.applet.eventManager.getEventsForMonth(
651 /* ============================================================
652 * EXTERNAL NAVIGATION ENTRY POINT
653 * ============================================================
655 * Allows external components (e.g. EventListView)
656 * to request navigation to a specific date.
659 public jumpToDate(date: Date): void {
660 this.displayedYear = date.getFullYear();
661 this.displayedMonth = date.getMonth();
662 this.selectedDay = date.getDate();
663 this.currentView = "DAY";
667 /* ============================================================
668 * CENTRAL RENDER DISPATCHER
669 * ============================================================
671 * This is the single entry point for rendering.
672 * It rebuilds navigation, content, footer and
673 * synchronizes external views.
677 * @brief Renders the complete calendar UI
679 * @details This is the central render dispatcher that:
680 * 1. Rebuilds navigation bar
681 * 2. Renders appropriate view based on currentView
682 * 3. Updates external views (EventListView)
685 * post UI is completely updated to reflect current state
687 public render(): void {
689 this.contentBox.destroy_children();
691 switch (this.currentView) {
693 this.renderDayView();
696 this.renderYearView();
699 this.renderMonthView();
703 const footer = new St.BoxLayout({
704 style_class: "calendar-footer",
707 this.contentBox.add_actor(footer);
709 this._updateExternalViews();
712 /* ============================================================
714 * ============================================================
716 * This method renders the classic month grid view.
718 * ────────────────────────────────────────────────────────────
719 * CONTEXT / PRECONDITIONS
720 * ────────────────────────────────────────────────────────────
722 * At the time this method is called, the following is already true:
724 * 1. The CalendarView instance exists and is fully initialized.
726 * 2. Global calendar state is valid:
727 * - this.displayedYear → year currently shown
728 * - this.displayedMonth → month currently shown (0–11)
729 * - this.selectedDay → null or a specific day number
731 * 3. Navigation has already been rendered via renderNav().
733 * 4. this.contentBox is empty and ready to receive new UI elements.
735 * 5. The EventManager is active and provides:
737 * - getEventsForDate(date)
739 * 6. Optional helpers may be available:
740 * - CalendarLogic (for holidays)
741 * - Tooltips.Tooltip (for hover details)
743 * This method does NOT persist state.
744 * It only reads state and builds UI accordingly.
746 * ────────────────────────────────────────────────────────────
748 * ────────────────────────────────────────────────────────────
750 * The month view consists of:
752 * - A 7-column grid (Monday → Sunday)
753 * - Optional week-number column on the left
754 * - Always 6 rows (maximum weeks a month can span)
757 * - Represents a single calendar day
761 * • event indicator dot
763 * • tooltip with details
765 * TODO (Architectural):
766 * If CalendarView is ever split into multiple files,
767 * this method would need:
768 * - Access to shared state (displayedYear, displayedMonth, selectedDay)
769 * - Access to EventManager and CalendarLogic
770 * Therefore it currently must remain a method of this class.
773 private renderMonthView(): void {
774 /* --------------------------------------------------------
775 * GRID INITIALIZATION
776 * --------------------------------------------------------
778 * St.Table is used instead of BoxLayout because:
779 * - We need a strict row/column layout
780 * - All cells should have equal size
783 const grid = new St.Table({
785 style_class: "calendar",
788 /* --------------------------------------------------------
789 * WEEK NUMBER COLUMN OFFSET
790 * --------------------------------------------------------
792 * If week numbers are enabled, column 0 is reserved
793 * for them and all weekday columns shift by +1.
796 const colOffset = this.applet.showWeekNumbers ? 1 : 0;
798 /* --------------------------------------------------------
800 * --------------------------------------------------------
802 * Adds localized weekday names (Mon–Sun) as the first row.
805 this.getDayNames().forEach((name, i) => {
809 style_class: "calendar-day-base",
811 { row: 0, col: i + colOffset }
815 /* --------------------------------------------------------
816 * DATE ITERATION SETUP
817 * --------------------------------------------------------
819 * We start at the first visible cell of the grid,
820 * which may belong to the previous month.
822 * The calendar uses Monday as the first weekday.
825 let iter = new Date(this.displayedYear, this.displayedMonth, 1);
827 // Convert JS Sunday-based index to Monday-based index
828 const firstWeekday = (iter.getDay() + 6) % 7;
830 // Move iterator back to the Monday of the first visible week
831 iter.setDate(iter.getDate() - firstWeekday);
833 /* --------------------------------------------------------
834 * NORMALIZED "TODAY" DATE
835 * --------------------------------------------------------
837 * Used for visual highlighting of the current day.
840 const today = new Date();
841 today.setHours(0, 0, 0, 0);
843 /* --------------------------------------------------------
844 * MAIN GRID LOOP (6 WEEKS × 7 DAYS)
845 * --------------------------------------------------------
847 * We always render 6 rows to keep layout stable,
848 * even for short months.
851 for (let row = 1; row <= 6; row++) {
853 /* ----------------------------------------------------
854 * WEEK NUMBER COLUMN (OPTIONAL)
855 * ----------------------------------------------------
857 * ISO week number calculation:
858 * - Thursday determines the week number
861 if (this.applet.showWeekNumbers) {
862 const kwDate = new Date(iter);
863 kwDate.setDate(kwDate.getDate() + 3);
867 text: this.getWeekNumber(kwDate).toString(),
868 style_class: "calendar-week-number",
874 /* ----------------------------------------------------
875 * DAY CELLS (MONDAY → SUNDAY)
876 * ----------------------------------------------------
879 for (let col = 0; col < 7; col++) {
881 iter.getMonth() !== this.displayedMonth;
884 iter.getTime() === today.getTime();
888 this.applet.eventManager.hasEvents(iter);
890 /* --------------------------------------------
891 * HOLIDAY HANDLING (OPTIONAL)
892 * --------------------------------------------
896 !isOtherMonth && this.applet.CalendarLogic
897 ? this.applet.CalendarLogic.getHolidaysForDate(
903 const isHoliday = holidays.length > 0;
905 /* --------------------------------------------
906 * CSS CLASS COMPOSITION
907 * --------------------------------------------
909 * Styling is entirely CSS-driven.
910 * Logic here only decides which classes apply.
913 const btnClasses = ["calendar-day"];
916 btnClasses.push("calendar-other-month-day");
919 btnClasses.push("calendar-today");
921 if (iter.getDay() === 0 || isHoliday)
922 btnClasses.push("calendar-nonwork-day");
924 /* --------------------------------------------
926 * --------------------------------------------
928 * Each day is a button so it can:
934 const btn = new St.Button({
937 style_class: btnClasses.join(" "),
940 /* --------------------------------------------
942 * --------------------------------------------
946 * - Event indicator dot
949 const content = new St.BoxLayout({
951 x_align: St.Align.MIDDLE,
956 text: iter.getDate().toString(),
957 style_class: "calendar-day-label",
963 text: hasEvents ? "•" : " ",
964 style_class: "calendar-day-event-dot-label",
968 btn.set_child(content);
970 /* --------------------------------------------
972 * --------------------------------------------
974 * Shows holidays and event summaries on hover.
977 if (Tooltips.Tooltip) {
978 const tooltipLines: string[] = [];
980 holidays.forEach(h => tooltipLines.push(h));
984 this.applet.eventManager.getEventsForDate(
987 events.forEach((e: any) =>
988 tooltipLines.push(`• ${e.summary}`)
992 if (tooltipLines.length > 0) {
993 new Tooltips.Tooltip(
995 tooltipLines.join("\n")
1000 /* --------------------------------------------
1002 * --------------------------------------------
1005 * - Updates global calendar state
1006 * - Switches to DAY view
1007 * - Triggers full re-render
1010 const d = iter.getDate();
1011 const m = iter.getMonth();
1012 const y = iter.getFullYear();
1014 btn.connect("clicked", () => {
1015 this.selectedDay = d;
1016 this.displayedMonth = m;
1017 this.displayedYear = y;
1018 this.currentView = "DAY";
1022 /* --------------------------------------------
1024 * --------------------------------------------
1029 col: col + colOffset,
1032 // Advance iterator to next day
1033 iter.setDate(iter.getDate() + 1);
1037 /* --------------------------------------------------------
1039 * --------------------------------------------------------
1041 * Add the fully constructed grid to the content area.
1044 this.contentBox.add_actor(grid);
1047 /* ============================================================
1049 * ============================================================
1051 * This view provides a high-level overview of an entire year.
1053 * ────────────────────────────────────────────────────────────
1054 * PURPOSE OF THE YEAR VIEW
1055 * ────────────────────────────────────────────────────────────
1057 * The Year View is intentionally minimalistic.
1058 * Its main purpose is NOT to display events directly,
1059 * but to act as a fast navigation hub:
1061 * YEAR → MONTH → DAY
1063 * In the current architecture, this view allows the user to:
1064 * - See all months of the selected year at once
1065 * - Quickly jump into a specific month
1068 * - Render individual days
1069 * - Show event dots or counts
1070 * - Display mini calendars
1072 * ────────────────────────────────────────────────────────────
1073 * DESIGN DECISION (IMPORTANT)
1074 * ────────────────────────────────────────────────────────────
1076 * The original design idea included:
1077 * - A "mini month grid" for each month
1078 * - Possibly event dots per month
1080 * This was deliberately NOT implemented (yet), because:
1082 * 1. Cinnamon applets have strict performance constraints.
1083 * Rendering 12 full mini-calendars would significantly
1084 * increase UI complexity and redraw cost.
1086 * 2. Event data retrieval in Cinnamon is asynchronous and
1087 * limited by the Cinnamon.CalendarServer API.
1089 * 3. There is no clean, officially supported way to request
1090 * aggregated per-month event summaries without fetching
1091 * full event ranges.
1093 * TODO (Future Enhancement):
1094 * Implement optional mini month grids per month, but ONLY if:
1095 * - EventManager provides cached per-month summaries
1096 * - Rendering can be done lazily or on-demand
1099 private renderYearView(): void {
1100 /* --------------------------------------------------------
1102 * --------------------------------------------------------
1105 * 1. Action area (currently mostly unused)
1106 * 2. Month selection grid
1109 const yearBox = new St.BoxLayout({
1111 style_class: "year-view-container",
1114 /* --------------------------------------------------------
1115 * ACTION AREA (TOP OF YEAR VIEW)
1116 * --------------------------------------------------------
1118 * This area was intended for global year-level actions,
1119 * such as importing calendars or bulk operations.
1121 * At the moment it only acts as a spacer.
1124 const actionArea = new St.BoxLayout({
1125 x_align: St.Align.MIDDLE,
1126 style: "padding: 10px;",
1129 /* --------------------------------------------------------
1130 * ICS IMPORT BUTTON (DISABLED / TODO)
1131 * --------------------------------------------------------
1133 * This button is intentionally commented out.
1137 * While importing ICS files sounds trivial, it is NOT
1138 * reliably solvable within the constraints of:
1140 * - Cinnamon.CalendarServer (mostly read-only via DBus)
1141 * - Evolution Data Server (EDS) permissions and sources
1142 * - GJS / GIR API inconsistencies
1145 * - "Modify existing events" often works
1146 * - "Create new events" is highly source-dependent
1147 * - Bulk ICS imports introduce complex edge cases:
1148 * • missing or duplicate UIDs
1149 * • read-only calendar sources
1150 * • partial failures without rollback
1152 * Therefore, this feature is currently disabled to avoid
1153 * misleading users with a broken or unreliable workflow.
1155 * TODO (Architectural):
1156 * If ICS import is ever re-enabled, the following must exist:
1157 * - A fully robust EventManager.create() implementation
1158 * - Proper source resolution and permission checks
1159 * - Clear user feedback for partial import failures
1162 /*const importBtn = new St.Button({
1163 label: _("Import a Calendar"),
1164 style_class: "calendar-event-button",
1168 importBtn.connect("clicked", () => {
1170 "[CalendarView] ICS import requested (not yet implemented)"
1172 this.onImportRequested?.();
1176 /* actionArea.add_actor(importBtn); */
1178 yearBox.add_actor(actionArea);
1180 /* --------------------------------------------------------
1181 * MONTH SELECTION GRID
1182 * --------------------------------------------------------
1184 * A simple grid of 12 buttons (3 columns × 4 rows),
1185 * each representing one month of the year.
1187 * This grid does NOT depend on event data.
1190 const grid = new St.Table({
1192 style_class: "calendar",
1195 /* --------------------------------------------------------
1196 * MONTH BUTTON CREATION
1197 * --------------------------------------------------------
1199 * We iterate over all 12 months (0–11).
1202 * - Displays the localized short month name
1203 * - Switches the view to MONTH when clicked
1206 for (let m = 0; m < 12; m++) {
1207 const btn = new St.Button({
1211 ).toLocaleString(this.LOCALE, {
1214 style_class: "calendar-month-label",
1217 /* ----------------------------------------------------
1219 * ----------------------------------------------------
1222 * - Updates displayedMonth
1223 * - Switches view mode to MONTH
1224 * - Triggers a full re-render
1227 btn.connect("clicked", () => {
1228 this.displayedMonth = m;
1229 this.currentView = "MONTH";
1233 /* ----------------------------------------------------
1235 * ----------------------------------------------------
1238 * Row = monthIndex / 3
1239 * Col = monthIndex % 3
1243 row: Math.floor(m / 3),
1248 /* --------------------------------------------------------
1250 * --------------------------------------------------------
1252 * Add the month grid to the year container,
1253 * then attach everything to the main content box.
1256 yearBox.add_actor(grid);
1257 this.contentBox.add_actor(yearBox);
1260 /* ============================================================
1262 * ============================================================
1264 * The Day View is the most interaction-heavy part of CalendarView.
1266 * It is responsible for:
1267 * - Displaying details for a single selected calendar day
1268 * - Showing holidays for that day
1269 * - Listing all events occurring on that date
1270 * - Handling UI state transitions for:
1272 * • Editing an existing event
1273 * • (Optionally) adding a new event
1275 * IMPORTANT CONTEXT (What must already be true before this runs):
1277 * - `this.displayedYear`, `this.displayedMonth` are set
1278 * - `this.selectedDay` is set (usually via:
1279 * • click in Month View
1280 * • jumpToDate() from EventListView)
1281 * - `this.currentView === "DAY"`
1282 * - EventManager is initialized and has cached events
1284 * This method MUST be called only from within CalendarView.render().
1285 * It relies heavily on internal class state and cannot be used
1286 * as a standalone component without refactoring.
1289 private renderDayView(): void {
1291 /* --------------------------------------------------------
1292 * ROOT CONTAINER FOR DAY VIEW
1293 * --------------------------------------------------------
1295 * This vertical box contains:
1296 * 1. Date header (weekday + date)
1297 * 2. Holiday rows (if any)
1298 * 3. Event list OR "No events"
1299 * 4. Add/Edit form (optional, depending on mode)
1301 * 6. Navigation back to Month View
1304 const box = new St.BoxLayout({
1306 style_class: "calendar-events-main-box",
1309 /* --------------------------------------------------------
1310 * RESOLVE SELECTED DATE
1311 * --------------------------------------------------------
1313 * selectedDay should normally be set.
1314 * Fallback to day "1" is a defensive safeguard and should
1315 * normally never be hit during regular operation.
1318 const selectedDate = new Date(
1320 this.displayedMonth,
1321 this.selectedDay || 1
1324 /* --------------------------------------------------------
1325 * DAY MODE GUARD (VERY IMPORTANT)
1326 * --------------------------------------------------------
1328 * dayMode controls the internal UI sub-state:
1329 * - "VIEW" → show events only
1330 * - "ADD" → show create form
1331 * - "EDIT" → show edit form for a specific event
1333 * This guard ensures:
1334 * - ADD / EDIT modes are ONLY valid for the exact date
1335 * they were initiated on.
1337 * Example problem without this guard:
1338 * - User clicks "Edit" on Jan 5
1339 * - Then navigates to Jan 6
1340 * - Edit form would still be shown for the wrong day
1343 * If the date changes → reset to VIEW mode.
1347 this.dayMode !== "VIEW" &&
1349 !this.dayModeDate ||
1350 this.dayModeDate.toDateString() !== selectedDate.toDateString()
1353 this.dayMode = "VIEW";
1354 this.editingEvent = null;
1355 this.dayModeDate = null;
1358 /* --------------------------------------------------------
1360 * --------------------------------------------------------
1362 * Displays the full localized date:
1363 * e.g. "Tuesday, 05 January 2026"
1368 text: selectedDate.toLocaleString(this.LOCALE, {
1374 style_class: "day-details-title",
1378 /* --------------------------------------------------------
1379 * HOLIDAYS SECTION (OPTIONAL)
1380 * --------------------------------------------------------
1382 * Holidays are resolved via CalendarLogic (if present).
1384 * This is completely independent from events.
1385 * Holidays are rendered as visual info rows only.
1388 if (this.applet.CalendarLogic) {
1390 this.applet.CalendarLogic.getHolidaysForDate(
1395 holidays.forEach(h => {
1396 const row = new St.BoxLayout({
1397 style_class: "calendar-event-button",
1399 "background-color: rgba(255,0,0,0.1);",
1405 "calendar-event-summary",
1412 /* --------------------------------------------------------
1413 * FETCH EVENTS FOR SELECTED DATE
1414 * --------------------------------------------------------
1416 * EventManager is responsible for all date filtering.
1417 * CalendarView never interprets start/end times itself.
1421 this.applet.eventManager.getEventsForDate(
1425 /* --------------------------------------------------------
1426 * EVENT LIST RENDERING
1427 * --------------------------------------------------------
1429 * Each event is rendered as:
1433 * No delete button exists here by design.
1434 * (Deletion semantics are non-trivial with EDS.)
1437 if (events.length > 0) {
1438 events.forEach((ev: any) => {
1439 const row = new St.BoxLayout({
1440 style_class: "calendar-event-button",
1447 style_class: "calendar-event-summary",
1452 const editBtn = new St.Button({
1454 style_class: "calendar-event-edit-button",
1457 editBtn.connect("clicked", () => {
1458 this.dayMode = "EDIT";
1459 this.editingEvent = ev;
1460 this.dayModeDate = selectedDate;
1464 row.add_actor(editBtn);
1469 /* --------------------------------------------------------
1470 * NO EVENTS PLACEHOLDER
1471 * --------------------------------------------------------
1473 * Only shown in VIEW mode.
1474 * Not shown when ADD or EDIT form is active.
1477 if (events.length === 0 && this.dayMode === "VIEW") {
1480 text: _("No events"),
1481 style_class: "calendar-events-no-events-label",
1486 /* --------------------------------------------------------
1487 * ADD / EDIT FORM INJECTION
1488 * --------------------------------------------------------
1490 * createTerminForm() dynamically builds a form UI.
1492 * IMPORTANT ARCHITECTURAL NOTE:
1494 * This tightly couples:
1496 * - Event creation/editing UI
1498 * TODO (Refactor Idea):
1499 * - Extract createTerminForm into a separate component
1500 * - Or move all form logic into EventListView or a Dialog
1502 * WHY THIS IS CURRENTLY PROBLEMATIC:
1504 * - Cinnamon CalendarServer + EDS via GJS/GIR is unreliable
1505 * for CREATE operations (especially description fields).
1506 * - Description handling is partially broken / inconsistent.
1507 * - Because of this, ADD functionality is currently limited.
1510 if (this.dayMode === "ADD") {
1512 this.createTerminForm(selectedDate)
1515 else if (this.dayMode === "EDIT" && this.editingEvent) {
1517 this.createTerminForm(selectedDate, this.editingEvent)
1521 /* --------------------------------------------------------
1522 * NAVIGATION BACK TO MONTH VIEW
1523 * --------------------------------------------------------
1525 * Always resets Day View state.
1528 const backBtn = new St.Button({
1529 label: _("Month view"),
1530 style_class: "nav-button",
1531 style: "margin-top: 15px;",
1534 backBtn.connect("clicked", () => {
1535 this.currentView = "MONTH";
1536 this.dayMode = "VIEW";
1537 this.editingEvent = null;
1538 this.dayModeDate = null;
1542 /* --------------------------------------------------------
1543 * ACTION BAR (CURRENTLY MOSTLY UNUSED)
1544 * --------------------------------------------------------
1546 * Originally planned to host the "Add event" button.
1548 * WHY IT IS COMMENTED OUT:
1550 * - Event creation via GJS / GIR / EDS is currently unstable
1551 * - Description fields are buggy or missing
1552 * - Creating events may silently fail depending on source
1555 * Re-enable ONLY when:
1556 * - EventManager.create() is fully reliable
1557 * - Description roundtrips correctly via CalendarServer
1560 const actionBar = new St.BoxLayout({
1561 style_class: "calendar-day-actions",
1562 x_align: St.Align.END
1566 if (this.dayMode === "VIEW") {
1567 const addBtn = new St.Button({
1568 label: _("Add event"),
1569 style_class: "calendar-event-button"
1572 addBtn.connect("clicked", () => {
1573 this.dayMode = "ADD";
1574 this.editingEvent = null;
1575 this.dayModeDate = selectedDate;
1579 actionBar.add_actor(addBtn);
1583 /* --------------------------------------------------------
1585 * --------------------------------------------------------
1588 box.add_actor(actionBar);
1589 box.add_actor(backBtn);
1590 this.contentBox.add_actor(box);
1593 /* ============================================================
1594 * CREATE / EDIT FORM
1595 * ============================================================
1597 * This method builds the inline form used to:
1598 * - Create a new calendar event
1599 * - Edit an existing calendar event
1601 * IMPORTANT CONTEXT (How we get here):
1603 * - This form is ONLY rendered from renderDayView()
1604 * - It is injected into the Day View depending on `dayMode`
1606 * - The surrounding popup is NOT a dialog but part of the
1607 * Cinnamon applet popup UI
1609 * WHY THIS IS INLINE (and not a dialog):
1611 * - Cinnamon applets have limited dialog APIs
1612 * - Keeping everything inside the popup avoids focus issues
1613 * - Rendering inline allows full reuse of CalendarView state
1615 * TODO (Architectural Improvement):
1616 * - Extract this form into a dedicated component or dialog
1617 * - Decouple UI logic from CalendarView
1618 * - Allow asynchronous validation / error feedback
1621 private createTerminForm(date: Date, editingEvent?: any): any {
1623 /* --------------------------------------------------------
1625 * --------------------------------------------------------
1627 * Vertical layout that contains:
1631 * - (Optional) description input (currently disabled)
1635 const box = new St.BoxLayout({
1637 style_class: "calendar-main-box",
1641 /* --------------------------------------------------------
1643 * --------------------------------------------------------
1645 * - If editing an existing event:
1647 * - If creating a new event:
1648 * → do NOT generate a UUID here
1650 * WHY NOT GENERATE UUID HERE?
1652 * - EDS (Evolution Data Server) is able to generate IDs
1653 * - Manual UUID handling is error-prone with EDS sources
1654 * - Let EventManager / backend decide when necessary
1657 const currentId = editingEvent ? editingEvent.id : undefined;
1659 /* --------------------------------------------------------
1661 * --------------------------------------------------------
1663 * This replaces a traditional checkbox.
1666 * - St does not have a native checkbox widget
1667 * - Button with toggle_mode is more consistent visually
1671 * → Time fields become visually disabled
1672 * → Time fields are non-interactive
1675 const isInitialFullDay = editingEvent ? editingEvent.isFullDay : false;
1676 const allDayCheckbox = new St.Button({
1677 label: isInitialFullDay ? "☑ " + _("All Day") : "☐ " + _("All Day"),
1678 style_class: "calendar-event-button",
1680 checked: isInitialFullDay,
1681 x_align: St.Align.START
1684 /* --------------------------------------------------------
1685 * TITLE / SUMMARY ENTRY
1686 * --------------------------------------------------------
1688 * This maps directly to the VEVENT SUMMARY field.
1689 * It is the ONLY required field for saving.
1692 const titleEntry = new St.Entry({
1693 hint_text: _("What? (Nice event)"),
1694 style_class: "calendar-event-summary",
1695 text: editingEvent ? editingEvent.summary : ""
1698 /* --------------------------------------------------------
1700 * --------------------------------------------------------
1702 * Time values are handled as strings (HH:MM) in the UI.
1703 * Conversion to Date objects happens only on Save.
1706 const formatTime = (d: Date) => {
1707 return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
1710 const startTimeStr = editingEvent
1711 ? formatTime(editingEvent.start)
1712 : this._getCurrentTime();
1714 const endTimeStr = editingEvent
1715 ? formatTime(editingEvent.end)
1716 : this._calculateDefaultEnd(startTimeStr);
1718 const descriptionStr =
1719 (editingEvent && typeof editingEvent.description === 'string')
1720 ? editingEvent.description
1723 const timeBox = new St.BoxLayout({
1725 style: "margin: 5px 0;"
1728 const startEntry = new St.Entry({
1730 style_class: "calendar-event-time-present",
1735 const endEntry = new St.Entry({
1737 style_class: "calendar-event-time-present",
1742 /* --------------------------------------------------------
1743 * TIME FIELD VISIBILITY / ENABLE LOGIC
1744 * --------------------------------------------------------
1746 * Centralized logic to enable / disable time fields
1747 * depending on the All-Day toggle.
1750 * - Reduced opacity when disabled
1751 * - Input and focus disabled
1754 const updateVisibility = () => {
1755 const isFullDay = allDayCheckbox.checked;
1757 allDayCheckbox.set_label(
1758 isFullDay ? "☑ " + _("All Day") : "☐ " + _("All Day")
1761 const opacity = isFullDay ? 128 : 255;
1762 startEntry.set_opacity(opacity);
1763 endEntry.set_opacity(opacity);
1764 startEntry.set_reactive(!isFullDay);
1765 endEntry.set_reactive(!isFullDay);
1766 startEntry.can_focus = !isFullDay;
1767 endEntry.can_focus = !isFullDay;
1770 allDayCheckbox.connect("clicked", () => {
1774 timeBox.add_actor(new St.Label({
1776 style: "margin-right: 5px;"
1778 timeBox.add_actor(startEntry);
1779 timeBox.add_actor(new St.Label({
1781 style: "margin: 0 5px;"
1783 timeBox.add_actor(endEntry);
1785 /* --------------------------------------------------------
1786 * DESCRIPTION FIELD (DISABLED / COMMENTED OUT)
1787 * --------------------------------------------------------
1789 * WHY THIS IS DISABLED:
1791 * - Cinnamon CalendarServer + GJS/GIR has known issues
1792 * with VEVENT DESCRIPTION handling
1793 * - set_description / set_description_list behave
1794 * inconsistently across environments
1795 * - Description data may be silently dropped or break
1799 * - Keep the UI code commented for future reference
1800 * - Do NOT expose broken functionality to users
1803 * - Re-enable when EDS + CalendarServer reliably
1804 * supports description roundtrips
1808 const descEntry = new St.Entry({
1809 hint_text: _("Description"),
1810 style_class: "calendar-event-row-content",
1812 text: descriptionStr
1817 descEntry.clutter_text.single_line_mode = false;
1818 descEntry.clutter_text.line_wrap = true;
1819 descEntry.clutter_text.set_activatable(false);
1822 /* --------------------------------------------------------
1824 * --------------------------------------------------------
1828 * - Build Date objects
1829 * - Delegate persistence to EventManager
1831 * IMPORTANT UX NOTE:
1834 * - The form closes immediately
1835 * - The popup re-renders
1837 * This feels abrupt for users.
1839 * TODO (UX Improvement):
1840 * - Keep form open until backend confirms success
1841 * - Show error feedback on failure
1844 const buttonBox = new St.BoxLayout({
1845 style: "margin-top: 10px;"
1848 const saveBtn = new St.Button({
1849 label: editingEvent ? _("Update") : _("Save"),
1850 style_class: "calendar-event-button",
1854 saveBtn.connect("clicked", () => {
1855 const title = titleEntry.get_text().trim();
1858 const isFullDay = allDayCheckbox.checked;
1859 const start = this._buildDateTime(date, startEntry.get_text());
1860 const end = this._buildDateTime(date, endEntry.get_text());
1862 this.applet.eventManager.addEvent({
1864 sourceUid: editingEvent ? editingEvent.sourceUid : undefined,
1866 // Description intentionally disabled due to EDS limitations
1870 isFullDay: isFullDay,
1871 color: editingEvent ? editingEvent.color : "#3498db"
1874 /* Reset Day View state */
1875 this.dayMode = "VIEW";
1876 this.editingEvent = null;
1880 /* --------------------------------------------------------
1882 * --------------------------------------------------------
1885 box.add_actor(allDayCheckbox);
1886 box.add_actor(titleEntry);
1887 box.add_actor(timeBox);
1888 /* box.add_actor(descEntry); */
1889 buttonBox.add_actor(saveBtn);
1890 box.add_actor(buttonBox);
1892 /* Ensure correct initial state */
1898 /* ============================================================
1900 * ============================================================
1902 * The following helper methods are pure utility functions
1903 * used throughout CalendarView.
1905 * They are intentionally kept INSIDE the class instead of
1906 * being moved to a shared utils module.
1909 * - They depend on this.LOCALE
1910 * - They are tightly coupled to calendar rendering logic
1911 * - Keeping them here improves readability for new developers
1913 * TODO (Refactoring Option):
1914 * - These helpers could be extracted into a separate
1915 * DateUtils module if CalendarView ever gets split
1916 * into multiple files.
1920 * Returns localized short weekday names.
1923 * ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
1926 * - Uses Intl.DateTimeFormat for proper localization
1927 * - Avoids hardcoding weekday strings
1929 * Implementation detail:
1930 * - We generate arbitrary dates (Jan 1–7, 2024)
1931 * - Only the weekday part is relevant
1933 private getDayNames(): string[] {
1934 const formatter = new Intl.DateTimeFormat(this.LOCALE, {
1938 return [1, 2, 3, 4, 5, 6, 7].map(d =>
1939 formatter.format(new Date(2024, 0, d))
1944 * Calculates ISO-8601 week number for a given date.
1947 * - JavaScript does NOT provide a native week number API
1948 * - Cinnamon calendar optionally displays week numbers
1950 * Implementation notes:
1951 * - Uses UTC to avoid timezone-related off-by-one errors
1952 * - Thursday-based week calculation per ISO standard
1955 * - ISO 8601 defines week 1 as the week containing Jan 4th
1957 private getWeekNumber(date: Date): number {
1966 // Move to Thursday of the current week
1968 d.getUTCDate() + 4 - (d.getUTCDay() || 7)
1971 const yearStart = new Date(
1972 Date.UTC(d.getUTCFullYear(), 0, 1)
1977 (d.getTime() - yearStart.getTime()) /
1984 /* ============================================================
1985 * TIME / DATE HELPERS FOR DAY VIEW FORMS
1986 * ============================================================
1988 * These helpers are used exclusively by:
1989 * - createTerminForm()
1990 * - Day View time handling
1992 * They convert between:
1993 * - UI-friendly strings (HH:MM)
1994 * - JavaScript Date objects
1996 * WHY THIS IS NECESSARY:
1997 * - St.Entry widgets only handle strings
1998 * - EventManager requires Date objects
1999 * - Keeping conversion logic centralized avoids bugs
2003 * Returns current local time formatted as HH:MM.
2005 * Used when creating a NEW event to pre-fill
2006 * the start time field.
2008 private _getCurrentTime(): string {
2009 const now = new Date();
2010 return `${String(now.getHours()).padStart(2, "0")}:${String(
2012 ).padStart(2, "0")}`;
2016 * Calculates a default end time (+1 hour)
2017 * based on a given start time string.
2020 * startTime = "14:30" → endTime = "15:30"
2024 * - Improving UX by avoiding empty end fields
2026 private _calculateDefaultEnd(startTime: string): string {
2027 const [h, m] = startTime.split(":").map(Number);
2028 const d = new Date();
2029 d.setHours(h + 1, m, 0, 0);
2031 return `${String(d.getHours()).padStart(2, "0")}:${String(
2033 ).padStart(2, "0")}`;
2037 * Builds a Date object from:
2038 * - A base calendar date
2039 * - A time string (HH:MM)
2041 * This is the final step before sending
2042 * data to EventManager / EDS.
2047 * → Date(2026-01-05T09:15:00)
2049 private _buildDateTime(date: Date, time: string): Date {
2050 const [h, m] = time.split(":").map(Number);
2051 const d = new Date(date);
2052 d.setHours(h, m, 0, 0);
2058/* ================================================================
2059 * HYBRID EXPORT STRATEGY (DEVELOPMENT vs PRODUCTION)
2060 * ================================================================
2062 * This section is EXTREMELY IMPORTANT.
2064 * DO NOT REMOVE unless you fully understand Cinnamon's
2065 * loading mechanisms.
2069 * Cinnamon applets run in a hybrid environment:
2071 * 1) DEVELOPMENT / BUILD TIME
2074 * - Bundlers / transpilers
2076 * 2) PRODUCTION / RUNTIME
2077 * - GJS (GNOME JavaScript)
2078 * - No real module loader
2079 * - Global namespace access
2081 * To support BOTH environments, we export the class in
2082 * two different ways.
2085/* ---------------------------------------------------------------
2086 * CommonJS-style export (build / tooling environments)
2087 * ---------------------------------------------------------------
2091 * - TypeScript compilation
2095if (typeof exports !== "undefined") {
2096 exports.CalendarView = CalendarView;
2099/* ---------------------------------------------------------------
2100 * Global export (Cinnamon runtime)
2101 * ---------------------------------------------------------------
2103 * Cinnamon loads applets by evaluating a single JS file.
2104 * There is NO module system at runtime.
2107 * - Classes MUST be attached to the global object
2108 * - Other files access them via global.CalendarView
2110 * The `(global as any)` cast:
2111 * - Is required to silence TypeScript
2112 * - Reflects the dynamic nature of GJS
2114 * WARNING TO FUTURE DEVELOPERS:
2116 * Removing this WILL:
2117 * - Work in development
2118 * - FAIL in production
2119 * - Break runtime imports silently
2122(global as any).CalendarView = CalendarView;