@gtkx/cli 0.3.5 → 0.4.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/dist/create.d.ts +16 -0
- package/dist/create.js +700 -0
- package/dist/dev-server.d.ts +5 -2
- package/package.json +3 -3
package/dist/create.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
+
/** Supported package managers for project scaffolding. */
|
|
1
2
|
export type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
|
|
3
|
+
/** Supported testing frameworks for project scaffolding. */
|
|
2
4
|
export type TestingFramework = "vitest" | "jest" | "node" | "none";
|
|
5
|
+
/**
|
|
6
|
+
* Options for creating a new GTKX application.
|
|
7
|
+
*/
|
|
3
8
|
export interface CreateOptions {
|
|
9
|
+
/** Project name (lowercase letters, numbers, and hyphens only). */
|
|
4
10
|
name?: string;
|
|
11
|
+
/** Application ID in reverse domain notation (e.g., com.example.myapp). */
|
|
5
12
|
appId?: string;
|
|
13
|
+
/** Package manager to use for installing dependencies. */
|
|
6
14
|
packageManager?: PackageManager;
|
|
15
|
+
/** Testing framework to set up. */
|
|
7
16
|
testing?: TestingFramework;
|
|
17
|
+
/** Whether to include Claude Code skills for AI assistance. */
|
|
18
|
+
claudeSkills?: boolean;
|
|
8
19
|
}
|
|
9
20
|
export declare const getTestScript: (testing: TestingFramework) => string | undefined;
|
|
10
21
|
export declare const generatePackageJson: (name: string, appId: string, testing: TestingFramework) => string;
|
|
@@ -13,4 +24,9 @@ export declare const getAddCommand: (pm: PackageManager, deps: string[], dev: bo
|
|
|
13
24
|
export declare const getRunCommand: (pm: PackageManager) => string;
|
|
14
25
|
export declare const isValidProjectName: (name: string) => boolean;
|
|
15
26
|
export declare const isValidAppId: (appId: string) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Creates a new GTKX application with interactive prompts.
|
|
29
|
+
* Scaffolds project structure, installs dependencies, and sets up configuration.
|
|
30
|
+
* @param options - Pre-filled options to skip interactive prompts
|
|
31
|
+
*/
|
|
16
32
|
export declare const createApp: (options?: CreateOptions) => Promise<void>;
|
package/dist/create.js
CHANGED
|
@@ -97,6 +97,685 @@ dist/
|
|
|
97
97
|
.DS_Store
|
|
98
98
|
`;
|
|
99
99
|
};
|
|
100
|
+
const generateSkillMd = () => {
|
|
101
|
+
return `---
|
|
102
|
+
name: developing-gtkx-apps
|
|
103
|
+
description: Build GTK4 desktop applications with GTKX React framework. Use when creating GTKX components, working with GTK widgets, handling signals, or building Linux desktop UIs with React.
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
# Developing GTKX Applications
|
|
107
|
+
|
|
108
|
+
GTKX is a React framework for building native GTK4 desktop applications on Linux. It uses a custom React reconciler to render React components as native GTK widgets.
|
|
109
|
+
|
|
110
|
+
## Quick Start
|
|
111
|
+
|
|
112
|
+
\`\`\`tsx
|
|
113
|
+
import { ApplicationWindow, render, quit } from "@gtkx/react";
|
|
114
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
115
|
+
|
|
116
|
+
const App = () => (
|
|
117
|
+
<ApplicationWindow title="My App" defaultWidth={800} defaultHeight={600}>
|
|
118
|
+
<Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
119
|
+
<Label.Root label="Hello, GTKX!" />
|
|
120
|
+
<Button label="Quit" onClicked={quit} />
|
|
121
|
+
</Box>
|
|
122
|
+
</ApplicationWindow>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
render(<App />, "com.example.myapp");
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
## Widget Patterns
|
|
129
|
+
|
|
130
|
+
### Container Widgets
|
|
131
|
+
|
|
132
|
+
**Box** - Linear layout:
|
|
133
|
+
\`\`\`tsx
|
|
134
|
+
<Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
135
|
+
<Label.Root label="First" />
|
|
136
|
+
<Label.Root label="Second" />
|
|
137
|
+
</Box>
|
|
138
|
+
\`\`\`
|
|
139
|
+
|
|
140
|
+
**Grid** - 2D positioning:
|
|
141
|
+
\`\`\`tsx
|
|
142
|
+
<Grid.Root spacing={10}>
|
|
143
|
+
<Grid.Child column={0} row={0}>
|
|
144
|
+
<Label.Root label="Top-left" />
|
|
145
|
+
</Grid.Child>
|
|
146
|
+
<Grid.Child column={1} row={0} columnSpan={2}>
|
|
147
|
+
<Label.Root label="Spans 2 columns" />
|
|
148
|
+
</Grid.Child>
|
|
149
|
+
</Grid.Root>
|
|
150
|
+
\`\`\`
|
|
151
|
+
|
|
152
|
+
**Stack** - Page-based container:
|
|
153
|
+
\`\`\`tsx
|
|
154
|
+
<Stack.Root visibleChildName="page1">
|
|
155
|
+
<Stack.Page name="page1" title="Page 1">
|
|
156
|
+
<Label.Root label="Content 1" />
|
|
157
|
+
</Stack.Page>
|
|
158
|
+
<Stack.Page name="page2" title="Page 2">
|
|
159
|
+
<Label.Root label="Content 2" />
|
|
160
|
+
</Stack.Page>
|
|
161
|
+
</Stack.Root>
|
|
162
|
+
\`\`\`
|
|
163
|
+
|
|
164
|
+
**Notebook** - Tabbed container:
|
|
165
|
+
\`\`\`tsx
|
|
166
|
+
<Notebook.Root>
|
|
167
|
+
<Notebook.Page label="Tab 1">
|
|
168
|
+
<Content1 />
|
|
169
|
+
</Notebook.Page>
|
|
170
|
+
<Notebook.Page label="Tab 2">
|
|
171
|
+
<Content2 />
|
|
172
|
+
</Notebook.Page>
|
|
173
|
+
</Notebook.Root>
|
|
174
|
+
\`\`\`
|
|
175
|
+
|
|
176
|
+
**Paned** - Resizable split:
|
|
177
|
+
\`\`\`tsx
|
|
178
|
+
<Paned.Root orientation={Gtk.Orientation.HORIZONTAL} position={280}>
|
|
179
|
+
<Paned.StartChild>
|
|
180
|
+
<SideBar />
|
|
181
|
+
</Paned.StartChild>
|
|
182
|
+
<Paned.EndChild>
|
|
183
|
+
<MainContent />
|
|
184
|
+
</Paned.EndChild>
|
|
185
|
+
</Paned.Root>
|
|
186
|
+
\`\`\`
|
|
187
|
+
|
|
188
|
+
### Virtual Scrolling Lists
|
|
189
|
+
|
|
190
|
+
**ListView** - High-performance scrollable list:
|
|
191
|
+
\`\`\`tsx
|
|
192
|
+
<ListView.Root
|
|
193
|
+
vexpand
|
|
194
|
+
renderItem={(item: Item | null) => (
|
|
195
|
+
<Label.Root label={item?.text ?? ""} />
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{items.map(item => (
|
|
199
|
+
<ListView.Item key={item.id} item={item} />
|
|
200
|
+
))}
|
|
201
|
+
</ListView.Root>
|
|
202
|
+
\`\`\`
|
|
203
|
+
|
|
204
|
+
**ColumnView** - Table with columns:
|
|
205
|
+
\`\`\`tsx
|
|
206
|
+
<ColumnView.Root
|
|
207
|
+
sortColumn="name"
|
|
208
|
+
sortOrder={Gtk.SortType.ASCENDING}
|
|
209
|
+
onSortChange={handleSort}
|
|
210
|
+
>
|
|
211
|
+
<ColumnView.Column
|
|
212
|
+
title="Name"
|
|
213
|
+
id="name"
|
|
214
|
+
expand
|
|
215
|
+
renderCell={(item: Item | null) => (
|
|
216
|
+
<Label.Root label={item?.name ?? ""} />
|
|
217
|
+
)}
|
|
218
|
+
/>
|
|
219
|
+
{items.map(item => (
|
|
220
|
+
<ColumnView.Item key={item.id} item={item} />
|
|
221
|
+
))}
|
|
222
|
+
</ColumnView.Root>
|
|
223
|
+
\`\`\`
|
|
224
|
+
|
|
225
|
+
**DropDown** - Selection widget:
|
|
226
|
+
\`\`\`tsx
|
|
227
|
+
<DropDown.Root
|
|
228
|
+
itemLabel={(opt: Option) => opt.label}
|
|
229
|
+
onSelectionChanged={(item, index) => setSelected(item.value)}
|
|
230
|
+
>
|
|
231
|
+
{options.map(opt => (
|
|
232
|
+
<DropDown.Item key={opt.value} item={opt} />
|
|
233
|
+
))}
|
|
234
|
+
</DropDown.Root>
|
|
235
|
+
\`\`\`
|
|
236
|
+
|
|
237
|
+
### Controlled Input
|
|
238
|
+
|
|
239
|
+
Entry requires two-way binding:
|
|
240
|
+
\`\`\`tsx
|
|
241
|
+
const [text, setText] = useState("");
|
|
242
|
+
|
|
243
|
+
<Entry
|
|
244
|
+
text={text}
|
|
245
|
+
onChanged={(entry) => setText(entry.getText())}
|
|
246
|
+
placeholder="Type here..."
|
|
247
|
+
/>
|
|
248
|
+
\`\`\`
|
|
249
|
+
|
|
250
|
+
### Declarative Menus
|
|
251
|
+
|
|
252
|
+
\`\`\`tsx
|
|
253
|
+
<ApplicationMenu>
|
|
254
|
+
<Menu.Submenu label="File">
|
|
255
|
+
<Menu.Item
|
|
256
|
+
label="New"
|
|
257
|
+
onActivate={handleNew}
|
|
258
|
+
accels="<Control>n"
|
|
259
|
+
/>
|
|
260
|
+
<Menu.Section>
|
|
261
|
+
<Menu.Item label="Quit" onActivate={quit} accels="<Control>q" />
|
|
262
|
+
</Menu.Section>
|
|
263
|
+
</Menu.Submenu>
|
|
264
|
+
</ApplicationMenu>
|
|
265
|
+
\`\`\`
|
|
266
|
+
|
|
267
|
+
## Signal Handling
|
|
268
|
+
|
|
269
|
+
GTK signals map to \`on<SignalName>\` props:
|
|
270
|
+
- \`clicked\` → \`onClicked\`
|
|
271
|
+
- \`toggled\` → \`onToggled\`
|
|
272
|
+
- \`changed\` → \`onChanged\`
|
|
273
|
+
- \`notify::selected\` → \`onNotifySelected\`
|
|
274
|
+
|
|
275
|
+
## Widget References
|
|
276
|
+
|
|
277
|
+
\`\`\`tsx
|
|
278
|
+
import { createRef } from "@gtkx/ffi";
|
|
279
|
+
|
|
280
|
+
const entryRef = createRef<Gtk.Entry>();
|
|
281
|
+
<Entry ref={entryRef} />
|
|
282
|
+
// Later: entryRef.current?.getText()
|
|
283
|
+
\`\`\`
|
|
284
|
+
|
|
285
|
+
## Portals
|
|
286
|
+
|
|
287
|
+
\`\`\`tsx
|
|
288
|
+
import { createPortal } from "@gtkx/react";
|
|
289
|
+
|
|
290
|
+
{createPortal(<AboutDialog programName="My App" />)}
|
|
291
|
+
\`\`\`
|
|
292
|
+
|
|
293
|
+
## Constraints
|
|
294
|
+
|
|
295
|
+
- **GTK is single-threaded**: All widget operations on main thread
|
|
296
|
+
- **Virtual lists need immutable data**: Use stable object references
|
|
297
|
+
- **ToggleButton auto-prevents feedback loops**: Safe for controlled state
|
|
298
|
+
- **Entry needs two-way binding**: Use \`onChanged\` to sync state
|
|
299
|
+
|
|
300
|
+
For detailed widget reference, see [WIDGETS.md](WIDGETS.md).
|
|
301
|
+
For code examples, see [EXAMPLES.md](EXAMPLES.md).
|
|
302
|
+
`;
|
|
303
|
+
};
|
|
304
|
+
const generateWidgetsMd = () => {
|
|
305
|
+
return `# GTKX Widget Reference
|
|
306
|
+
|
|
307
|
+
## Container Widgets
|
|
308
|
+
|
|
309
|
+
### Box
|
|
310
|
+
Linear layout container.
|
|
311
|
+
|
|
312
|
+
\`\`\`tsx
|
|
313
|
+
<Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
314
|
+
<Label.Root label="Child 1" />
|
|
315
|
+
<Label.Root label="Child 2" />
|
|
316
|
+
</Box>
|
|
317
|
+
\`\`\`
|
|
318
|
+
|
|
319
|
+
Props:
|
|
320
|
+
- \`orientation\`: \`Gtk.Orientation.HORIZONTAL\` | \`Gtk.Orientation.VERTICAL\`
|
|
321
|
+
- \`spacing\`: number (pixels between children)
|
|
322
|
+
- \`homogeneous\`: boolean (equal child sizes)
|
|
323
|
+
|
|
324
|
+
### Grid
|
|
325
|
+
2D grid layout with explicit positioning.
|
|
326
|
+
|
|
327
|
+
\`\`\`tsx
|
|
328
|
+
<Grid.Root spacing={10} rowSpacing={5} columnSpacing={5}>
|
|
329
|
+
<Grid.Child column={0} row={0}>
|
|
330
|
+
<Label.Root label="Top-left" />
|
|
331
|
+
</Grid.Child>
|
|
332
|
+
<Grid.Child column={1} row={0} columnSpan={2}>
|
|
333
|
+
<Label.Root label="Spans 2 columns" />
|
|
334
|
+
</Grid.Child>
|
|
335
|
+
</Grid.Root>
|
|
336
|
+
\`\`\`
|
|
337
|
+
|
|
338
|
+
Grid.Child props (consumed, not passed to GTK):
|
|
339
|
+
- \`column\`: number (0-indexed)
|
|
340
|
+
- \`row\`: number (0-indexed)
|
|
341
|
+
- \`columnSpan\`: number (default 1)
|
|
342
|
+
- \`rowSpan\`: number (default 1)
|
|
343
|
+
|
|
344
|
+
### Stack
|
|
345
|
+
Shows one child at a time, switchable by name.
|
|
346
|
+
|
|
347
|
+
\`\`\`tsx
|
|
348
|
+
<Stack.Root visibleChildName="page1">
|
|
349
|
+
<Stack.Page name="page1" title="First" iconName="document-new">
|
|
350
|
+
<Content1 />
|
|
351
|
+
</Stack.Page>
|
|
352
|
+
<Stack.Page name="page2" title="Second">
|
|
353
|
+
<Content2 />
|
|
354
|
+
</Stack.Page>
|
|
355
|
+
</Stack.Root>
|
|
356
|
+
\`\`\`
|
|
357
|
+
|
|
358
|
+
Stack.Page props (consumed):
|
|
359
|
+
- \`name\`: string (required, unique identifier)
|
|
360
|
+
- \`title\`: string (display title)
|
|
361
|
+
- \`iconName\`: string (icon name)
|
|
362
|
+
|
|
363
|
+
### Notebook
|
|
364
|
+
Tabbed container with visible tabs.
|
|
365
|
+
|
|
366
|
+
\`\`\`tsx
|
|
367
|
+
<Notebook.Root>
|
|
368
|
+
<Notebook.Page label="Tab 1">
|
|
369
|
+
<Content1 />
|
|
370
|
+
</Notebook.Page>
|
|
371
|
+
<Notebook.Page label="Tab 2">
|
|
372
|
+
<Content2 />
|
|
373
|
+
</Notebook.Page>
|
|
374
|
+
</Notebook.Root>
|
|
375
|
+
\`\`\`
|
|
376
|
+
|
|
377
|
+
Notebook.Page props (consumed):
|
|
378
|
+
- \`label\`: string (tab label)
|
|
379
|
+
|
|
380
|
+
### Paned
|
|
381
|
+
Resizable split container with draggable divider.
|
|
382
|
+
|
|
383
|
+
\`\`\`tsx
|
|
384
|
+
<Paned.Root
|
|
385
|
+
orientation={Gtk.Orientation.HORIZONTAL}
|
|
386
|
+
position={280}
|
|
387
|
+
shrinkStartChild={false}
|
|
388
|
+
shrinkEndChild={false}
|
|
389
|
+
>
|
|
390
|
+
<Paned.StartChild>
|
|
391
|
+
<SidePanel />
|
|
392
|
+
</Paned.StartChild>
|
|
393
|
+
<Paned.EndChild>
|
|
394
|
+
<MainContent />
|
|
395
|
+
</Paned.EndChild>
|
|
396
|
+
</Paned.Root>
|
|
397
|
+
\`\`\`
|
|
398
|
+
|
|
399
|
+
Props:
|
|
400
|
+
- \`orientation\`: \`Gtk.Orientation.HORIZONTAL\` | \`Gtk.Orientation.VERTICAL\`
|
|
401
|
+
- \`position\`: number (divider position in pixels)
|
|
402
|
+
- \`shrinkStartChild\`: boolean
|
|
403
|
+
- \`shrinkEndChild\`: boolean
|
|
404
|
+
|
|
405
|
+
## Virtual Scrolling Widgets
|
|
406
|
+
|
|
407
|
+
### ListView
|
|
408
|
+
High-performance scrollable list with virtual rendering.
|
|
409
|
+
|
|
410
|
+
\`\`\`tsx
|
|
411
|
+
<ListView.Root
|
|
412
|
+
vexpand
|
|
413
|
+
renderItem={(item: Item | null) => (
|
|
414
|
+
<Label.Root label={item?.text ?? ""} />
|
|
415
|
+
)}
|
|
416
|
+
>
|
|
417
|
+
{items.map(item => (
|
|
418
|
+
<ListView.Item key={item.id} item={item} />
|
|
419
|
+
))}
|
|
420
|
+
</ListView.Root>
|
|
421
|
+
\`\`\`
|
|
422
|
+
|
|
423
|
+
Props:
|
|
424
|
+
- \`renderItem\`: \`(item: T | null) => ReactElement\` (required)
|
|
425
|
+
- Standard scrollable props
|
|
426
|
+
|
|
427
|
+
### ColumnView
|
|
428
|
+
Table with sortable columns.
|
|
429
|
+
|
|
430
|
+
\`\`\`tsx
|
|
431
|
+
<ColumnView.Root
|
|
432
|
+
sortColumn="name"
|
|
433
|
+
sortOrder={Gtk.SortType.ASCENDING}
|
|
434
|
+
onSortChange={(column, order) => handleSort(column, order)}
|
|
435
|
+
>
|
|
436
|
+
<ColumnView.Column
|
|
437
|
+
title="Name"
|
|
438
|
+
id="name"
|
|
439
|
+
expand
|
|
440
|
+
resizable
|
|
441
|
+
renderCell={(item: Item | null) => (
|
|
442
|
+
<Label.Root label={item?.name ?? ""} />
|
|
443
|
+
)}
|
|
444
|
+
/>
|
|
445
|
+
{items.map(item => (
|
|
446
|
+
<ColumnView.Item key={item.id} item={item} />
|
|
447
|
+
))}
|
|
448
|
+
</ColumnView.Root>
|
|
449
|
+
\`\`\`
|
|
450
|
+
|
|
451
|
+
### DropDown
|
|
452
|
+
Selection dropdown with custom rendering.
|
|
453
|
+
|
|
454
|
+
\`\`\`tsx
|
|
455
|
+
<DropDown.Root
|
|
456
|
+
itemLabel={(opt: Option) => opt.label}
|
|
457
|
+
onSelectionChanged={(item, index) => setSelected(item.value)}
|
|
458
|
+
>
|
|
459
|
+
{options.map(opt => (
|
|
460
|
+
<DropDown.Item key={opt.value} item={opt} />
|
|
461
|
+
))}
|
|
462
|
+
</DropDown.Root>
|
|
463
|
+
\`\`\`
|
|
464
|
+
|
|
465
|
+
## Input Widgets
|
|
466
|
+
|
|
467
|
+
### Entry
|
|
468
|
+
Single-line text input. Requires two-way binding for controlled behavior.
|
|
469
|
+
|
|
470
|
+
\`\`\`tsx
|
|
471
|
+
const [text, setText] = useState("");
|
|
472
|
+
|
|
473
|
+
<Entry
|
|
474
|
+
text={text}
|
|
475
|
+
onChanged={(entry) => setText(entry.getText())}
|
|
476
|
+
placeholder="Enter text..."
|
|
477
|
+
/>
|
|
478
|
+
\`\`\`
|
|
479
|
+
|
|
480
|
+
### ToggleButton
|
|
481
|
+
Toggle button with controlled state. Auto-prevents signal feedback loops.
|
|
482
|
+
|
|
483
|
+
\`\`\`tsx
|
|
484
|
+
const [active, setActive] = useState(false);
|
|
485
|
+
|
|
486
|
+
<ToggleButton.Root
|
|
487
|
+
active={active}
|
|
488
|
+
onToggled={() => setActive(!active)}
|
|
489
|
+
label="Toggle me"
|
|
490
|
+
/>
|
|
491
|
+
\`\`\`
|
|
492
|
+
|
|
493
|
+
## Display Widgets
|
|
494
|
+
|
|
495
|
+
### Label.Root
|
|
496
|
+
\`\`\`tsx
|
|
497
|
+
<Label.Root label="Hello World" halign={Gtk.Align.START} wrap useMarkup />
|
|
498
|
+
\`\`\`
|
|
499
|
+
|
|
500
|
+
### Button
|
|
501
|
+
\`\`\`tsx
|
|
502
|
+
<Button label="Click me" onClicked={() => handleClick()} iconName="document-new" />
|
|
503
|
+
\`\`\`
|
|
504
|
+
|
|
505
|
+
### MenuButton
|
|
506
|
+
\`\`\`tsx
|
|
507
|
+
<MenuButton.Root label="Options" iconName="open-menu">
|
|
508
|
+
<MenuButton.Popover>
|
|
509
|
+
<PopoverMenu.Root>
|
|
510
|
+
<Menu.Item label="Action" onActivate={handle} />
|
|
511
|
+
</PopoverMenu.Root>
|
|
512
|
+
</MenuButton.Popover>
|
|
513
|
+
</MenuButton.Root>
|
|
514
|
+
\`\`\`
|
|
515
|
+
|
|
516
|
+
## Menu Widgets
|
|
517
|
+
|
|
518
|
+
### ApplicationMenu
|
|
519
|
+
\`\`\`tsx
|
|
520
|
+
<ApplicationMenu>
|
|
521
|
+
<Menu.Submenu label="File">
|
|
522
|
+
<Menu.Item label="New" onActivate={handleNew} accels="<Control>n" />
|
|
523
|
+
<Menu.Section>
|
|
524
|
+
<Menu.Item label="Quit" onActivate={quit} accels="<Control>q" />
|
|
525
|
+
</Menu.Section>
|
|
526
|
+
</Menu.Submenu>
|
|
527
|
+
</ApplicationMenu>
|
|
528
|
+
\`\`\`
|
|
529
|
+
|
|
530
|
+
### Menu.Item
|
|
531
|
+
Props:
|
|
532
|
+
- \`label\`: string
|
|
533
|
+
- \`onActivate\`: \`() => void\`
|
|
534
|
+
- \`accels\`: string | string[] (e.g., "<Control>n")
|
|
535
|
+
|
|
536
|
+
### Menu.Section
|
|
537
|
+
Groups menu items with optional label.
|
|
538
|
+
|
|
539
|
+
### Menu.Submenu
|
|
540
|
+
Nested submenu.
|
|
541
|
+
|
|
542
|
+
## Window Widgets
|
|
543
|
+
|
|
544
|
+
### ApplicationWindow
|
|
545
|
+
\`\`\`tsx
|
|
546
|
+
<ApplicationWindow
|
|
547
|
+
title="My App"
|
|
548
|
+
defaultWidth={800}
|
|
549
|
+
defaultHeight={600}
|
|
550
|
+
showMenubar
|
|
551
|
+
onCloseRequest={() => false}
|
|
552
|
+
>
|
|
553
|
+
<MainContent />
|
|
554
|
+
</ApplicationWindow>
|
|
555
|
+
\`\`\`
|
|
556
|
+
|
|
557
|
+
## Common Props
|
|
558
|
+
|
|
559
|
+
All widgets support:
|
|
560
|
+
- \`hexpand\` / \`vexpand\`: boolean (expand to fill space)
|
|
561
|
+
- \`halign\` / \`valign\`: \`Gtk.Align.START\` | \`CENTER\` | \`END\` | \`FILL\`
|
|
562
|
+
- \`marginStart\` / \`marginEnd\` / \`marginTop\` / \`marginBottom\`: number
|
|
563
|
+
- \`sensitive\`: boolean (enabled/disabled)
|
|
564
|
+
- \`visible\`: boolean
|
|
565
|
+
- \`cssClasses\`: string[]
|
|
566
|
+
`;
|
|
567
|
+
};
|
|
568
|
+
const generateExamplesMd = () => {
|
|
569
|
+
return `# GTKX Code Examples
|
|
570
|
+
|
|
571
|
+
## Application Structure
|
|
572
|
+
|
|
573
|
+
### Basic App with State
|
|
574
|
+
|
|
575
|
+
\`\`\`tsx
|
|
576
|
+
import { Orientation } from "@gtkx/ffi/gtk";
|
|
577
|
+
import { ApplicationWindow, Box, Label, quit } from "@gtkx/react";
|
|
578
|
+
import { useCallback, useState } from "react";
|
|
579
|
+
|
|
580
|
+
interface Todo {
|
|
581
|
+
id: number;
|
|
582
|
+
text: string;
|
|
583
|
+
completed: boolean;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let nextId = 1;
|
|
587
|
+
|
|
588
|
+
export const App = () => {
|
|
589
|
+
const [todos, setTodos] = useState<Todo[]>([]);
|
|
590
|
+
|
|
591
|
+
const addTodo = useCallback((text: string) => {
|
|
592
|
+
setTodos((prev) => [...prev, { id: nextId++, text, completed: false }]);
|
|
593
|
+
}, []);
|
|
594
|
+
|
|
595
|
+
const toggleTodo = useCallback((id: number) => {
|
|
596
|
+
setTodos((prev) =>
|
|
597
|
+
prev.map((todo) =>
|
|
598
|
+
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
|
599
|
+
)
|
|
600
|
+
);
|
|
601
|
+
}, []);
|
|
602
|
+
|
|
603
|
+
return (
|
|
604
|
+
<ApplicationWindow title="Todo App" defaultWidth={400} defaultHeight={500} onCloseRequest={quit}>
|
|
605
|
+
<Box orientation={Orientation.VERTICAL} spacing={16} marginTop={16} marginStart={16} marginEnd={16}>
|
|
606
|
+
<Label.Root label="Todo App" />
|
|
607
|
+
</Box>
|
|
608
|
+
</ApplicationWindow>
|
|
609
|
+
);
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
export const appId = "com.gtkx.todo";
|
|
613
|
+
\`\`\`
|
|
614
|
+
|
|
615
|
+
## Layout Patterns
|
|
616
|
+
|
|
617
|
+
### Grid for Forms
|
|
618
|
+
|
|
619
|
+
\`\`\`tsx
|
|
620
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
621
|
+
import { Button, Entry, Grid, Label } from "@gtkx/react";
|
|
622
|
+
import { useState } from "react";
|
|
623
|
+
|
|
624
|
+
const FormLayout = () => {
|
|
625
|
+
const [name, setName] = useState("");
|
|
626
|
+
const [email, setEmail] = useState("");
|
|
627
|
+
|
|
628
|
+
return (
|
|
629
|
+
<Grid.Root rowSpacing={8} columnSpacing={12}>
|
|
630
|
+
<Grid.Child column={0} row={0}>
|
|
631
|
+
<Label.Root label="Name:" halign={Gtk.Align.END} />
|
|
632
|
+
</Grid.Child>
|
|
633
|
+
<Grid.Child column={1} row={0}>
|
|
634
|
+
<Entry text={name} onChanged={(e) => setName(e.getText())} hexpand />
|
|
635
|
+
</Grid.Child>
|
|
636
|
+
<Grid.Child column={0} row={1}>
|
|
637
|
+
<Label.Root label="Email:" halign={Gtk.Align.END} />
|
|
638
|
+
</Grid.Child>
|
|
639
|
+
<Grid.Child column={1} row={1}>
|
|
640
|
+
<Entry text={email} onChanged={(e) => setEmail(e.getText())} hexpand />
|
|
641
|
+
</Grid.Child>
|
|
642
|
+
<Grid.Child column={0} row={2} columnSpan={2}>
|
|
643
|
+
<Button label="Submit" halign={Gtk.Align.END} marginTop={8} />
|
|
644
|
+
</Grid.Child>
|
|
645
|
+
</Grid.Root>
|
|
646
|
+
);
|
|
647
|
+
};
|
|
648
|
+
\`\`\`
|
|
649
|
+
|
|
650
|
+
### Stack with StackSwitcher
|
|
651
|
+
|
|
652
|
+
\`\`\`tsx
|
|
653
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
654
|
+
import { Box, Label, Stack, StackSwitcher } from "@gtkx/react";
|
|
655
|
+
|
|
656
|
+
const TabContainer = () => (
|
|
657
|
+
<Box orientation={Gtk.Orientation.VERTICAL} spacing={8}>
|
|
658
|
+
<StackSwitcher.Root
|
|
659
|
+
ref={(switcher: Gtk.StackSwitcher | null) => {
|
|
660
|
+
if (switcher) {
|
|
661
|
+
const stack = switcher.getParent()?.getLastChild() as Gtk.Stack | null;
|
|
662
|
+
if (stack) switcher.setStack(stack);
|
|
663
|
+
}
|
|
664
|
+
}}
|
|
665
|
+
/>
|
|
666
|
+
<Stack.Root transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT} transitionDuration={200}>
|
|
667
|
+
<Stack.Page name="page1" title="First">
|
|
668
|
+
<Label.Root label="First Page Content" />
|
|
669
|
+
</Stack.Page>
|
|
670
|
+
<Stack.Page name="page2" title="Second">
|
|
671
|
+
<Label.Root label="Second Page Content" />
|
|
672
|
+
</Stack.Page>
|
|
673
|
+
</Stack.Root>
|
|
674
|
+
</Box>
|
|
675
|
+
);
|
|
676
|
+
\`\`\`
|
|
677
|
+
|
|
678
|
+
## Virtual Scrolling Lists
|
|
679
|
+
|
|
680
|
+
### ListView with Custom Rendering
|
|
681
|
+
|
|
682
|
+
\`\`\`tsx
|
|
683
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
684
|
+
import { Box, Label, ListView } from "@gtkx/react";
|
|
685
|
+
|
|
686
|
+
interface Task {
|
|
687
|
+
id: string;
|
|
688
|
+
title: string;
|
|
689
|
+
completed: boolean;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const tasks: Task[] = [
|
|
693
|
+
{ id: "1", title: "Learn GTK4", completed: true },
|
|
694
|
+
{ id: "2", title: "Build React app", completed: false },
|
|
695
|
+
];
|
|
696
|
+
|
|
697
|
+
const TaskList = () => (
|
|
698
|
+
<Box cssClasses={["card"]} heightRequest={250}>
|
|
699
|
+
<ListView.Root
|
|
700
|
+
vexpand
|
|
701
|
+
renderItem={(task: Task | null) => (
|
|
702
|
+
<Label.Root
|
|
703
|
+
label={task?.title ?? ""}
|
|
704
|
+
cssClasses={task?.completed ? ["dim-label"] : []}
|
|
705
|
+
halign={Gtk.Align.START}
|
|
706
|
+
marginStart={12}
|
|
707
|
+
marginTop={8}
|
|
708
|
+
marginBottom={8}
|
|
709
|
+
/>
|
|
710
|
+
)}
|
|
711
|
+
>
|
|
712
|
+
{tasks.map((task) => (
|
|
713
|
+
<ListView.Item key={task.id} item={task} />
|
|
714
|
+
))}
|
|
715
|
+
</ListView.Root>
|
|
716
|
+
</Box>
|
|
717
|
+
);
|
|
718
|
+
\`\`\`
|
|
719
|
+
|
|
720
|
+
## Menus
|
|
721
|
+
|
|
722
|
+
### MenuButton with PopoverMenu
|
|
723
|
+
|
|
724
|
+
\`\`\`tsx
|
|
725
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
726
|
+
import { Box, Label, Menu, MenuButton, PopoverMenu } from "@gtkx/react";
|
|
727
|
+
import { useState } from "react";
|
|
728
|
+
|
|
729
|
+
const MenuDemo = () => {
|
|
730
|
+
const [lastAction, setLastAction] = useState<string | null>(null);
|
|
731
|
+
|
|
732
|
+
return (
|
|
733
|
+
<Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
734
|
+
<Label.Root label={\`Last action: \${lastAction ?? "(none)"}\`} />
|
|
735
|
+
<MenuButton.Root label="Actions">
|
|
736
|
+
<MenuButton.Popover>
|
|
737
|
+
<PopoverMenu.Root>
|
|
738
|
+
<Menu.Item label="New" onActivate={() => setLastAction("New")} accels="<Control>n" />
|
|
739
|
+
<Menu.Item label="Open" onActivate={() => setLastAction("Open")} accels="<Control>o" />
|
|
740
|
+
<Menu.Item label="Save" onActivate={() => setLastAction("Save")} accels="<Control>s" />
|
|
741
|
+
</PopoverMenu.Root>
|
|
742
|
+
</MenuButton.Popover>
|
|
743
|
+
</MenuButton.Root>
|
|
744
|
+
</Box>
|
|
745
|
+
);
|
|
746
|
+
};
|
|
747
|
+
\`\`\`
|
|
748
|
+
|
|
749
|
+
## Component Props Pattern
|
|
750
|
+
|
|
751
|
+
### List Item Component
|
|
752
|
+
|
|
753
|
+
\`\`\`tsx
|
|
754
|
+
import { Box, Button, CheckButton, Label } from "@gtkx/react";
|
|
755
|
+
import { Orientation } from "@gtkx/ffi/gtk";
|
|
756
|
+
|
|
757
|
+
interface Todo {
|
|
758
|
+
id: number;
|
|
759
|
+
text: string;
|
|
760
|
+
completed: boolean;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
interface TodoItemProps {
|
|
764
|
+
todo: Todo;
|
|
765
|
+
onToggle: (id: number) => void;
|
|
766
|
+
onDelete: (id: number) => void;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export const TodoItem = ({ todo, onToggle, onDelete }: TodoItemProps) => (
|
|
770
|
+
<Box orientation={Orientation.HORIZONTAL} spacing={8}>
|
|
771
|
+
<CheckButton active={todo.completed} onToggled={() => onToggle(todo.id)} />
|
|
772
|
+
<Label.Root label={todo.text} hexpand cssClasses={todo.completed ? ["dim-label"] : []} />
|
|
773
|
+
<Button iconName="edit-delete-symbolic" onClicked={() => onDelete(todo.id)} cssClasses={["flat"]} />
|
|
774
|
+
</Box>
|
|
775
|
+
);
|
|
776
|
+
\`\`\`
|
|
777
|
+
`;
|
|
778
|
+
};
|
|
100
779
|
const generateExampleTest = (testing) => {
|
|
101
780
|
const imports = testing === "vitest"
|
|
102
781
|
? `import { describe, it, expect, afterEach } from "vitest";`
|
|
@@ -203,6 +882,11 @@ const suggestAppId = (name) => {
|
|
|
203
882
|
const sanitized = name.replace(/-/g, "");
|
|
204
883
|
return `org.gtkx.${sanitized}`;
|
|
205
884
|
};
|
|
885
|
+
/**
|
|
886
|
+
* Creates a new GTKX application with interactive prompts.
|
|
887
|
+
* Scaffolds project structure, installs dependencies, and sets up configuration.
|
|
888
|
+
* @param options - Pre-filled options to skip interactive prompts
|
|
889
|
+
*/
|
|
206
890
|
export const createApp = async (options = {}) => {
|
|
207
891
|
p.intro("Create GTKX App");
|
|
208
892
|
const name = options.name ??
|
|
@@ -274,6 +958,15 @@ export const createApp = async (options = {}) => {
|
|
|
274
958
|
p.cancel("Operation cancelled");
|
|
275
959
|
process.exit(0);
|
|
276
960
|
}
|
|
961
|
+
const claudeSkills = options.claudeSkills ??
|
|
962
|
+
(await p.confirm({
|
|
963
|
+
message: "Include Claude Code skills?",
|
|
964
|
+
initialValue: true,
|
|
965
|
+
}));
|
|
966
|
+
if (p.isCancel(claudeSkills)) {
|
|
967
|
+
p.cancel("Operation cancelled");
|
|
968
|
+
process.exit(0);
|
|
969
|
+
}
|
|
277
970
|
const projectPath = resolve(process.cwd(), name);
|
|
278
971
|
const s = p.spinner();
|
|
279
972
|
s.start("Creating project structure...");
|
|
@@ -287,6 +980,13 @@ export const createApp = async (options = {}) => {
|
|
|
287
980
|
writeFileSync(join(projectPath, "src", "app.tsx"), generateAppTsx(name, appId));
|
|
288
981
|
writeFileSync(join(projectPath, "src", "index.tsx"), generateIndexTsx());
|
|
289
982
|
writeFileSync(join(projectPath, ".gitignore"), generateGitignore());
|
|
983
|
+
if (claudeSkills) {
|
|
984
|
+
const skillsDir = join(projectPath, ".claude", "skills", "developing-gtkx-apps");
|
|
985
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
986
|
+
writeFileSync(join(skillsDir, "SKILL.md"), generateSkillMd());
|
|
987
|
+
writeFileSync(join(skillsDir, "WIDGETS.md"), generateWidgetsMd());
|
|
988
|
+
writeFileSync(join(skillsDir, "EXAMPLES.md"), generateExamplesMd());
|
|
989
|
+
}
|
|
290
990
|
if (testing === "vitest") {
|
|
291
991
|
writeFileSync(join(projectPath, "vitest.config.ts"), generateVitestConfig());
|
|
292
992
|
writeFileSync(join(projectPath, "tests", "app.test.tsx"), generateExampleTest(testing));
|
package/dist/dev-server.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { type InlineConfig, type ViteDevServer } from "vite";
|
|
2
|
+
/**
|
|
3
|
+
* Options for creating a GTKX development server.
|
|
4
|
+
*/
|
|
2
5
|
export interface DevServerOptions {
|
|
3
|
-
/** Path to the app entry file (e.g., "./src/app.tsx") */
|
|
6
|
+
/** Path to the app entry file (e.g., "./src/app.tsx"). */
|
|
4
7
|
entry: string;
|
|
5
|
-
/** Vite configuration overrides */
|
|
8
|
+
/** Vite configuration overrides for customizing the dev server. */
|
|
6
9
|
vite?: InlineConfig;
|
|
7
10
|
}
|
|
8
11
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI for GTKX - create and develop GTK4 React applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtk",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@vitejs/plugin-react": "5.1.2",
|
|
48
48
|
"citty": "0.1.6",
|
|
49
49
|
"vite": "7.2.7",
|
|
50
|
-
"@gtkx/ffi": "0.
|
|
51
|
-
"@gtkx/react": "0.
|
|
50
|
+
"@gtkx/ffi": "0.4.0",
|
|
51
|
+
"@gtkx/react": "0.4.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"react": "^19"
|