Gnome shell extension local dev
Skill papanito/ai-skills/skills/gnome-shell-extension-local-dev
[MIRROR] My personal AI skills
npx -y skills add papanito/ai-skills --skill gnome-shell-extension-local-devAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Build, symlink-install, enable, debug, and package a GNOME Shell extension locally on GNOME 46–50 (Wayland-safe). Use when creating or iterating on any gnome-shell extension.
SKILL.md
7.4 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it
GNOME Shell Extension Local Dev (GNOME 46–50)
When to use
Creating, iterating on, or packaging a GNOME Shell extension. Covers project structure, import paths, gettext, threading, live install/enable, debugging, and CI packaging.
Project structure (GNOME 50 verified)
<uuid>@<domain>/
metadata.json # uuid, name, shell-version[], version (int), version-name (str), settings-schema, gettext-domain
extension.js # import { Extension, gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js'
prefs.js # import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'
codeburn.js (etc) # helper modules — receive gettext as a parameter, never import it
stylesheet.css
schemas/<schema>.gschema.xml
install.sh # symlink/enable/reload script (pure bash)
Makefile # install/link/uninstall/schemas targets
.gitlab-ci.yml # pack/validate/upload/release pipeline
UUID format: [email protected]. The extension dir name MUST equal the uuid.
Import paths (critical — getting either wrong silently breaks loading)
- extension.js:
import { Extension, gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js'(lowercaseshell) - prefs.js:
import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'(capitalShell/Extensions, different pathjs/extensions/prefs.js) - helper modules: do NOT import gettext. Receive it as a constructor/method parameter from extension.js.
gettext (GNOME 45+ ESM)
gettextfromextension.jscan ONLY be called while an extension's code is executing- Calling
_()at module-import time (top-level const, helper module import) throwsError: gettext can only be called from extensions→ extension fails to load (state INITIALIZED, stuck in error, indicator never appears) - Correct pattern: in
extension.js, pass_(the module-level import) into helper constructors:new MyIndicator(settings, _). Helpers store asthis._and callthis._('...')only inside method bodies (render/init time) - In
prefs.js, usethis.gettext.bind(this)as a local_insidefillPreferencesWindow - Add
gettext-domaintometadata.json - Debug:
gdbus call --session --dest org.gnome.Shell.Extensions --object-path /org/gnome/Shell/Extensions --method org.gnome.Shell.Extensions.GetExtensionErrors <uuid>
Threading (GJS)
GLib.Thread.try_new()is NOT introspectable in GJS — throws immediately- Use
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { ... return GLib.SOURCE_REMOVE; })for synchronous spawns that take ~1s - The shell tolerates a brief block at idle priority
GObject signals
- Declare in
registerClass:GObject.registerClass({ Signals: { 'my-signal': {} } }, class ...) - Emit with matching arg count:
this.emit('my-signal')for 0-arg signals emit('my-signal', null)on a 0-arg signal throwsSignal requires 0 args got 1
Install for live editing (symlink)
[email protected]
EXT=~/.local/share/gnome-shell/extensions/$UUID
ln -sfn "$PWD" "$EXT"
# compile schema into BOTH the extension's schemas/ dir AND the user schemas dir
glib-compile-schemas --strict --targetdir=schemas schemas
cp schemas/gschemas.compiled ~/.local/share/glib-2.0/schemas/gschemas.compiled
glib-compile-schemas not on PATH (NixOS)
GLIB_CS=$(command -v glib-compile-schemas || ls /nix/store/*-glib-*-dev/bin/glib-compile-schemas | head -1)
Enable + reload via DBus
# DBus interface: org.gnome.Shell.Extensions at /org/gnome/Shell/Extensions
gdbus call --session --dest org.gnome.Shell.Extensions \
--object-path /org/gnome/Shell/Extensions \
--method org.gnome.Shell.Extensions.EnableExtension <uuid>
gdbus call --session --dest org.gnome.Shell.Extensions \
--object-path /org/gnome/Shell/Extensions \
--method org.gnome.Shell.Extensions.GetExtensionInfo <uuid>
EnableExtensionreturns(true,)for known extensions,(false,)for unknownGetExtensionInforeturns(@a{sv} {},)(empty dict) for unknown extensionsReloadExtensionis NOT implemented on GNOME 50.2
Shell scan limitation (Wayland)
- GNOME Shell scans
~/.local/share/gnome-shell/extensionsonly at startup - No DBus method triggers a fresh scan
- A freshly symlinked extension needs a one-time shell restart (log out/in)
- After first scan,
DisableExtension+EnableExtensioncan toggle live ReloadExtensionnot implemented → code edits also need restart on GNOME 50.2- Pre-add uuid to
enabled-extensionsgsettings so it auto-enables on next login
UninstallExtension DANGER
UninstallExtensionDBus method (andgnome-extensions uninstall) FOLLOWS SYMLINKS- On a symlinked extension, it recursively deletes the REAL source directory including .git
- NEVER call UninstallExtension on a symlinked extension
- To remove:
rm ~/.local/share/gnome-shell/extensions/<uuid>(removes symlink only)
gsettings enabled-extensions (pure bash)
# Parse GLib variant text: ['a','b'] or @as []
parse() { gsettings get org.gnome.shell enabled-extensions | sed "s/^@\w\+\s*\[//; s/^\[//; s/\]$//; s/'//g; s/, /\n/g" | sed '/^$/d'; }
# Rebuild: ['a','b']
join() { local items=() line; while IFS= read -r line || [[ -n "$line" ]]; do [[ -z "$line" ]] && continue; items+=("'$line'"); done; local j; j="$(IFS=,; printf '%s' "${items[*]}")"; printf '[%s]' "$j"; }
# Append idempotent
add() { local list; list="$(parse)"; printf '%s\n' "${list}" | grep -qxF "$UUID" && return 0; gsettings set org.gnome.shell enabled-extensions "$(printf '%s\n%s\n' "${list}" "$UUID" | join)"; }
Debugging
# Check extension state and errors
gdbus call --session --dest org.gnome.Shell.Extensions \
--object-path /org/gnome/Shell/Extensions \
--method org.gnome.Shell.Extensions.GetExtensionErrors <uuid>
gdbus call --session --dest org.gnome.Shell.Extensions \
--object-path /org/gnome/Shell/Extensions \
--method org.gnome.Shell.Extensions.GetExtensionInfo <uuid>
# Check journal for stack traces
journalctl --user -b | grep -i "codeburn\|<uuid>" | tail -20
Packaging (CI)
gnome-extensions packsegfaults in Fedora 41 CI. Use manual zip:
mkdir -p dist/staging/schemas
cp metadata.json extension.js prefs.js *.js stylesheet.css dist/staging/
cp schemas/*.gschema.xml schemas/gschemas.compiled dist/staging/schemas/
cd dist/staging && zip -r ../<uuid>.shell-extension.zip . && cd ../..
gnome-extensions pack --extra-source=<file>is needed for helper modules (codeburn.js, ui.js) — without it they're omitted from the zip- ZIP structure: files at archive root,
schemas/subdir — no top-level directory wrapper
CI gotchas
[skip ci]in commit messages permanently poisons the tag — GitLab skips ALL pipelines for that commit. Useworkflow:ruleswith$CI_COMMIT_MESSAGE =~ /^release:.*/+when: neverinsteadCI_REPOSITORY_URLembeds the CI token (gitlab-ci-token:[MASKED]@...). UseCI_PROJECT_URLfor constructing push URLspython3not available inbefore_scripton Fedora 41 (beforednf install). Usesedfor file patching- YAML colons in script strings break parsing. Use
|block scalars for lines containing:followed by text
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.