@gtkx/cli 0.13.3 → 0.15.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.
@@ -24,7 +24,7 @@ export type DevServerOptions = {
24
24
  * import { render } from "@gtkx/react";
25
25
  *
26
26
  * const server = await createDevServer({
27
- * entry: "./src/dev.tsx",
27
+ * entry: "./src/dev.tsx",
28
28
  * });
29
29
  *
30
30
  * const mod = await server.ssrLoadModule("./src/dev.tsx");
@@ -21,7 +21,7 @@ import { swcSsrRefresh } from "./vite-plugin-swc-ssr-refresh.js";
21
21
  * import { render } from "@gtkx/react";
22
22
  *
23
23
  * const server = await createDevServer({
24
- * entry: "./src/dev.tsx",
24
+ * entry: "./src/dev.tsx",
25
25
  * });
26
26
  *
27
27
  * const mod = await server.ssrLoadModule("./src/dev.tsx");
@@ -1,5 +1,6 @@
1
1
  import * as net from "node:net";
2
2
  import { getNativeId, getNativeObject } from "@gtkx/ffi";
3
+ import { Value } from "@gtkx/ffi/gobject";
3
4
  import * as Gtk from "@gtkx/ffi/gtk";
4
5
  import { DEFAULT_SOCKET_PATH, IpcRequestSchema, IpcResponseSchema, McpError, McpErrorCode, methodNotFoundError, widgetNotFoundError, } from "@gtkx/mcp";
5
6
  import { getApplication } from "@gtkx/react";
@@ -354,7 +355,28 @@ class McpClient {
354
355
  if (!widget) {
355
356
  throw widgetNotFoundError(p.widgetId);
356
357
  }
357
- const signalArgs = (p.args ?? []);
358
+ const signalArgs = (p.args ?? []).map((arg) => {
359
+ switch (arg.type) {
360
+ case "boolean":
361
+ return Value.newFromBoolean(arg.value);
362
+ case "int":
363
+ return Value.newFromInt(arg.value);
364
+ case "uint":
365
+ return Value.newFromUint(arg.value);
366
+ case "int64":
367
+ return Value.newFromInt64(arg.value);
368
+ case "uint64":
369
+ return Value.newFromUint64(arg.value);
370
+ case "float":
371
+ return Value.newFromFloat(arg.value);
372
+ case "double":
373
+ return Value.newFromDouble(arg.value);
374
+ case "string":
375
+ return Value.newFromString(arg.value);
376
+ default:
377
+ throw new McpError(McpErrorCode.INVALID_REQUEST, `Unknown argument type: ${arg.type}`);
378
+ }
379
+ });
358
380
  await testing.fireEvent(widget, p.signal, ...signalArgs);
359
381
  return { success: true };
360
382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/cli",
3
- "version": "0.13.3",
3
+ "version": "0.15.0",
4
4
  "description": "CLI for GTKX - create and develop GTK4 React applications",
5
5
  "keywords": [
6
6
  "gtkx",
@@ -58,19 +58,19 @@
58
58
  "ejs": "^3.1.10",
59
59
  "react-refresh": "^0.18.0",
60
60
  "vite": "^7.3.1",
61
- "@gtkx/mcp": "0.13.3",
62
- "@gtkx/ffi": "0.13.3",
63
- "@gtkx/react": "0.13.3"
61
+ "@gtkx/mcp": "0.15.0",
62
+ "@gtkx/react": "0.15.0",
63
+ "@gtkx/ffi": "0.15.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/ejs": "^3.1.5",
67
67
  "@types/react-refresh": "^0.14.7",
68
68
  "memfs": "^4.51.1",
69
- "@gtkx/testing": "0.13.3"
69
+ "@gtkx/testing": "0.15.0"
70
70
  },
71
71
  "peerDependencies": {
72
72
  "react": "^19",
73
- "@gtkx/testing": "0.13.3"
73
+ "@gtkx/testing": "0.15.0"
74
74
  },
75
75
  "peerDependenciesMeta": {
76
76
  "@gtkx/testing": {
@@ -127,14 +127,14 @@ const TodoList = () => {
127
127
 
128
128
  return (
129
129
  <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} marginStart={16} marginEnd={16} marginTop={16} marginBottom={16}>
130
- <GtkBox orientation={Gtk.Orientation.HORIZONTAL} spacing={8}>
130
+ <GtkBox spacing={8}>
131
131
  <GtkEntry text={input} onChanged={(e) => setInput(e.getText())} hexpand placeholderText="New todo..." />
132
132
  <GtkButton label="Add" onClicked={addTodo} cssClasses={["suggested-action"]} />
133
133
  </GtkBox>
134
134
  <GtkScrolledWindow vexpand cssClasses={["card"]}>
135
135
  <x.ListView<Todo>
136
136
  renderItem={(todo) => (
137
- <GtkBox orientation={Gtk.Orientation.HORIZONTAL} spacing={8} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
137
+ <GtkBox spacing={8} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
138
138
  <GtkLabel label={todo?.text ?? ""} hexpand halign={Gtk.Align.START} />
139
139
  <GtkButton iconName="edit-delete-symbolic" cssClasses={["flat"]} onClicked={() => todo && deleteTodo(todo.id)} />
140
140
  </GtkBox>
@@ -176,7 +176,7 @@ const SidebarNav = () => {
176
176
  const [currentPage, setCurrentPage] = useState("home");
177
177
 
178
178
  return (
179
- <GtkPaned orientation={Gtk.Orientation.HORIZONTAL} position={200}>
179
+ <GtkPaned position={200}>
180
180
  <x.Slot for={GtkPaned} id="startChild">
181
181
  <GtkScrolledWindow cssClasses={["sidebar"]}>
182
182
  <x.ListView<Page>
@@ -287,7 +287,7 @@ const SettingsPage = () => {
287
287
  <AdwExpanderRow title="Notification Settings" subtitle="Configure alerts">
288
288
  <AdwSwitchRow title="Sound" active />
289
289
  <AdwSwitchRow title="Badges" active />
290
- <AdwSwitchRow title="Lock Screen" active={false} />
290
+ <AdwSwitchRow title="Lock Screen" />
291
291
  </AdwExpanderRow>
292
292
  </AdwPreferencesGroup>
293
293
  </AdwPreferencesPage>
@@ -339,7 +339,7 @@ const FileTable = () => {
339
339
 
340
340
  return (
341
341
  <GtkScrolledWindow vexpand cssClasses={["card"]}>
342
- <GtkColumnView estimatedRowHeight={48} sortColumn={sortColumn} sortOrder={sortOrder} onSortChange={handleSort}>
342
+ <GtkColumnView estimatedRowHeight={48} sortColumn={sortColumn} sortOrder={sortOrder} onSortChanged={handleSort}>
343
343
  <x.ColumnViewColumn<FileItem> title="Name" id="name" expand sortable renderCell={(f) => <GtkLabel label={f?.name ?? ""} />} />
344
344
  <x.ColumnViewColumn<FileItem> title="Size" id="size" fixedWidth={100} sortable renderCell={(f) => <GtkLabel label={`${f?.size ?? 0} KB`} />} />
345
345
  <x.ColumnViewColumn<FileItem> title="Modified" id="modified" fixedWidth={120} sortable renderCell={(f) => <GtkLabel label={f?.modified ?? ""} />} />
@@ -668,7 +668,7 @@ const FileBrowser = () => {
668
668
  selected={selected ? [selected] : []}
669
669
  onSelectionChanged={(ids) => setSelected(ids[0] ?? null)}
670
670
  renderItem={(item) => (
671
- <GtkBox orientation={Gtk.Orientation.HORIZONTAL} spacing={8}>
671
+ <GtkBox spacing={8}>
672
672
  <GtkImage iconName={item?.isDirectory ? "folder-symbolic" : "text-x-generic-symbolic"} />
673
673
  <GtkLabel label={item?.name ?? ""} halign={Gtk.Align.START} />
674
674
  </GtkBox>
@@ -701,7 +701,15 @@ const ViewSwitcher = () => {
701
701
 
702
702
  return (
703
703
  <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
704
- <AdwToggleGroup active={view} onToggled={setView} halign={Gtk.Align.CENTER}>
704
+ <AdwToggleGroup
705
+ activeName={view}
706
+ onNotify={(group, prop) => {
707
+ if (prop === "active-name") {
708
+ setView(group.getActiveName() ?? "list");
709
+ }
710
+ }}
711
+ halign={Gtk.Align.CENTER}
712
+ >
705
713
  <x.Toggle id="list" iconName="view-list-symbolic" tooltip="List view" />
706
714
  <x.Toggle id="grid" iconName="view-grid-symbolic" tooltip="Grid view" />
707
715
  </AdwToggleGroup>
@@ -52,7 +52,7 @@ GTK signals map to `on<SignalName>` props: `clicked` → `onClicked`, `toggled`
52
52
  Some widgets require children in specific slots:
53
53
 
54
54
  ```tsx
55
- <GtkPaned orientation={Gtk.Orientation.HORIZONTAL}>
55
+ <GtkPaned>
56
56
  <x.Slot for={GtkPaned} id="startChild"><Sidebar /></x.Slot>
57
57
  <x.Slot for={GtkPaned} id="endChild"><Content /></x.Slot>
58
58
  </GtkPaned>
@@ -20,7 +20,7 @@
20
20
  Linear layout (horizontal or vertical).
21
21
 
22
22
  ```tsx
23
- <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} homogeneous={false}>
23
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
24
24
  {children}
25
25
  </GtkBox>
26
26
  ```
@@ -68,7 +68,7 @@ Custom tab widget:
68
68
  ```tsx
69
69
  <x.NotebookPage>
70
70
  <x.NotebookPageTab>
71
- <GtkBox orientation={Gtk.Orientation.HORIZONTAL} spacing={4}>
71
+ <GtkBox spacing={4}>
72
72
  <GtkImage iconName="folder-symbolic" />
73
73
  <GtkLabel label="Files" />
74
74
  </GtkBox>
@@ -81,7 +81,7 @@ Custom tab widget:
81
81
  Resizable split with draggable divider. **Requires Slot components.**
82
82
 
83
83
  ```tsx
84
- <GtkPaned orientation={Gtk.Orientation.HORIZONTAL} position={280} shrinkStartChild={false}>
84
+ <GtkPaned position={280} shrinkStartChild={false}>
85
85
  <x.Slot for={GtkPaned} id="startChild"><Sidebar /></x.Slot>
86
86
  <x.Slot for={GtkPaned} id="endChild"><MainContent /></x.Slot>
87
87
  </GtkPaned>
@@ -99,6 +99,22 @@ Stack widgets on top of each other. First child is base layer, additional childr
99
99
  </GtkOverlay>
100
100
  ```
101
101
 
102
+ ### GtkFixed
103
+ Absolute positioning. Use `x.FixedChild` wrapper for children.
104
+
105
+ ```tsx
106
+ <GtkFixed>
107
+ <x.FixedChild x={20} y={30}>
108
+ <GtkLabel label="Top Left" />
109
+ </x.FixedChild>
110
+ <x.FixedChild x={200} y={100}>
111
+ <GtkLabel label="Middle" />
112
+ </x.FixedChild>
113
+ </GtkFixed>
114
+ ```
115
+
116
+ **FixedChild props:** `x`, `y` (pixel coordinates)
117
+
102
118
  ### GtkScrolledWindow
103
119
  Scrollable container.
104
120
 
@@ -153,7 +169,7 @@ Grid-based virtual scrolling.
153
169
  Table with sortable columns.
154
170
 
155
171
  ```tsx
156
- <GtkColumnView estimatedRowHeight={48} sortColumn="name" sortOrder={Gtk.SortType.ASCENDING} onSortChange={handleSort}>
172
+ <GtkColumnView estimatedRowHeight={48} sortColumn="name" sortOrder={Gtk.SortType.ASCENDING} onSortChanged={handleSort}>
157
173
  <x.ColumnViewColumn<Item>
158
174
  title="Name"
159
175
  id="name"
@@ -193,7 +209,7 @@ Hierarchical tree with expand/collapse.
193
209
  selected={selectedId ? [selectedId] : []}
194
210
  onSelectionChanged={(ids) => setSelectedId(ids[0])}
195
211
  renderItem={(item, row) => (
196
- <GtkBox orientation={Gtk.Orientation.HORIZONTAL} spacing={8}>
212
+ <GtkBox spacing={8}>
197
213
  <GtkImage iconName={item?.isDirectory ? "folder-symbolic" : "text-x-generic-symbolic"} />
198
214
  <GtkLabel label={item?.name ?? ""} />
199
215
  </GtkBox>
@@ -256,7 +272,6 @@ Slider with optional marks.
256
272
 
257
273
  ```tsx
258
274
  <GtkScale
259
- orientation={Gtk.Orientation.HORIZONTAL}
260
275
  drawValue
261
276
  valuePos={Gtk.PositionType.TOP}
262
277
  >
@@ -466,7 +481,7 @@ Expandable settings row with optional action widget.
466
481
  </x.ExpanderRowAction>
467
482
  <x.ExpanderRowRow>
468
483
  <AdwSwitchRow title="Option 1" active />
469
- <AdwSwitchRow title="Option 2" active={false} />
484
+ <AdwSwitchRow title="Option 2" />
470
485
  </x.ExpanderRowRow>
471
486
  </AdwExpanderRow>
472
487
  ```
@@ -485,7 +500,16 @@ Input in list row.
485
500
  Segmented button group for mutually exclusive options.
486
501
 
487
502
  ```tsx
488
- <AdwToggleGroup active={viewMode} onToggled={setViewMode}>
503
+ const [viewMode, setViewMode] = useState("list");
504
+
505
+ <AdwToggleGroup
506
+ activeName={viewMode}
507
+ onNotify={(group, prop) => {
508
+ if (prop === "active-name") {
509
+ setViewMode(group.getActiveName() ?? "list");
510
+ }
511
+ }}
512
+ >
489
513
  <x.Toggle id="list" iconName="view-list-symbolic" tooltip="List view" />
490
514
  <x.Toggle id="grid" iconName="view-grid-symbolic" tooltip="Grid view" />
491
515
  <x.Toggle id="flow" label="Flow" />
@@ -554,3 +578,192 @@ const [selected, setSelected] = useState(items[0]);
554
578
  | `AdwSpinner` | Loading indicator |
555
579
  | `AdwWindowTitle` | Title + subtitle for header bars |
556
580
  | `AdwButtonRow` | Button styled as list row |
581
+
582
+ ---
583
+
584
+ ## Drag and Drop
585
+
586
+ All widgets support drag-and-drop through props. Use `onDragPrepare`, `onDragBegin`, and `onDragEnd` to make a widget draggable, and `dropTypes`, `onDrop`, `onDropEnter`, and `onDropLeave` to accept drops.
587
+
588
+ ```tsx
589
+ import * as Gdk from "@gtkx/ffi/gdk";
590
+ import { Type, Value } from "@gtkx/ffi/gobject";
591
+ import { GtkButton, GtkBox, GtkLabel } from "@gtkx/react";
592
+ import { useState } from "react";
593
+
594
+ const DraggableButton = ({ label }: { label: string }) => {
595
+ return (
596
+ <GtkButton
597
+ label={label}
598
+ onDragPrepare={() => Gdk.ContentProvider.newForValue(Value.newFromString(label))}
599
+ />
600
+ );
601
+ };
602
+
603
+ const DropZone = () => {
604
+ const [dropped, setDropped] = useState<string | null>(null);
605
+
606
+ return (
607
+ <GtkBox
608
+ dropTypes={[Type.STRING]}
609
+ onDrop={(value: Value) => {
610
+ setDropped(value.getString());
611
+ return true;
612
+ }}
613
+ >
614
+ <GtkLabel label={dropped ?? "Drop here"} />
615
+ </GtkBox>
616
+ );
617
+ };
618
+ ```
619
+
620
+ ## GValue Factories
621
+
622
+ Create typed values for drag-and-drop and signal emission:
623
+
624
+ | Factory | Description |
625
+ | ------------------------------ | ----------------------------- |
626
+ | `Value.newFromString(str)` | String values |
627
+ | `Value.newFromDouble(num)` | 64-bit floating point |
628
+ | `Value.newFromInt(num)` | 32-bit signed integer |
629
+ | `Value.newFromBoolean(bool)` | Boolean values |
630
+ | `Value.newFromObject(obj)` | GObject instances |
631
+ | `Value.newFromBoxed(boxed)` | Boxed types (Gdk.RGBA, etc.) |
632
+ | `Value.newFromEnum(gtype, n)` | Enum values (requires GType) |
633
+ | `Value.newFromFlags(gtype, n)` | Flags values (requires GType) |
634
+
635
+ Type constants for `dropTypes`: `Type.STRING`, `Type.INT`, `Type.DOUBLE`, `Type.BOOLEAN`, `Type.OBJECT`.
636
+
637
+ ## Custom Drawing
638
+
639
+ Render custom graphics with `GtkDrawingArea` using the `onDraw` callback:
640
+
641
+ ```tsx
642
+ import { GtkDrawingArea } from "@gtkx/react";
643
+ import type { Context } from "@gtkx/ffi/cairo";
644
+ import * as Gtk from "@gtkx/ffi/gtk";
645
+
646
+ const Canvas = () => {
647
+ const handleDraw = (self: Gtk.DrawingArea, cr: Context, width: number, height: number) => {
648
+ cr.setSourceRgb(0.2, 0.4, 0.8);
649
+ cr.rectangle(10, 10, width - 20, height - 20);
650
+ cr.fill();
651
+ };
652
+
653
+ return <GtkDrawingArea contentWidth={400} contentHeight={300} onDraw={handleDraw} />;
654
+ };
655
+ ```
656
+
657
+ Use `onGestureDragBegin`, `onGestureDragUpdate`, `onGestureDragEnd` for interactive drawing. Call `widget.queueDraw()` to trigger redraws.
658
+
659
+ ## Adjustment
660
+
661
+ Configure adjustable widgets declaratively with `x.Adjustment`:
662
+
663
+ ```tsx
664
+ import { x, GtkScale, GtkBox, GtkLabel } from "@gtkx/react";
665
+ import * as Gtk from "@gtkx/ffi/gtk";
666
+ import { useState } from "react";
667
+
668
+ const VolumeControl = () => {
669
+ const [volume, setVolume] = useState(50);
670
+
671
+ return (
672
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
673
+ <GtkScale drawValue hexpand>
674
+ <x.Adjustment value={volume} lower={0} upper={100} stepIncrement={1} pageIncrement={10} onValueChanged={setVolume} />
675
+ </GtkScale>
676
+ <GtkLabel label={`Volume: ${Math.round(volume)}%`} />
677
+ </GtkBox>
678
+ );
679
+ };
680
+ ```
681
+
682
+ Works with `GtkScale`, `GtkScrollbar`, `GtkScaleButton`, `GtkSpinButton`, `GtkListBox`.
683
+
684
+ **Props:** `value`, `lower`, `upper`, `stepIncrement`, `pageIncrement`, `pageSize`, `onValueChanged`
685
+
686
+ ## TextBuffer
687
+
688
+ Configure `GtkTextView` buffers declaratively with `x.TextBuffer`:
689
+
690
+ ```tsx
691
+ import { x, GtkTextView, GtkScrolledWindow } from "@gtkx/react";
692
+ import * as Gtk from "@gtkx/ffi/gtk";
693
+ import { useState } from "react";
694
+
695
+ const TextEditor = () => {
696
+ const [text, setText] = useState("Hello, World!");
697
+
698
+ return (
699
+ <GtkScrolledWindow minContentHeight={200}>
700
+ <GtkTextView wrapMode={Gtk.WrapMode.WORD_CHAR}>
701
+ <x.TextBuffer text={text} enableUndo onTextChanged={setText} />
702
+ </GtkTextView>
703
+ </GtkScrolledWindow>
704
+ );
705
+ };
706
+ ```
707
+
708
+ **Props:** `text`, `enableUndo`, `onTextChanged`, `onCanUndoChanged`, `onCanRedoChanged`
709
+
710
+ ## SourceBuffer
711
+
712
+ Configure `GtkSourceView` buffers with syntax highlighting using `x.SourceBuffer`:
713
+
714
+ ```tsx
715
+ import { x, GtkSourceView, GtkScrolledWindow } from "@gtkx/react";
716
+ import { useState } from "react";
717
+
718
+ const CodeEditor = () => {
719
+ const [code, setCode] = useState('console.log("Hello!");');
720
+
721
+ return (
722
+ <GtkScrolledWindow minContentHeight={300}>
723
+ <GtkSourceView showLineNumbers highlightCurrentLine tabWidth={4}>
724
+ <x.SourceBuffer
725
+ text={code}
726
+ language="typescript"
727
+ styleScheme="Adwaita-dark"
728
+ highlightSyntax
729
+ highlightMatchingBrackets
730
+ enableUndo
731
+ onTextChanged={setCode}
732
+ />
733
+ </GtkSourceView>
734
+ </GtkScrolledWindow>
735
+ );
736
+ };
737
+ ```
738
+
739
+ **Props:** `text`, `language`, `styleScheme`, `highlightSyntax`, `highlightMatchingBrackets`, `enableUndo`, `onTextChanged`, `onCanUndoChanged`, `onCanRedoChanged`, `onCursorMoved`
740
+
741
+ ## Keyboard Shortcuts
742
+
743
+ Attach shortcuts with `x.ShortcutController` and `x.Shortcut`:
744
+
745
+ ```tsx
746
+ import { x, GtkBox, GtkLabel } from "@gtkx/react";
747
+ import * as Gtk from "@gtkx/ffi/gtk";
748
+ import { useState } from "react";
749
+
750
+ const App = () => {
751
+ const [count, setCount] = useState(0);
752
+
753
+ return (
754
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} focusable>
755
+ <x.ShortcutController scope={Gtk.ShortcutScope.LOCAL}>
756
+ <x.Shortcut trigger="<Control>equal" onActivate={() => setCount((c) => c + 1)} />
757
+ <x.Shortcut trigger="<Control>minus" onActivate={() => setCount((c) => c - 1)} />
758
+ </x.ShortcutController>
759
+ <GtkLabel label={`Count: ${count}`} />
760
+ </GtkBox>
761
+ );
762
+ };
763
+ ```
764
+
765
+ **Scopes:** `LOCAL` (widget focus), `MANAGED` (parent managed), `GLOBAL` (window-wide)
766
+
767
+ **Trigger syntax:** `<Control>s`, `<Control><Shift>s`, `<Alt>F4`, `<Primary>q`, `F5`
768
+
769
+ **Multiple triggers:** `trigger={["F5", "<Control>r"]}`