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', _('Label chips with the project'), _('Name the sessions, not just their states.'))); dispGroup.add(this._switchRow(settings, 'abbreviate-names', _('Shorten names to three characters'), _('“dev-skills” becomes “ds”. Collisions get a digit by seniority, so a label already on screen never changes.'))); dispGroup.add(this._spinRow(settings, 'max-chips', _('Chips shown'), _('The most urgent sessions get a chip; the rest are counted as “+N”.'), 1, 12)); dispGroup.add(this._switchRow(settings, 'show-age', _('Show time in state'), _('Shown on the most urgent session only.'))); 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'), _('Name the zellij tab each session runs in. 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']); // Checked before reading: file_get_contents throws on a missing file, // so without this the person who has installed nothing -- the one who // most needs the instructions -- is told the file cannot be parsed. if (!GLib.file_test(path, GLib.FileTest.EXISTS)) return _('No ~/.claude/settings.json yet — run the install command below'); 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'); return `${_('Installed for')}: ${events.join(', ')}`; } catch (e) { return _('~/.claude/settings.json could not be parsed'); } } _spinRow(settings, key, title, subtitle, lower, upper) { const row = new Adw.SpinRow({ title, subtitle, adjustment: new Gtk.Adjustment({ lower, upper, step_increment: 1, page_increment: 1, }), }); settings.bind(key, row, 'value', Gio.SettingsBindFlags.DEFAULT); return row; } _switchRow(settings, key, title, subtitle) { const row = new Adw.SwitchRow({ title, subtitle }); settings.bind(key, row, 'active', Gio.SettingsBindFlags.DEFAULT); return row; } }