-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwidget.ts
More file actions
91 lines (73 loc) · 2.96 KB
/
widget.ts
File metadata and controls
91 lines (73 loc) · 2.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import type { Theme } from "@mariozechner/pi-coding-agent";
import type { Component, TUI } from "@mariozechner/pi-tui";
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
import type { LoopManager, TrackedLoop } from "./loop-manager";
function formatDuration(secs: number): string {
if (!Number.isFinite(secs) || secs <= 0) return "0s";
const s = Math.floor(secs);
const m = Math.floor(s / 60);
const h = Math.floor(m / 60);
const ss = s % 60;
const mm = m % 60;
if (h > 0) return `${h}h${mm}m`;
if (m > 0) return `${m}m${ss}s`;
return `${ss}s`;
}
function tildePath(p: string | null): string {
if (!p) return "";
const home = process.env.HOME;
if (home && p.startsWith(home)) return `~${p.slice(home.length)}`;
return p;
}
function loopDisplayPath(loop: TrackedLoop): string {
const p = loop.worktree ?? loop.directory ?? "";
return tildePath(p);
}
export class RalphWidget implements Component {
private readonly tui: TUI;
private readonly theme: Theme;
private readonly loopManager: LoopManager;
private unsubscribe: (() => void) | null = null;
constructor(tui: TUI, theme: Theme, loopManager: LoopManager) {
this.tui = tui;
this.theme = theme;
this.loopManager = loopManager;
this.unsubscribe = this.loopManager.onChange(() => {
this.tui.requestRender();
});
}
render(width: number): string[] {
const loops = this.loopManager.getLoops();
if (loops.length === 0) return [];
const focused = this.loopManager.getFocused();
if (!focused) return [];
const idx = loops.findIndex((l) => l.id === focused.id);
const pos = idx >= 0 ? idx + 1 : 1;
const prefix = this.theme.fg("accent", "ralph ") + this.theme.fg("dim", "> ");
const path = loopDisplayPath(focused) || focused.id;
const iter = focused.maxIterations > 0 ? `[${focused.iteration}/${focused.maxIterations}]` : "";
const hat = focused.hat ? focused.hat : "";
const elapsed = focused.elapsedSecs > 0 ? formatDuration(focused.elapsedSecs) : "";
const status = focused.status && focused.status !== "unknown" ? focused.status : "";
const right = this.theme.fg("dim", ` [${pos}/${loops.length}]`);
// Compose, then truncate to width.
let mid = this.theme.fg("text", path);
if (iter) mid += " " + this.theme.fg("muted", iter);
if (hat) mid += " " + this.theme.fg("accent", hat);
if (elapsed) mid += " " + this.theme.fg("dim", elapsed);
if (status) mid += " " + this.theme.fg("dim", status);
const full = prefix + mid + right;
// Truncate while preserving the right-side loop index.
const rightWidth = visibleWidth(right);
const maxLeft = Math.max(0, width - rightWidth);
const left = truncateToWidth(prefix + mid, maxLeft, "...");
return [truncateToWidth(left + right, width, "")];
}
invalidate(): void {
// Theme change will recreate the widget via setWidget factory in index.ts.
}
dispose?(): void {
this.unsubscribe?.();
this.unsubscribe = null;
}
}