From 46f4e58fbf7b6281f649f9d9680a2ea940676d25 Mon Sep 17 00:00:00 2001 From: Joao Ferrete Date: Mon, 10 Aug 2026 18:24:07 -0300 Subject: [PATCH 1/2] feat: apply a profile when `active-profile` is written externally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only the preferences process listened to this key, and only to refresh its own UI — so another program could set it and nothing happened on screen. Wiring it in `enable()` turns the key into a real entry point: writing it with gsettings applies the profile, which lets an external program drive the borders without knowing the nine appearance keys or duplicating the per-camera calibration. The calibration stays here, where it belongs. No feedback loop: `applyProfile` writes the key back with the same value, and GSettings does not emit `changed` when the value is unchanged. The handler is disconnected in `disable()` alongside the existing ones. Note: on Wayland the GNOME Shell cannot reload an extension, so this only takes effect after logout/login. Co-Authored-By: Claude Opus 5 (1M context) --- extension.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/extension.js b/extension.js index c6e6f57..7accaff 100644 --- a/extension.js +++ b/extension.js @@ -49,6 +49,24 @@ export default class LighterExtension extends Extension { if (SYNC_KEYS.includes(key)) this._sync(); }); + + // Applying a profile by writing `active-profile` from outside. + // + // Only the preferences process listened to this key before, and only + // to refresh its own UI — so an external writer could set it and + // nothing would happen on screen. Wiring it here makes the key a + // real entry point: `gsettings set ... active-profile ` applies + // the profile, which lets another program drive the borders without + // knowing the nine appearance keys or duplicating the calibration. + // + // No feedback loop: `applyProfile` writes `active-profile` back with + // the same value, and GSettings does not emit `changed` when the + // value is unchanged. + this._activeProfileId = this._settings.connect('changed::active-profile', () => { + const id = this._settings.get_string('active-profile'); + if (id) + this._profiles.applyProfile(id); + }); this._monitorsChangedId = Main.layoutManager.connect( 'monitors-changed', () => this._sync()); @@ -73,6 +91,11 @@ export default class LighterExtension extends Extension { this._settingsId = 0; } + if (this._activeProfileId) { + this._settings.disconnect(this._activeProfileId); + this._activeProfileId = 0; + } + if (this._monitorsChangedId) { Main.layoutManager.disconnect(this._monitorsChangedId); this._monitorsChangedId = 0; From 36529f92bde60ead0d89aa05cf7df6787ed5db62 Mon Sep 17 00:00:00 2001 From: Joao Ferrete Date: Mon, 10 Aug 2026 18:27:18 -0300 Subject: [PATCH 2/2] docs: document driving Lighter from another program The `active-profile` change turns gsettings into a real entry point, but a capability nobody can find is not a capability. Adds a README section with commands that were run on a live install, not written from memory: - `--schemadir` is required, because an extension's schema is not in the default search path. Omitting it fails with "No such schema". - Profile ids are UUIDs generated at creation, so the section shows how to look one up by name and says not to hardcode it. - The `profiles` key holds a JSON document wrapped in a GVariant string, which is why the example pipes through sed before jq. Also warns about the two things that cost real debugging time: auto-switch competing with an external controller for the same state, and Wayland needing a logout for code changes (settings changes apply immediately). Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 5 +++++ README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ec3b3..8d043f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 application ID or a regular expression on the window title). - Build tooling: `Makefile` (build/lint/pot/pack/install/nested), ESLint config, GitHub Actions CI, and gettext translation scaffolding (`po/`). +- External control: writing `active-profile` from outside the extension now applies + that profile. Previously only the preferences process listened to the key, and only + to refresh its own UI, so an external writer changed nothing on screen. This makes + `gsettings` a supported way for another program to drive the borders without + duplicating the calibration. - `CONTRIBUTING.md` and this changelog. - Brazilian Portuguese translation. diff --git a/README.md b/README.md index e33232b..4be0d64 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ for laptop users who need extra illumination without external gear. - **Multi-monitor support**: Show on the primary monitor or all screens. - **Seamless integration**: Native GNOME Shell feel with a toggle in the top bar. - **Convenient shortcut**: `Ctrl + Alt + L` opens the preferences. +- **Scriptable**: apply a profile or toggle the light from any other program by + writing the extension's GSettings keys — see [Controlling Lighter from another + program](#controlling-lighter-from-another-program). ## Requirements @@ -74,6 +77,51 @@ match rule — an application ID (e.g. `firefox.desktop`) and/or a window title matches, the optional *default profile* is applied. Manual selection always works and takes effect immediately. +### Controlling Lighter from another program + +Every appearance setting lives in GSettings, and the extension reacts to external +writes. That makes `gsettings` a supported entry point: another program can drive the +borders without reimplementing them, and without knowing how your borders are +calibrated. + +The schema is not in the default search path, so `--schemadir` is required: + +```bash +SCHEMA_DIR="$HOME/.local/share/gnome-shell/extensions/lighter@gnome-shell-extensions.ferrete.com/schemas" +SCHEMA=org.gnome.shell.extensions.lighter + +# Turn the light on and off +gsettings --schemadir "$SCHEMA_DIR" set $SCHEMA enabled true +gsettings --schemadir "$SCHEMA_DIR" set $SCHEMA enabled false + +# Apply a saved profile by its id +gsettings --schemadir "$SCHEMA_DIR" set $SCHEMA active-profile '' +``` + +**Look profile ids up by name, never hardcode them.** They are UUIDs generated when +the profile is created, so a hardcoded id breaks as soon as you recreate the profile. +The `profiles` key holds a JSON document shaped like +`{"version": 1, "profiles": [{"id": "…", "name": "Meet", "settings": {…}}]}`: + +```bash +gsettings --schemadir "$SCHEMA_DIR" get $SCHEMA profiles \ + | sed "s/^'//; s/'$//" \ + | jq -r '.profiles[] | select(.name == "Meet") | .id' +``` + +Writing `active-profile` applies the profile, including turning the light on. The +appearance keys (`thickness-*`, `opacity`, `color`, `temperature`, `multi-monitor`, +`maximize-brightness`) can also be set individually and take effect immediately. + +Two things worth knowing: + +- **Turn off *Auto-switch profiles* while an external program is in charge**, or the + window watcher will apply its own profile on the next focus change and overwrite + what your program just did. The two are independent controllers of the same state. +- **Changes to the extension's own code need a logout/login on Wayland**, because the + GNOME Shell cannot reload an extension there. Changes to *settings* apply + immediately — only code changes need the session restart. + ## Development See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide. Quick start: