diff --git a/gnome-shell-extensions.spec b/gnome-shell-extensions.spec index 22df6ee..afa0d71 100644 --- a/gnome-shell-extensions.spec +++ b/gnome-shell-extensions.spec @@ -38,6 +38,8 @@ Patch: 0001-Include-status-icons-in-classic-session.patch Patch: window-list-reordering.patch +Patch: heads-up-display-from-file.patch + %description GNOME Shell Extensions is a collection of extensions providing additional and optional functionality to GNOME Shell. diff --git a/heads-up-display-from-file.patch b/heads-up-display-from-file.patch new file mode 100644 index 0000000..fe006a3 --- /dev/null +++ b/heads-up-display-from-file.patch @@ -0,0 +1,483 @@ +From c9845372267ecc085143b3cfa4f7eaf39091b41a Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 9 May 2025 13:29:06 +0200 +Subject: [PATCH 1/5] heads-up-display: Split out getMessageText() helper + +The new method will make it easier to add alternative sources +for the message text. +--- + extensions/heads-up-display/extension.js | 10 ++++++++-- + 1 file changed, 8 insertions(+), 2 deletions(-) + +diff --git a/extensions/heads-up-display/extension.js b/extensions/heads-up-display/extension.js +index a71b5925..5f72e168 100644 +--- a/extensions/heads-up-display/extension.js ++++ b/extensions/heads-up-display/extension.js +@@ -183,6 +183,13 @@ export default class HeadsUpDisplayExtension extends Extension { + this._onUserIdle.bind(this)); + } + ++ _getMessageText() { ++ return { ++ heading: this._settings.get_string('message-heading'), ++ body: this._settings.get_string('message-body'), ++ }; ++ } ++ + _updateMessage() { + if (this._messageInhibitedUntilIdle) { + if (this._message) +@@ -237,8 +244,7 @@ export default class HeadsUpDisplayExtension extends Extension { + } + } + +- const heading = this._settings.get_string('message-heading'); +- const body = this._settings.get_string('message-body'); ++ const {heading, body} = this._getMessageText(); + + if (!heading && !body) { + this._dismissMessage(); +-- +2.49.0 + + +From a121cf4e1a21e78b74de7b2f33f40f3201eaaa50 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 9 May 2025 13:29:42 +0200 +Subject: [PATCH 2/5] heads-up-display: Update message text asynchronously + +We will soon allow reading the banner text from a file. Prepare +for that by making the method asynchronous. +--- + extensions/heads-up-display/extension.js | 27 +++++++++++++----------- + 1 file changed, 15 insertions(+), 12 deletions(-) + +diff --git a/extensions/heads-up-display/extension.js b/extensions/heads-up-display/extension.js +index 5f72e168..3ae6eabb 100644 +--- a/extensions/heads-up-display/extension.js ++++ b/extensions/heads-up-display/extension.js +@@ -66,7 +66,7 @@ export default class HeadsUpDisplayExtension extends Extension { + enable() { + this._settings = this.getSettings('org.gnome.shell.extensions.heads-up-display'); + this._settings.connectObject('changed', +- () => this._updateMessage(), this); ++ () => this._updateMessage().catch(logError), this); + + this._idleMonitor = global.backend.get_core_idle_monitor(); + this._messageInhibitedUntilIdle = false; +@@ -123,15 +123,15 @@ export default class HeadsUpDisplayExtension extends Extension { + + _onStartupComplete() { + Main.overview.connectObject( +- 'showing', () => this._updateMessage(), +- 'hidden', () => this._updateMessage(), ++ 'showing', () => this._updateMessage().catch(logError), ++ 'hidden', () => this._updateMessage().catch(logError), + this); + Main.layoutManager.panelBox.connectObject('notify::visible', +- () => this._updateMessage(), this); ++ () => this._updateMessage().catch(logError), this); + Main.sessionMode.connectObject('updated', + () => this._onSessionModeUpdated(), this); + +- this._updateMessage(); ++ this._updateMessage().catch(logError); + } + + _onSessionModeUpdated() { +@@ -140,13 +140,14 @@ export default class HeadsUpDisplayExtension extends Extension { + + const dialog = Main.screenShield._dialog; + if (!Main.sessionMode.isGreeter && dialog && !this._screenShieldVisibleId) { +- this._screenShieldVisibleId = dialog._clock.connect('notify::visible', this._updateMessage.bind(this)); ++ this._screenShieldVisibleId = dialog._clock.connect('notify::visible', ++ () => this._updateMessage().catch(logError)); + this._screenShieldDestroyId = dialog._clock.connect('destroy', () => { + this._screenShieldVisibleId = 0; + this._screenShieldDestroyId = 0; + }); + } +- this._updateMessage(); ++ this._updateMessage().catch(logError); + } + + _stopWatchingForIdle() { +@@ -168,7 +169,7 @@ export default class HeadsUpDisplayExtension extends Extension { + + _onUserIdle() { + this._messageInhibitedUntilIdle = false; +- this._updateMessage(); ++ this._updateMessage().catch(logError); + } + + _watchForIdle() { +@@ -183,14 +184,16 @@ export default class HeadsUpDisplayExtension extends Extension { + this._onUserIdle.bind(this)); + } + +- _getMessageText() { ++ async _getMessageText() { ++ await false; ++ + return { + heading: this._settings.get_string('message-heading'), + body: this._settings.get_string('message-body'), + }; + } + +- _updateMessage() { ++ async _updateMessage() { + if (this._messageInhibitedUntilIdle) { + if (this._message) + this._dismissMessage(); +@@ -244,7 +247,7 @@ export default class HeadsUpDisplayExtension extends Extension { + } + } + +- const {heading, body} = this._getMessageText(); ++ const {heading, body} = await this._getMessageText(); + + if (!heading && !body) { + this._dismissMessage(); +@@ -276,7 +279,7 @@ export default class HeadsUpDisplayExtension extends Extension { + + this._watchForIdle(); + this._messageInhibitedUntilIdle = true; +- this._updateMessage(); ++ this._updateMessage().catch(logError); + } + + _dismissMessage() { +-- +2.49.0 + + +From 88bff8c0a592e72f5797c834dfa8519b69c1f15b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 9 May 2025 14:49:15 +0200 +Subject: [PATCH 3/5] heads-up-display: Add `message-path` and `-source` + settings + +The new settings allow reading the message text from a file instead +of GSettings. This is mainly useful for `/etc/motd` and similar +mechanisms, to show the same message for both graphical and +non-graphical logins. +--- + ...ll.extensions.heads-up-display.gschema.xml | 20 +++++++++++++++++++ + 1 file changed, 20 insertions(+) + +diff --git a/extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml b/extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml +index 1e2119c8..a9552642 100644 +--- a/extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml ++++ b/extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml +@@ -5,6 +5,10 @@ SPDX-License-Identifier: GPL-2.0-or-later + --> + + ++ ++ ++ ++ + + +@@ -14,6 +18,15 @@ SPDX-License-Identifier: GPL-2.0-or-later + Number of seconds until message is reshown after user goes idle. + + ++ ++ "settings" ++ ++ Banner message source ++ ++ ++ The source of the text banner message on the login screen. ++ ++ + + "" + Message to show at top of display +@@ -28,6 +41,13 @@ SPDX-License-Identifier: GPL-2.0-or-later + A message to always show at the top of the screen. + + ++ ++ "" ++ Banner message path ++ ++ Path to text file with message to show in banner ++ ++ + + true + Show on login screen +-- +2.49.0 + + +From 706aa6d5ee89bfb196964b5f1ffd1373ba66d4c4 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 9 May 2025 14:49:15 +0200 +Subject: [PATCH 4/5] heads-up-display: Support loading message text from file + +Support the new `message-path` and `message-source` settings, which +allows loading the message text from a path instead of GSettings. + +This is mainly useful for `/etc/motd` and similar mechanisms, to +show the same message for both graphical and non-graphical logins. +--- + extensions/heads-up-display/extension.js | 70 +++++++++++++++++++++++- + 1 file changed, 69 insertions(+), 1 deletion(-) + +diff --git a/extensions/heads-up-display/extension.js b/extensions/heads-up-display/extension.js +index 3ae6eabb..5a377415 100644 +--- a/extensions/heads-up-display/extension.js ++++ b/extensions/heads-up-display/extension.js +@@ -2,6 +2,7 @@ + // + // SPDX-License-Identifier: GPL-2.0-or-later + ++import Gio from 'gi://Gio'; + import GObject from 'gi://GObject'; + import Meta from 'gi://Meta'; + import Mtk from 'gi://Mtk'; +@@ -13,6 +14,8 @@ import {MonitorConstraint} from 'resource:///org/gnome/shell/ui/layout.js'; + + import {HeadsUpMessage} from './headsUpMessage.js'; + ++Gio._promisify(Gio.File.prototype, 'load_contents_async'); ++ + var HeadsUpConstraint = GObject.registerClass({ + Properties: { + 'offset': GObject.ParamSpec.int( +@@ -67,6 +70,7 @@ export default class HeadsUpDisplayExtension extends Extension { + this._settings = this.getSettings('org.gnome.shell.extensions.heads-up-display'); + this._settings.connectObject('changed', + () => this._updateMessage().catch(logError), this); ++ this._cachedMessage = {}; + + this._idleMonitor = global.backend.get_core_idle_monitor(); + this._messageInhibitedUntilIdle = false; +@@ -81,6 +85,7 @@ export default class HeadsUpDisplayExtension extends Extension { + + disable() { + this._dismissMessage(); ++ this._clearMessageFile(); + + this._stopWatchingForIdle(); + +@@ -184,8 +189,71 @@ export default class HeadsUpDisplayExtension extends Extension { + this._onUserIdle.bind(this)); + } + ++ _clearMessageFile() { ++ this._messageFileMonitor?.disconnectObject(this); ++ this._messageFileMonitor = null; ++ ++ this._messageFile = null; ++ } ++ ++ _updateMessageFile() { ++ const path = this._settings.get_string('message-source') === 'file' ++ ? this._settings.get_string('message-path') ++ : null; ++ const file = path ++ ? Gio.File.new_for_path(path) ++ : null; ++ ++ if (!file && !this._messageFile) ++ return false; ++ ++ if (file && this._messageFile && this._messageFile.equal(file)) ++ return false; ++ ++ this._clearMessageFile(); ++ this._messageFile = file; ++ ++ if (file) { ++ this._messageFileMonitor = file.monitor_file(Gio.FileMonitorFlags.NONE, null); ++ this._messageFileMonitor.connectObject( ++ 'changed', () => this._messageFileChanged().catch(logError), this); ++ } ++ ++ return true; ++ } ++ ++ async _messageFileChanged() { ++ await this._updateCachedMessage(); ++ await this._updateMessage(); ++ } ++ ++ async _updateCachedMessage() { ++ let heading = null, body = null; ++ ++ if (this._messageFile) { ++ try { ++ const [contents] = await this._messageFile.load_contents_async(null); ++ const message = new TextDecoder().decode(contents); ++ ++ const sep = '\n\n'; ++ const paragraphs = message.split(sep); ++ if (paragraphs.length > 1) ++ heading = paragraphs.shift(); ++ body = paragraphs.join(sep); ++ } catch (e) { ++ console.error(`Failed to read banner from ${this._messageFile.get_path()}: ${e.message}`); ++ } ++ } ++ ++ this._cachedMessage = {heading, body}; ++ } ++ + async _getMessageText() { +- await false; ++ if (this._updateMessageFile()) ++ await this._updateCachedMessage(); ++ ++ if (this._settings.get_string('message-source') === 'file') ++ return this._cachedMessage; + + return { + heading: this._settings.get_string('message-heading'), +-- +2.49.0 + + +From 1468fedb6e15225741aae830b30555f63a228ae5 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 9 May 2025 13:27:35 +0200 +Subject: [PATCH 5/5] heads-up-display: Expose `message-path` and `-source` + settings in prefs + +Allow switching between directly entered message text and selecting +a file from the extension's prefs dialog. +--- + extensions/heads-up-display/prefs.js | 88 +++++++++++++++++++++++++++- + 1 file changed, 85 insertions(+), 3 deletions(-) + +diff --git a/extensions/heads-up-display/prefs.js b/extensions/heads-up-display/prefs.js +index 304c8813..1d2bea4c 100644 +--- a/extensions/heads-up-display/prefs.js ++++ b/extensions/heads-up-display/prefs.js +@@ -4,11 +4,14 @@ + + import Adw from 'gi://Adw'; + import Gio from 'gi://Gio'; ++import GLib from 'gi://GLib'; + import GObject from 'gi://GObject'; + import Gtk from 'gi://Gtk'; + + import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; + ++Gio._promisify(Gtk.FileDialog.prototype, 'open'); ++ + class GeneralGroup extends Adw.PreferencesGroup { + static { + GObject.registerClass(this); +@@ -64,21 +67,100 @@ class MessageGroup extends Adw.PreferencesGroup { + title: _('Message'), + }); + ++ this._settings = settings; ++ ++ const actionGroup = new Gio.SimpleActionGroup(); ++ const selectAction = new Gio.SimpleAction({name: 'select-file'}); ++ actionGroup.add_action(selectAction); ++ actionGroup.add_action(settings.create_action('message-source')); ++ this.insert_action_group('message-group', actionGroup); ++ ++ selectAction.connect('activate', ++ () => this._selectFile().catch(logError)); ++ ++ const textCheck = new Gtk.CheckButton({ ++ action_name: 'message-group.message-source', ++ action_target: new GLib.Variant('s', 'settings'), ++ }); ++ const useTextRow = new Adw.ActionRow({ ++ title: _('Use fixed message text'), ++ activatable_widget: textCheck, ++ }); ++ useTextRow.add_prefix(textCheck); ++ ++ this.add(useTextRow); ++ + const textView = new Gtk.TextView({ + accepts_tab: false, + wrap_mode: Gtk.WrapMode.WORD, ++ height_request: 80, + top_margin: 6, + bottom_margin: 6, + left_margin: 6, + right_margin: 6, +- vexpand: true, + }); +- textView.add_css_class('card'); + + settings.bind('message-body', + textView.get_buffer(), 'text', + Gio.SettingsBindFlags.DEFAULT); +- this.add(textView); ++ ++ const textRow = new Adw.PreferencesRow({ ++ child: textView, ++ }); ++ this.add(textRow); ++ ++ textCheck.bind_property('active', ++ textRow, 'sensitive', ++ GObject.BindingFlags.SYNC_CREATE); ++ ++ const fileCheck = new Gtk.CheckButton({ ++ action_name: 'message-group.message-source', ++ action_target: new GLib.Variant('s', 'file'), ++ group: textCheck, ++ }); ++ const useFileRow = new Adw.ActionRow({ ++ title: _('Read message text from file'), ++ activatable_widget: fileCheck, ++ }); ++ useFileRow.add_prefix(fileCheck); ++ ++ this.add(useFileRow); ++ ++ const selectButton = new Gtk.Button({ ++ label: _('Select…'), ++ action_name: 'message-group.select-file', ++ valign: Gtk.Align.CENTER, ++ }); ++ ++ const fileRow = new Adw.ActionRow(); ++ fileRow.add_suffix(selectButton); ++ this.add(fileRow); ++ ++ fileCheck.bind_property('active', ++ fileRow, 'sensitive', ++ GObject.BindingFlags.SYNC_CREATE); ++ ++ function updateFileLabel() { ++ const path = settings.get_string('message-path'); ++ fileRow.title = path || _('None'); ++ } ++ ++ settings.connect_object('changed::message-path', ++ () => updateFileLabel(), ++ this, GObject.ConnectFlags.DEFAULT); ++ updateFileLabel(); ++ } ++ ++ async _selectFile() { ++ const fileDialog = new Gtk.FileDialog({ ++ modal: true, ++ default_filter: new Gtk.FileFilter({ ++ mime_types: ['text/plain'], ++ }), ++ }); ++ const file = await fileDialog.open(this.get_root(), null); ++ if (file) ++ this._settings.set_string('message-path', file.get_path()); + } + } + +-- +2.49.0 +