skills/padparadscho/skills/js-gnome-extensions

js-gnome-extensions

SKILL.md

GNOME Shell Extensions

Build extensions for GNOME Shell 45+ using GJS with ESModules.

Key Resources

Architecture Overview

Extensions run inside the gnome-shell process using Clutter/St toolkits (not GTK). Preferences run in a separate GTK4/Adwaita process.

Library stack (bottom-up):

  • Clutter — Actor-based toolkit, layout managers, animations
  • St — Shell Toolkit: buttons, icons, labels, entries, scroll views (CSS-styleable)
  • Meta (Mutter) — displays, workspaces, windows, keybindings
  • Shellglobal object, app tracking, utilities
  • js/ui/ — GNOME Shell JS modules (Main, Panel, PopupMenu, QuickSettings, etc.)

Import conventions in extension.js:

import Clutter from "gi://Clutter";
import GObject from "gi://GObject";
import Gio from "gi://Gio";
import GLib from "gi://GLib";
import Meta from "gi://Meta";
import Shell from "gi://Shell";
import St from "gi://St";

import {
  Extension,
  gettext as _,
} from "resource:///org/gnome/shell/extensions/extension.js";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js";
import * as PopupMenu from "resource:///org/gnome/shell/ui/popupMenu.js";
import * as QuickSettings from "resource:///org/gnome/shell/ui/quickSettings.js";

Import conventions in prefs.js:

import Gdk from "gi://Gdk?version=4.0";
import Gtk from "gi://Gtk?version=4.0";
import Adw from "gi://Adw";
import Gio from "gi://Gio";

import {
  ExtensionPreferences,
  gettext as _,
} from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";

Critical rule: Never import GTK/Gdk/Adw in extension.js. Never import Clutter/Meta/St/Shell in prefs.js.

Extension Lifecycle

Required Files

  1. metadata.json — UUID, name, description, shell-version, url
  2. extension.js — Default export: subclass of Extension

Optional Files

  • prefs.js — Subclass of ExtensionPreferences (GTK4/Adwaita)
  • stylesheet.css — CSS for St widgets in gnome-shell (not prefs)
  • schemas/ — GSettings schema XML + compiled binary
  • locale/ — Gettext translation .mo files

enable()/disable() Contract

import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";

export default class MyExtension extends Extension {
  enable() {
    // Create objects, connect signals, add UI
  }

  disable() {
    // MUST undo everything done in enable():
    // - Destroy all created widgets
    // - Disconnect all signals
    // - Remove all GLib.timeout/idle sources
    // - Null out all references
  }
}

Rules (enforced by extension review):

  • Do NOT create GObject instances or connect signals in constructor()
  • constructor() may only set up static data (RegExp, Map, etc.) and call super(metadata)
  • Everything created in enable() MUST be cleaned up in disable()
  • disable() is called on lock screen (unless session-modes includes unlock-dialog)

Reference Files

Detailed documentation is split into reference files. Read the appropriate file based on the task:

  • references/review-guidelines.md — Complete review rules for extensions.gnome.org submission. Read before submitting or when reviewing extension code for compliance.
  • references/ui-patterns.md — Panel indicators, popup menus, quick settings (toggles, sliders, menus), dialogs, notifications, and search providers with complete code examples. Read when building any UI component.
  • references/preferences.md — GSettings schemas, prefs.js with GTK4/Adwaita, settings binding. Read when implementing extension preferences.
  • references/development.md — Getting started, testing, debugging, translations, InjectionManager, and packaging. Read when setting up a new extension or debugging.
  • references/porting-guide.md — Breaking changes for GNOME Shell 45–49. Read when porting an extension to a newer version.

Quick Start: Panel Indicator Extension

Minimal working extension with a panel icon:

metadata.json

{
  "uuid": "my-extension@example.com",
  "name": "My Extension",
  "description": "Does something useful",
  "shell-version": ["47", "48", "49"],
  "url": "https://github.com/user/my-extension"
}

extension.js

import St from "gi://St";

import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js";

export default class MyExtension extends Extension {
  enable() {
    this._indicator = new PanelMenu.Button(0.0, this.metadata.name, false);

    const icon = new St.Icon({
      icon_name: "face-laugh-symbolic",
      style_class: "system-status-icon",
    });
    this._indicator.add_child(icon);

    Main.panel.addToStatusArea(this.uuid, this._indicator);
  }

  disable() {
    this._indicator?.destroy();
    this._indicator = null;
  }
}

Install & Test

# Create extension directory
mkdir -p ~/.local/share/gnome-shell/extensions/my-extension@example.com
# Copy files there, then:

# Wayland: run nested session
dbus-run-session gnome-shell --devkit --wayland   # GNOME 49+
dbus-run-session gnome-shell --nested --wayland    # GNOME 48 and earlier

# Enable extension in nested session
gnome-extensions enable my-extension@example.com

# Watch logs
journalctl -f -o cat /usr/bin/gnome-shell

Common Patterns Cheatsheet

Connect a signal (and clean up)

enable() {
    this._handlerId = someObject.connect('some-signal', () => { /* ... */ });
}
disable() {
    if (this._handlerId) {
        someObject.disconnect(this._handlerId);
        this._handlerId = null;
    }
}

Add a timeout (and clean up)

enable() {
    this._timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 5, () => {
        // do work
        return GLib.SOURCE_CONTINUE; // or GLib.SOURCE_REMOVE
    });
}
disable() {
    if (this._timeoutId) {
        GLib.Source.remove(this._timeoutId);
        this._timeoutId = null;
    }
}

Use GSettings

enable() {
    this._settings = this.getSettings(); // uses metadata settings-schema
    this._settings.bind('show-indicator', this._indicator, 'visible',
        Gio.SettingsBindFlags.DEFAULT);
}
disable() {
    this._settings = null;
}

Override a method (InjectionManager)

import {
  Extension,
  InjectionManager,
} from "resource:///org/gnome/shell/extensions/extension.js";
import { Panel } from "resource:///org/gnome/shell/ui/panel.js";

export default class MyExtension extends Extension {
  enable() {
    this._injectionManager = new InjectionManager();
    this._injectionManager.overrideMethod(
      Panel.prototype,
      "toggleCalendar",
      (originalMethod) => {
        return function (...args) {
          console.debug("Calendar toggled!");
          originalMethod.call(this, ...args);
        };
      },
    );
  }
  disable() {
    this._injectionManager.clear();
    this._injectionManager = null;
  }
}

Packaging for Submission

cd ~/.local/share/gnome-shell/extensions/my-extension@example.com
gnome-extensions pack --podir=po --extra-source=utils.js .

# GNOME 49+: upload directly
gnome-extensions upload --accept-tos
Weekly Installs
7
First Seen
Feb 7, 2026
Installed on
amp7
opencode7
kimi-cli7
codex7
github-copilot7
gemini-cli7