import Adw from 'gi://Adw'; import Gtk from 'gi://Gtk'; import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; export default class ClaudeCodeStatusPreferences extends ExtensionPreferences { fillPreferencesWindow(window) { const settings = this.getSettings(); const page = new Adw.PreferencesPage({ title: _('General'), icon_name: 'utilities-terminal-symbolic', }); window.add(page); const dispGroup = new Adw.PreferencesGroup({ title: _('Panel') }); page.add(dispGroup); dispGroup.add(this._switchRow(settings, 'show-project-name', _('Show project name'), _('Name the session that needs attention, not just its state.'))); dispGroup.add(this._switchRow(settings, 'show-age', _('Show time in state'), _('How long it has been working, or waiting for you.'))); dispGroup.add(this._switchRow(settings, 'hide-when-idle', _('Hide when nothing is running'), _('Remove the indicator from the panel while no session is active.'))); const zellijGroup = new Adw.PreferencesGroup({ title: _('zellij'), description: _('Sessions running inside zellij can be located by tab name instead of by path.'), }); page.add(zellijGroup); zellijGroup.add(this._switchRow(settings, 'zellij-integration', _('Resolve tab names'), _('Show the zellij tab in the menu and switch to it on click. Ignored when zellij is not installed.'))); // --- Hooks --------------------------------------------------------- // The indicator is only as good as the hooks feeding it, and a silent // panel looks identical whether nothing is running or nothing is // installed. Say which it is. const hooksGroup = new Adw.PreferencesGroup({ title: _('Hooks'), description: _('Claude Code writes one state file per session; the panel watches them.'), }); page.add(hooksGroup); hooksGroup.add(new Adw.ActionRow({ title: _('Status'), subtitle: this._hooksStatus(), })); hooksGroup.add(new Adw.ActionRow({ title: _('State directory'), subtitle: this._stateDir(), })); const installRow = new Adw.ActionRow({ title: _('Install command'), subtitle: `${this.path}/hooks/install.py`, }); const copyButton = new Gtk.Button({ icon_name: 'edit-copy-symbolic', valign: Gtk.Align.CENTER, tooltip_text: _('Copy to clipboard'), }); copyButton.connect('clicked', () => { window.get_clipboard().set(`${this.path}/hooks/install.py`); }); installRow.add_suffix(copyButton); hooksGroup.add(installRow); } _stateDir() { const base = GLib.getenv('XDG_STATE_HOME') || GLib.build_filenamev([GLib.get_home_dir(), '.local', 'state']); return GLib.build_filenamev([base, 'claude-code-status']); } _hooksStatus() { const path = GLib.build_filenamev([GLib.get_home_dir(), '.claude', 'settings.json']); try { const [ok, bytes] = GLib.file_get_contents(path); if (!ok) return _('~/.claude/settings.json is unreadable'); const settings = JSON.parse(new TextDecoder().decode(bytes)); const events = Object.entries(settings.hooks ?? {}) .filter(([, groups]) => groups.some(g => (g.hooks ?? []).some(h => (h.command ?? '').includes('claude-status-hook.py')))) .map(([event]) => event); if (!events.length) return _('Not installed — run the install command below, then restart your sessions'); return `${_('Installed for')}: ${events.join(', ')}`; } catch (e) { return _('~/.claude/settings.json could not be parsed'); } } _switchRow(settings, key, title, subtitle) { const row = new Adw.SwitchRow({ title, subtitle }); settings.bind(key, row, 'active', Gio.SettingsBindFlags.DEFAULT); return row; } }