im-pickle-rick 0.1.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 +242 -0
- package/bin.js +3 -0
- package/dist/pickle +0 -0
- package/dist/worker-executor.js +207 -0
- package/package.json +53 -0
- package/src/games/GameSidebarManager.test.ts +64 -0
- package/src/games/GameSidebarManager.ts +78 -0
- package/src/games/gameboy/GameboyView.test.ts +25 -0
- package/src/games/gameboy/GameboyView.ts +100 -0
- package/src/games/gameboy/gameboy-polyfills.ts +313 -0
- package/src/games/index.test.ts +9 -0
- package/src/games/index.ts +4 -0
- package/src/games/snake/SnakeGame.test.ts +35 -0
- package/src/games/snake/SnakeGame.ts +145 -0
- package/src/games/snake/SnakeView.test.ts +25 -0
- package/src/games/snake/SnakeView.ts +290 -0
- package/src/index.test.ts +24 -0
- package/src/index.ts +141 -0
- package/src/services/commands/worker.test.ts +14 -0
- package/src/services/commands/worker.ts +262 -0
- package/src/services/config/index.ts +2 -0
- package/src/services/config/settings.test.ts +42 -0
- package/src/services/config/settings.ts +220 -0
- package/src/services/config/state.test.ts +88 -0
- package/src/services/config/state.ts +130 -0
- package/src/services/config/types.ts +39 -0
- package/src/services/execution/index.ts +1 -0
- package/src/services/execution/pickle-source.test.ts +88 -0
- package/src/services/execution/pickle-source.ts +264 -0
- package/src/services/execution/prompt.test.ts +93 -0
- package/src/services/execution/prompt.ts +322 -0
- package/src/services/execution/sequential.test.ts +91 -0
- package/src/services/execution/sequential.ts +422 -0
- package/src/services/execution/worker-client.ts +94 -0
- package/src/services/execution/worker-executor.ts +41 -0
- package/src/services/execution/worker.test.ts +73 -0
- package/src/services/git/branch.test.ts +147 -0
- package/src/services/git/branch.ts +128 -0
- package/src/services/git/diff.test.ts +113 -0
- package/src/services/git/diff.ts +323 -0
- package/src/services/git/index.ts +4 -0
- package/src/services/git/pr.test.ts +104 -0
- package/src/services/git/pr.ts +192 -0
- package/src/services/git/worktree.test.ts +99 -0
- package/src/services/git/worktree.ts +141 -0
- package/src/services/providers/base.test.ts +86 -0
- package/src/services/providers/base.ts +438 -0
- package/src/services/providers/codex.test.ts +39 -0
- package/src/services/providers/codex.ts +208 -0
- package/src/services/providers/gemini.test.ts +40 -0
- package/src/services/providers/gemini.ts +169 -0
- package/src/services/providers/index.test.ts +28 -0
- package/src/services/providers/index.ts +41 -0
- package/src/services/providers/opencode.test.ts +64 -0
- package/src/services/providers/opencode.ts +228 -0
- package/src/services/providers/types.ts +44 -0
- package/src/skills/code-implementer.md +105 -0
- package/src/skills/code-researcher.md +78 -0
- package/src/skills/implementation-planner.md +105 -0
- package/src/skills/plan-reviewer.md +100 -0
- package/src/skills/prd-drafter.md +123 -0
- package/src/skills/research-reviewer.md +79 -0
- package/src/skills/ruthless-refactorer.md +52 -0
- package/src/skills/ticket-manager.md +135 -0
- package/src/types/index.ts +2 -0
- package/src/types/rpc.ts +14 -0
- package/src/types/tasks.ts +50 -0
- package/src/types.d.ts +9 -0
- package/src/ui/common.ts +28 -0
- package/src/ui/components/FilePickerView.test.ts +79 -0
- package/src/ui/components/FilePickerView.ts +161 -0
- package/src/ui/components/MultiLineInput.test.ts +27 -0
- package/src/ui/components/MultiLineInput.ts +233 -0
- package/src/ui/components/SessionChip.test.ts +69 -0
- package/src/ui/components/SessionChip.ts +481 -0
- package/src/ui/components/ToyboxSidebar.test.ts +36 -0
- package/src/ui/components/ToyboxSidebar.ts +329 -0
- package/src/ui/components/refactor_plan.md +35 -0
- package/src/ui/controllers/DashboardController.integration.test.ts +43 -0
- package/src/ui/controllers/DashboardController.ts +650 -0
- package/src/ui/dashboard.test.ts +43 -0
- package/src/ui/dashboard.ts +309 -0
- package/src/ui/dialogs/DashboardDialog.test.ts +146 -0
- package/src/ui/dialogs/DashboardDialog.ts +399 -0
- package/src/ui/dialogs/Dialog.test.ts +50 -0
- package/src/ui/dialogs/Dialog.ts +241 -0
- package/src/ui/dialogs/DialogSidebar.test.ts +60 -0
- package/src/ui/dialogs/DialogSidebar.ts +71 -0
- package/src/ui/dialogs/DiffViewDialog.test.ts +57 -0
- package/src/ui/dialogs/DiffViewDialog.ts +510 -0
- package/src/ui/dialogs/PRPreviewDialog.test.ts +50 -0
- package/src/ui/dialogs/PRPreviewDialog.ts +346 -0
- package/src/ui/dialogs/test-utils.ts +232 -0
- package/src/ui/file-picker-utils.test.ts +71 -0
- package/src/ui/file-picker-utils.ts +200 -0
- package/src/ui/input-chrome.test.ts +62 -0
- package/src/ui/input-chrome.ts +172 -0
- package/src/ui/logger.test.ts +68 -0
- package/src/ui/logger.ts +45 -0
- package/src/ui/mock-factory.ts +6 -0
- package/src/ui/spinner.test.ts +65 -0
- package/src/ui/spinner.ts +41 -0
- package/src/ui/test-setup.ts +300 -0
- package/src/ui/theme.test.ts +23 -0
- package/src/ui/theme.ts +16 -0
- package/src/ui/views/LandingView.integration.test.ts +21 -0
- package/src/ui/views/LandingView.test.ts +24 -0
- package/src/ui/views/LandingView.ts +221 -0
- package/src/ui/views/LogView.test.ts +24 -0
- package/src/ui/views/LogView.ts +277 -0
- package/src/ui/views/ToyboxView.test.ts +46 -0
- package/src/ui/views/ToyboxView.ts +323 -0
- package/src/utils/clipboard.test.ts +86 -0
- package/src/utils/clipboard.ts +100 -0
- package/src/utils/index.test.ts +68 -0
- package/src/utils/index.ts +95 -0
- package/src/utils/persona.test.ts +12 -0
- package/src/utils/persona.ts +8 -0
- package/src/utils/project-root.test.ts +38 -0
- package/src/utils/project-root.ts +22 -0
- package/src/utils/resources.test.ts +64 -0
- package/src/utils/resources.ts +92 -0
- package/src/utils/search.test.ts +48 -0
- package/src/utils/search.ts +103 -0
- package/src/utils/session-tracker.test.ts +46 -0
- package/src/utils/session-tracker.ts +67 -0
- package/src/utils/spinner.test.ts +54 -0
- package/src/utils/spinner.ts +87 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { mock } from "bun:test";
|
|
2
|
+
|
|
3
|
+
export class MockRenderable {
|
|
4
|
+
public id = "";
|
|
5
|
+
public visible = true;
|
|
6
|
+
public zIndex = 0;
|
|
7
|
+
public _children: any[] = [];
|
|
8
|
+
public _handlers: Record<string, Function[]> = {};
|
|
9
|
+
public _ctx: any;
|
|
10
|
+
|
|
11
|
+
constructor(renderer: any, options: any) {
|
|
12
|
+
this._ctx = renderer;
|
|
13
|
+
this.id = options?.id || "";
|
|
14
|
+
this.visible = options?.visible !== false;
|
|
15
|
+
this._children = [];
|
|
16
|
+
this._handlers = {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public add(child: any) {
|
|
20
|
+
if (child) this._children.push(child);
|
|
21
|
+
}
|
|
22
|
+
public remove(id: string) {
|
|
23
|
+
this._children = this._children.filter((c: any) => c.id !== id);
|
|
24
|
+
}
|
|
25
|
+
public getChildren() {
|
|
26
|
+
return this._children || [];
|
|
27
|
+
}
|
|
28
|
+
public destroy() {}
|
|
29
|
+
public destroyRecursively() {}
|
|
30
|
+
public onMouse() {}
|
|
31
|
+
public focus() {}
|
|
32
|
+
public blur() {}
|
|
33
|
+
public requestRender() {}
|
|
34
|
+
|
|
35
|
+
public on(event: string, handler: Function) {
|
|
36
|
+
if (!this._handlers[event]) this._handlers[event] = [];
|
|
37
|
+
this._handlers[event].push(handler);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public off(event: string, handler: Function) {
|
|
41
|
+
if (this._handlers[event]) {
|
|
42
|
+
this._handlers[event] = this._handlers[event].filter((h: any) => h !== handler);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public emit(event: string, ...args: any[]) {
|
|
47
|
+
if (this._handlers[event]) {
|
|
48
|
+
this._handlers[event].forEach((h: any) => h(...args));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class MockCliRenderer {
|
|
54
|
+
public root = new MockRenderable(null, { id: "root" });
|
|
55
|
+
public keyInput: any;
|
|
56
|
+
public _internalKeyInput: any;
|
|
57
|
+
public requestRender = mock(() => {});
|
|
58
|
+
public addInputHandler = mock(() => {});
|
|
59
|
+
public registerLifecyclePass = mock(() => {});
|
|
60
|
+
public on = mock(() => {});
|
|
61
|
+
public off = mock(() => {});
|
|
62
|
+
public start = mock(() => {});
|
|
63
|
+
public stop = mock(() => {});
|
|
64
|
+
public getSelection = mock(() => ({ isSelecting: false }));
|
|
65
|
+
public clearSelection = mock(() => {});
|
|
66
|
+
public focusRenderable = mock(() => {});
|
|
67
|
+
public useMouse = false;
|
|
68
|
+
|
|
69
|
+
constructor() {
|
|
70
|
+
this.keyInput = {
|
|
71
|
+
on: mock(function(this: any, event: string, handler: Function) {
|
|
72
|
+
if (!this._handlers) this._handlers = {};
|
|
73
|
+
if (!this._handlers[event]) this._handlers[event] = [];
|
|
74
|
+
this._handlers[event].push(handler);
|
|
75
|
+
}),
|
|
76
|
+
off: mock(function(this: any, event: string, handler: Function) {
|
|
77
|
+
if (this._handlers && this._handlers[event]) {
|
|
78
|
+
this._handlers[event] = this._handlers[event].filter((h: any) => h !== handler);
|
|
79
|
+
}
|
|
80
|
+
}),
|
|
81
|
+
emit: mock(function(this: any, event: string, ...args: any[]) {
|
|
82
|
+
if (this._handlers && this._handlers[event]) {
|
|
83
|
+
this._handlers[event].forEach((h: any) => h(...args));
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
86
|
+
removeListener: mock(() => {}),
|
|
87
|
+
_handlers: {} as Record<string, Function[]>
|
|
88
|
+
};
|
|
89
|
+
this._internalKeyInput = {
|
|
90
|
+
onInternal: mock(() => {}),
|
|
91
|
+
offInternal: mock(() => {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const mockAppendFile = mock(async () => {});
|
|
97
|
+
export const mockMkdir = mock(async () => {});
|
|
98
|
+
export const mockWriteFile = mock(async () => {});
|
|
99
|
+
export const mockReadFile = mock(async () => "mock content");
|
|
100
|
+
|
|
101
|
+
// Global mock registration
|
|
102
|
+
mock.module("@opentui/core", () => {
|
|
103
|
+
return {
|
|
104
|
+
CliRenderer: MockCliRenderer,
|
|
105
|
+
createCliRenderer: mock(async () => new MockCliRenderer()),
|
|
106
|
+
Renderable: MockRenderable,
|
|
107
|
+
InputRenderable: class extends MockRenderable {
|
|
108
|
+
public height = 1;
|
|
109
|
+
public plainText = "";
|
|
110
|
+
public value = "";
|
|
111
|
+
public virtualLineCount = 1;
|
|
112
|
+
public maxLength = 1000;
|
|
113
|
+
public yogaNode = { markDirty: mock(() => {}) };
|
|
114
|
+
public placeholder = "";
|
|
115
|
+
public _placeholder = "";
|
|
116
|
+
public position = "relative";
|
|
117
|
+
public onContentChange: () => void = () => {};
|
|
118
|
+
constructor(ctx: any, options: any) {
|
|
119
|
+
super(ctx, options);
|
|
120
|
+
this.value = options?.value || "";
|
|
121
|
+
this.plainText = this.value;
|
|
122
|
+
this.maxLength = options?.maxLength ?? 1000;
|
|
123
|
+
this.height = options?.height ?? 1;
|
|
124
|
+
|
|
125
|
+
// Ensure methods that might be overridden in subclasses are on the prototype OR
|
|
126
|
+
// allow them to be shadowed by subclass properties.
|
|
127
|
+
// We'll keep them as instance properties for now but use bind to allow super calls
|
|
128
|
+
}
|
|
129
|
+
public deleteCharBackward = mock(() => true);
|
|
130
|
+
public deleteChar = mock(() => true);
|
|
131
|
+
public handleKeyPress = mock(() => true);
|
|
132
|
+
public newLine = mock(() => true);
|
|
133
|
+
public handlePaste = mock(() => {});
|
|
134
|
+
public insertText = mock(function(this: any, text: string) { this.plainText += text; });
|
|
135
|
+
public setText = mock(function(this: any, text: string) { this.plainText = text; });
|
|
136
|
+
public submit = mock(() => true);
|
|
137
|
+
public removeHighlightsByRef = mock(() => {});
|
|
138
|
+
public addHighlightByCharRange = mock(() => {});
|
|
139
|
+
public syntaxStyle = { registerStyle: mock(() => 1), destroy: mock(() => {}) };
|
|
140
|
+
},
|
|
141
|
+
BoxRenderable: class extends MockRenderable {
|
|
142
|
+
public backgroundColor: any = { r: 0, g: 0, b: 0, a: 1 };
|
|
143
|
+
public borderColor: any = { r: 0, g: 0, b: 0, a: 1 };
|
|
144
|
+
public right = 0;
|
|
145
|
+
public height: any = 0;
|
|
146
|
+
public minHeight = 0;
|
|
147
|
+
public flexDirection = "column";
|
|
148
|
+
public alignItems = "stretch";
|
|
149
|
+
public justifyContent = "flex-start";
|
|
150
|
+
public paddingTop = 0;
|
|
151
|
+
constructor(renderer: any, options: any) {
|
|
152
|
+
super(renderer, options);
|
|
153
|
+
this.height = options?.height || 0;
|
|
154
|
+
this.minHeight = options?.minHeight || 0;
|
|
155
|
+
if (options?.backgroundColor) {
|
|
156
|
+
this.backgroundColor = typeof options.backgroundColor === 'string'
|
|
157
|
+
? { r: 0, g: 0, b: 0, a: 1, toString: () => options.backgroundColor }
|
|
158
|
+
: options.backgroundColor;
|
|
159
|
+
}
|
|
160
|
+
if (options?.borderColor) {
|
|
161
|
+
this.borderColor = typeof options.borderColor === 'string'
|
|
162
|
+
? { r: 0, g: 0, b: 0, a: 1, toString: () => options.borderColor }
|
|
163
|
+
: options.borderColor;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
ScrollBoxRenderable: class extends MockRenderable {
|
|
168
|
+
public stopAutoScroll = mock(() => {});
|
|
169
|
+
},
|
|
170
|
+
TextRenderable: class extends MockRenderable {
|
|
171
|
+
public content = "";
|
|
172
|
+
public fg = "";
|
|
173
|
+
constructor(renderer: any, options: any) {
|
|
174
|
+
super(renderer, options);
|
|
175
|
+
this.content = options?.content || "";
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
SelectRenderable: class extends MockRenderable {
|
|
179
|
+
public options: any[] = [];
|
|
180
|
+
constructor(renderer: any, options: any) {
|
|
181
|
+
super(renderer, options);
|
|
182
|
+
this.options = options?.options || [];
|
|
183
|
+
}
|
|
184
|
+
public setSelectedIndex = mock(() => {});
|
|
185
|
+
public moveUp = mock(() => {});
|
|
186
|
+
public moveDown = mock(() => {});
|
|
187
|
+
public selectCurrent = mock(() => {});
|
|
188
|
+
},
|
|
189
|
+
TabSelectRenderable: class extends MockRenderable {
|
|
190
|
+
constructor(renderer: any, options: any) {
|
|
191
|
+
super(renderer, options);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
ASCIIFontRenderable: class extends MockRenderable {},
|
|
195
|
+
DiffRenderable: class extends MockRenderable {},
|
|
196
|
+
FrameBufferRenderable: class extends MockRenderable {},
|
|
197
|
+
MarkdownRenderable: class extends MockRenderable {},
|
|
198
|
+
fg: () => (text: string) => text,
|
|
199
|
+
RGBA: {
|
|
200
|
+
fromValues: mock((r: number, g: number, b: number, a: number) => ({ r, g, b, a, toString: () => `rgba(${r},${g},${b},${a})` })),
|
|
201
|
+
fromHex: mock((hex: string) => {
|
|
202
|
+
if (typeof hex !== 'string') return { r: 0, g: 0, b: 0, a: 1, toString: () => String(hex) };
|
|
203
|
+
const r = parseInt(hex.slice(1, 3), 16) || 0;
|
|
204
|
+
const g = parseInt(hex.slice(3, 5), 16) || 0;
|
|
205
|
+
const b = parseInt(hex.slice(5, 7), 16) || 0;
|
|
206
|
+
return { r, g, b, a: 1, toString: () => hex };
|
|
207
|
+
}),
|
|
208
|
+
fromInts: mock((r: number, g: number, b: number, a: number) => ({ r, g, b, a, toString: () => `rgba(${r},${g},${b},${a})` })),
|
|
209
|
+
},
|
|
210
|
+
parseColor: mock((c: string) => {
|
|
211
|
+
if (typeof c === 'string' && c.startsWith("#")) {
|
|
212
|
+
const r = parseInt(c.slice(1, 3), 16) || 0;
|
|
213
|
+
const g = parseInt(c.slice(3, 5), 16) || 0;
|
|
214
|
+
const b = parseInt(c.slice(5, 7), 16) || 0;
|
|
215
|
+
return { r, g, b, a: 1, toString: () => c };
|
|
216
|
+
}
|
|
217
|
+
return { r: 0, g: 0, b: 0, a: 1, toString: () => String(c) };
|
|
218
|
+
}),
|
|
219
|
+
parseColorHex: mock((hex: string) => {
|
|
220
|
+
if (typeof hex !== 'string') return { r: 0, g: 0, b: 0, a: 1, toString: () => String(hex) };
|
|
221
|
+
const r = parseInt(hex.slice(1, 3), 16) || 0;
|
|
222
|
+
const g = parseInt(hex.slice(3, 5), 16) || 0;
|
|
223
|
+
const b = parseInt(hex.slice(5, 7), 16) || 0;
|
|
224
|
+
return { r, g, b, a: 1, toString: () => hex };
|
|
225
|
+
}),
|
|
226
|
+
rgbToHex: mock((rgbaOrR: any, gArg?: number, bArg?: number) => {
|
|
227
|
+
// Handle both RGBA object and individual numbers
|
|
228
|
+
const r = typeof rgbaOrR === 'object' ? rgbaOrR.r : rgbaOrR;
|
|
229
|
+
const g = typeof rgbaOrR === 'object' ? rgbaOrR.g : gArg ?? 0;
|
|
230
|
+
const b = typeof rgbaOrR === 'object' ? rgbaOrR.b : bArg ?? 0;
|
|
231
|
+
const toHex = (n: number) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, '0');
|
|
232
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
233
|
+
}),
|
|
234
|
+
createTimeline: mock(() => {
|
|
235
|
+
const tl: any = {
|
|
236
|
+
_actions: [] as Function[],
|
|
237
|
+
add: function(target: any, options: any) {
|
|
238
|
+
const { onUpdate, onComplete, duration, ease, ...props } = options || {};
|
|
239
|
+
const anim = { progress: 1 };
|
|
240
|
+
tl._actions.push(() => {
|
|
241
|
+
if (target) Object.assign(target, props);
|
|
242
|
+
if (onUpdate) onUpdate(anim);
|
|
243
|
+
if (onComplete) onComplete(anim);
|
|
244
|
+
});
|
|
245
|
+
queueMicrotask(() => {
|
|
246
|
+
while (tl._actions.length > 0) {
|
|
247
|
+
const action = tl._actions.shift();
|
|
248
|
+
if (action) action();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
return tl;
|
|
252
|
+
},
|
|
253
|
+
play: function() {
|
|
254
|
+
while (tl._actions.length > 0) {
|
|
255
|
+
const action = tl._actions.shift();
|
|
256
|
+
if (action) action();
|
|
257
|
+
}
|
|
258
|
+
return tl;
|
|
259
|
+
},
|
|
260
|
+
pause: function() { return tl; },
|
|
261
|
+
};
|
|
262
|
+
return tl;
|
|
263
|
+
}),
|
|
264
|
+
StyledText: class {
|
|
265
|
+
constructor(public chunks: any[]) {}
|
|
266
|
+
public toString() {
|
|
267
|
+
return this.chunks.map(c => typeof c === 'string' ? c : (c.text || '')).join('');
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
TextAttributes: { BOLD: 1 },
|
|
271
|
+
Timeline: class {},
|
|
272
|
+
SelectRenderableEvents: { CHANGE: "change", SELECTION_CHANGED: "selection_changed", ITEM_SELECTED: "item_selected" },
|
|
273
|
+
TabSelectRenderableEvents: { CHANGE: "change" },
|
|
274
|
+
InputRenderableEvents: {
|
|
275
|
+
ENTER: "enter",
|
|
276
|
+
INPUT: "input",
|
|
277
|
+
SUBMIT: "submit",
|
|
278
|
+
},
|
|
279
|
+
RenderableEvents: {
|
|
280
|
+
FOCUS: "focus",
|
|
281
|
+
BLUR: "blur",
|
|
282
|
+
},
|
|
283
|
+
SyntaxStyle: {
|
|
284
|
+
create: mock(() => ({ registerStyle: mock(() => 1), destroy: mock(() => {}) })),
|
|
285
|
+
fromStyles: mock(() => ({ registerStyle: mock(() => 1), destroy: mock(() => {}) })),
|
|
286
|
+
},
|
|
287
|
+
MouseParser: class {
|
|
288
|
+
parseMouseEvent = mock(() => null);
|
|
289
|
+
},
|
|
290
|
+
engine: {
|
|
291
|
+
attach: mock(() => {}),
|
|
292
|
+
},
|
|
293
|
+
MouseParserEvents: {
|
|
294
|
+
MOUSE_DOWN: "mousedown",
|
|
295
|
+
MOUSE_UP: "mouseup",
|
|
296
|
+
MOUSE_MOVE: "mousemove",
|
|
297
|
+
MOUSE_WHEEL: "mousewheel",
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import { THEME } from "./theme.js";
|
|
3
|
+
|
|
4
|
+
describe("UI Theme", () => {
|
|
5
|
+
test("THEME should contain all required palette keys", () => {
|
|
6
|
+
const requiredKeys = [
|
|
7
|
+
"bg", "surface", "footer", "text", "dim", "accent",
|
|
8
|
+
"darkAccent", "blue", "white", "green", "error",
|
|
9
|
+
"warning", "orange"
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
for (const [key, value] of Object.entries(THEME)) {
|
|
13
|
+
expect(requiredKeys).toContain(key);
|
|
14
|
+
expect(typeof value).toBe("string");
|
|
15
|
+
expect(value).toMatch(/^#[0-9a-fA-F]{6}$/);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("THEME should have specific high-fidelity colors", () => {
|
|
20
|
+
expect(THEME.bg).toBe("#050f05");
|
|
21
|
+
expect(THEME.accent).toBe("#76ff03");
|
|
22
|
+
});
|
|
23
|
+
});
|
package/src/ui/theme.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// High-Fidelity "Pickle Sleek" Palette
|
|
2
|
+
export const THEME = {
|
|
3
|
+
bg: "#050f05",
|
|
4
|
+
surface: "#0a140a",
|
|
5
|
+
footer: "#2e7d32",
|
|
6
|
+
text: "#ccff90",
|
|
7
|
+
dim: "#4e6e50",
|
|
8
|
+
accent: "#76ff03",
|
|
9
|
+
darkAccent: "#212d21",
|
|
10
|
+
blue: "#2196f3",
|
|
11
|
+
white: "#ffffff",
|
|
12
|
+
green: "#4caf50",
|
|
13
|
+
error: "#ff5252",
|
|
14
|
+
warning: "#76ff03",
|
|
15
|
+
orange: "#ffab40",
|
|
16
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { expect, test, describe, beforeEach, mock } from "bun:test";
|
|
2
|
+
import "../test-setup.js";
|
|
3
|
+
import { createMockRenderer } from "../mock-factory.js";
|
|
4
|
+
import { createLandingView } from "./LandingView.js";
|
|
5
|
+
|
|
6
|
+
describe("LandingView Integration", () => {
|
|
7
|
+
let mockRenderer: any;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockRenderer = createMockRenderer();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("should create landing view", async () => {
|
|
14
|
+
const onEnter = mock(() => {});
|
|
15
|
+
const result = await createLandingView(mockRenderer as any, onEnter);
|
|
16
|
+
|
|
17
|
+
expect(result).toBeDefined();
|
|
18
|
+
expect(result.root).toBeDefined();
|
|
19
|
+
expect(result.input).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { expect, test, describe, mock, beforeEach } from "bun:test";
|
|
2
|
+
import "../test-setup.js";
|
|
3
|
+
import { createMockRenderer } from "../mock-factory.ts";
|
|
4
|
+
|
|
5
|
+
mock.module("../file-picker-utils.js", () => ({
|
|
6
|
+
setupFilePicker: mock(() => () => {}),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { createLandingView } from "./LandingView.js";
|
|
10
|
+
|
|
11
|
+
describe("LandingView", () => {
|
|
12
|
+
let mockRenderer: any;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockRenderer = createMockRenderer();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("should create landing view", async () => {
|
|
19
|
+
const onEnter = mock(() => {});
|
|
20
|
+
const view = await createLandingView(mockRenderer, onEnter);
|
|
21
|
+
expect(view.root).toBeDefined();
|
|
22
|
+
expect(view.input).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CliRenderer,
|
|
3
|
+
BoxRenderable,
|
|
4
|
+
TextRenderable,
|
|
5
|
+
KeyEvent,
|
|
6
|
+
} from "@opentui/core";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { HEADER_LINES, getLineColor } from "../common.js";
|
|
10
|
+
import { THEME } from "../theme.js";
|
|
11
|
+
import { setupFilePicker, FilePickerState } from "../file-picker-utils.js";
|
|
12
|
+
import { getCurrentBranch } from "../../services/git/branch.js";
|
|
13
|
+
import { isGameboyActive } from "../../games/gameboy/GameboyView.js";
|
|
14
|
+
import { MultiLineInputRenderable, MultiLineInputEvents } from "../components/MultiLineInput.js";
|
|
15
|
+
import { buildVerticalBar, createInputContainerMouseHandler, createProviderMetadataRow, createCtrlCExitHandler } from "../input-chrome.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates the landing view for the Pickle Rick CLI.
|
|
19
|
+
* Returns the root container and the input component for further event handling.
|
|
20
|
+
*/
|
|
21
|
+
export async function createLandingView(
|
|
22
|
+
renderer: CliRenderer,
|
|
23
|
+
onEnter: (prompt: string, mode: "pickle" | "pickle-prd") => void
|
|
24
|
+
) {
|
|
25
|
+
const INPUT_CHROME_LINES = 4;
|
|
26
|
+
let mode: "pickle" | "pickle-prd" = "pickle";
|
|
27
|
+
|
|
28
|
+
// Fetch Metadata
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
let version = "?.?.?";
|
|
31
|
+
try {
|
|
32
|
+
const packageJsonPath = path.resolve(process.cwd(), "package.json");
|
|
33
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
34
|
+
const content = fs.readFileSync(packageJsonPath, "utf-8");
|
|
35
|
+
const pkg = JSON.parse(content);
|
|
36
|
+
version = pkg.version || "?.?.?";
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// Ignore
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const root = new BoxRenderable(renderer, {
|
|
43
|
+
id: "landing-root",
|
|
44
|
+
width: "100%",
|
|
45
|
+
height: "100%",
|
|
46
|
+
flexDirection: "column",
|
|
47
|
+
justifyContent: "center",
|
|
48
|
+
alignItems: "center",
|
|
49
|
+
backgroundColor: THEME.bg,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const headerContainer = new BoxRenderable(renderer, {
|
|
53
|
+
id: "landing-header",
|
|
54
|
+
flexDirection: "column",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
marginBottom: 2,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
HEADER_LINES.forEach((line, i) => {
|
|
60
|
+
const color = getLineColor(i);
|
|
61
|
+
headerContainer.add(
|
|
62
|
+
new TextRenderable(renderer, {
|
|
63
|
+
id: `landing-header-line-${i}`,
|
|
64
|
+
content: line.trimEnd(),
|
|
65
|
+
fg: color,
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const inputContainer = new BoxRenderable(renderer, {
|
|
71
|
+
id: "landing-input-container",
|
|
72
|
+
width: 80,
|
|
73
|
+
minHeight: 5,
|
|
74
|
+
flexDirection: "column",
|
|
75
|
+
backgroundColor: THEME.surface,
|
|
76
|
+
paddingLeft: 1,
|
|
77
|
+
paddingRight: 1,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const input = new MultiLineInputRenderable(renderer, {
|
|
81
|
+
id: "landing-input",
|
|
82
|
+
flexGrow: 1,
|
|
83
|
+
placeholder: "I turned myself into a TUI, Morty! *Belch* Ask me anything...",
|
|
84
|
+
textColor: THEME.text,
|
|
85
|
+
focusedTextColor: THEME.text,
|
|
86
|
+
minHeight: 1,
|
|
87
|
+
maxHeight: 10,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const inputRow = new BoxRenderable(renderer, {
|
|
91
|
+
id: "landing-input-row",
|
|
92
|
+
width: "100%",
|
|
93
|
+
flexDirection: "row",
|
|
94
|
+
alignItems: "center",
|
|
95
|
+
});
|
|
96
|
+
inputRow.add(input);
|
|
97
|
+
|
|
98
|
+
const { row: metadataRow } = createProviderMetadataRow(renderer, "landing");
|
|
99
|
+
|
|
100
|
+
inputContainer.add(new BoxRenderable(renderer, { id: "landing-spacer1", height: 1 }));
|
|
101
|
+
inputContainer.add(inputRow);
|
|
102
|
+
inputContainer.add(new BoxRenderable(renderer, { id: "landing-spacer2", height: 1 }));
|
|
103
|
+
inputContainer.add(metadataRow);
|
|
104
|
+
inputContainer.add(new BoxRenderable(renderer, { id: "landing-spacer3", height: 1 }));
|
|
105
|
+
|
|
106
|
+
// Bottom footer bar with cwd and version
|
|
107
|
+
const footerBar = new BoxRenderable(renderer, {
|
|
108
|
+
id: "landing-footer-bar",
|
|
109
|
+
width: "100%",
|
|
110
|
+
height: 1,
|
|
111
|
+
flexDirection: "row",
|
|
112
|
+
justifyContent: "space-between",
|
|
113
|
+
position: "absolute",
|
|
114
|
+
bottom: 0,
|
|
115
|
+
left: 0,
|
|
116
|
+
paddingLeft: 1,
|
|
117
|
+
paddingRight: 1,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const footerCwd = new TextRenderable(renderer, {
|
|
121
|
+
id: "landing-footer-cwd",
|
|
122
|
+
content: cwd,
|
|
123
|
+
fg: THEME.dim,
|
|
124
|
+
});
|
|
125
|
+
footerBar.add(footerCwd);
|
|
126
|
+
|
|
127
|
+
const footerVersion = new TextRenderable(renderer, {
|
|
128
|
+
id: "landing-footer-version",
|
|
129
|
+
content: version,
|
|
130
|
+
fg: THEME.dim,
|
|
131
|
+
});
|
|
132
|
+
footerBar.add(footerVersion);
|
|
133
|
+
|
|
134
|
+
// Fetch branch asynchronously
|
|
135
|
+
getCurrentBranch().then((branch) => {
|
|
136
|
+
if (branch) {
|
|
137
|
+
footerCwd.content = `${cwd}:${branch}`;
|
|
138
|
+
renderer.requestRender();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const inputDecorativeBar = new TextRenderable(renderer, {
|
|
143
|
+
id: "landing-decorative-bar",
|
|
144
|
+
content: buildVerticalBar(inputContainer.minHeight ?? 5),
|
|
145
|
+
fg: THEME.accent,
|
|
146
|
+
position: "absolute",
|
|
147
|
+
left: 0,
|
|
148
|
+
top: 0,
|
|
149
|
+
});
|
|
150
|
+
inputContainer.add(inputDecorativeBar);
|
|
151
|
+
|
|
152
|
+
const footerHints = new BoxRenderable(renderer, {
|
|
153
|
+
id: "landing-footer-hints",
|
|
154
|
+
width: 80,
|
|
155
|
+
flexDirection: "row",
|
|
156
|
+
justifyContent: "flex-end",
|
|
157
|
+
marginTop: 1,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const hintsText = new TextRenderable(renderer, {
|
|
161
|
+
id: "landing-hints-text",
|
|
162
|
+
content: "ctrl+c exit",
|
|
163
|
+
fg: THEME.dim,
|
|
164
|
+
});
|
|
165
|
+
footerHints.add(hintsText);
|
|
166
|
+
|
|
167
|
+
inputContainer.onMouse = createInputContainerMouseHandler(inputContainer, input);
|
|
168
|
+
|
|
169
|
+
const pickerState: FilePickerState = { activePicker: null };
|
|
170
|
+
|
|
171
|
+
input.on(MultiLineInputEvents.INPUT, (value: string) => {
|
|
172
|
+
const minHeight = typeof inputContainer.minHeight === "number" ? inputContainer.minHeight : 5;
|
|
173
|
+
const inputHeight = typeof input.height === "number" ? input.height : 1;
|
|
174
|
+
const nextHeight = Math.max(minHeight, inputHeight + INPUT_CHROME_LINES);
|
|
175
|
+
if (inputContainer.height !== nextHeight) {
|
|
176
|
+
inputContainer.height = nextHeight;
|
|
177
|
+
inputDecorativeBar.content = buildVerticalBar(nextHeight);
|
|
178
|
+
renderer.requestRender();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
input.on(MultiLineInputEvents.SUBMIT, (value: string) => {
|
|
183
|
+
if (pickerState.activePicker || pickerState.justClosed || isGameboyActive()) return;
|
|
184
|
+
onEnter(value, mode);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const onKey = createCtrlCExitHandler({
|
|
188
|
+
renderer,
|
|
189
|
+
hintText: hintsText,
|
|
190
|
+
originalContent: "ctrl+c exit",
|
|
191
|
+
shouldSkip: () => !root.visible || !!pickerState.activePicker || isGameboyActive(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
renderer.keyInput.on("keypress", (key: KeyEvent) => {
|
|
195
|
+
if (key.name === "return" && key.shift) {
|
|
196
|
+
input.value = input.value + "\n";
|
|
197
|
+
renderer.requestRender();
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
renderer.keyInput.on("keypress", onKey);
|
|
204
|
+
|
|
205
|
+
setupFilePicker(renderer, input, inputContainer, pickerState, {
|
|
206
|
+
bottom: () => {
|
|
207
|
+
const height = inputContainer.height;
|
|
208
|
+
return typeof height === "number" ? height : 5;
|
|
209
|
+
},
|
|
210
|
+
width: "100%",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
root.add(headerContainer);
|
|
214
|
+
root.add(inputContainer);
|
|
215
|
+
root.add(footerHints);
|
|
216
|
+
root.add(footerBar);
|
|
217
|
+
|
|
218
|
+
input.focus();
|
|
219
|
+
|
|
220
|
+
return { root, input };
|
|
221
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { expect, test, describe, mock, beforeEach } from "bun:test";
|
|
2
|
+
import "../test-setup.js";
|
|
3
|
+
import { createMockRenderer } from "../mock-factory.ts";
|
|
4
|
+
|
|
5
|
+
mock.module("node:fs/promises", () => ({
|
|
6
|
+
readFile: mock(async () => "line1\nline2\nline3"),
|
|
7
|
+
stat: mock(async () => ({ size: 100 })),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { LogView } from "./LogView.js";
|
|
11
|
+
|
|
12
|
+
describe("LogView", () => {
|
|
13
|
+
let mockRenderer: any;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mockRenderer = createMockRenderer();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("should initialize", () => {
|
|
20
|
+
const view = new LogView(mockRenderer, "test.log");
|
|
21
|
+
expect(view).toBeDefined();
|
|
22
|
+
expect(view.root).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
});
|