diff --git a/package-lock.json b/package-lock.json
index 49889aa..7b81294 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"@girs/gsk-4.0": "^4.1.0",
"@girs/gtk-4.0": "^4.1.0",
"@girs/meta-18": "^4.1.0",
+ "@girs/pango-1.0": "^4.1.0",
"@girs/shell-18": "^4.1.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^63.2.0",
diff --git a/package.json b/package.json
index 515cd93..5dc56ba 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@girs/gsk-4.0": "^4.1.0",
"@girs/gtk-4.0": "^4.1.0",
"@girs/meta-18": "^4.1.0",
+ "@girs/pango-1.0": "^4.1.0",
"@girs/shell-18": "^4.1.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^63.2.0",
diff --git a/src/data/icons/add-symbolic.svg b/src/data/icons/add-symbolic.svg
new file mode 100644
index 0000000..fee4e37
--- /dev/null
+++ b/src/data/icons/add-symbolic.svg
@@ -0,0 +1,26 @@
+
+
+
diff --git a/src/data/icons/trash-symbolic.svg b/src/data/icons/trash-symbolic.svg
new file mode 100644
index 0000000..1f038cd
--- /dev/null
+++ b/src/data/icons/trash-symbolic.svg
@@ -0,0 +1,29 @@
+
+
+
diff --git a/src/data/org.gnome.shell.extensions.nightthemeswitcher.gschema.xml b/src/data/org.gnome.shell.extensions.nightthemeswitcher.gschema.xml
index e0414a6..3e11e71 100644
--- a/src/data/org.gnome.shell.extensions.nightthemeswitcher.gschema.xml
+++ b/src/data/org.gnome.shell.extensions.nightthemeswitcher.gschema.xml
@@ -28,9 +28,15 @@ SPDX-License-Identifier: GPL-3.0-or-later
""
+
+ []
+
""
+
+ []
+
diff --git a/src/data/preferences.gresource.xml b/src/data/preferences.gresource.xml
index a158eb7..4c5813f 100644
--- a/src/data/preferences.gresource.xml
+++ b/src/data/preferences.gresource.xml
@@ -7,8 +7,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
assets/preview-window-dark.svg
assets/preview-window-light.svg
+ icons/add-symbolic.svg
icons/code-symbolic.svg
icons/translate-symbolic.svg
+ icons/trash-symbolic.svg
icons/nightthemeswitcher-symbolic.svg
icons/appearance-symbolic.svg
icons/commands-symbolic.svg
@@ -19,6 +21,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
ui/AppearanceChooser.ui
ui/AppearancePage.ui
ui/BackgroundButton.ui
+ ui/CommandsEditor.ui
ui/CommandsPage.ui
ui/ContributePage.ui
ui/ShortcutButton.ui
diff --git a/src/data/ui/CommandsEditor.ui b/src/data/ui/CommandsEditor.ui
new file mode 100644
index 0000000..7637a6f
--- /dev/null
+++ b/src/data/ui/CommandsEditor.ui
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/data/ui/CommandsPage.ui b/src/data/ui/CommandsPage.ui
index 9469013..d441258 100644
--- a/src/data/ui/CommandsPage.ui
+++ b/src/data/ui/CommandsPage.ui
@@ -10,27 +10,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
nightthemeswitcher-commands-symbolic
- Run commands
-
-
- center
-
-
-
- Sunrise
- terminal
- 0 -1 font-desc "Mono"
-
-
-
-
- Sunset
- terminal
- 0 -1 font-desc "Mono"
+
+ Run commands
+
+
+ Sunrise commands
+
+
+
+
+ Sunset commands
+
+
diff --git a/src/meson.build b/src/meson.build
index 3a17d6d..1e54d90 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -40,6 +40,7 @@ preferences = [
'preferences/AppearancePreview.js',
'preferences/BackgroundButton.js',
'preferences/ColorChooser.js',
+ 'preferences/CommandsEditor.js',
'preferences/CommandsPage.js',
'preferences/ContributePage.js',
'preferences/ShortcutButton.js',
diff --git a/src/metadata.json.in b/src/metadata.json.in
index 01a8126..8166c36 100644
--- a/src/metadata.json.in
+++ b/src/metadata.json.in
@@ -6,7 +6,7 @@
"settings-schema": "@DNS@",
"url": "https://nightthemeswitcher.romainvigier.fr",
"session-modes": ["unlock-dialog", "user"],
- "shell-version": ["50"],
+ "shell-version": ["51"],
"version": @VERSION@,
"build-type": "@BUILD_TYPE@"
}
diff --git a/src/migrations.js b/src/migrations.js
index 6f096c7..8836337 100644
--- a/src/migrations.js
+++ b/src/migrations.js
@@ -9,6 +9,7 @@ import Gio from "gi://Gio";
*/
export function migrate(extension) {
initializeAccentColorSettings(extension);
+ migrateCommandsSettings(extension);
}
/**
@@ -25,3 +26,24 @@ function initializeAccentColorSettings(extension) {
if (accentColorSettings.get_string("night") === "unset")
accentColorSettings.set_string("night", interfaceSettings.get_string("accent-color"));
}
+
+/**
+ * Move the commands from single strings to arrays of strings.
+ * @param {ExtensionBase} extension The extension base class.
+ */
+function migrateCommandsSettings(extension) {
+ const commandsSettings = extension.getSettings(`${extension.metadata["settings-schema"]}.commands`);
+
+ const oldSunriseCommand = commandsSettings.get_string("sunrise");
+ const oldSunsetCommand = commandsSettings.get_string("sunset");
+
+ if (oldSunriseCommand) {
+ commandsSettings.set_strv("sunrise-commands", [oldSunriseCommand]);
+ commandsSettings.set_string("sunrise", "");
+ }
+
+ if (oldSunsetCommand) {
+ commandsSettings.set_strv("sunset-commands", [oldSunsetCommand]);
+ commandsSettings.set_string("sunset", "");
+ }
+}
diff --git a/src/modules/CommandsSwitcher.js b/src/modules/CommandsSwitcher.js
index 10924f3..3ba49f3 100644
--- a/src/modules/CommandsSwitcher.js
+++ b/src/modules/CommandsSwitcher.js
@@ -33,9 +33,11 @@ export class CommandsSwitcher extends Switcher {
#onTimeChanged(time) {
if (time === Time.UNKNOWN) return;
- const command = this.#settings.get_string(time === Time.DAY ? "sunrise" : "sunset");
- if (!command) return;
- GLib.spawn_async(null, ["sh", "-c", command], null, GLib.SpawnFlags.SEARCH_PATH, null);
- debug.message(`Spawned ${time} command.`);
+ const commands = this.#settings.get_strv(time === Time.DAY ? "sunrise-commands" : "sunset-commands");
+ if (commands.length === 0) return;
+ commands.forEach((command) => {
+ GLib.spawn_async(null, ["sh", "-c", command], null, GLib.SpawnFlags.SEARCH_PATH, null);
+ });
+ debug.message(`Spawned ${time} commands.`);
}
}
diff --git a/src/preferences/CommandsEditor.js b/src/preferences/CommandsEditor.js
new file mode 100644
index 0000000..9e49ffe
--- /dev/null
+++ b/src/preferences/CommandsEditor.js
@@ -0,0 +1,108 @@
+// SPDX-FileCopyrightText: Night Theme Switcher Contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Adw from "gi://Adw";
+import GObject from "gi://GObject";
+import Gtk from "gi://Gtk";
+import Pango from "gi://Pango";
+
+import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
+
+export class CommandsEditor extends Adw.PreferencesGroup {
+ #commands = new Gtk.StringList();
+
+ static {
+ GObject.registerClass(
+ {
+ GTypeName: "CommandsEditor",
+ Template: "resource:///org/gnome/Shell/Extensions/nightthemeswitcher/preferences/ui/CommandsEditor.ui",
+ Properties: {
+ commands: GObject.ParamSpec.jsobject(
+ "commands",
+ "Commands",
+ "Array of commands",
+ GObject.ParamFlags.READWRITE,
+ ),
+ },
+ },
+ this,
+ );
+ }
+
+ constructor({ ...params } = {}) {
+ super(params);
+ this.bind_model(this.#commands, (item) => this.#createCommandEntryRow(item));
+ }
+
+ get commands() {
+ const commands = [];
+ for (let i = 0; i < this.#commands.get_n_items(); i++) {
+ const command = this.#commands.get_string(i);
+ if (command) commands.push(command);
+ }
+ return commands;
+ }
+
+ set commands(commands) {
+ this.#commands.splice(0, this.#commands.get_n_items(), commands);
+ this.notify("commands");
+ }
+
+ /**
+ * Create an entry row from the given string object.
+ * @param {Gtk.StringObject} strObject The string object.
+ * @returns {Adw.EntryRow} The created entry row.
+ */
+ #createCommandEntryRow(strObject) {
+ const findItemPositionInCommands = () => {
+ let position = -1;
+ for (let i = 0; i < this.#commands.get_n_items(); i++) {
+ const listItem = this.#commands.get_item(i);
+ if (listItem === strObject) {
+ position = i;
+ break;
+ }
+ }
+ return position;
+ };
+
+ const attrList = Pango.AttrList.new();
+ attrList.insert(Pango.attr_family_new("monospace"));
+
+ const entryRow = new Adw.EntryRow({
+ title: _("Command"),
+ text: strObject.string,
+ showApplyButton: true,
+ inputHints: Gtk.InputHints.NO_SPELLCHECK,
+ inputPurpose: Gtk.InputPurpose.TERMINAL,
+ attributes: attrList,
+ });
+ entryRow.connect("apply", () => {
+ const position = findItemPositionInCommands(strObject);
+ if (position === -1) return;
+ this.#commands.splice(position, 1, [entryRow.text]);
+ this.notify("commands");
+ });
+
+ const removeButton = new Gtk.Button({
+ tooltipText: _("Remove command"),
+ iconName: "nightthemeswitcher-trash-symbolic",
+ valign: Gtk.Align.CENTER,
+ cssClasses: ["circular", "flat"],
+ });
+ removeButton.connect("clicked", () => {
+ const position = findItemPositionInCommands(strObject);
+ if (position === -1) return;
+ this.#commands.remove(position);
+ this.notify("commands");
+ });
+
+ entryRow.add_suffix(removeButton);
+
+ return entryRow;
+ }
+
+ onAddButtonClicked() {
+ this.#commands.append("");
+ }
+}
diff --git a/src/preferences/CommandsPage.js b/src/preferences/CommandsPage.js
index c8ebc9f..7e9b24b 100644
--- a/src/preferences/CommandsPage.js
+++ b/src/preferences/CommandsPage.js
@@ -11,17 +11,31 @@ export class CommandsPage extends Adw.PreferencesPage {
{
GTypeName: "CommandsPage",
Template: "resource:///org/gnome/Shell/Extensions/nightthemeswitcher/preferences/ui/CommandsPage.ui",
- InternalChildren: ["enabled_switch", "sunrise_entry", "sunset_entry"],
+ InternalChildren: ["enabled_switch", "sunrise_commands_editor", "sunset_commands_editor"],
},
this,
);
}
- constructor({ settings, ...params } = {}) {
+ constructor({ commandsSettings, ...params } = {}) {
super(params);
- settings.bind("enabled", this._enabled_switch, "active", Gio.SettingsBindFlags.DEFAULT);
- settings.bind("sunrise", this._sunrise_entry, "text", Gio.SettingsBindFlags.DEFAULT);
- settings.bind("sunset", this._sunset_entry, "text", Gio.SettingsBindFlags.DEFAULT);
+ commandsSettings.bind("enabled", this._enabled_switch, "active", Gio.SettingsBindFlags.DEFAULT);
+
+ this._sunrise_commands_editor.commands = commandsSettings.get_strv("sunrise-commands");
+ commandsSettings.connect("changed::sunrise-commands", () => {
+ this._sunrise_commands_editor.commands = commandsSettings.get_strv("sunrise-commands");
+ });
+ this._sunrise_commands_editor.connect("notify::commands", () => {
+ commandsSettings.set_strv("sunrise-commands", this._sunrise_commands_editor.commands);
+ });
+
+ this._sunset_commands_editor.commands = commandsSettings.get_strv("sunset-commands");
+ commandsSettings.connect("changed::sunset-commands", () => {
+ this._sunset_commands_editor.commands = commandsSettings.get_strv("sunset-commands");
+ });
+ this._sunset_commands_editor.connect("notify::commands", () => {
+ commandsSettings.set_strv("sunset-commands", this._sunset_commands_editor.commands);
+ });
}
}
diff --git a/src/prefs.js b/src/prefs.js
index 24d6c18..2ed3376 100644
--- a/src/prefs.js
+++ b/src/prefs.js
@@ -48,6 +48,7 @@ export default class NightThemeSwitcherPreferences extends ExtensionPreferences
const { AppearancePreview } = await import("./preferences/AppearancePreview.js");
const { BackgroundButton } = await import("./preferences/BackgroundButton.js");
const { ColorChooser } = await import("./preferences/ColorChooser.js");
+ const { CommandsEditor } = await import("./preferences/CommandsEditor.js");
const { CommandsPage } = await import("./preferences/CommandsPage.js");
const { ContributePage } = await import("./preferences/ContributePage.js");
const { ShortcutButton } = await import("./preferences/ShortcutButton.js");
@@ -60,6 +61,7 @@ export default class NightThemeSwitcherPreferences extends ExtensionPreferences
GObject.type_ensure(AppearancePreview);
GObject.type_ensure(BackgroundButton);
GObject.type_ensure(ColorChooser);
+ GObject.type_ensure(CommandsEditor);
GObject.type_ensure(CommandsPage);
GObject.type_ensure(ContributePage);
GObject.type_ensure(ShortcutButton);
@@ -71,7 +73,7 @@ export default class NightThemeSwitcherPreferences extends ExtensionPreferences
new AppearancePage({
accentColorSettings: this.getSettings(`${this.metadata["settings-schema"]}.accent-color`),
}),
- new CommandsPage({ settings: this.getSettings(`${this.metadata["settings-schema"]}.commands`) }),
+ new CommandsPage({ commandsSettings: this.getSettings(`${this.metadata["settings-schema"]}.commands`) }),
new TweaksPage({
locationSettings: this.getSettings(`${this.metadata["settings-schema"]}.location`),
timeSettings: this.getSettings(`${this.metadata["settings-schema"]}.time`),