aboutsummaryrefslogtreecommitdiff
path: root/.local/share/gnome-shell
diff options
context:
space:
mode:
authorRaul Benencia <id@rbenencia.name>2026-04-13 08:11:52 -0700
committerRaul Benencia <id@rbenencia.name>2026-04-13 08:34:32 -0700
commit90c1704f870aaafac7fb181e954b29740b39f7d1 (patch)
treea2576d55271c33743a1d6ab7f6fb36b2ed733b3a /.local/share/gnome-shell
parent45332a3aa41d4f7155d7816cea56bed5d04624af (diff)
org-agenda-shellHEADmaster
Initial version of org-agenda-shell. Alpha.
Diffstat (limited to '.local/share/gnome-shell')
-rw-r--r--.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/extension.js548
-rw-r--r--.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/metadata.json11
-rw-r--r--.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/prefs.js33
-rw-r--r--.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/schemas/org.gnome.shell.extensions.org-agenda-indicator.gschema.xml14
-rw-r--r--.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/stylesheet.css38
5 files changed, 644 insertions, 0 deletions
diff --git a/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/extension.js b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/extension.js
new file mode 100644
index 0000000..b92048c
--- /dev/null
+++ b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/extension.js
@@ -0,0 +1,548 @@
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
+import Clutter from 'gi://Clutter';
+import Pango from 'gi://Pango';
+
+import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
+import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
+
+const SNAPSHOT_RELATIVE_PATH = '.cache/org-agenda-shell/today.json';
+const REFRESH_INTERVAL_SECONDS = 60;
+const RELOAD_DELAY_MS = 200;
+const PANEL_TITLE_MAX = 20;
+
+const InfoMenuItem = GObject.registerClass(
+class InfoMenuItem extends PopupMenu.PopupBaseMenuItem {
+ _init(primaryText, secondaryText = '', {overdue = false} = {}) {
+ super._init({reactive: false, can_focus: false});
+
+ const box = new St.BoxLayout({
+ vertical: true,
+ x_expand: true,
+ style_class: 'org-agenda-indicator-row',
+ });
+
+ this._primaryLabel = new St.Label({
+ text: primaryText,
+ x_expand: true,
+ style_class: 'org-agenda-indicator-row-title',
+ });
+ this._primaryLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+
+ if (overdue)
+ this._primaryLabel.add_style_class_name('org-agenda-indicator-overdue');
+
+ box.add_child(this._primaryLabel);
+
+ if (secondaryText) {
+ this._secondaryLabel = new St.Label({
+ text: secondaryText,
+ x_expand: true,
+ style_class: 'org-agenda-indicator-row-meta',
+ });
+ this._secondaryLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+ box.add_child(this._secondaryLabel);
+ }
+
+ this.add_child(box);
+ }
+});
+
+const SectionHeaderItem = GObject.registerClass(
+class SectionHeaderItem extends PopupMenu.PopupBaseMenuItem {
+ _init(text) {
+ super._init({reactive: false, can_focus: false});
+
+ const label = new St.Label({
+ text,
+ x_expand: true,
+ style_class: 'org-agenda-indicator-section-label',
+ });
+ const box = new St.BoxLayout({
+ x_expand: true,
+ style_class: 'org-agenda-indicator-section',
+ });
+
+ box.add_child(label);
+ this.add_child(box);
+ }
+});
+
+const Indicator = GObject.registerClass(
+class Indicator extends PanelMenu.Button {
+ _init(settings) {
+ super._init(0.0, 'Org Agenda Indicator');
+
+ this._settings = settings;
+ this._snapshotPath = GLib.build_filenamev([
+ GLib.get_home_dir(),
+ SNAPSHOT_RELATIVE_PATH,
+ ]);
+ this._snapshotDir = GLib.path_get_dirname(this._snapshotPath);
+ this._snapshotParentDir = GLib.path_get_dirname(this._snapshotDir);
+ this._snapshotDirBaseName = GLib.path_get_basename(this._snapshotDir);
+ this._snapshotBaseName = GLib.path_get_basename(this._snapshotPath);
+ this._directoryMonitor = null;
+ this._monitoredDirectory = null;
+ this._refreshSourceId = 0;
+ this._reloadSourceId = 0;
+ this._state = {
+ status: 'loading',
+ taskCount: 0,
+ todayCount: 0,
+ overdueCount: 0,
+ date: null,
+ generatedAt: null,
+ generatedEpoch: null,
+ tasks: [],
+ clockedIn: null,
+ error: null,
+ };
+
+ this._label = new St.Label({
+ text: '...',
+ y_align: Clutter.ActorAlign.CENTER,
+ name: 'org-agenda-indicator-label',
+ });
+ this._label.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+ this.add_child(this._label);
+
+ this._showClockedInChangedId = this._settings.connect(
+ 'changed::show-clocked-in-task',
+ () => {
+ this._syncLabel();
+ this._rebuildMenu();
+ });
+
+ this._refreshSourceId = GLib.timeout_add_seconds(
+ GLib.PRIORITY_DEFAULT,
+ REFRESH_INTERVAL_SECONDS,
+ () => {
+ this._loadSnapshot();
+ return GLib.SOURCE_CONTINUE;
+ });
+
+ this._startMonitoring();
+ this._loadSnapshot();
+ }
+
+ destroy() {
+ if (this._reloadSourceId) {
+ GLib.source_remove(this._reloadSourceId);
+ this._reloadSourceId = 0;
+ }
+
+ if (this._refreshSourceId) {
+ GLib.source_remove(this._refreshSourceId);
+ this._refreshSourceId = 0;
+ }
+
+ this._directoryMonitor?.cancel();
+ this._directoryMonitor = null;
+ if (this._showClockedInChangedId) {
+ this._settings.disconnect(this._showClockedInChangedId);
+ this._showClockedInChangedId = 0;
+ }
+
+ super.destroy();
+ }
+
+ _startMonitoring() {
+ this._directoryMonitor?.cancel();
+ this._directoryMonitor = null;
+
+ const targetDirectory = GLib.file_test(this._snapshotDir, GLib.FileTest.IS_DIR)
+ ? this._snapshotDir
+ : this._snapshotParentDir;
+ const directory = Gio.File.new_for_path(targetDirectory);
+
+ try {
+ this._directoryMonitor = directory.monitor_directory(
+ Gio.FileMonitorFlags.WATCH_MOVES,
+ null);
+ this._monitoredDirectory = targetDirectory;
+ this._directoryMonitor.connect('changed', (_monitor, file, otherFile, eventType) => {
+ if (!this._eventTouchesSnapshot(file, otherFile, eventType))
+ return;
+
+ this._scheduleReload();
+ });
+ } catch (error) {
+ this._monitoredDirectory = null;
+ this._setState({
+ status: 'error',
+ taskCount: 0,
+ todayCount: 0,
+ overdueCount: 0,
+ date: null,
+ generatedAt: null,
+ generatedEpoch: null,
+ tasks: [],
+ clockedIn: null,
+ error: `Unable to monitor ${this._snapshotDir}: ${error.message}`,
+ });
+ }
+ }
+
+ _eventTouchesSnapshot(file, otherFile, eventType) {
+ const relevantEvents = new Set([
+ Gio.FileMonitorEvent.CHANGED,
+ Gio.FileMonitorEvent.CHANGES_DONE_HINT,
+ Gio.FileMonitorEvent.CREATED,
+ Gio.FileMonitorEvent.MOVED_IN,
+ Gio.FileMonitorEvent.MOVED_OUT,
+ Gio.FileMonitorEvent.DELETED,
+ Gio.FileMonitorEvent.ATTRIBUTE_CHANGED,
+ ]);
+
+ if (!relevantEvents.has(eventType))
+ return false;
+
+ const names = [
+ file?.get_basename(),
+ otherFile?.get_basename(),
+ ];
+
+ if (this._monitoredDirectory === this._snapshotParentDir)
+ return names.includes(this._snapshotDirBaseName);
+
+ return names.includes(this._snapshotBaseName);
+ }
+
+ _scheduleReload() {
+ if (this._reloadSourceId)
+ return;
+
+ this._reloadSourceId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ RELOAD_DELAY_MS,
+ () => {
+ this._reloadSourceId = 0;
+ this._loadSnapshot();
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _loadSnapshot() {
+ const snapshotFile = Gio.File.new_for_path(this._snapshotPath);
+
+ if ((this._monitoredDirectory === this._snapshotParentDir &&
+ GLib.file_test(this._snapshotDir, GLib.FileTest.IS_DIR)) ||
+ (this._monitoredDirectory === this._snapshotDir &&
+ !GLib.file_test(this._snapshotDir, GLib.FileTest.IS_DIR)))
+ this._startMonitoring();
+
+ try {
+ if (!snapshotFile.query_exists(null)) {
+ this._setState({
+ status: 'missing',
+ taskCount: 0,
+ todayCount: 0,
+ overdueCount: 0,
+ date: null,
+ generatedAt: null,
+ generatedEpoch: null,
+ tasks: [],
+ clockedIn: null,
+ error: null,
+ });
+ return;
+ }
+
+ const [, contents] = snapshotFile.load_contents(null);
+ const payload = JSON.parse(new TextDecoder().decode(contents));
+ const currentDate = GLib.DateTime.new_now_local().format('%F');
+
+ if (payload.date !== currentDate) {
+ this._setState({
+ status: 'stale',
+ taskCount: 0,
+ todayCount: 0,
+ overdueCount: 0,
+ date: payload.date ?? null,
+ generatedAt: payload.generated_at ?? null,
+ generatedEpoch: payload.generated_epoch ?? null,
+ tasks: [],
+ clockedIn: payload.clocked_in ?? null,
+ error: null,
+ });
+ return;
+ }
+
+ const tasks = Array.isArray(payload.today_tasks) ? payload.today_tasks : [];
+ const overdueCount = Number(payload.overdue_count ?? tasks.filter(task => task.is_overdue).length);
+ const todayCount = Number(payload.today_count ?? tasks.length - overdueCount);
+
+ this._setState({
+ status: 'ready',
+ taskCount: Number(payload.task_count ?? tasks.length),
+ todayCount,
+ overdueCount,
+ date: payload.date ?? null,
+ generatedAt: payload.generated_at ?? null,
+ generatedEpoch: payload.generated_epoch ?? null,
+ tasks,
+ clockedIn: payload.clocked_in ?? null,
+ error: null,
+ });
+ } catch (error) {
+ this._setState({
+ status: 'error',
+ taskCount: 0,
+ todayCount: 0,
+ overdueCount: 0,
+ date: null,
+ generatedAt: null,
+ generatedEpoch: null,
+ tasks: [],
+ clockedIn: null,
+ error: error.message,
+ });
+ }
+ }
+
+ _setState(nextState) {
+ this._state = nextState;
+ this._syncLabel();
+ this._rebuildMenu();
+ }
+
+ _syncLabel() {
+ switch (this._state.status) {
+ case 'ready':
+ this._label.set_text(this._panelText());
+ break;
+ case 'loading':
+ this._label.set_text('...');
+ break;
+ case 'missing':
+ this._label.set_text('-');
+ break;
+ default:
+ this._label.set_text('?');
+ break;
+ }
+ }
+
+ _rebuildMenu() {
+ this.menu.removeAll();
+
+ this.menu.addMenuItem(new InfoMenuItem(this._summaryText(), this._summaryMetaText()));
+
+ if (this._state.error) {
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+ this.menu.addMenuItem(new InfoMenuItem('Unable to read agenda snapshot', this._state.error));
+ return;
+ }
+
+ if (this._state.status !== 'ready')
+ return;
+
+ const todayTasks = this._state.tasks.filter(task => !task.is_overdue);
+ const overdueTasks = this._state.tasks.filter(task => task.is_overdue);
+
+ if (this._showClockedInTask() && this._state.clockedIn) {
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+ this.menu.addMenuItem(new SectionHeaderItem('Clocked In'));
+ this.menu.addMenuItem(this._clockedInItem(this._state.clockedIn));
+ }
+
+ if (todayTasks.length > 0 || overdueTasks.length > 0) {
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+ }
+
+ if (todayTasks.length > 0) {
+ this.menu.addMenuItem(new SectionHeaderItem('Today'));
+ for (const task of todayTasks)
+ this.menu.addMenuItem(this._taskItem(task));
+ }
+
+ if (overdueTasks.length > 0) {
+ if (todayTasks.length > 0)
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this.menu.addMenuItem(new SectionHeaderItem('Overdue'));
+ for (const task of overdueTasks)
+ this.menu.addMenuItem(this._taskItem(task));
+ }
+ }
+
+ _panelText() {
+ const compactCount = this._compactCountText();
+
+ if (this._showClockedInTask() && this._state.clockedIn?.title) {
+ const title = this._truncate(this._state.clockedIn.title, PANEL_TITLE_MAX);
+ return compactCount ? `● ${title} · ${compactCount}` : `● ${title}`;
+ }
+
+ return compactCount;
+ }
+
+ _compactCountText() {
+ const {todayCount, overdueCount} = this._state;
+
+ if (overdueCount > 0 && todayCount > 0)
+ return `${todayCount}+${overdueCount}!`;
+
+ if (overdueCount > 0)
+ return `${overdueCount}!`;
+
+ return String(todayCount);
+ }
+
+ _summaryText() {
+ switch (this._state.status) {
+ case 'ready':
+ if (this._state.taskCount === 0)
+ return this._state.clockedIn ? 'Clock is running' : 'No scheduled tasks';
+
+ return this._countPhrase(this._state.todayCount, 'today') +
+ (this._state.overdueCount > 0 ? `, ${this._countPhrase(this._state.overdueCount, 'overdue')}` : '');
+ case 'missing':
+ return 'Agenda snapshot not found';
+ case 'stale':
+ return `Agenda snapshot is stale (${this._state.date ?? 'unknown date'})`;
+ case 'loading':
+ return 'Loading org agenda snapshot';
+ default:
+ return 'Unable to read agenda snapshot';
+ }
+ }
+
+ _summaryMetaText() {
+ const parts = [];
+
+ if (this._showClockedInTask() && this._state.clockedIn)
+ parts.push(this._clockSummaryText(this._state.clockedIn));
+
+ if (this._state.generatedEpoch)
+ parts.push(`Updated ${this._timeOfDay(this._state.generatedEpoch)}`);
+ else if (this._state.generatedAt)
+ parts.push(`Updated ${this._state.generatedAt}`);
+
+ return parts.join(' ');
+ }
+
+ _showClockedInTask() {
+ return this._settings.get_boolean('show-clocked-in-task');
+ }
+
+ _countPhrase(count, label) {
+ if (count === 1)
+ return `1 ${label}`;
+
+ return `${count} ${label}`;
+ }
+
+ _clockedInItem(task) {
+ return new InfoMenuItem(
+ task.title ?? 'Clock running',
+ this._clockTaskMeta(task));
+ }
+
+ _taskItem(task) {
+ return new InfoMenuItem(
+ this._taskPrimaryText(task),
+ this._taskMetaText(task),
+ {overdue: task.is_overdue});
+ }
+
+ _taskPrimaryText(task) {
+ const pieces = [];
+
+ if (task.time)
+ pieces.push(task.time);
+
+ pieces.push(task.title ?? 'Untitled task');
+
+ return pieces.join(' ');
+ }
+
+ _taskMetaText(task) {
+ const pieces = [];
+
+ if (task.state)
+ pieces.push(task.state);
+
+ if (task.category)
+ pieces.push(task.category);
+
+ if (task.is_overdue && task.scheduled_for)
+ pieces.push(`scheduled ${this._friendlyDate(task.scheduled_for)}`);
+
+ return pieces.join(' · ');
+ }
+
+ _clockTaskMeta(task) {
+ const pieces = [];
+
+ if (task.started_epoch)
+ pieces.push(`started ${this._timeOfDay(task.started_epoch)}`);
+
+ const elapsed = this._elapsedClockText(task.started_epoch);
+ if (elapsed)
+ pieces.push(elapsed);
+
+ if (task.state)
+ pieces.push(task.state);
+
+ if (task.category)
+ pieces.push(task.category);
+
+ return pieces.join(' · ');
+ }
+
+ _clockSummaryText(task) {
+ const elapsed = this._elapsedClockText(task.started_epoch);
+ return elapsed ? `Clock ${elapsed}` : 'Clock running';
+ }
+
+ _elapsedClockText(startedEpoch) {
+ if (!startedEpoch)
+ return '';
+
+ const roundedMinutes = Math.max(0, Math.floor((Date.now() / 1000 - startedEpoch) / 60));
+ const hours = Math.floor(roundedMinutes / 60);
+ const remainder = roundedMinutes % 60;
+
+ if (hours > 0)
+ return `${hours}h ${remainder}m`;
+
+ return `${roundedMinutes}m`;
+ }
+
+ _friendlyDate(dateText) {
+ const date = GLib.DateTime.new_from_iso8601(
+ `${dateText}T00:00:00`,
+ GLib.TimeZone.new_local());
+ return date ? date.format('%b %-d') : dateText;
+ }
+
+ _timeOfDay(epoch) {
+ const date = GLib.DateTime.new_from_unix_local(epoch);
+ return date ? date.format('%H:%M') : '';
+ }
+
+ _truncate(text, maxLength) {
+ if (!text || text.length <= maxLength)
+ return text ?? '';
+
+ return `${text.slice(0, maxLength - 1)}…`;
+ }
+});
+
+export default class OrgAgendaIndicatorExtension extends Extension {
+ enable() {
+ this._indicator = new Indicator(this.getSettings());
+ Main.panel.addToStatusArea(this.uuid, this._indicator);
+ }
+
+ disable() {
+ this._indicator?.destroy();
+ this._indicator = null;
+ }
+}
diff --git a/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/metadata.json b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/metadata.json
new file mode 100644
index 0000000..de62ebd
--- /dev/null
+++ b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/metadata.json
@@ -0,0 +1,11 @@
+{
+ "uuid": "org-agenda-indicator@rbenencia.name",
+ "extension-id": "org-agenda-indicator",
+ "name": "Org Agenda Indicator",
+ "description": "Show today's org-agenda tasks in the top bar.",
+ "settings-schema": "org.gnome.shell.extensions.org-agenda-indicator",
+ "shell-version": ["49"],
+ "session-modes": ["user"],
+ "url": "https://rbenencia.name",
+ "version": 1
+}
diff --git a/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/prefs.js b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/prefs.js
new file mode 100644
index 0000000..08db40d
--- /dev/null
+++ b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/prefs.js
@@ -0,0 +1,33 @@
+import Adw from 'gi://Adw';
+import Gtk from 'gi://Gtk';
+
+import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
+
+export default class OrgAgendaIndicatorPreferences extends ExtensionPreferences {
+ fillPreferencesWindow(window) {
+ const settings = this.getSettings('org.gnome.shell.extensions.org-agenda-indicator');
+ const page = new Adw.PreferencesPage();
+ const group = new Adw.PreferencesGroup({
+ title: 'Display',
+ description: 'Configure what the indicator shows.',
+ });
+ const row = new Adw.ActionRow({
+ title: 'Show clocked-in task',
+ subtitle: 'Display the active Org clock in the panel and menu when one is running.',
+ });
+ const toggle = new Gtk.Switch({
+ active: settings.get_boolean('show-clocked-in-task'),
+ valign: Gtk.Align.CENTER,
+ });
+
+ toggle.connect('notify::active', widget => {
+ settings.set_boolean('show-clocked-in-task', widget.get_active());
+ });
+
+ row.add_suffix(toggle);
+ row.activatable_widget = toggle;
+ group.add(row);
+ page.add(group);
+ window.add(page);
+ }
+}
diff --git a/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/schemas/org.gnome.shell.extensions.org-agenda-indicator.gschema.xml b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/schemas/org.gnome.shell.extensions.org-agenda-indicator.gschema.xml
new file mode 100644
index 0000000..317b9a1
--- /dev/null
+++ b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/schemas/org.gnome.shell.extensions.org-agenda-indicator.gschema.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schemalist>
+ <schema id="org.gnome.shell.extensions.org-agenda-indicator"
+ path="/org/gnome/shell/extensions/org-agenda-indicator/">
+ <key name="show-clocked-in-task" type="b">
+ <default>true</default>
+ <summary>Show clocked-in task</summary>
+ <description>
+ When enabled, the indicator displays the active Org clock in the panel
+ and in a dedicated menu section.
+ </description>
+ </key>
+ </schema>
+</schemalist>
diff --git a/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/stylesheet.css b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/stylesheet.css
new file mode 100644
index 0000000..ed690bb
--- /dev/null
+++ b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/stylesheet.css
@@ -0,0 +1,38 @@
+#org-agenda-indicator-label {
+ margin: 0 6px;
+ max-width: 24em;
+}
+
+.org-agenda-indicator-overdue {
+ font-weight: 600;
+}
+
+.org-agenda-indicator-section {
+ padding: 6px 12px 2px;
+}
+
+.org-agenda-indicator-section-label {
+ font-size: 0.82em;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ opacity: 0.7;
+ text-transform: uppercase;
+}
+
+.org-agenda-indicator-row {
+ min-width: 22em;
+ padding: 3px 0;
+}
+
+.org-agenda-indicator-row-title {
+ font-weight: 600;
+}
+
+.org-agenda-indicator-row-meta {
+ font-size: 0.9em;
+ opacity: 0.75;
+}
+
+.org-agenda-indicator-row-title.org-agenda-indicator-overdue {
+ color: #d97706;
+}
nihil fit ex nihilo