Make the panel position and the label width settings
Two things that were constants and had no business being constants. Placement: the box (left, centre, right) and the index within it. The default is unchanged -- centre, index 1, immediately right of the clock -- and an index past the end of a box lands at the end, so a large one means "last". Applied at once, no reload. Moving is done by rebuilding the indicator rather than by moving the actor. addToStatusArea is what registers it under the uuid and there is no documented call to move one between boxes; everything else reaches into Main.panel's private boxes. It costs one re-read of a few small state files and only when the setting is touched. Label width: three characters by default, settable up to ten, not down. Three is enough to keep initials apart and narrow enough not to shove the clock about; below three, distinct projects start sharing a label. A wider label takes more initials rather than a longer prefix -- a prefix collapses dev-skills and dev-conventions at any width -- so dev-skills stays "ds" however wide the setting, while claude-code-gnome-extension becomes "ccge" at four. A disambiguating digit still eats into the label instead of extending past the width, so a chip that gains one does not push the row. Sticky labels work against that setting: kept labels are kept whatever width they were cut at, so widening would leave every session on screen at its old width until it ended. The width is therefore the one thing that discards the map -- a relabelling that was asked for is not a label moving under your hand. The combo row is 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. Only the write direction is wired up, and the test covers it, because a row that stores its index instead of its value looks fine until the shell reads the key. Fixed on the way, found by watching the centre box grow 2 -> 3 -> 4 -> 5 across four moves: PanelMenu.ButtonBox connects `this._onDestroy.bind(this)` in its _init, and its _onDestroy is what destroys the container -- the St.Bin the panel box actually holds. The name resolves through the prototype chain, so this extension's own _onDestroy had been silently replacing the shell's since the beginning, leaving an empty container in the panel on every teardown. Renamed to _teardown; the box now stays at two children across moves and across enable/disable cycles. Verified in a nested shell on a copy of the extension carrying temporary logging, since the shell refuses screenshots to non-portal callers: every box, indices 0, 1 and 9, and widths 3, 5, 8, 10 and back, with no JS errors and no leftover actors.
This commit is contained in:
+27
-11
@@ -1,4 +1,4 @@
|
||||
// Three-character chip labels for the panel.
|
||||
// Short chip labels for the panel: three characters by default, settable up.
|
||||
//
|
||||
// A chip per session only pays off if the label stays put. Two rules do that:
|
||||
// labels are assigned oldest-session-first, so a session starting now takes the
|
||||
@@ -9,7 +9,17 @@
|
||||
//
|
||||
// Imports nothing, so it runs under plain node or gjs.
|
||||
|
||||
const MAX = 3;
|
||||
// Both entry points take a width, and both default to this: the module is
|
||||
// imported by tests and by the indicator alike, and a caller that forgets the
|
||||
// setting should get the documented default rather than a stray one.
|
||||
const DEFAULT_WIDTH = 3;
|
||||
|
||||
/** Widths arrive from GSettings and from tests. One character is the floor:
|
||||
* at zero every label would be empty and the collision loop would not end. */
|
||||
function usable(width) {
|
||||
const n = Math.trunc(Number(width));
|
||||
return Number.isFinite(n) && n >= 1 ? n : DEFAULT_WIDTH;
|
||||
}
|
||||
|
||||
/** Split on separators and camelCase humps: "pet-project-server", "outlineMcp". */
|
||||
function segments(name) {
|
||||
@@ -22,29 +32,35 @@ function segments(name) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/** Up to three characters for a project name.
|
||||
/** Up to `width` characters for a project name.
|
||||
*
|
||||
* Initials for multi-segment names, first letters for single words. Initials
|
||||
* matter more than they look: a plain prefix collapses "dev-skills" and
|
||||
* "dev-conventions" onto the same "dev", which is the exact case this has to
|
||||
* keep apart.
|
||||
* keep apart. A wider label takes more initials, not a longer prefix, for the
|
||||
* same reason.
|
||||
*/
|
||||
export function abbreviate(name) {
|
||||
export function abbreviate(name, width = DEFAULT_WIDTH) {
|
||||
const parts = segments(String(name ?? ''));
|
||||
if (!parts.length)
|
||||
return '?';
|
||||
const raw = parts.length > 1
|
||||
? parts.map(p => p[0]).join('')
|
||||
: parts[0];
|
||||
return raw.slice(0, MAX).toLowerCase();
|
||||
return raw.slice(0, usable(width)).toLowerCase();
|
||||
}
|
||||
|
||||
/** Assign a label to every session, reusing the ones already handed out.
|
||||
*
|
||||
* `previous` is the mapping from the last run; pass the returned map back in.
|
||||
* Sessions absent from `sessions` drop out, which frees their label for reuse.
|
||||
*
|
||||
* `width` applies to labels handed out now. Kept labels are kept whatever
|
||||
* width they were cut at -- stickiness outranks it, and the caller that
|
||||
* changes the width is the one that has to drop the old map.
|
||||
*/
|
||||
export function assignChips(sessions, previous = new Map()) {
|
||||
export function assignChips(sessions, previous = new Map(), width = DEFAULT_WIDTH) {
|
||||
const max = usable(width);
|
||||
const labels = new Map();
|
||||
const taken = new Set();
|
||||
|
||||
@@ -61,13 +77,13 @@ export function assignChips(sessions, previous = new Map()) {
|
||||
.sort((a, b) => (a.since || 0) - (b.since || 0));
|
||||
|
||||
for (const session of fresh) {
|
||||
const base = abbreviate(session.base);
|
||||
const base = abbreviate(session.base, max);
|
||||
let label = base;
|
||||
// Digits eat into the base rather than extending past three characters,
|
||||
// so every chip stays the same width and the row does not ripple.
|
||||
// Digits eat into the base rather than extending past the width, so
|
||||
// every chip stays the same size and the row does not ripple.
|
||||
for (let n = 2; taken.has(label); n++) {
|
||||
const suffix = String(n);
|
||||
label = base.slice(0, Math.max(1, MAX - suffix.length)) + suffix;
|
||||
label = base.slice(0, Math.max(1, max - suffix.length)) + suffix;
|
||||
}
|
||||
labels.set(session.sessionId, label);
|
||||
taken.add(label);
|
||||
|
||||
+23
-5
@@ -73,7 +73,14 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
|
||||
// other way -- another extension rebuilding the panel boxes -- would
|
||||
// leave the timer and the file monitor running against a disposed
|
||||
// actor, screaming into the log every 20 seconds.
|
||||
this.connect('destroy', () => this._onDestroy());
|
||||
//
|
||||
// Not named _onDestroy, which is the name PanelMenu.ButtonBox gives its
|
||||
// own handler. It connects `this._onDestroy.bind(this)` in _init, and
|
||||
// that resolves through the prototype chain -- so a subclass method of
|
||||
// that name silently replaces it, and the St.Bin the panel box actually
|
||||
// holds is never destroyed. Measured: an empty container stayed behind
|
||||
// in the box on every teardown.
|
||||
this.connect('destroy', () => this._teardown());
|
||||
this._store.start();
|
||||
}
|
||||
|
||||
@@ -81,8 +88,8 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
|
||||
|
||||
_buildPanel() {
|
||||
// One chip per session rather than one aggregate: with five projects
|
||||
// open, "the most urgent one" answers a question you did not ask. The
|
||||
// row sits right of the clock, so it grows away from the centre.
|
||||
// open, "the most urgent one" answers a question you did not ask.
|
||||
// Where the row sits in the panel is a setting; extension.js places it.
|
||||
this._chipBox = new St.BoxLayout({
|
||||
style_class: 'panel-status-menu-box ccs-panel-box',
|
||||
y_align: Clutter.ActorAlign.CENTER,
|
||||
@@ -176,6 +183,17 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
|
||||
const showAge = this._settings.get_boolean('show-age');
|
||||
const abbreviate = this._settings.get_boolean('abbreviate-names');
|
||||
const maxChips = this._settings.get_int('max-chips');
|
||||
const width = this._settings.get_int('abbrev-length');
|
||||
|
||||
// Labels are sticky by design, which here works against the setting:
|
||||
// widening would leave every session on screen at its old width until
|
||||
// it ended. Changing the width is the one thing that discards the map
|
||||
// -- a relabelling the person asked for is not a label moving under
|
||||
// their hand.
|
||||
if (width !== this._chipWidth) {
|
||||
this._chipLabels = new Map();
|
||||
this._chipWidth = width;
|
||||
}
|
||||
|
||||
this._chipLabels = assignChips(
|
||||
sessions.map(s => ({
|
||||
@@ -186,7 +204,7 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
|
||||
since: s.started || s.since,
|
||||
base: projectName(s.cwd),
|
||||
})),
|
||||
this._chipLabels);
|
||||
this._chipLabels, width);
|
||||
|
||||
const labelFor = session => abbreviate
|
||||
? this._chipLabels.get(session.sessionId)
|
||||
@@ -398,7 +416,7 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
|
||||
|
||||
// ---- Teardown -------------------------------------------------------
|
||||
|
||||
_onDestroy() {
|
||||
_teardown() {
|
||||
if (this._destroyed)
|
||||
return;
|
||||
this._destroyed = true;
|
||||
|
||||
Reference in New Issue
Block a user