aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md3
-rw-r--r--desktop/Prefs.qml14
-rw-r--r--desktop/island/calendar/Cal.js28
-rw-r--r--desktop/island/calendar/CalendarPanel.qml2
-rw-r--r--desktop/island/calendar/EventEditor.qml11
-rw-r--r--desktop/island/calendar/test_cal.js13
-rw-r--r--desktop/modules/island/IslandCalendarModule.qml76
-rw-r--r--desktop/shell.qml2
8 files changed, 137 insertions, 12 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 6f637c6..bda9651 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -17,7 +17,8 @@ modules.
icons, sunset, idle (the former appearance shell), notifications, autostart, shells,
the assistant's daemon and presets, qtmaildir, git,
Hyprland's keyboard shortcuts, options and monitors, fonts, the Sonix boot logo,
- deck-it (the tablet dashboard's autostart and config)
+ deck-it (the tablet dashboard's autostart and config),
+ the island bar (the calendar's default event length)
island/ top-centre pills on DP-1: clock, volume, Sonix player, tray, layout,
privacy, calendar, OBS
assistant/ the assistant canvas on SUPER+Home, a view over desktop-assistant
diff --git a/desktop/Prefs.qml b/desktop/Prefs.qml
index ef2ac72..0861258 100644
--- a/desktop/Prefs.qml
+++ b/desktop/Prefs.qml
@@ -84,6 +84,15 @@ Singleton {
root.write(Object.assign({}, root.doc, { bootlogo: root.bootlogo }));
}
+ // The island bar's settings (modules/island): the length a new event gets,
+ // and that its end keeps from the start, in minutes.
+ readonly property var islandDefaults: ({ eventMinutes: 60 })
+ property var island: islandDefaults
+ function setIsland(changes) {
+ root.island = Object.assign({}, root.island, changes);
+ root.write(Object.assign({}, root.doc, { island: root.island }));
+ }
+
// The whole parsed object, so a write keeps keys this version does not
// know about.
property var doc: ({})
@@ -154,6 +163,11 @@ Singleton {
const bl = parsed.bootlogo && typeof parsed.bootlogo === "object" && !Array.isArray(parsed.bootlogo)
? parsed.bootlogo : {};
root.bootlogo = Object.assign({}, root.bootlogoDefaults, bl);
+ const isl = parsed.island && typeof parsed.island === "object" ? parsed.island : {};
+ root.island = {
+ eventMinutes: Number.isInteger(isl.eventMinutes) && isl.eventMinutes >= 5 && isl.eventMinutes <= 1440
+ ? isl.eventMinutes : 60,
+ };
}
FileView {
diff --git a/desktop/island/calendar/Cal.js b/desktop/island/calendar/Cal.js
index 6cf492d..6ec7bb3 100644
--- a/desktop/island/calendar/Cal.js
+++ b/desktop/island/calendar/Cal.js
@@ -112,14 +112,28 @@ function stepTime(s, minutes) {
return pad(Math.floor(m / 60)) + ":" + pad(m % 60);
}
-// A new event on `day`: the next whole hour, one hour long, alarm 1h (the
-// most common alarm in the live calendars). Capped at 22:00 so the end stays
-// on the same day.
-function newDraft(day, now, calendar) {
+// A new event on `day`: the next whole hour, `minutes` long (the island's
+// default event length), alarm 1h (the most common alarm in the live
+// calendars). Capped at 22:00 so a short event stays on the same day.
+function newDraft(day, now, calendar, minutes) {
const h = Math.min(22, now.getHours() + 1);
- return { uid: "", source: "", calendar: calendar, title: "", allDay: false,
- startDay: day, endDay: day, startTime: pad(h) + ":00", endTime: pad(h + 1) + ":00",
- repeat: "none", until: "", alarm: "1h", location: "", notes: "", override: false };
+ return withStart({ uid: "", source: "", calendar: calendar, title: "", allDay: false,
+ startDay: day, endDay: day, startTime: pad(h) + ":00", endTime: pad(h) + ":00",
+ repeat: "none", until: "", alarm: "1h", location: "", notes: "", override: false },
+ pad(h) + ":00", minutes);
+}
+
+// The draft with a new start time, as typed, the end following it `minutes` later,
+// into the next day if need be. A multi-day event keeps its end: snapping it
+// to the start's day would throw the other days away.
+function withStart(draft, time, minutes) {
+ const d = Object.assign({}, draft, { startTime: time });
+ const n = normTime(time);
+ if (!validTime(n) || d.startDay !== d.endDay) return d;
+ const t = +n.slice(0, 2) * 60 + +n.slice(3, 5) + (minutes > 0 ? minutes : 60);
+ d.endDay = addDays(d.startDay, Math.floor(t / 1440));
+ d.endTime = pad(Math.floor(t % 1440 / 60)) + ":" + pad(t % 60);
+ return d;
}
// calctl's event JSON as an editor draft. `source` is the calendar the event
diff --git a/desktop/island/calendar/CalendarPanel.qml b/desktop/island/calendar/CalendarPanel.qml
index 4a0a011..d28a856 100644
--- a/desktop/island/calendar/CalendarPanel.qml
+++ b/desktop/island/calendar/CalendarPanel.qml
@@ -82,7 +82,7 @@ Item {
}
function newEvent() {
- editor.load(Cal.newDraft(panel.selected, new Date(), Calendar.defaultCalendar));
+ editor.load(Cal.newDraft(panel.selected, new Date(), Calendar.defaultCalendar, Prefs.island.eventMinutes));
panel.editing = true;
}
diff --git a/desktop/island/calendar/EventEditor.qml b/desktop/island/calendar/EventEditor.qml
index 41815e3..727e1df 100644
--- a/desktop/island/calendar/EventEditor.qml
+++ b/desktop/island/calendar/EventEditor.qml
@@ -55,7 +55,14 @@ Column {
function setTime(key, value) {
(key === "startTime" ? startTime : endTime).input.text = value;
- ed.set(key, value);
+ if (key === "startTime") ed.setStart(value);
+ else ed.set(key, value);
+ }
+
+ // The end follows the start by the island's default event length.
+ function setStart(text) {
+ ed.draft = Cal.withStart(ed.draft, text, Prefs.island.eventMinutes);
+ endTime.input.text = ed.draft.endTime;
}
// A click in the time picker: an hour keeps the minutes, a minute keeps
@@ -195,7 +202,7 @@ Column {
id: startTime
width: 62
visible: !ed.draft.allDay
- input.onTextEdited: ed.set("startTime", input.text)
+ input.onTextEdited: ed.setStart(input.text)
input.onEditingFinished: if (Cal.normTime(input.text) !== input.text) ed.setTime("startTime", Cal.normTime(input.text))
MouseArea {
anchors.fill: parent
diff --git a/desktop/island/calendar/test_cal.js b/desktop/island/calendar/test_cal.js
index de3d1c7..853cbb6 100644
--- a/desktop/island/calendar/test_cal.js
+++ b/desktop/island/calendar/test_cal.js
@@ -17,7 +17,7 @@ const assert = require("assert");
const src = fs.readFileSync(__dirname + "/Cal.js", "utf8").replace(/^\.pragma library$/m, "");
const C = new Function(src + "\nreturn { pad, addDays, today, daysBetween, monthCells, shiftMonth," +
- " lastDay, onDay, dayEvents, dots, timeLabel, validTime, normTime, stepTime, newDraft, fromEvent, pick," +
+ " lastDay, onDay, dayEvents, dots, timeLabel, validTime, normTime, stepTime, newDraft, withStart, fromEvent, pick," +
" toPayload };")();
// Dates are plain YYYY-MM-DD strings, arithmetic in UTC so DST never moves a day.
@@ -91,6 +91,17 @@ assert.strictEqual(d.alarm, "1h");
assert.strictEqual(d.calendar, "home");
assert.strictEqual(d.source, "");
assert.strictEqual(C.newDraft("2026-10-20", new Date(2026, 9, 13, 23, 10), "home").startTime, "22:00");
+assert.strictEqual(C.newDraft("2026-10-20", new Date(2026, 9, 13, 9, 20), "home", 90).endTime, "11:30");
+
+// The end follows the start, by the default length, past midnight too.
+const w = C.withStart(d, "23:30", 60);
+assert.strictEqual(w.startTime, "23:30");
+assert.strictEqual(w.endTime, "00:30");
+assert.strictEqual(w.endDay, "2026-10-21");
+assert.strictEqual(C.withStart(d, "14:00", 45).endTime, "14:45");
+assert.strictEqual(C.withStart(d, "9.3", 60).endTime, "11:00");
+const multi = Object.assign({}, d, { endDay: "2026-10-22", endTime: "18:00" });
+assert.strictEqual(C.withStart(multi, "08:00", 60).endTime, "18:00");
// From calctl's JSON.
const fe = C.fromEvent({ uid: "u", calendar: "home", title: "Trip", allDay: true, start: "2026-10-14",
diff --git a/desktop/modules/island/IslandCalendarModule.qml b/desktop/modules/island/IslandCalendarModule.qml
new file mode 100644
index 0000000..6525d11
--- /dev/null
+++ b/desktop/modules/island/IslandCalendarModule.qml
@@ -0,0 +1,76 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import QtQuick
+import "../.."
+import "../appearance"
+
+// The island bar's settings group, starting with the calendar's.
+Module {
+ id: mod
+ name: "island-calendar"
+ label: "Calendar"
+ // nf-md-calendar, present in Inconsolata Nerd Font.
+ icon: "\u{f00ed}"
+ settings: true
+ group: "Island bar"
+ keywords: ["island", "calendar", "event", "length", "duration", "end time"]
+ searchItems: [{ text: "Default event length", key: "eventMinutes" }]
+
+ // The draft, as typed; dirty while it differs from what is saved.
+ property string draft: String(Prefs.island.eventMinutes)
+ readonly property bool valid: /^\d+$/.test(draft) && +draft >= 5 && +draft <= 1440
+ dirty: draft !== String(Prefs.island.eventMinutes)
+ function refresh() { if (!mod.dirty) mod.draft = String(Prefs.island.eventMinutes); }
+
+ page: Component {
+ Page {
+ title: "Calendar"
+ actions: Component {
+ SaveBar {
+ dirty: mod.dirty; valid: mod.valid
+ error: mod.valid ? "" : "Length is 5 to 1440 minutes."
+ onSave: Prefs.setIsland({ eventMinutes: +mod.draft })
+ onDiscard: mod.draft = String(Prefs.island.eventMinutes)
+ }
+ }
+ Column {
+ objectName: "eventMinutes"
+ width: parent.width
+ spacing: 8
+ Row {
+ spacing: 8
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ text: "Default event length"
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ }
+ // Read on every change: a click on Save ends no editing.
+ Field { width: 60; value: mod.draft; onValueChanged: mod.draft = value }
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ text: "minutes"
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ }
+ }
+ Text {
+ width: parent.width
+ wrapMode: Text.WordWrap
+ text: "A new event gets this length, and its end keeps it when the start moves."
+ color: Theme.overlay
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+ }
+ }
+ }
+}
diff --git a/desktop/shell.qml b/desktop/shell.qml
index f5ca7d4..dd440c0 100644
--- a/desktop/shell.qml
+++ b/desktop/shell.qml
@@ -22,6 +22,7 @@ import "modules/appearance"
import "modules/assistant"
import "modules/autostart"
import "modules/deckit"
+import "modules/island"
import "modules/qtmaildir"
import "modules/sbbuilder"
import "modules/shells"
@@ -112,6 +113,7 @@ ShellRoot {
OptionsModule {},
MonitorsModule {},
ObsScenesModule {},
+ IslandCalendarModule {},
// deck-it's four pages, one draft.
DeckItModule {},
DeckItDashboardModule {},