@tryinget/pi-activity-strip 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -157
- package/bin/pi-activity-strip-claude-hook.mjs +71 -0
- package/bin/pi-activity-strip.mjs +50 -60
- package/native/bin/linux-x64-gnu/artifact.json +21 -0
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/Cargo.lock +1045 -0
- package/native/panel/Cargo.toml +23 -0
- package/native/panel/src/app.rs +606 -0
- package/native/panel/src/card_view.rs +602 -0
- package/native/panel/src/main.rs +31 -0
- package/native/panel/src/protocol.rs +297 -0
- package/native/panel/src/runtime.rs +151 -0
- package/native/panel/src/style.css +250 -0
- package/package.json +16 -4
- package/src/broker/server.mjs +43 -7
- package/src/broker/session-store.mjs +41 -13
- package/src/client/broker-client.mjs +10 -3
- package/src/client/session-telemetry.mjs +54 -14
- package/src/common/activity-order.mjs +15 -9
- package/src/common/agent-identity.mjs +163 -0
- package/src/common/ak-tasks.mjs +354 -0
- package/src/common/alignment-controller.mjs +24 -42
- package/src/common/card-display.mjs +5 -0
- package/src/common/claude-events.mjs +190 -0
- package/src/common/claude-hook-config.mjs +56 -0
- package/src/common/claude-transcript.mjs +163 -0
- package/src/common/codex-transcript.mjs +153 -0
- package/src/common/compatibility.mjs +28 -13
- package/src/common/constants.mjs +10 -28
- package/src/common/contracts.d.ts +68 -4
- package/src/common/contracts.ts +105 -4
- package/src/common/ghostty-present.mjs +100 -0
- package/src/common/ghostty-theme.mjs +149 -0
- package/src/common/niri-focus.mjs +209 -140
- package/src/common/protocol.mjs +48 -16
- package/src/common/session-cards.mjs +105 -0
- package/src/common/status-report.mjs +53 -1
- package/src/common/surface-bindings.mjs +234 -0
- package/src/common/telemetry.mjs +8 -1
- package/src/common/terminal-identity.mjs +212 -0
- package/src/common/window-placement.mjs +59 -0
- package/src/common/workspace-view.mjs +161 -0
- package/src/native/agent-discovery.mjs +294 -0
- package/src/native/ak-runtime.mjs +164 -0
- package/src/native/codex-discovery.mjs +128 -0
- package/src/native/ghostty-tab-inventory.py +99 -0
- package/src/native/height-repair.mjs +154 -0
- package/src/native/main.mjs +499 -0
- package/src/native/panel-projection.mjs +161 -0
- package/src/native/placement.mjs +225 -0
- package/src/native/tab-inventory.mjs +188 -0
- package/src/native/theme-runtime.mjs +72 -0
- package/src/native/theme-source.mjs +197 -0
- package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +20 -2
- package/src/common/electron.mjs +0 -57
- package/src/electron/main.mjs +0 -499
- package/src/electron/niri-native-window-runtime.mjs +0 -178
- package/src/electron/preload.cjs +0 -50
- package/src/electron/renderer-visibility-runtime.mjs +0 -107
- package/src/electron/workspace-view-runtime.mjs +0 -360
- package/src/ui/strip-html.mjs +0 -500
package/src/common/contracts.ts
CHANGED
|
@@ -9,7 +9,22 @@ export type SessionState = "idle" | "thinking" | "tool" | "waiting" | "success"
|
|
|
9
9
|
export interface SessionSnapshot {
|
|
10
10
|
sessionId: string;
|
|
11
11
|
publisherId: string;
|
|
12
|
+
publisherSequence: number;
|
|
12
13
|
processId: number;
|
|
14
|
+
terminalKind: "ghostty-surface" | "unbound";
|
|
15
|
+
terminalKey: string;
|
|
16
|
+
terminalFamily: string;
|
|
17
|
+
terminalSurfaceId: string;
|
|
18
|
+
cardId?: string;
|
|
19
|
+
publisherCount?: number;
|
|
20
|
+
publisherIds?: string[];
|
|
21
|
+
publisherRecordKeys?: string[];
|
|
22
|
+
publisherSessionIds?: string[];
|
|
23
|
+
akTasks?: AkTaskChip[];
|
|
24
|
+
akTaskOverflow?: number;
|
|
25
|
+
windowId?: number | null;
|
|
26
|
+
placement?: "title" | "host" | "binding";
|
|
27
|
+
surfaceVisible?: boolean;
|
|
13
28
|
cwd: string;
|
|
14
29
|
repoLabel: string;
|
|
15
30
|
sessionName: string;
|
|
@@ -29,6 +44,32 @@ export interface SessionSnapshot {
|
|
|
29
44
|
errorMessage: string;
|
|
30
45
|
}
|
|
31
46
|
|
|
47
|
+
export type AkTaskChipState = "active" | "deferred" | "orphaned";
|
|
48
|
+
|
|
49
|
+
/** One read-only AK task reference joined onto a terminal card by the native projection. */
|
|
50
|
+
export interface AkTaskChip {
|
|
51
|
+
id: number;
|
|
52
|
+
title: string;
|
|
53
|
+
state: AkTaskChipState;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A live-lease task claim parsed from read-only `ak task list` output. */
|
|
57
|
+
export interface AkTaskClaim {
|
|
58
|
+
id: number;
|
|
59
|
+
title: string;
|
|
60
|
+
repo: string;
|
|
61
|
+
sessionId: string;
|
|
62
|
+
leaseExpiresAt: number;
|
|
63
|
+
claimedAt: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A task with an active deferral parsed from read-only `ak task deferred` output. */
|
|
67
|
+
export interface AkTaskDeferred {
|
|
68
|
+
id: number;
|
|
69
|
+
title: string;
|
|
70
|
+
repo: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
32
73
|
export interface BrokerSnapshot {
|
|
33
74
|
generatedAt: number;
|
|
34
75
|
sessions: SessionSnapshot[];
|
|
@@ -38,13 +79,49 @@ export type ActivityStripRuntimeState = "starting" | "ready" | "error";
|
|
|
38
79
|
|
|
39
80
|
export interface ActivityStripRuntimeStatus {
|
|
40
81
|
state: ActivityStripRuntimeState;
|
|
82
|
+
backend?: "native-layer-shell";
|
|
41
83
|
startedAt: number;
|
|
42
84
|
readyAt?: number | null;
|
|
43
85
|
windowVisible?: boolean;
|
|
44
86
|
displayServer?: "wayland" | "x11" | "headless" | "unknown";
|
|
45
87
|
windowManager?: string | null;
|
|
46
88
|
displayCount?: number | null;
|
|
47
|
-
alignmentMode?: "
|
|
89
|
+
alignmentMode?: "layer-shell";
|
|
90
|
+
rendererCardCount?: number;
|
|
91
|
+
rendererCardIds?: string[];
|
|
92
|
+
rendererHiddenTabCardCount?: number;
|
|
93
|
+
surfaceBindingCount?: number;
|
|
94
|
+
unplacedSurfaceCount?: number;
|
|
95
|
+
agentTabCount?: number;
|
|
96
|
+
themeScheme?: "dark" | "light" | "unknown";
|
|
97
|
+
themeName?: string | null;
|
|
98
|
+
themeSource?: string | null;
|
|
99
|
+
themeFingerprint?: string;
|
|
100
|
+
themeError?: string;
|
|
101
|
+
agentScanError?: string | null;
|
|
102
|
+
heightRepairState?: "enabled" | "disabled";
|
|
103
|
+
heightRepairCount?: number;
|
|
104
|
+
lastHeightRepairAt?: number;
|
|
105
|
+
tabInventoryState?: "pending" | "ready" | "failed" | "unavailable" | "disabled";
|
|
106
|
+
tabInventoryDetail?: string | null;
|
|
107
|
+
tabInventoryFrameCount?: number;
|
|
108
|
+
tabInventoryTabCount?: number;
|
|
109
|
+
tabInventoryProbedAt?: number | null;
|
|
110
|
+
akTaskState?: "pending" | "ok" | "unavailable" | "disabled";
|
|
111
|
+
akTaskError?: string | null;
|
|
112
|
+
akTaskClaimCount?: number;
|
|
113
|
+
akTaskDeferredCount?: number;
|
|
114
|
+
akTaskOrphanCount?: number;
|
|
115
|
+
akTaskRenderedCount?: number;
|
|
116
|
+
rendererVisibilityTransitionCount?: number;
|
|
117
|
+
controllerPid?: number;
|
|
118
|
+
panelPid?: number | null;
|
|
119
|
+
panelExpanded?: boolean;
|
|
120
|
+
clickThrough?: boolean;
|
|
121
|
+
panelMoveCount?: number;
|
|
122
|
+
panelActivationCount?: number;
|
|
123
|
+
panelRestartCount?: number;
|
|
124
|
+
lastMovedCardId?: string | null;
|
|
48
125
|
warnings?: string[];
|
|
49
126
|
error?: string | null;
|
|
50
127
|
}
|
|
@@ -65,6 +142,7 @@ export interface BrokerClientOptions {
|
|
|
65
142
|
|
|
66
143
|
export interface SessionStoreOptions {
|
|
67
144
|
staleAfterMs?: number;
|
|
145
|
+
maxSessions?: number;
|
|
68
146
|
}
|
|
69
147
|
|
|
70
148
|
export interface ActivityStripBrokerOptions {
|
|
@@ -76,7 +154,14 @@ export interface ActivityStripBrokerOptions {
|
|
|
76
154
|
remove(sessionId: string, publisherId?: string): boolean;
|
|
77
155
|
};
|
|
78
156
|
getRuntimeStatus?: () => ActivityStripRuntimeStatus | undefined;
|
|
79
|
-
focusSession?: (
|
|
157
|
+
focusSession?: (targetId: string) => Promise<{
|
|
158
|
+
ok: boolean;
|
|
159
|
+
error?: string;
|
|
160
|
+
windowId?: number;
|
|
161
|
+
presented?: boolean;
|
|
162
|
+
verified?: boolean;
|
|
163
|
+
}>;
|
|
164
|
+
focusStrip?: () => Promise<{ ok: boolean; error?: string }>;
|
|
80
165
|
}
|
|
81
166
|
|
|
82
167
|
export interface ToolCallDescription {
|
|
@@ -143,6 +228,21 @@ export interface SessionTelemetryOptions {
|
|
|
143
228
|
pi?: TelemetryPiLike;
|
|
144
229
|
cwd?: string;
|
|
145
230
|
sessionName?: string;
|
|
231
|
+
env?: NodeJS.ProcessEnv;
|
|
232
|
+
stdinIsTTY?: boolean;
|
|
233
|
+
resolveTerminalIdentity?: (options: {
|
|
234
|
+
env?: NodeJS.ProcessEnv;
|
|
235
|
+
hasUI?: boolean;
|
|
236
|
+
stdinIsTTY?: boolean;
|
|
237
|
+
ttyPath?: string;
|
|
238
|
+
processId?: number;
|
|
239
|
+
ancestorExecutable?: string;
|
|
240
|
+
}) => {
|
|
241
|
+
terminalKind: "ghostty-surface" | "unbound";
|
|
242
|
+
terminalKey: string;
|
|
243
|
+
terminalFamily: string;
|
|
244
|
+
terminalSurfaceId: string;
|
|
245
|
+
};
|
|
146
246
|
transport?: {
|
|
147
247
|
publish: (session: SessionSnapshot) => Promise<unknown>;
|
|
148
248
|
remove: (session: { sessionId: string; publisherId: string }) => Promise<unknown>;
|
|
@@ -151,13 +251,14 @@ export interface SessionTelemetryOptions {
|
|
|
151
251
|
|
|
152
252
|
export interface ActivityStripCompatibilityReport {
|
|
153
253
|
ok: boolean;
|
|
254
|
+
backend: string;
|
|
154
255
|
displayServer: "wayland" | "x11" | "headless" | "unknown";
|
|
155
256
|
windowManager: string | null;
|
|
156
|
-
electronPath: string | null;
|
|
157
257
|
displayCount: number | null;
|
|
158
|
-
alignmentMode: "
|
|
258
|
+
alignmentMode: "layer-shell";
|
|
159
259
|
primaryDisplayOnly: boolean;
|
|
160
260
|
clickThroughDefault: boolean;
|
|
261
|
+
tabInventory?: { available: boolean; detail: string };
|
|
161
262
|
blockers: string[];
|
|
162
263
|
warnings: string[];
|
|
163
264
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: "presents one Ghostty surface through the running process's session-bus present-surface action"
|
|
3
|
+
// read_when:
|
|
4
|
+
// - "changing hidden-tab activation, Ghostty D-Bus targeting, or its fail-closed conditions"
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import { normalizeGhosttySurfaceId } from "./terminal-identity.mjs";
|
|
8
|
+
|
|
9
|
+
const GHOSTTY_OBJECT_PATHS = new Map([
|
|
10
|
+
["main", "/com/mitchellh/ghostty"],
|
|
11
|
+
["legacy", "/com/tryinget/ghosttysidequest"],
|
|
12
|
+
]);
|
|
13
|
+
export const GHOSTTY_PRESENT_SURFACE_TIMEOUT_MS = 2000;
|
|
14
|
+
|
|
15
|
+
/** @typedef {(file: string, args: string[], options: object) => Promise<{stdout?: string}>} ExecFileAsync */
|
|
16
|
+
/** @typedef {{ok: true; busName: string} | {ok: false; error: string}} PresentSurfaceResult */
|
|
17
|
+
|
|
18
|
+
/** @param {unknown} family */
|
|
19
|
+
export function ghosttyObjectPathForFamily(family) {
|
|
20
|
+
return GHOSTTY_OBJECT_PATHS.get(String(family ?? "")) ?? "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Every running Ghostty process, single-instance server or standalone window, exports
|
|
25
|
+
* `org.gtk.Actions` on its unique bus name. `present-surface` selects the tab owning the surface
|
|
26
|
+
* and presents its window; a surface id the process does not own is ignored by Ghostty itself.
|
|
27
|
+
* The target is the exact host process already proven to contain the surface, never a
|
|
28
|
+
* well-known name that another build could claim.
|
|
29
|
+
* @param {{execFileAsync: ExecFileAsync; env?: NodeJS.ProcessEnv; hostPid: number; terminalFamily: unknown; surfaceId: unknown; timeout?: number}} options
|
|
30
|
+
* @returns {Promise<PresentSurfaceResult>}
|
|
31
|
+
*/
|
|
32
|
+
export async function presentGhosttySurface({
|
|
33
|
+
execFileAsync,
|
|
34
|
+
env = process.env,
|
|
35
|
+
hostPid,
|
|
36
|
+
terminalFamily,
|
|
37
|
+
surfaceId,
|
|
38
|
+
timeout = GHOSTTY_PRESENT_SURFACE_TIMEOUT_MS,
|
|
39
|
+
}) {
|
|
40
|
+
const objectPath = ghosttyObjectPathForFamily(terminalFamily);
|
|
41
|
+
const normalizedSurfaceId = normalizeGhosttySurfaceId(surfaceId);
|
|
42
|
+
if (
|
|
43
|
+
!objectPath ||
|
|
44
|
+
!normalizedSurfaceId ||
|
|
45
|
+
normalizedSurfaceId === "0" ||
|
|
46
|
+
!Number.isInteger(hostPid) ||
|
|
47
|
+
hostPid <= 0
|
|
48
|
+
) {
|
|
49
|
+
return { ok: false, error: "the surface target is incomplete" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let rows;
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await execFileAsync(
|
|
55
|
+
"busctl",
|
|
56
|
+
["--user", "list", "--no-pager", "--no-legend"],
|
|
57
|
+
{ env, timeout },
|
|
58
|
+
);
|
|
59
|
+
rows = String(stdout ?? "")
|
|
60
|
+
.split("\n")
|
|
61
|
+
.map((line) => line.trim().split(/\s+/));
|
|
62
|
+
} catch {
|
|
63
|
+
return { ok: false, error: "the session bus is unavailable" };
|
|
64
|
+
}
|
|
65
|
+
const uniqueNames = rows
|
|
66
|
+
.filter(
|
|
67
|
+
(fields) => fields[0]?.startsWith(":") && Number.parseInt(fields[1] ?? "", 10) === hostPid,
|
|
68
|
+
)
|
|
69
|
+
.map((fields) => String(fields[0]));
|
|
70
|
+
if (uniqueNames.length === 0) {
|
|
71
|
+
return { ok: false, error: "the Ghostty process is not on the session bus" };
|
|
72
|
+
}
|
|
73
|
+
if (uniqueNames.length > 1) {
|
|
74
|
+
return { ok: false, error: "the Ghostty process owns several bus connections" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await execFileAsync(
|
|
79
|
+
"busctl",
|
|
80
|
+
[
|
|
81
|
+
"--user",
|
|
82
|
+
"call",
|
|
83
|
+
uniqueNames[0],
|
|
84
|
+
objectPath,
|
|
85
|
+
"org.gtk.Actions",
|
|
86
|
+
"Activate",
|
|
87
|
+
"sava{sv}",
|
|
88
|
+
"present-surface",
|
|
89
|
+
"1",
|
|
90
|
+
"t",
|
|
91
|
+
normalizedSurfaceId,
|
|
92
|
+
"0",
|
|
93
|
+
],
|
|
94
|
+
{ env, timeout },
|
|
95
|
+
);
|
|
96
|
+
return { ok: true, busName: uniqueNames[0] };
|
|
97
|
+
} catch {
|
|
98
|
+
return { ok: false, error: "Ghostty rejected present-surface" };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: "resolves the colours Ghostty is currently using into the ribbon's palette"
|
|
3
|
+
// read_when:
|
|
4
|
+
// - "changing how the ribbon follows the Ghostty theme or the day/night switch"
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ghostty writes its palette as `key = value` lines, in its config and in each theme file. A theme
|
|
9
|
+
* setting may name one theme, or one per colour scheme as `light:Name,dark:Name`. The ribbon reads
|
|
10
|
+
* the same files the terminal does, so the two always agree without any configuration of its own.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const HEX_COLOR = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
14
|
+
export const PALETTE_SIZE = 16;
|
|
15
|
+
|
|
16
|
+
/** @typedef {{background: string; foreground: string; accent: string; thinking: string; tool: string; waiting: string; success: string; error: string}} RibbonPalette */
|
|
17
|
+
|
|
18
|
+
/** Everforest-neutral defaults, used only when no theme can be read. */
|
|
19
|
+
/** @type {RibbonPalette} */
|
|
20
|
+
export const FALLBACK_PALETTE = Object.freeze({
|
|
21
|
+
background: "#1e2326",
|
|
22
|
+
foreground: "#d3c6aa",
|
|
23
|
+
accent: "#7fbbb3",
|
|
24
|
+
thinking: "#7fbbb3",
|
|
25
|
+
tool: "#dbbc7f",
|
|
26
|
+
waiting: "#e69875",
|
|
27
|
+
success: "#a7c080",
|
|
28
|
+
error: "#e67e80",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** @param {unknown} value */
|
|
32
|
+
export function normalizeHexColor(value) {
|
|
33
|
+
const raw = String(value ?? "").trim();
|
|
34
|
+
if (!HEX_COLOR.test(raw)) return "";
|
|
35
|
+
const body = raw.replace(/^#/, "").toLowerCase();
|
|
36
|
+
const full =
|
|
37
|
+
body.length === 3
|
|
38
|
+
? body
|
|
39
|
+
.split("")
|
|
40
|
+
.map((character) => character + character)
|
|
41
|
+
.join("")
|
|
42
|
+
: body;
|
|
43
|
+
return `#${full}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read every `key = value` pair, ignoring comments and blank lines. Repeated keys are kept in
|
|
48
|
+
* order, because `palette` appears once per colour index.
|
|
49
|
+
* @param {string} text
|
|
50
|
+
*/
|
|
51
|
+
export function parseGhosttyConfig(text) {
|
|
52
|
+
/** @type {Map<string, string[]>} */
|
|
53
|
+
const entries = new Map();
|
|
54
|
+
for (const line of String(text ?? "").split("\n")) {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
57
|
+
const separator = trimmed.indexOf("=");
|
|
58
|
+
if (separator <= 0) continue;
|
|
59
|
+
const key = trimmed.slice(0, separator).trim().toLowerCase();
|
|
60
|
+
const value = trimmed.slice(separator + 1).trim();
|
|
61
|
+
entries.set(key, [...(entries.get(key) ?? []), value]);
|
|
62
|
+
}
|
|
63
|
+
return entries;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Names of the themes a config selects. `light:A,dark:B` follows the desktop's colour scheme; a
|
|
68
|
+
* bare name is used for both.
|
|
69
|
+
* @param {string} value
|
|
70
|
+
* @returns {{light: string; dark: string}}
|
|
71
|
+
*/
|
|
72
|
+
export function parseThemeSetting(value) {
|
|
73
|
+
const raw = String(value ?? "").trim();
|
|
74
|
+
if (!raw) return { light: "", dark: "" };
|
|
75
|
+
if (!/(^|,)\s*(light|dark)\s*:/i.test(raw)) return { light: raw, dark: raw };
|
|
76
|
+
const themes = { light: "", dark: "" };
|
|
77
|
+
for (const part of raw.split(",")) {
|
|
78
|
+
const match = part.match(/^\s*(light|dark)\s*:\s*(.+?)\s*$/i);
|
|
79
|
+
if (match) themes[match[1].toLowerCase() === "dark" ? "dark" : "light"] = match[2];
|
|
80
|
+
}
|
|
81
|
+
return themes;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Colours from a theme file or a config that carries them inline.
|
|
86
|
+
* @param {string} text
|
|
87
|
+
*/
|
|
88
|
+
export function parseGhosttyTheme(text) {
|
|
89
|
+
const entries = parseGhosttyConfig(text);
|
|
90
|
+
/** @type {string[]} */
|
|
91
|
+
const palette = new Array(PALETTE_SIZE).fill("");
|
|
92
|
+
for (const value of entries.get("palette") ?? []) {
|
|
93
|
+
const match = String(value).match(/^\s*(\d+)\s*=\s*(.+?)\s*$/);
|
|
94
|
+
if (!match) continue;
|
|
95
|
+
const index = Number.parseInt(match[1], 10);
|
|
96
|
+
const color = normalizeHexColor(match[2]);
|
|
97
|
+
if (Number.isInteger(index) && index >= 0 && index < PALETTE_SIZE && color) {
|
|
98
|
+
palette[index] = color;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** @param {string} key */
|
|
102
|
+
const single = (key) => normalizeHexColor((entries.get(key) ?? []).at(-1));
|
|
103
|
+
return {
|
|
104
|
+
background: single("background"),
|
|
105
|
+
foreground: single("foreground"),
|
|
106
|
+
cursor: single("cursor-color"),
|
|
107
|
+
selectionBackground: single("selection-background"),
|
|
108
|
+
palette,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @param {ReturnType<typeof parseGhosttyTheme>} theme @param {number} index @param {string} fallback */
|
|
113
|
+
function paletteColor(theme, index, fallback) {
|
|
114
|
+
return theme?.palette?.[index] || fallback;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Map terminal colours onto the ribbon's roles. State colours reuse the terminal's own semantics,
|
|
119
|
+
* so a card reads the same way as output in the tab it points at: green settled, yellow working,
|
|
120
|
+
* red failed, and the cursor colour for a session that wants the operator.
|
|
121
|
+
* @param {ReturnType<typeof parseGhosttyTheme> | null} theme
|
|
122
|
+
* @returns {RibbonPalette}
|
|
123
|
+
*/
|
|
124
|
+
export function buildRibbonPalette(theme) {
|
|
125
|
+
if (!theme?.background || !theme?.foreground) return { ...FALLBACK_PALETTE };
|
|
126
|
+
const blue = paletteColor(theme, 4, FALLBACK_PALETTE.thinking);
|
|
127
|
+
return {
|
|
128
|
+
background: theme.background,
|
|
129
|
+
foreground: theme.foreground,
|
|
130
|
+
accent: blue,
|
|
131
|
+
thinking: blue,
|
|
132
|
+
tool: paletteColor(theme, 3, FALLBACK_PALETTE.tool),
|
|
133
|
+
waiting: theme.cursor || paletteColor(theme, 5, FALLBACK_PALETTE.waiting),
|
|
134
|
+
success: paletteColor(theme, 2, FALLBACK_PALETTE.success),
|
|
135
|
+
error: paletteColor(theme, 1, FALLBACK_PALETTE.error),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The GTK colour definitions the panel prepends to its stylesheet. Everything else the ribbon
|
|
141
|
+
* draws is derived from these in CSS, so a theme change is a handful of values.
|
|
142
|
+
* @param {RibbonPalette} palette
|
|
143
|
+
*/
|
|
144
|
+
export function paletteDefinitions(palette) {
|
|
145
|
+
const colors = { ...FALLBACK_PALETTE, ...(palette ?? {}) };
|
|
146
|
+
return Object.entries(colors)
|
|
147
|
+
.map(([name, value]) => `@define-color pi_${name} ${normalizeHexColor(value) || "#000000"};`)
|
|
148
|
+
.join("\n");
|
|
149
|
+
}
|