@herbertgao/pi-extensions 2026.9.19 → 2026.9.20

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.
Files changed (186) hide show
  1. package/README.md +1 -8
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +76 -59
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +76 -59
  4. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/preview.webp +0 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +93 -34
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +346 -117
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +6 -36
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/index.ts +2 -2
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -29
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +330 -360
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +56 -108
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/session.ts +97 -15
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +80 -45
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/flush-docked-bash.ts +36 -0
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-layout.ts +229 -0
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +479 -0
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/startup-header.ts +92 -61
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +58 -38
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +19 -3
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +1080 -229
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +232 -259
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +38 -48
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +8 -2
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +2 -7
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +277 -113
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +15 -6
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +18 -72
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +61 -26
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +25 -57
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-component.ts +125 -0
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +227 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +136 -0
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +115 -0
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-inline.ts +324 -0
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-layout.ts +762 -0
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +152 -0
  37. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-palette.ts +410 -0
  38. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-parse.ts +535 -0
  39. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +21 -3219
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +44 -0
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +568 -0
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +19 -18
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +2 -2
  45. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +1 -0
  46. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +142 -192
  47. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/mcp-title.ts +39 -0
  48. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{message-display.ts → tool/message-display.ts} +24 -90
  49. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +426 -0
  50. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +90 -132
  51. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{show-more-hint.ts → tool/show-more-hint.ts} +10 -0
  52. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +8 -26
  53. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-color.ts +44 -0
  54. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +76 -0
  55. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/component-tree.ts +34 -0
  56. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +28 -0
  57. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/fullscreen-detect.ts +51 -0
  58. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +124 -0
  59. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/sgr-mouse.ts +70 -0
  60. package/node_modules/@herbertgao/pi-cc-extensions/package.json +7 -7
  61. package/package.json +3 -7
  62. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.en.svg +0 -101
  63. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.svg +0 -101
  64. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/welcome.webp +0 -0
  65. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/render-utils.ts +0 -5
  66. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +0 -30
  67. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-display-utils.ts +0 -12
  68. package/node_modules/pi-footer/CHANGELOG.md +0 -145
  69. package/node_modules/pi-footer/LICENSE +0 -21
  70. package/node_modules/pi-footer/README.md +0 -354
  71. package/node_modules/pi-footer/package.json +0 -68
  72. package/node_modules/pi-footer/src/cache.ts +0 -106
  73. package/node_modules/pi-footer/src/colors.ts +0 -237
  74. package/node_modules/pi-footer/src/config.ts +0 -202
  75. package/node_modules/pi-footer/src/event-widgets.ts +0 -41
  76. package/node_modules/pi-footer/src/extension-statuses.ts +0 -102
  77. package/node_modules/pi-footer/src/git.ts +0 -135
  78. package/node_modules/pi-footer/src/index.ts +0 -268
  79. package/node_modules/pi-footer/src/metrics.ts +0 -141
  80. package/node_modules/pi-footer/src/presets.ts +0 -383
  81. package/node_modules/pi-footer/src/render.ts +0 -114
  82. package/node_modules/pi-footer/src/separators.ts +0 -52
  83. package/node_modules/pi-footer/src/types.ts +0 -122
  84. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +0 -22
  85. package/node_modules/pi-footer/src/ui/color-options.ts +0 -195
  86. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +0 -61
  87. package/node_modules/pi-footer/src/ui/edit-colors.ts +0 -13
  88. package/node_modules/pi-footer/src/ui/events.ts +0 -35
  89. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +0 -60
  90. package/node_modules/pi-footer/src/ui/extension-statuses.ts +0 -73
  91. package/node_modules/pi-footer/src/ui/fields.ts +0 -252
  92. package/node_modules/pi-footer/src/ui/global-menu.ts +0 -126
  93. package/node_modules/pi-footer/src/ui/helpers.ts +0 -44
  94. package/node_modules/pi-footer/src/ui/layout.ts +0 -17
  95. package/node_modules/pi-footer/src/ui/line-list.ts +0 -19
  96. package/node_modules/pi-footer/src/ui/model.ts +0 -64
  97. package/node_modules/pi-footer/src/ui/navigation.ts +0 -29
  98. package/node_modules/pi-footer/src/ui/option-edit.ts +0 -93
  99. package/node_modules/pi-footer/src/ui/overlay-render.ts +0 -103
  100. package/node_modules/pi-footer/src/ui/screen-context.ts +0 -18
  101. package/node_modules/pi-footer/src/ui/screen-controller.ts +0 -27
  102. package/node_modules/pi-footer/src/ui/screen-render.ts +0 -32
  103. package/node_modules/pi-footer/src/ui/screen-state.ts +0 -20
  104. package/node_modules/pi-footer/src/ui/screen.ts +0 -243
  105. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +0 -92
  106. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +0 -52
  107. package/node_modules/pi-footer/src/ui/screens/controller.ts +0 -12
  108. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +0 -68
  109. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +0 -115
  110. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +0 -61
  111. package/node_modules/pi-footer/src/ui/screens/global.ts +0 -75
  112. package/node_modules/pi-footer/src/ui/screens/line-list.ts +0 -108
  113. package/node_modules/pi-footer/src/ui/screens/main.ts +0 -99
  114. package/node_modules/pi-footer/src/ui/screens/terminal.ts +0 -121
  115. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +0 -154
  116. package/node_modules/pi-footer/src/ui/terminal-menu.ts +0 -32
  117. package/node_modules/pi-footer/src/ui/theme.ts +0 -48
  118. package/node_modules/pi-footer/src/ui/title-bar.ts +0 -100
  119. package/node_modules/pi-footer/src/ui/widget-actions.ts +0 -76
  120. package/node_modules/pi-footer/src/ui.ts +0 -66
  121. package/node_modules/pi-footer/src/widgets/context.ts +0 -28
  122. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +0 -17
  123. package/node_modules/pi-footer/src/widgets/core/context-window.ts +0 -24
  124. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +0 -19
  125. package/node_modules/pi-footer/src/widgets/core/cwd.ts +0 -108
  126. package/node_modules/pi-footer/src/widgets/core/event.ts +0 -34
  127. package/node_modules/pi-footer/src/widgets/core/external-status.ts +0 -102
  128. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +0 -20
  129. package/node_modules/pi-footer/src/widgets/core/model.ts +0 -35
  130. package/node_modules/pi-footer/src/widgets/core/provider.ts +0 -17
  131. package/node_modules/pi-footer/src/widgets/core/session-name.ts +0 -17
  132. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +0 -17
  133. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +0 -17
  134. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +0 -17
  135. package/node_modules/pi-footer/src/widgets/git/branch.ts +0 -81
  136. package/node_modules/pi-footer/src/widgets/git/clean.ts +0 -19
  137. package/node_modules/pi-footer/src/widgets/git/deletions.ts +0 -17
  138. package/node_modules/pi-footer/src/widgets/git/diff.ts +0 -38
  139. package/node_modules/pi-footer/src/widgets/git/insertions.ts +0 -17
  140. package/node_modules/pi-footer/src/widgets/git/remote.ts +0 -17
  141. package/node_modules/pi-footer/src/widgets/git/root.ts +0 -17
  142. package/node_modules/pi-footer/src/widgets/git/sha.ts +0 -17
  143. package/node_modules/pi-footer/src/widgets/git/staged.ts +0 -17
  144. package/node_modules/pi-footer/src/widgets/git/status.ts +0 -19
  145. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +0 -17
  146. package/node_modules/pi-footer/src/widgets/git/untracked.ts +0 -17
  147. package/node_modules/pi-footer/src/widgets/instance.ts +0 -118
  148. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +0 -18
  149. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +0 -32
  150. package/node_modules/pi-footer/src/widgets/layout/separator.ts +0 -57
  151. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +0 -31
  152. package/node_modules/pi-footer/src/widgets/options.ts +0 -122
  153. package/node_modules/pi-footer/src/widgets/project/runtime.ts +0 -361
  154. package/node_modules/pi-footer/src/widgets/registry.ts +0 -223
  155. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +0 -17
  156. package/node_modules/pi-footer/src/widgets/session/compactions.ts +0 -17
  157. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +0 -18
  158. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +0 -18
  159. package/node_modules/pi-footer/src/widgets/session/messages.ts +0 -19
  160. package/node_modules/pi-footer/src/widgets/session/session-id.ts +0 -17
  161. package/node_modules/pi-footer/src/widgets/session/session-start.ts +0 -18
  162. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +0 -17
  163. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +0 -19
  164. package/node_modules/pi-footer/src/widgets/session/total-time.ts +0 -18
  165. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +0 -17
  166. package/node_modules/pi-footer/src/widgets/store.ts +0 -26
  167. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +0 -55
  168. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +0 -18
  169. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +0 -18
  170. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +0 -108
  171. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +0 -26
  172. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +0 -29
  173. package/node_modules/pi-footer/src/widgets/tokens/context.ts +0 -29
  174. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +0 -51
  175. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +0 -25
  176. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +0 -18
  177. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +0 -25
  178. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +0 -18
  179. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +0 -20
  180. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +0 -25
  181. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +0 -18
  182. package/node_modules/pi-footer/src/widgets/types.ts +0 -181
  183. package/node_modules/pi-footer/src/widgets/utils/colors.ts +0 -11
  184. package/node_modules/pi-footer/src/widgets/utils/context.ts +0 -112
  185. package/node_modules/pi-footer/src/widgets/utils/session.ts +0 -19
  186. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +0 -78
@@ -1,243 +0,0 @@
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
- }
@@ -1,92 +0,0 @@
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
- }
@@ -1,52 +0,0 @@
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
- }
@@ -1,12 +0,0 @@
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
- }
@@ -1,68 +0,0 @@
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
- }
@@ -1,115 +0,0 @@
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
- }
@@ -1,61 +0,0 @@
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
- }