Files
blelock/.github/copilot-instructions.md

5.7 KiB

Copilot Instructions — blelock

Environment

  • OS: Fedora — use dnf to install packages, not apt-get.
  • Node.js/npm must be installed via dnf install nodejs npm if 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:

  1. BleMonitor polls org.bluez.Device1.RSSI (cached D-Bus property) every N seconds.
  2. Each poll feeds into thresholds.ts pure functions to detect state transitions.
  3. On transition, BleMonitor.onSignalBelowThreshold() or onSignalAboveThreshold() is called — these are intentionally empty hooks wired up in extension.ts.
  4. BleUnlockMenuToggle subscribes to rssi-changed and devices-changed GObject 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 .js extension 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), not declare.

Extension lifecycle rules

  • Nothing is created in constructor() — only in enable().
  • 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 on disable() 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; tests thresholds.ts pure functions only.
  • tests/integration/ — vitest with GI stubs in tests/mocks/; tests BleMonitor state-machine via MockBleMonitor that 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 removed get_maximized().
  • Mtk.Rectangle — replaces Meta.Rectangle if 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/log is used instead.

BlueZ D-Bus

  • ObjectManager: org.bluez at path /, interface org.freedesktop.DBus.ObjectManager.
  • Adapter: /org/bluez/hci0, interface org.bluez.Adapter1.
  • Device RSSI: property RSSI (type n, int16) on org.bluez.Device1. Read via get_cached_property('RSSI') — no extra D-Bus round-trip.
  • Discovery is triggered by StartDiscovery/StopDiscovery on the adapter, then GetManagedObjects is 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.