5.7 KiB
5.7 KiB
Copilot Instructions — blelock
Environment
- OS: Fedora — use
dnfto install packages, notapt-get. - Node.js/npm must be installed via
dnf install nodejs npmif not present.
GNOME Shell 49 extension that locks/unlocks the GNOME session based on the
Bluetooth RSSI of a nearby device. The extension code lives in generated/.
Build, Test, and Install
All commands run from generated/.
cd generated/
npm install # install typescript, @girs types, vitest
make # compile TypeScript → dist/
make install # pack + gnome-extensions install --force
make test # run all vitest tests
npm run test:unit # unit tests only (pure logic, no GI)
npm run test:integration # integration tests with mocked D-Bus
npx vitest run tests/unit/thresholds.test.ts # single test file
After any TypeScript change, rebuild with make then restart the nested shell.
Nested-shell testing (GNOME 49):
dbus-run-session gnome-shell --devkit --wayland # start nested session
gnome-extensions enable ble-unlock@blelock.github.io
journalctl -f -o cat /usr/bin/gnome-shell # watch logs
gjs tests/gnome-shell/extension.test.js # GJS integration tests (inside nested session)
GSettings schema must be compiled before installing:
glib-compile-schemas schemas/ # run from generated/
Architecture
generated/
├── thresholds.ts pure logic: classifySignal / shouldLock / shouldUnlock
├── bleMonitor.ts GObject that polls BlueZ RSSI via Gio.DBusProxy
├── quickSettingsMenu.ts Quick Settings toggle + device-list + threshold controls
├── extension.ts Extension enable/disable; wires monitor → indicator
├── prefs.ts GTK4/Adwaita preferences window (separate process)
└── schemas/ GSettings schema (5 keys; see below)
Data flow:
BleMonitorpollsorg.bluez.Device1.RSSI(cached D-Bus property) every N seconds.- Each poll feeds into
thresholds.tspure functions to detect state transitions. - On transition,
BleMonitor.onSignalBelowThreshold()oronSignalAboveThreshold()is called — these are intentionally empty hooks wired up inextension.ts. BleUnlockMenuTogglesubscribes torssi-changedanddevices-changedGObject signals to keep the Quick Settings UI current.
Two-process boundary:
- Shell process (
extension.ts,bleMonitor.ts,quickSettingsMenu.ts): GI libs only — Clutter, St, Meta, Gio, GLib, GObject. No Gtk/Adw/Gdk. - Prefs process (
prefs.ts): GTK4 + Adwaita + Gio/GLib only. No Clutter/Meta/St/Shell.
Key Conventions
TypeScript for GJS
module: "NodeNext"in tsconfig — local imports use.jsextension even though source is.ts.- GObject subclasses use the static-block registration pattern:
class Foo extends GObject.Object { static { GObject.registerClass({ GTypeName: 'Foo', Signals: {...} }, this); } } GObject.registerClass(class Foo extends Bar {...})wrapper is used for Quick Settings UI classes.- Fields on GObject subclasses are declared with initial values (
private _x: T | null = null), notdeclare.
Extension lifecycle rules
- Nothing is created in
constructor()— only inenable(). disable()must stop all GLib timers, disconnect all signals, null all references, and destroy all widgets.- This extension uses
"session-modes": ["user", "unlock-dialog"]—disable()is not called on screen lock. The JSDoc ondisable()explains this.
GSettings schema
Schema ID: org.gnome.shell.extensions.bleunlock
Path: /org/gnome/shell/extensions/bleunlock/
| Key | Type | Default |
|---|---|---|
enabled |
boolean | false |
device-address |
string | "" |
rssi-lock-threshold |
int | -75 |
rssi-unlock-threshold |
int | -65 |
poll-interval |
int | 3 |
Testing strategy
tests/unit/— vitest, no GI dependencies; teststhresholds.tspure functions only.tests/integration/— vitest with GI stubs intests/mocks/; tests BleMonitor state-machine viaMockBleMonitorthat replays the same threshold logic without D-Bus.tests/gnome-shell/— GJS/Jasmine script; runs inside a nested GNOME Shell 49 session.- Mocks live in
tests/mocks/(GLib, Gio, GObject, Clutter, St). Extend them when adding new GI API calls.
GNOME 49 API notes
- Nested testing:
dbus-run-session gnome-shell --devkit --wayland(not--nested). Meta.Window.is_maximized()— use this, not the removedget_maximized().Mtk.Rectangle— replacesMeta.Rectangleif rectangle types are needed.Meta.WaylandClient.new_subprocess()— use this on GNOME 49 for any companion subprocess.ExtensionBase.getLogger()is available (GNOME 48+) but not yet adopted;console.warn/logis used instead.
BlueZ D-Bus
- ObjectManager:
org.bluezat path/, interfaceorg.freedesktop.DBus.ObjectManager. - Adapter:
/org/bluez/hci0, interfaceorg.bluez.Adapter1. - Device RSSI: property
RSSI(typen, int16) onorg.bluez.Device1. Read viaget_cached_property('RSSI')— no extra D-Bus round-trip. - Discovery is triggered by
StartDiscovery/StopDiscoveryon the adapter, thenGetManagedObjectsis called to refresh the device list.
Skills
Two Copilot skills in .agents/skills/ provide additional context for extension work:
gjs-gnome-extensions— Quick Settings patterns, ESModule imports, lifecycle rules, packaging.gnome-shell-extension-dev— GNOME 47-49 compatibility, subprocess/Wayland, review readiness.
Invoke with @gjs-gnome-extensions or @gnome-shell-extension-dev when making shell-side or version-sensitive changes.