This guide is for developers who want to contribute to or modify the Knocker GNOME extension.
-
Development Tools:
sudo apt install gjs gnome-shell-extension-tool git
-
GNOME Shell Development:
- GNOME Shell 48 or 49
- Looking Glass debugger (Alt+F2, type
lg)
-
Knocker-CLI:
- Installed and configured for testing
- User systemd service set up
git clone https://github.com/FarisZR/knocker-gnome.git
cd knocker-gnome
./install.shknocker-gnome/
├── extension.js # Main extension entry point
├── knockerService.js # Service/CLI command interface
├── knockerMonitor.js # Journald log monitor
├── knockerQuickSettings.js # UI components
├── prefs.js # Preferences dialog
├── metadata.json # Extension metadata
├── schemas/ # GSettings schemas
│ └── org.gnome.shell.extensions.knocker.gschema.xml
├── docs/ # Documentation
│ ├── ARCHITECTURE.md
│ ├── INSTALLATION.md
│ └── TESTING.md
├── build.sh # Build script
└── install.sh # Install script
Edit the JavaScript files as needed. The main modules are:
- extension.js: Extension lifecycle (enable/disable)
- knockerService.js: Systemd and CLI interactions
- knockerMonitor.js: Log monitoring and state management
- knockerQuickSettings.js: UI implementation
- prefs.js: Settings UI
After making changes:
# Reinstall the extension
./install.sh
# Reload GNOME Shell
# X11: Alt+F2, type 'r', press Enter
# Wayland: Log out and log back in
# Or disable/enable the extension
gnome-extensions disable Knocker@fariszr.com
gnome-extensions enable Knocker@fariszr.comAlways have logs running to catch errors:
Terminal 1 - GNOME Shell logs:
journalctl -f -o cat /usr/bin/gnome-shellTerminal 2 - Knocker service logs:
journalctl --user -u knocker.service -f -o json-prettyTerminal 3 - Preferences logs (when testing prefs):
journalctl -f -o cat /usr/bin/gjsGNOME Shell's debugging tool (Alt+F2, type lg):
// Get the extension
let ext = imports.ui.main.extensionManager.lookup('Knocker@fariszr.com');
// Check if enabled
ext.state
// Access extension instance (when running)
ext.stateObj
// Check service status
ext.stateObj._knockerService.isServiceActive()
// View current state
ext.stateObj._knockerMonitor.getState()- ES6+: Use modern JavaScript features
- Async/await: Preferred for async operations (but not for lifecycle methods)
- Arrow functions: Use for callbacks
- Template literals: Use for string interpolation
- Const/let: Never use
var
- Classes: PascalCase (
KnockerService) - Methods/Functions: camelCase (
startService) - Private methods: Prefix with underscore (
_processEntry) - Constants: UPPER_SNAKE_CASE (
KNOCKER_SCHEMA_VERSION) - GObject properties: kebab-case in definitions, camelCase in code
Always handle errors:
// Good
try {
await someAsyncOperation();
} catch (e) {
console.error('Operation failed:', e);
Main.notifyError('Title', 'User-friendly message');
}
// Better - with cleanup
try {
await someAsyncOperation();
} catch (e) {
console.error('Operation failed:', e);
this._cleanup();
Main.notifyError('Title', 'User-friendly message');
}Always clean up in destroy() methods:
destroy() {
// Cancel async operations
if (this._cancellable) {
this._cancellable.cancel();
}
// Remove timeouts - CRITICAL: always remove all timeouts
if (this._timeoutId) {
GLib.source_remove(this._timeoutId);
this._timeoutId = null;
}
// Remove signal handlers
if (this._signalId) {
someObject.disconnect(this._signalId);
this._signalId = null;
}
// Call parent
super.destroy();
}Important: Timeout Management
When creating timeouts, always:
- Store the timeout ID in an instance variable
- Remove any existing timeout before creating a new one
- Clear the timeout ID when it executes
- Remove all timeouts in destroy()
// Before creating a timeout
if (this._myTimeoutId) {
GLib.source_remove(this._myTimeoutId);
this._myTimeoutId = null;
}
this._myTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 5, () => {
this._myTimeoutId = null; // Clear on execute
this._doSomething();
return GLib.SOURCE_REMOVE;
});For UI components, use GObject.registerClass:
import GObject from 'gi://GObject';
import QuickSettings from 'resource:///org/gnome/shell/ui/quickSettings.js';
export const MyToggle = GObject.registerClass(
class MyToggle extends QuickSettings.QuickToggle {
_init(params) {
super._init({
title: 'My Toggle',
iconName: 'icon-name-symbolic',
...params
});
// Your initialization
}
// Your methods
});Follow the test cases in docs/TESTING.md.
-
Add console.log():
console.log('Debug info:', variable); console.error('Error:', error); console.warn('Warning:', warning);
-
Use Looking Glass:
- Alt+F2, type
lg - Errors tab shows JavaScript errors
- Evaluator tab for running code
- Alt+F2, type
-
Check return values:
const result = await someOperation(); console.log('Result:', result);
-
Monitor specific events:
this._knockerMonitor.on(KnockerEvent.ERROR, (data) => { console.log('Error event:', data); });
-
Add to schema:
<key name="my-new-setting" type="b"> <default>true</default> <summary>My Setting</summary> <description>Description of the setting</description> </key>
-
Add to preferences:
const mySettingRow = new Adw.SwitchRow({ title: _('My Setting'), subtitle: _('Description'), }); settings.bind('my-new-setting', mySettingRow, 'active', Gio.SettingsBindFlags.DEFAULT); group.add(mySettingRow);
-
Use in extension:
const settings = this._extensionObject.getSettings(); if (settings.get_boolean('my-new-setting')) { // Do something }
-
Recompile schemas:
cd ~/.local/share/gnome-shell/extensions/Knocker@fariszr.com glib-compile-schemas schemas/
// In knockerQuickSettings.js, in _setupMenu():
const myAction = this.menu.addAction('My Action', () => {
this._handleMyAction();
});-
Add to KnockerEvent enum:
export const KnockerEvent = { // ... existing events MY_NEW_EVENT: 'MyNewEvent', };
-
Handle in _processEntry:
case KnockerEvent.MY_NEW_EVENT: // Update state this._currentState.myValue = eventData.myValue; break;
-
Listen in UI:
this._knockerMonitor.on(KnockerEvent.MY_NEW_EVENT, (data) => { this._updateUI(); });
./build.shThis creates Knocker@fariszr.com.shell-extension.zip.
gnome-extensions install Knocker@fariszr.com.shell-extension.zip- Test thoroughly: Run all test cases from
docs/TESTING.md - Check syntax:
node --check *.js - No errors in logs: Check GNOME Shell logs
- Update docs: If adding features, update documentation
- Follow style: Match existing code style
# Create a branch
git checkout -b feature/my-feature
# Make changes and commit
git add .
git commit -m "Add my feature"
# Push and create PR
git push origin feature/my-featureShort summary (50 chars or less)
Longer description if needed. Explain what and why,
not how. Wrap at 72 characters.
- Can use bullet points
- For multiple changes
Fixes #123
- Check for syntax errors in logs
- Verify all imports are correct
- Check metadata.json is valid JSON
- Ensure schemas are compiled
- Reinstall the extension:
./install.sh - Disable and re-enable:
gnome-extensions disable/enable Knocker@fariszr.com - Restart GNOME Shell (X11 only: Alt+F2, 'r')
- Check that you're editing the right files (not cached versions)
- Ensure schema is in
schemas/directory - Compile with:
glib-compile-schemas schemas/ - Check schema ID matches in metadata.json and schema file
- Verify extension is getting settings:
this.getSettings()
- Check event listeners are connected
- Verify monitor is running: check for journalctl process
- Ensure knocker is logging events
- Check for errors in _updateUI() method
- Use async/await for subprocess calls
- Don't block the main thread
- Use GLib.timeout_add for delayed operations
- Always remove timeouts in destroy()
- Cancel ongoing operations
- Disconnect signal handlers
- Close file streams
- Only parse recent logs on startup
- Use efficient JSON parsing
- Filter events early
- Never execute unsanitized user input
- Validate all data from journald
- Use systemd user services (not root)
- Don't store sensitive data in settings
- Support for multiple knocker profiles
- Historical knock log viewer
- Configurable notification preferences per event type
- Network status integration
- Quick knock from panel icon
- Keyboard shortcuts