@herbertgao/pi-extensions 2026.8.3 → 2026.8.4
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 +41 -0
- package/examples/pi-footer.json +275 -0
- package/node_modules/pi-footer/CHANGELOG.md +145 -0
- package/node_modules/pi-footer/LICENSE +21 -0
- package/node_modules/pi-footer/README.md +354 -0
- package/node_modules/pi-footer/package.json +68 -0
- package/node_modules/pi-footer/src/cache.ts +106 -0
- package/node_modules/pi-footer/src/colors.ts +237 -0
- package/node_modules/pi-footer/src/config.ts +202 -0
- package/node_modules/pi-footer/src/event-widgets.ts +41 -0
- package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
- package/node_modules/pi-footer/src/git.ts +135 -0
- package/node_modules/pi-footer/src/index.ts +268 -0
- package/node_modules/pi-footer/src/metrics.ts +141 -0
- package/node_modules/pi-footer/src/presets.ts +383 -0
- package/node_modules/pi-footer/src/render.ts +114 -0
- package/node_modules/pi-footer/src/separators.ts +52 -0
- package/node_modules/pi-footer/src/types.ts +122 -0
- package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
- package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
- package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
- package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
- package/node_modules/pi-footer/src/ui/events.ts +35 -0
- package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
- package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
- package/node_modules/pi-footer/src/ui/fields.ts +252 -0
- package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
- package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
- package/node_modules/pi-footer/src/ui/layout.ts +17 -0
- package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
- package/node_modules/pi-footer/src/ui/model.ts +64 -0
- package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
- package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
- package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
- package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
- package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
- package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
- package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
- package/node_modules/pi-footer/src/ui/screen.ts +243 -0
- package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
- package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
- package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
- package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
- package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
- package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
- package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
- package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
- package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
- package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
- package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
- package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
- package/node_modules/pi-footer/src/ui/theme.ts +48 -0
- package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
- package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
- package/node_modules/pi-footer/src/ui.ts +66 -0
- package/node_modules/pi-footer/src/widgets/context.ts +28 -0
- package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
- package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
- package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
- package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
- package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
- package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
- package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
- package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
- package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
- package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
- package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
- package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
- package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
- package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
- package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
- package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
- package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
- package/node_modules/pi-footer/src/widgets/options.ts +122 -0
- package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
- package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
- package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
- package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
- package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
- package/node_modules/pi-footer/src/widgets/store.ts +26 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
- package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
- package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
- package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
- package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/types.ts +181 -0
- package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
- package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
- package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
- package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
- package/package.json +7 -2
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import { cloneConfig } from "../config.js";
|
|
4
|
+
import type { GetExtensionStatuses } from "../extension-statuses.js";
|
|
5
|
+
import type { StatuslineConfig, StatuslineData } from "../types.js";
|
|
6
|
+
import { WidgetStore } from "../widgets/store.js";
|
|
7
|
+
import type { Widget } from "../widgets/types.js";
|
|
8
|
+
import { ConfigLifecycle, type ConfigUiResult } from "./config-lifecycle.js";
|
|
9
|
+
import { escapeTarget } from "./helpers.js";
|
|
10
|
+
import { activeLineCount, visibleRowCount } from "./layout.js";
|
|
11
|
+
import { CONFIG_UI_HEIGHT_RATIO, type ScreenView } from "./model.js";
|
|
12
|
+
import { OverlayRender } from "./overlay-render.js";
|
|
13
|
+
import type { ScreenContext } from "./screen-context.js";
|
|
14
|
+
import { ScreenController } from "./screen-controller.js";
|
|
15
|
+
import { ScreenRender } from "./screen-render.js";
|
|
16
|
+
import { createScreenState, type ScreenState } from "./screen-state.js";
|
|
17
|
+
import { AddWidgetScreen } from "./screens/add-widget.js";
|
|
18
|
+
import { ConfirmExitScreen } from "./screens/confirm-exit.js";
|
|
19
|
+
import { EditColorsScreen } from "./screens/edit-colors.js";
|
|
20
|
+
import { EditWidgetScreen } from "./screens/edit-widget.js";
|
|
21
|
+
import { ExtensionStatusRowScreen } from "./screens/extension-status-row.js";
|
|
22
|
+
import { GlobalScreen } from "./screens/global.js";
|
|
23
|
+
import { LineListScreen } from "./screens/line-list.js";
|
|
24
|
+
import { MainScreen } from "./screens/main.js";
|
|
25
|
+
import { ColorLevelConfirmScreen, TerminalScreen, TerminalState } from "./screens/terminal.js";
|
|
26
|
+
import { WidgetListScreen } from "./screens/widget-list.js";
|
|
27
|
+
import { createUiTheme, type ThemeProvider, type UiTheme } from "./theme.js";
|
|
28
|
+
import { RETRO_FRAME_INTERVAL_MS } from "./title-bar.js";
|
|
29
|
+
|
|
30
|
+
export class StatuslineConfigScreen {
|
|
31
|
+
private readonly state: ScreenState;
|
|
32
|
+
private readonly lifecycle: ConfigLifecycle;
|
|
33
|
+
private readonly theme: UiTheme;
|
|
34
|
+
private readonly screenRender: ScreenRender;
|
|
35
|
+
private readonly overlayRender: OverlayRender;
|
|
36
|
+
private readonly screenContext: ScreenContext;
|
|
37
|
+
private readonly screenController: ScreenController;
|
|
38
|
+
private saving = false;
|
|
39
|
+
private readonly animationTimer: ReturnType<typeof setInterval>;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
config: StatuslineConfig,
|
|
43
|
+
private readonly previewData: StatuslineData,
|
|
44
|
+
private readonly getExtensionStatuses: GetExtensionStatuses,
|
|
45
|
+
private readonly requestRender: () => void,
|
|
46
|
+
private readonly getTerminalRows: () => number,
|
|
47
|
+
private readonly props: {
|
|
48
|
+
onChange(config: StatuslineConfig): void;
|
|
49
|
+
onSave(config: StatuslineConfig): Promise<void>;
|
|
50
|
+
onClose(result: ConfigUiResult): void;
|
|
51
|
+
getTheme: ThemeProvider;
|
|
52
|
+
},
|
|
53
|
+
) {
|
|
54
|
+
this.state = createScreenState(WidgetStore.fromConfig(cloneConfig(config)));
|
|
55
|
+
this.lifecycle = new ConfigLifecycle(config);
|
|
56
|
+
this.theme = createUiTheme(props.getTheme);
|
|
57
|
+
this.screenRender = new ScreenRender(this.theme);
|
|
58
|
+
this.overlayRender = new OverlayRender(this.theme, this.screenRender);
|
|
59
|
+
this.screenContext = this.createScreenContext();
|
|
60
|
+
this.screenController = new ScreenController(this.screenContext);
|
|
61
|
+
this.registerScreens();
|
|
62
|
+
this.animationTimer = setInterval(this.requestRender, RETRO_FRAME_INTERVAL_MS);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render(width: number): string[] {
|
|
66
|
+
return this.overlayRender.render({
|
|
67
|
+
width,
|
|
68
|
+
terminalRows: this.getTerminalRows(),
|
|
69
|
+
activeLineCount: this.getActiveLineCount(),
|
|
70
|
+
visibleRowCount: this.getVisibleRowCount(),
|
|
71
|
+
store: this.state.store,
|
|
72
|
+
previewData: this.previewData,
|
|
73
|
+
getExtensionStatuses: this.getExtensionStatuses,
|
|
74
|
+
theme: this.props.getTheme(),
|
|
75
|
+
requestRender: this.requestRender,
|
|
76
|
+
configStateText: this.lifecycle.label
|
|
77
|
+
? this.theme.configStateLabel(this.lifecycle.state, this.lifecycle.label)
|
|
78
|
+
: "",
|
|
79
|
+
body: this.screenController.renderScreen(width),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
invalidate(): void {}
|
|
84
|
+
|
|
85
|
+
dispose(): void {
|
|
86
|
+
clearInterval(this.animationTimer);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
handleInput(data: string): void {
|
|
90
|
+
if (matchesKey(data, Key.ctrl("s"))) {
|
|
91
|
+
void this.saveConfig(false);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (this.state.view === "confirm-color-level" || this.state.view === "confirm-exit") {
|
|
96
|
+
this.screenController.handleInput(data);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (matchesKey(data, Key.escape)) {
|
|
101
|
+
const target = escapeTarget(this.state.view);
|
|
102
|
+
if (target === "close") this.requestExit();
|
|
103
|
+
else this.show(target);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.screenController.handleInput(data);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private registerScreens(): void {
|
|
111
|
+
const terminalState = new TerminalState();
|
|
112
|
+
this.screenController.register("main", new MainScreen(this.screenContext, this.screenRender));
|
|
113
|
+
this.screenController.register(
|
|
114
|
+
"line-list",
|
|
115
|
+
new LineListScreen(this.screenContext, this.screenRender, "Edit lines", "widget-list"),
|
|
116
|
+
);
|
|
117
|
+
this.screenController.register(
|
|
118
|
+
"color-line-list",
|
|
119
|
+
new LineListScreen(this.screenContext, this.screenRender, "Edit colors", "color-widget-list"),
|
|
120
|
+
);
|
|
121
|
+
this.screenController.register(
|
|
122
|
+
"widget-list",
|
|
123
|
+
new WidgetListScreen(this.screenContext, this.screenRender, false),
|
|
124
|
+
);
|
|
125
|
+
this.screenController.register(
|
|
126
|
+
"color-widget-list",
|
|
127
|
+
new WidgetListScreen(this.screenContext, this.screenRender, true),
|
|
128
|
+
);
|
|
129
|
+
this.screenController.register(
|
|
130
|
+
"add-widget",
|
|
131
|
+
new AddWidgetScreen(this.screenContext, this.screenRender),
|
|
132
|
+
);
|
|
133
|
+
this.screenController.register(
|
|
134
|
+
"edit-widget",
|
|
135
|
+
new EditWidgetScreen(this.screenContext, this.screenRender),
|
|
136
|
+
);
|
|
137
|
+
this.screenController.register(
|
|
138
|
+
"edit-colors",
|
|
139
|
+
new EditColorsScreen(this.screenContext, this.screenRender),
|
|
140
|
+
);
|
|
141
|
+
this.screenController.register(
|
|
142
|
+
"terminal",
|
|
143
|
+
new TerminalScreen(this.screenContext, this.screenRender, terminalState),
|
|
144
|
+
);
|
|
145
|
+
this.screenController.register(
|
|
146
|
+
"confirm-color-level",
|
|
147
|
+
new ColorLevelConfirmScreen(this.screenContext, this.screenRender, terminalState),
|
|
148
|
+
);
|
|
149
|
+
this.screenController.register(
|
|
150
|
+
"confirm-exit",
|
|
151
|
+
new ConfirmExitScreen(this.screenContext, this.screenRender),
|
|
152
|
+
);
|
|
153
|
+
this.screenController.register(
|
|
154
|
+
"global",
|
|
155
|
+
new GlobalScreen(this.screenContext, this.screenRender),
|
|
156
|
+
);
|
|
157
|
+
this.screenController.register(
|
|
158
|
+
"extension-status-row",
|
|
159
|
+
new ExtensionStatusRowScreen(this.screenContext, this.screenRender),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private createScreenContext(): ScreenContext {
|
|
164
|
+
return {
|
|
165
|
+
state: this.state,
|
|
166
|
+
theme: this.theme,
|
|
167
|
+
getExtensionStatuses: this.getExtensionStatuses,
|
|
168
|
+
currentLine: () => this.currentLine(),
|
|
169
|
+
currentWidget: () => this.currentWidget(),
|
|
170
|
+
visibleRowCount: () => this.getVisibleRowCount(),
|
|
171
|
+
show: (view) => this.show(view),
|
|
172
|
+
emitChange: () => this.emitChange(),
|
|
173
|
+
save: (exitAfterSave) => void this.saveConfig(exitAfterSave),
|
|
174
|
+
exitWithoutSaving: () => this.exitWithoutSaving(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private show(view: ScreenView): void {
|
|
179
|
+
this.state.view = view;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private currentLine(): Widget[] {
|
|
183
|
+
return this.state.store.lines[this.state.selectedLine] ?? this.state.store.lines[0] ?? [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private currentWidget(): Widget | undefined {
|
|
187
|
+
return this.currentLine()[this.state.selectedWidget];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private getVisibleRowCount(): number {
|
|
191
|
+
return visibleRowCount(
|
|
192
|
+
this.getTerminalRows(),
|
|
193
|
+
CONFIG_UI_HEIGHT_RATIO,
|
|
194
|
+
this.getActiveLineCount(),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private getActiveLineCount(): number {
|
|
199
|
+
return activeLineCount(this.state.store.lines);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private requestExit(): void {
|
|
203
|
+
if (!this.lifecycle.dirty) {
|
|
204
|
+
this.closeWithResult(this.lifecycle.closeResult(false));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.state.viewBeforeConfirmExit = this.state.view;
|
|
208
|
+
this.show("confirm-exit");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private exitWithoutSaving(): void {
|
|
212
|
+
this.closeWithResult(this.lifecycle.closeResult(false));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async saveConfig(exitAfterSave: boolean): Promise<void> {
|
|
216
|
+
if (this.saving) return;
|
|
217
|
+
this.saving = true;
|
|
218
|
+
this.lifecycle.beginSave();
|
|
219
|
+
this.requestRender();
|
|
220
|
+
try {
|
|
221
|
+
const config = cloneConfig(this.state.store.toConfig());
|
|
222
|
+
await this.props.onSave(config);
|
|
223
|
+
this.lifecycle.markSaved(config);
|
|
224
|
+
if (exitAfterSave) this.closeWithResult(this.lifecycle.closeResult(true));
|
|
225
|
+
} catch {
|
|
226
|
+
this.lifecycle.markSaveFailed();
|
|
227
|
+
// Error notification is handled by the save callback.
|
|
228
|
+
} finally {
|
|
229
|
+
this.saving = false;
|
|
230
|
+
this.requestRender();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private closeWithResult(result: ConfigUiResult): void {
|
|
235
|
+
this.props.onChange(result.config);
|
|
236
|
+
this.props.onClose(result);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private emitChange(): void {
|
|
240
|
+
this.lifecycle.markChanged();
|
|
241
|
+
this.props.onChange(cloneConfig(this.state.store.toConfig()));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import { registry, type WidgetDefinition } from "../../widgets/registry.js";
|
|
4
|
+
import { clamp, isPrintable, wrap } from "../helpers.js";
|
|
5
|
+
import { pageSelection } from "../navigation.js";
|
|
6
|
+
import { Controller } from "./controller.js";
|
|
7
|
+
|
|
8
|
+
export class AddWidgetScreen extends Controller {
|
|
9
|
+
private selected = 0;
|
|
10
|
+
private filter = "";
|
|
11
|
+
|
|
12
|
+
renderScreen(width: number): string[] {
|
|
13
|
+
const items = this.filterWidgets();
|
|
14
|
+
const visibleCount = this.ctx.visibleRowCount();
|
|
15
|
+
const start = clamp(
|
|
16
|
+
this.selected - Math.floor(visibleCount / 2),
|
|
17
|
+
0,
|
|
18
|
+
Math.max(0, items.length - visibleCount),
|
|
19
|
+
);
|
|
20
|
+
const visible = items.slice(start, start + visibleCount);
|
|
21
|
+
const lines = [
|
|
22
|
+
this.render.line(
|
|
23
|
+
this.render.menuTitle("Add Widget", "Search or select a widget to add"),
|
|
24
|
+
width,
|
|
25
|
+
),
|
|
26
|
+
this.render.line(this.ctx.theme.dim(this.hint()), width),
|
|
27
|
+
this.render.line(
|
|
28
|
+
this.ctx.theme.dim(this.countLabel(items.length, start, visible.length)),
|
|
29
|
+
width,
|
|
30
|
+
),
|
|
31
|
+
];
|
|
32
|
+
visible.forEach((definition, offset) => {
|
|
33
|
+
const index = start + offset;
|
|
34
|
+
const text = this.itemLabel(definition);
|
|
35
|
+
lines.push(this.render.menuLine(index === this.selected, text, width));
|
|
36
|
+
});
|
|
37
|
+
return lines;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
handleInput(data: string): void {
|
|
41
|
+
const items = this.filterWidgets();
|
|
42
|
+
if (matchesKey(data, Key.up))
|
|
43
|
+
this.selected = wrap(this.selected - 1, Math.max(1, items.length));
|
|
44
|
+
else if (matchesKey(data, Key.down))
|
|
45
|
+
this.selected = wrap(this.selected + 1, Math.max(1, items.length));
|
|
46
|
+
else if (matchesKey(data, Key.pageUp)) this.page(items.length, -1);
|
|
47
|
+
else if (matchesKey(data, Key.pageDown)) this.page(items.length, 1);
|
|
48
|
+
else if (matchesKey(data, Key.backspace)) {
|
|
49
|
+
this.filter = this.filter.slice(0, -1);
|
|
50
|
+
this.selected = 0;
|
|
51
|
+
} else if (matchesKey(data, Key.enter)) {
|
|
52
|
+
const definition = items[this.selected];
|
|
53
|
+
if (!definition) return;
|
|
54
|
+
this.ctx
|
|
55
|
+
.currentLine()
|
|
56
|
+
.splice(this.ctx.state.selectedWidget + 1, 0, registry.createWidget(definition.type));
|
|
57
|
+
this.ctx.state.selectedWidget += 1;
|
|
58
|
+
this.ctx.show("widget-list");
|
|
59
|
+
this.ctx.emitChange();
|
|
60
|
+
} else if (isPrintable(data)) {
|
|
61
|
+
this.filter += data.toLowerCase();
|
|
62
|
+
this.selected = 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private hint(): string {
|
|
67
|
+
return `type to filter • ↑/↓ select • pgup/pgdn jump • enter add • esc back • filter: ${this.filter || "(none)"}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private countLabel(total: number, start: number, visibleLength: number): string {
|
|
71
|
+
const range = total === 0 ? "0-0" : `${start + 1}-${Math.min(total, start + visibleLength)}`;
|
|
72
|
+
return `${total} result(s), showing ${range}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private filterWidgets(): WidgetDefinition[] {
|
|
76
|
+
const filter = this.filter.trim();
|
|
77
|
+
if (!filter) return [...registry.definitions];
|
|
78
|
+
return registry.definitions.filter((definition) =>
|
|
79
|
+
`${definition.category} ${definition.label} ${definition.type}`
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.includes(filter.toLowerCase()),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private itemLabel(definition: WidgetDefinition): string {
|
|
86
|
+
return `${definition.category} / ${definition.label} ${this.ctx.theme.dim(definition.description)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private page(length: number, delta: -1 | 1): void {
|
|
90
|
+
this.selected = pageSelection(this.selected, length, this.ctx.visibleRowCount(), delta);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import { wrap } from "../helpers.js";
|
|
4
|
+
import { Controller } from "./controller.js";
|
|
5
|
+
|
|
6
|
+
const ITEMS = ["Save & Exit", "Exit without saving", "Return to config UI"] as const;
|
|
7
|
+
const HINT = "↑/↓ select • enter confirm • s save • x discard • esc/r back";
|
|
8
|
+
|
|
9
|
+
export class ConfirmExitScreen extends Controller {
|
|
10
|
+
private selected = 0;
|
|
11
|
+
|
|
12
|
+
renderScreen(width: number): string[] {
|
|
13
|
+
return [
|
|
14
|
+
this.render.line(
|
|
15
|
+
this.render.menuTitle("Unsaved changes", "Choose how to close configuration"),
|
|
16
|
+
width,
|
|
17
|
+
),
|
|
18
|
+
this.render.line(this.ctx.theme.dim(HINT), width),
|
|
19
|
+
this.render.line(
|
|
20
|
+
this.ctx.theme.warning("You have unsaved pi-footer configuration changes."),
|
|
21
|
+
width,
|
|
22
|
+
),
|
|
23
|
+
...ITEMS.map((item, index) => this.render.menuLine(index === this.selected, item, width)),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
handleInput(data: string): void {
|
|
28
|
+
if (matchesKey(data, Key.escape)) this.returnToConfigUi();
|
|
29
|
+
else if (matchesKey(data, Key.up) || matchesKey(data, Key.left))
|
|
30
|
+
this.selected = wrap(this.selected - 1, ITEMS.length);
|
|
31
|
+
else if (matchesKey(data, Key.down) || matchesKey(data, Key.right))
|
|
32
|
+
this.selected = wrap(this.selected + 1, ITEMS.length);
|
|
33
|
+
else if (matchesKey(data, Key.enter)) this.selectedAction();
|
|
34
|
+
else this.shortcutAction(data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private selectedAction(): void {
|
|
38
|
+
if (this.selected === 0) return this.ctx.save(true);
|
|
39
|
+
if (this.selected === 1) return this.ctx.exitWithoutSaving();
|
|
40
|
+
return this.returnToConfigUi();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private shortcutAction(data: string): void {
|
|
44
|
+
if (data === "s") return this.ctx.save(true);
|
|
45
|
+
if (data === "x") return this.ctx.exitWithoutSaving();
|
|
46
|
+
if (data === "r") return this.returnToConfigUi();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private returnToConfigUi(): void {
|
|
50
|
+
this.ctx.show(this.ctx.state.viewBeforeConfirmExit);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ScreenContext } from "../screen-context.js";
|
|
2
|
+
import type { ScreenRender } from "../screen-render.js";
|
|
3
|
+
|
|
4
|
+
export abstract class Controller {
|
|
5
|
+
constructor(
|
|
6
|
+
protected ctx: ScreenContext,
|
|
7
|
+
protected readonly render: ScreenRender,
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
abstract renderScreen(width: number): string[];
|
|
11
|
+
abstract handleInput(data: string): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import type { Widget } from "../../widgets/types.js";
|
|
4
|
+
import {
|
|
5
|
+
applyColorDigit,
|
|
6
|
+
applyColorOptionField,
|
|
7
|
+
deleteColorDigit,
|
|
8
|
+
EDIT_COLORS_HINT,
|
|
9
|
+
} from "../color-options.js";
|
|
10
|
+
import { editColorsFieldRows, editColorsTitle } from "../edit-colors.js";
|
|
11
|
+
import { colorFields } from "../fields.js";
|
|
12
|
+
import { isPrintable, wrap } from "../helpers.js";
|
|
13
|
+
import type { ColorOptionField } from "../model.js";
|
|
14
|
+
import { Controller } from "./controller.js";
|
|
15
|
+
|
|
16
|
+
export class EditColorsScreen extends Controller {
|
|
17
|
+
private selected = 0;
|
|
18
|
+
|
|
19
|
+
renderScreen(width: number): string[] {
|
|
20
|
+
const widget = this.ctx.currentWidget();
|
|
21
|
+
if (!widget) return [this.render.line(this.ctx.theme.warning("No widget selected"), width)];
|
|
22
|
+
const fields = editColorsFieldRows(widget);
|
|
23
|
+
this.clampSelection(fields.length);
|
|
24
|
+
return [
|
|
25
|
+
this.render.line(
|
|
26
|
+
this.render.menuTitle(editColorsTitle(widget), "Select a field to edit its colors"),
|
|
27
|
+
width,
|
|
28
|
+
),
|
|
29
|
+
this.render.line(this.ctx.theme.dim(EDIT_COLORS_HINT), width),
|
|
30
|
+
...fields.map((field, index) => this.render.menuLine(index === this.selected, field, width)),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
handleInput(data: string): void {
|
|
35
|
+
const widget = this.ctx.currentWidget();
|
|
36
|
+
if (!widget) return;
|
|
37
|
+
const fields = colorFields(widget);
|
|
38
|
+
this.clampSelection(fields.length);
|
|
39
|
+
const field = fields[this.selected];
|
|
40
|
+
if (!field) return;
|
|
41
|
+
if (matchesKey(data, Key.up)) this.selected = wrap(this.selected - 1, fields.length);
|
|
42
|
+
else if (matchesKey(data, Key.down)) this.selected = wrap(this.selected + 1, fields.length);
|
|
43
|
+
else if (matchesKey(data, Key.left)) this.adjustColorField(widget, field, -1);
|
|
44
|
+
else if (matchesKey(data, Key.right) || matchesKey(data, Key.enter))
|
|
45
|
+
this.adjustColorField(widget, field, 1);
|
|
46
|
+
else if (matchesKey(data, Key.backspace)) this.deleteColorDigit(widget, field);
|
|
47
|
+
else if (isPrintable(data)) this.applyColorDigit(widget, field, data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private clampSelection(fieldCount: number): void {
|
|
51
|
+
this.selected = Math.min(Math.max(this.selected, 0), Math.max(0, fieldCount - 1));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private applyColorDigit(widget: Widget, field: ColorOptionField, digit: string): void {
|
|
55
|
+
if (!applyColorDigit(widget, field, digit)) return;
|
|
56
|
+
this.ctx.emitChange();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private deleteColorDigit(widget: Widget, field: ColorOptionField): void {
|
|
60
|
+
if (!deleteColorDigit(widget, field)) return;
|
|
61
|
+
this.ctx.emitChange();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private adjustColorField(widget: Widget, field: ColorOptionField, delta: number): void {
|
|
65
|
+
applyColorOptionField(widget, field, delta);
|
|
66
|
+
this.ctx.emitChange();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import { registry } from "../../widgets/registry.js";
|
|
4
|
+
import type { Widget } from "../../widgets/types.js";
|
|
5
|
+
import { eventWidgetUsageLines } from "../events.js";
|
|
6
|
+
import { cycleExternalStatusKey, statusKeyPickerLines } from "../extension-status-picker.js";
|
|
7
|
+
import { fieldsForWidget, fieldValue } from "../fields.js";
|
|
8
|
+
import { isPrintable, wrap } from "../helpers.js";
|
|
9
|
+
import type { OptionField } from "../model.js";
|
|
10
|
+
import { applyOptionField, applyOptionFieldEdit } from "../option-edit.js";
|
|
11
|
+
import { Controller } from "./controller.js";
|
|
12
|
+
|
|
13
|
+
const HINT = "↑/↓ field • ←/→ change • type text • backspace delete • esc back";
|
|
14
|
+
|
|
15
|
+
export class EditWidgetScreen extends Controller {
|
|
16
|
+
private selected = 0;
|
|
17
|
+
|
|
18
|
+
renderScreen(width: number): string[] {
|
|
19
|
+
const widget = this.ctx.currentWidget();
|
|
20
|
+
if (!widget) return [this.render.line(this.ctx.theme.warning("No widget selected"), width)];
|
|
21
|
+
const fields = fieldsForWidget(widget);
|
|
22
|
+
this.clampSelection(fields.length);
|
|
23
|
+
const selectedField = fields[this.selected];
|
|
24
|
+
const lines = [
|
|
25
|
+
this.render.line(
|
|
26
|
+
this.render.menuTitle(this.widgetTitle(widget), "Select a field to edit its options"),
|
|
27
|
+
width,
|
|
28
|
+
),
|
|
29
|
+
this.render.line(this.ctx.theme.dim(HINT), width),
|
|
30
|
+
...fields.map((field, index) =>
|
|
31
|
+
this.render.menuLine(
|
|
32
|
+
index === this.selected,
|
|
33
|
+
`${field.label}: ${fieldValue(widget, field)}`,
|
|
34
|
+
width,
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
if (selectedField?.editAction === "external-status-key") {
|
|
40
|
+
lines.push(
|
|
41
|
+
...statusKeyPickerLines(
|
|
42
|
+
this.ctx.getExtensionStatuses,
|
|
43
|
+
this.ctx.state.store.settings.extensionStatusRow,
|
|
44
|
+
width,
|
|
45
|
+
(content, lineWidth) => this.render.line(content, lineWidth),
|
|
46
|
+
(value) => this.ctx.theme.dim(value),
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (widget.type === "event") {
|
|
52
|
+
lines.push(
|
|
53
|
+
...eventWidgetUsageLines(
|
|
54
|
+
widget,
|
|
55
|
+
width,
|
|
56
|
+
(content, lineWidth) => this.render.line(content, lineWidth),
|
|
57
|
+
(value) => this.ctx.theme.dim(value),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
handleInput(data: string): void {
|
|
66
|
+
const widget = this.ctx.currentWidget();
|
|
67
|
+
if (!widget) return;
|
|
68
|
+
const fields = fieldsForWidget(widget);
|
|
69
|
+
this.clampSelection(fields.length);
|
|
70
|
+
const field = fields[this.selected];
|
|
71
|
+
if (!field) return;
|
|
72
|
+
if (matchesKey(data, Key.up)) this.selected = wrap(this.selected - 1, fields.length);
|
|
73
|
+
else if (matchesKey(data, Key.down)) this.selected = wrap(this.selected + 1, fields.length);
|
|
74
|
+
else if (matchesKey(data, Key.left)) this.adjustField(widget, field, -1);
|
|
75
|
+
else if (matchesKey(data, Key.right) || matchesKey(data, Key.enter))
|
|
76
|
+
this.adjustField(widget, field, 1);
|
|
77
|
+
else if (matchesKey(data, Key.backspace) && field.kind === "text")
|
|
78
|
+
this.applyTextFieldEdit(widget, field, { kind: "deleteText" });
|
|
79
|
+
else if (isPrintable(data) && field.kind === "text")
|
|
80
|
+
this.applyTextFieldEdit(widget, field, { kind: "appendText", text: data });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private widgetTitle(widget: Widget): string {
|
|
84
|
+
return `Edit ${registry.spec(widget.type).label}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private clampSelection(fieldCount: number): void {
|
|
88
|
+
this.selected = Math.min(Math.max(this.selected, 0), Math.max(0, fieldCount - 1));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private applyTextFieldEdit(
|
|
92
|
+
widget: Widget,
|
|
93
|
+
field: OptionField,
|
|
94
|
+
edit: { kind: "appendText"; text: string } | { kind: "deleteText" },
|
|
95
|
+
): void {
|
|
96
|
+
if (applyOptionFieldEdit(widget, field, edit) !== "changed") return;
|
|
97
|
+
this.ctx.emitChange();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private adjustField(widget: Widget, field: OptionField, delta: number): void {
|
|
101
|
+
const result = applyOptionField(widget, field, delta);
|
|
102
|
+
if (result === "external-status-key") {
|
|
103
|
+
if (
|
|
104
|
+
!cycleExternalStatusKey(
|
|
105
|
+
widget,
|
|
106
|
+
this.ctx.getExtensionStatuses,
|
|
107
|
+
this.ctx.state.store.settings.extensionStatusRow,
|
|
108
|
+
delta,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return;
|
|
112
|
+
} else if (result === "unchanged") return;
|
|
113
|
+
this.ctx.emitChange();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
extensionStatusRowCount,
|
|
5
|
+
extensionStatusRowLines,
|
|
6
|
+
toggleExtensionStatusRowSelection,
|
|
7
|
+
} from "../extension-statuses.js";
|
|
8
|
+
import { wrap } from "../helpers.js";
|
|
9
|
+
import { pageSelection } from "../navigation.js";
|
|
10
|
+
import { Controller } from "./controller.js";
|
|
11
|
+
|
|
12
|
+
export class ExtensionStatusRowScreen extends Controller {
|
|
13
|
+
private selected = 0;
|
|
14
|
+
|
|
15
|
+
renderScreen(width: number): string[] {
|
|
16
|
+
return extensionStatusRowLines(
|
|
17
|
+
this.ctx.state.store.settings,
|
|
18
|
+
this.ctx.getExtensionStatuses,
|
|
19
|
+
this.selected,
|
|
20
|
+
width,
|
|
21
|
+
(title, subtitle) => this.render.menuTitle(title, subtitle),
|
|
22
|
+
(content, lineWidth) => this.render.line(content, lineWidth),
|
|
23
|
+
(selected, content, lineWidth) => this.render.menuLine(selected, content, lineWidth),
|
|
24
|
+
(value) => this.ctx.theme.dim(value),
|
|
25
|
+
(value) => this.ctx.theme.success(value),
|
|
26
|
+
(value) => this.ctx.theme.warning(value),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
handleInput(data: string): void {
|
|
31
|
+
const count = extensionStatusRowCount(
|
|
32
|
+
this.ctx.state.store.settings,
|
|
33
|
+
this.ctx.getExtensionStatuses,
|
|
34
|
+
);
|
|
35
|
+
if (matchesKey(data, Key.up)) this.selected = wrap(this.selected - 1, Math.max(1, count));
|
|
36
|
+
else if (matchesKey(data, Key.down))
|
|
37
|
+
this.selected = wrap(this.selected + 1, Math.max(1, count));
|
|
38
|
+
else if (matchesKey(data, Key.pageUp)) this.page(count, -1);
|
|
39
|
+
else if (matchesKey(data, Key.pageDown)) this.page(count, 1);
|
|
40
|
+
else if (
|
|
41
|
+
matchesKey(data, Key.space) ||
|
|
42
|
+
matchesKey(data, Key.left) ||
|
|
43
|
+
matchesKey(data, Key.right) ||
|
|
44
|
+
matchesKey(data, Key.enter)
|
|
45
|
+
) {
|
|
46
|
+
if (
|
|
47
|
+
toggleExtensionStatusRowSelection(
|
|
48
|
+
this.ctx.state.store.settings,
|
|
49
|
+
this.ctx.getExtensionStatuses,
|
|
50
|
+
this.selected,
|
|
51
|
+
)
|
|
52
|
+
) {
|
|
53
|
+
this.ctx.emitChange();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private page(count: number, delta: -1 | 1): void {
|
|
59
|
+
this.selected = pageSelection(this.selected, count, this.ctx.visibleRowCount(), delta);
|
|
60
|
+
}
|
|
61
|
+
}
|