Files
av f8e85d5703 Put the clock on the working chips, and on all of them
The counter rode on the first chip, whichever state it happened to be in.
It now shows on every chip that is working and on no other: how long a
session has been at it is the thing that decides whether to wait for it,
where the age of an idle one only says how long ago you stopped looking.

The setting keeps its key and changes its name to "Show working time".

CLAUDE.md is new, and its first rule is the one broken twice: writing to
dconf from a shell attached to the live session bus wipes settings that
cannot be recovered.
2026-08-10 17:21:43 +03:00

176 lines
7.5 KiB
JavaScript

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 placeGroup = new Adw.PreferencesGroup({
title: _('Placement'),
description: _('Where the row of chips sits in the top bar. Applied at once — no need to reload.'),
});
page.add(placeGroup);
placeGroup.add(this._comboRow(settings, 'panel-box',
_('Panel box'),
_('The centre box holds the clock; the right one is the system status area.'),
[
{ value: 'left', label: _('Left') },
{ value: 'center', label: _('Centre') },
{ value: 'right', label: _('Right') },
]));
placeGroup.add(this._spinRow(settings, 'panel-position',
_('Position in that box'),
_('0 is first. In the centre box, 1 puts the chips just right of the clock. Past the end means last.'),
0, 10));
const dispGroup = new Adw.PreferencesGroup({ title: _('Chips') });
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'),
_('“dev-skills” becomes “ds”. Collisions get a digit by seniority, so a label already on screen never changes.')));
dispGroup.add(this._spinRow(settings, 'abbrev-length',
_('Label length'),
_('Characters a shortened label may use. Three keeps the row narrow; longer reads more like the name. Changing it relabels every session at once.'),
3, 10));
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 working time'),
_('A counter on every session that is working, so you can see how long it has been at it. The menu lists ages either way.')));
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;
}
/** A string key with a fixed set of values.
*
* Bound by hand: Gio.Settings.bind maps a boolean to 'active' and an int
* to 'value', but a string to a selected index needs bind_with_mapping,
* which is not introspectable. The write direction is the only one wired
* up -- while this window is open, this row is the only thing that writes
* the key, and it is read afresh every time the window is built.
*/
_comboRow(settings, key, title, subtitle, options) {
const row = new Adw.ComboRow({
title, subtitle,
model: Gtk.StringList.new(options.map(o => o.label)),
});
const values = options.map(o => o.value);
const current = values.indexOf(settings.get_string(key));
// Set before connecting, so restoring the stored value is not itself
// taken for a change the person made.
row.selected = current < 0 ? 0 : current;
row.connect('notify::selected', () => {
const value = values[row.selected];
if (value)
settings.set_string(key, value);
});
return row;
}
_switchRow(settings, key, title, subtitle) {
const row = new Adw.SwitchRow({ title, subtitle });
settings.bind(key, row, 'active', Gio.SettingsBindFlags.DEFAULT);
return row;
}
}