aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/island
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-13 09:55:00 +0100
committerDanilo M. <danix@danix.xyz>2026-11-13 09:55:00 +0100
commit7829a18d8049087eaa963fb922a8701cc9851979 (patch)
tree0ce6c851a96ff4f337a47ac40d6253a9f8f7f079 /desktop/island
parent70d2b0c0e2e83dd2f17bd49dc2f06d39a7f7f4cf (diff)
downloadquickshell-7829a18d8049087eaa963fb922a8701cc9851979.tar.gz
quickshell-7829a18d8049087eaa963fb922a8701cc9851979.zip
feat(island): date and time format settings
The clock pill hard-coded "ddd dd/MM" and "HH:mm", and the calendar its day headings. A new Date and time page in the Island bar group picks a 24- or 12-hour clock and one of 22 date formats, grouped by order (day, month or year first) and each shown as today's date. The calendar's headings and editor dates keep spelling the month out and follow the chosen order; agenda times and the hour picker go compact on the 12-hour clock ("9:30p", "1p"), where width is short. A preview mocks the pill and panel with the unsaved choice. The formats live in Cal.js (DATE_GROUPS), which Prefs imports to validate island.dateFormat. Event times still store and show as 24-hour HH:MM, since that is what calctl speaks, but normTime now also accepts am and pm ("9.30pm", "12am"), rejecting an hour outside 1 to 12 with one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/island')
-rw-r--r--desktop/island/ClockPill.qml5
-rw-r--r--desktop/island/calendar/Cal.js52
-rw-r--r--desktop/island/calendar/CalendarPanel.qml10
-rw-r--r--desktop/island/calendar/EventEditor.qml2
-rw-r--r--desktop/island/calendar/test_cal.js19
5 files changed, 71 insertions, 17 deletions
diff --git a/desktop/island/ClockPill.qml b/desktop/island/ClockPill.qml
index 4ed8e38..1d18503 100644
--- a/desktop/island/ClockPill.qml
+++ b/desktop/island/ClockPill.qml
@@ -12,6 +12,7 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
+import "calendar/Cal.js" as Cal
import ".."
import "../modules/vm"
import "calendar"
@@ -62,13 +63,13 @@ Capsule {
Text {
anchors.verticalCenter: parent.verticalCenter
- text: Qt.formatDateTime(clock.date, "ddd dd/MM")
+ text: Qt.formatDateTime(clock.date, Cal.shortDateFmt(Prefs.island.dateFormat))
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 }
color: Theme.subtext
}
Text {
anchors.verticalCenter: parent.verticalCenter
- text: Qt.formatDateTime(clock.date, "HH:mm")
+ text: Qt.formatDateTime(clock.date, Cal.timeFmt(Prefs.island.h12))
font { family: Theme.fontFamily; pixelSize: Theme.fontSize; weight: Font.DemiBold }
color: Theme.text
}
diff --git a/desktop/island/calendar/Cal.js b/desktop/island/calendar/Cal.js
index 6ec7bb3..d12ed29 100644
--- a/desktop/island/calendar/Cal.js
+++ b/desktop/island/calendar/Cal.js
@@ -83,10 +83,42 @@ function dots(events, day, calendars) {
return calendars.filter(c => events.some(e => e.calendar === c.name && onDay(e, day)));
}
-function timeLabel(ev, day) {
+// The island's clock settings (Prefs.island): h12 for a 12-hour clock, and
+// dateFormat, the clock pill's date as one of DATE_FORMATS (Qt format
+// strings). The calendar's longer dates follow its order, day or month
+// first. clockTime formats calctl's "HH:MM", compact ("9:30p") where width
+// is short.
+var DATE_GROUPS = [
+ { title: "Day first", formats: ["ddd dd/MM", "dd/MM", "dd/MM/yy", "dd/MM/yyyy", "ddd dd/MM/yyyy",
+ "dd.MM.yyyy", "dd-MM-yyyy", "d MMM", "ddd d MMM", "d MMM yyyy"] },
+ { title: "Month first", formats: ["ddd MM/dd", "MM/dd", "MM/dd/yy", "MM/dd/yyyy", "ddd MM/dd/yyyy",
+ "MMM d", "ddd MMM d", "MMM d, yyyy"] },
+ { title: "Year first", formats: ["yyyy-MM-dd", "ddd yyyy-MM-dd", "yyyy/MM/dd", "ddd MM-dd"] }];
+var DATE_FORMATS = DATE_GROUPS.reduce((a, g) => a.concat(g.formats), []);
+function timeFmt(h12) { return h12 ? "h:mm AP" : "HH:mm"; }
+function shortDateFmt(fmt) { return DATE_FORMATS.includes(fmt) ? fmt : DATE_FORMATS[0]; }
+// Day first unless the month comes before the day, the weekday name aside.
+function dayFirst(fmt) {
+ const f = shortDateFmt(fmt).replace(/ddd+/, "");
+ return f.indexOf("d") < f.indexOf("M");
+}
+function dayFmt(fmt, long) {
+ if (dayFirst(fmt)) return long ? "dddd d MMMM" : "ddd d MMM";
+ return long ? "dddd, MMMM d" : "ddd MMM d";
+}
+function clockTime(hhmm, h12) {
+ if (!h12 || !validTime(hhmm)) return hhmm;
+ const h = +hhmm.slice(0, 2);
+ return (h % 12 || 12) + hhmm.slice(2) + (h < 12 ? "a" : "p");
+}
+function hourLabel(h, h12) {
+ return h12 ? (h % 12 || 12) + (h < 12 ? "a" : "p") : pad(h);
+}
+
+function timeLabel(ev, day, h12) {
if (ev.allDay) return "all day";
- const s = ev.start.slice(0, 10) === day ? ev.start.slice(11, 16) : "…";
- const e = ev.end.slice(0, 10) === day ? ev.end.slice(11, 16) : "…";
+ const s = ev.start.slice(0, 10) === day ? clockTime(ev.start.slice(11, 16), h12) : "…";
+ const e = ev.end.slice(0, 10) === day ? clockTime(ev.end.slice(11, 16), h12) : "…";
return s + "–" + e;
}
@@ -94,12 +126,18 @@ function validTime(s) {
return /^([01]\d|2[0-3]):[0-5]\d$/.test(s);
}
-// Loose typing as HH:MM: "9.30", "9:30", "930" and "9" mean what they look
-// like. Anything else comes back unchanged, for validTime to reject.
+// Loose typing as HH:MM: "9.30", "9:30", "930", "9", and with am or pm
+// ("9.30pm", "12am") for the 12-hour clock, mean what they look like.
+// Anything else comes back unchanged, for validTime to reject.
function normTime(s) {
- const m = /^\s*(\d{1,2})(?:[:.,h ]?(\d{2}))?\s*$/.exec(s);
+ const m = /^\s*(\d{1,2})(?:[:.,h ]?(\d{2}))?\s*(?:([ap])\.?m?\.?)?\s*$/i.exec(s);
if (!m) return s;
- const t = pad(+m[1]) + ":" + (m[2] || "00");
+ let h = +m[1];
+ if (m[3]) {
+ if (h < 1 || h > 12) return s;
+ h = h % 12 + (m[3].toLowerCase() === "p" ? 12 : 0);
+ }
+ const t = pad(h) + ":" + (m[2] || "00");
return validTime(t) ? t : s;
}
diff --git a/desktop/island/calendar/CalendarPanel.qml b/desktop/island/calendar/CalendarPanel.qml
index d28a856..fef672c 100644
--- a/desktop/island/calendar/CalendarPanel.qml
+++ b/desktop/island/calendar/CalendarPanel.qml
@@ -226,8 +226,8 @@ Item {
delegate: TimeCell {
required property int index
width: (panel.gridWidth - 10) / 6
- label: Cal.pad(index)
- on: hours.cur.slice(0, 3) === label + ":"
+ label: Cal.hourLabel(index, Prefs.island.h12)
+ on: hours.cur.slice(0, 2) === Cal.pad(index)
onClicked: editor.pickTime(index, null)
}
}
@@ -291,7 +291,7 @@ Item {
Text {
text: panel.selected === "" ? ""
: Qt.formatDate(new Date(+panel.selected.slice(0, 4), +panel.selected.slice(5, 7) - 1,
- +panel.selected.slice(8, 10)), "dddd d MMMM")
+ +panel.selected.slice(8, 10)), Cal.dayFmt(Prefs.island.dateFormat, true))
color: Theme.text
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
}
@@ -327,9 +327,9 @@ Item {
Text {
id: when
x: 16
- width: 82
+ width: Prefs.island.h12 ? 92 : 82
anchors.verticalCenter: parent.verticalCenter
- text: Cal.timeLabel(row.modelData, panel.selected)
+ text: Cal.timeLabel(row.modelData, panel.selected, Prefs.island.h12)
color: Theme.subtext
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 }
}
diff --git a/desktop/island/calendar/EventEditor.qml b/desktop/island/calendar/EventEditor.qml
index 727e1df..e55396b 100644
--- a/desktop/island/calendar/EventEditor.qml
+++ b/desktop/island/calendar/EventEditor.qml
@@ -109,7 +109,7 @@ Column {
function dayLabel(day) {
const d = new Date(+day.slice(0, 4), +day.slice(5, 7) - 1, +day.slice(8, 10));
- return Qt.formatDate(d, "ddd d MMM");
+ return Qt.formatDate(d, Cal.dayFmt(Prefs.island.dateFormat, false));
}
// An unhandled Escape from any field lands here: leave the editor, not
diff --git a/desktop/island/calendar/test_cal.js b/desktop/island/calendar/test_cal.js
index 853cbb6..5a9c1c0 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, withStart, fromEvent, pick," +
+ " lastDay, onDay, dayEvents, dots, timeLabel, validTime, normTime, clockTime, hourLabel, shortDateFmt, dayFmt, stepTime, newDraft, withStart, fromEvent, pick," +
" toPayload };")();
// Dates are plain YYYY-MM-DD strings, arithmetic in UTC so DST never moves a day.
@@ -79,9 +79,24 @@ assert.strictEqual(C.stepTime("bad", 15), "bad");
assert.strictEqual(C.stepTime("9.30", 15), "09:45");
for (const [i, o] of [["9.30", "09:30"], ["9:30", "09:30"], ["930", "09:30"], ["1230", "12:30"],
["9", "09:00"], [" 14h15 ", "14:15"], ["09:05", "09:05"],
- ["24.00", "24.00"], ["9.7", "9.7"], ["ab", "ab"], ["", ""]])
+ ["24.00", "24.00"], ["9.30pm", "21:30"], ["12am", "00:00"], ["12 PM", "12:00"],
+ ["9:30 a.m.", "09:30"], ["13pm", "13pm"], ["9.7", "9.7"], ["ab", "ab"], ["", ""]])
assert.strictEqual(C.normTime(i), o, i);
+// The 12-hour clock, compact; 24-hour leaves calctl's time alone.
+assert.strictEqual(C.clockTime("00:05", true), "12:05a");
+assert.strictEqual(C.clockTime("12:00", true), "12:00p");
+assert.strictEqual(C.clockTime("21:30", true), "9:30p");
+assert.strictEqual(C.clockTime("21:30", false), "21:30");
+assert.strictEqual(C.hourLabel(0, true), "12a");
+assert.strictEqual(C.hourLabel(13, true), "1p");
+assert.strictEqual(C.hourLabel(7, false), "07");
+assert.strictEqual(C.shortDateFmt("bogus"), "ddd dd/MM");
+assert.strictEqual(C.dayFmt("ddd MM/dd", false), "ddd MMM d");
+assert.strictEqual(C.dayFmt("ddd dd/MM", true), "dddd d MMMM");
+assert.strictEqual(C.dayFmt("yyyy-MM-dd", false), "ddd MMM d");
+assert.strictEqual(C.dayFmt("d MMM yyyy", false), "ddd d MMM");
+
// A new event: the next whole hour, one hour long, alarm 1h.
const d = C.newDraft("2026-10-20", new Date(2026, 9, 13, 9, 20), "home");
assert.strictEqual(d.startTime, "10:00");