@nil-/xit 0.0.1 → 0.0.3
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 +286 -3
- package/index.d.ts +21 -1
- package/index.js +10 -1
- package/package.json +9 -1
- package/message.proto +0 -49
- package/src/binding.d.ts +0 -1
- package/src/binding.js +0 -10
package/README.md
CHANGED
|
@@ -1,5 +1,288 @@
|
|
|
1
|
-
#
|
|
1
|
+
# nil/xit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
_nil/xit_ is a small project designed to bridge the gap between C++ and Svelte for creating GUI applications.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Note: _Xit_ is a temporary name. It's my playful way of saying, "This is my xit (💩)."
|
|
6
|
+
|
|
7
|
+
## Motivation
|
|
8
|
+
|
|
9
|
+
This project isn't intended to replace existing, more mature GUI frameworks. Writing GUI applications in C++ can be complex, with frameworks like Qt, ImGui, and others requiring a deep understanding of setup and usage.
|
|
10
|
+
|
|
11
|
+
With _nil/xit_, you can write your UI in Svelte and add hooks/bindings to control the interface. While you'll need some familiarity with Svelte, it's a relatively small hurdle to overcome compared to mastering traditional C++ GUI frameworks.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
This library is developed in a repo with `vcpkg`.
|
|
16
|
+
|
|
17
|
+
To consume the library, add _nil_ registry to your _vcpkg-configuration.json_
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg-configuration.schema.json",
|
|
22
|
+
"default-registry": {
|
|
23
|
+
"kind": "git",
|
|
24
|
+
"repository": "https://github.com/Microsoft/vcpkg",
|
|
25
|
+
"baseline": "da21e45c9ae689f63aa27f3a4ee0e64fe6b16494"
|
|
26
|
+
},
|
|
27
|
+
"registries": [
|
|
28
|
+
{
|
|
29
|
+
"kind": "git",
|
|
30
|
+
"repository": "https://github.com/njaldea/nil-vcpkg-ports",
|
|
31
|
+
"baseline": "02b02d3bc913cbd52da7654cb6f46207e6a9c6d1",
|
|
32
|
+
"packages": ["nil", "nil-service", "nil-xit"]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Create your target binary and link nil::xit.
|
|
39
|
+
|
|
40
|
+
```cmake
|
|
41
|
+
project(YOUR_PROJECT)
|
|
42
|
+
|
|
43
|
+
find_package(nil-xit CONFIG REQUIRED)
|
|
44
|
+
|
|
45
|
+
add_executable(${PROJECT_NAME} main.cpp)
|
|
46
|
+
target_link_libraries(${PROJECT_NAME} PRIVATE nil::xit)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The following libraries are required to use _nil/xit_ alongside `vcpkg`:
|
|
50
|
+
|
|
51
|
+
- [nil/service](https://github.com/njaldea/nil-service)
|
|
52
|
+
|
|
53
|
+
The following are the libraries needed to compile from source if vcpkg is available.
|
|
54
|
+
|
|
55
|
+
- `boost` (`asio` and `beast`)
|
|
56
|
+
- `protobuf`
|
|
57
|
+
|
|
58
|
+
TODO: publish libraries other than `vcpkg`
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
The C++ API primarily sets up the network service.
|
|
63
|
+
The GUI itself is not part of the C++ API but is served externally, for example, from [https://xit-ui.vercel.app](https://xit-ui.vercel.app).
|
|
64
|
+
|
|
65
|
+
At present, the frontend UI connects to a specified URL. For example, if you spawn a WebSocket server on port 1101, you can access the UI by visiting:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
https://xit-ui.vercel.app/localhost:1101/id-1
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The Svelte files provided to the framework are sent to the frontend, where they are recompiled into a consumable module. You can use any JavaScript libraries available on npm, typically through [https://unpkg.com](https://unpkg.com).
|
|
72
|
+
|
|
73
|
+
Note: Every page refresh will trigger a recompilation of the files. Caching will be implemented as the library matures.
|
|
74
|
+
|
|
75
|
+
## Example
|
|
76
|
+
|
|
77
|
+
### C++ Code
|
|
78
|
+
|
|
79
|
+
```cpp
|
|
80
|
+
#include <nil/service/ws/Server.hpp>
|
|
81
|
+
|
|
82
|
+
#include <nil/xit.hpp>
|
|
83
|
+
|
|
84
|
+
#include <iostream>
|
|
85
|
+
#include <thread>
|
|
86
|
+
|
|
87
|
+
int main()
|
|
88
|
+
{
|
|
89
|
+
nil::service::ws::Server server({.port = 1101});
|
|
90
|
+
// https://xit-ui.vercel.app/{server or ip:port}/{frame id}
|
|
91
|
+
// https://xit-ui.vercel.app/localhost:1101/frame-id
|
|
92
|
+
|
|
93
|
+
auto core = nil::xit::make_core(server);
|
|
94
|
+
|
|
95
|
+
auto& frame = add_frame(
|
|
96
|
+
core,
|
|
97
|
+
"frame-id",
|
|
98
|
+
"/absolute/path/to/your/svelte/file.svelte"
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
auto& str_bind = bind(
|
|
102
|
+
frame,
|
|
103
|
+
"binding_0_1",
|
|
104
|
+
"initial value",
|
|
105
|
+
// this is to test gui -> cpp data flow
|
|
106
|
+
[](const std::string& value)
|
|
107
|
+
{
|
|
108
|
+
// local value is already updated when this is called.
|
|
109
|
+
std::cout << "value changed: " << value << std::endl;
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// this is to test cpp -> gui data flow
|
|
114
|
+
const std::thread th(
|
|
115
|
+
[&]()
|
|
116
|
+
{
|
|
117
|
+
std::string line;
|
|
118
|
+
std::cout << "input here: ";
|
|
119
|
+
while (std::getline(std::cin, line))
|
|
120
|
+
{
|
|
121
|
+
// will not update the local data inside str_bind
|
|
122
|
+
post(str_bind, line);
|
|
123
|
+
|
|
124
|
+
// if local data needs to be overwritten, use `set` instead
|
|
125
|
+
// this will update the local data without waiting for an update
|
|
126
|
+
// from the UI.
|
|
127
|
+
set(str_bind, line);
|
|
128
|
+
std::cout << "input here: ";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
server.run();
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Svelte UI
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<script>
|
|
142
|
+
import Text from "./components/Text.svelte";
|
|
143
|
+
|
|
144
|
+
import { getContext } from "svelte";
|
|
145
|
+
|
|
146
|
+
const { binding } = getContext("nil.xit");
|
|
147
|
+
const str_binding = binding.string("binding_0_1", "world");
|
|
148
|
+
</script>
|
|
149
|
+
|
|
150
|
+
<Text bind:value="{$str_binding}" placeholder="placeholder" label="text label here" />
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Supported Binding Types
|
|
154
|
+
|
|
155
|
+
- `std::int64_t`
|
|
156
|
+
- `std::string`
|
|
157
|
+
- `std::vector\<std::int8_t>`
|
|
158
|
+
|
|
159
|
+
These are accessible from a context provided to the user.
|
|
160
|
+
|
|
161
|
+
```html
|
|
162
|
+
<script>
|
|
163
|
+
import { getContext } from "svelte";
|
|
164
|
+
|
|
165
|
+
const { binding } = getContext("nil.xit");
|
|
166
|
+
|
|
167
|
+
const store_string = binding.string("id-string", "default_value"); // string
|
|
168
|
+
const store_number = binding.number("id-number", 1101); // number
|
|
169
|
+
const store_buffer = binding.buffer("id-buffer", []); // UInt8Array
|
|
170
|
+
</script>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## How about JSON?
|
|
174
|
+
|
|
175
|
+
JSON is not directly supported. This is intentional since the frontend might not be a browser. It will be up to the user to serialize and deserialize this kind of data.
|
|
176
|
+
|
|
177
|
+
Example below is an example for handling json by simply stringifying it.
|
|
178
|
+
|
|
179
|
+
### C++ Code
|
|
180
|
+
|
|
181
|
+
```cpp
|
|
182
|
+
struct JSON
|
|
183
|
+
{
|
|
184
|
+
std::string buffer;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
namespace nil::xit
|
|
188
|
+
{
|
|
189
|
+
template <>
|
|
190
|
+
struct buffer_type<JSON>
|
|
191
|
+
{
|
|
192
|
+
static JSON deserialize(const void* data, std::uint64_t size)
|
|
193
|
+
{
|
|
194
|
+
return {.buffer = {static_cast<const char*>(data), size}};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static std::vector<std::uint8_t> serialize(const JSON& value)
|
|
198
|
+
{
|
|
199
|
+
return {value.buffer.begin(), value.buffer.end()};
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
...
|
|
205
|
+
{
|
|
206
|
+
auto& json_editor = add_frame(
|
|
207
|
+
core,
|
|
208
|
+
"frame-id",
|
|
209
|
+
"/absolute/path/to/your/svelte/file.svelte"
|
|
210
|
+
);
|
|
211
|
+
bind<JSON>(
|
|
212
|
+
json_editor,
|
|
213
|
+
"json_binding",
|
|
214
|
+
JSON{.buffer = R"({ "hello": "hello this is buffer" })"},
|
|
215
|
+
[](const JSON& value) { std::cout << "value changed: " << value.buffer << std::endl; }
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Svelte UI
|
|
221
|
+
|
|
222
|
+
```html
|
|
223
|
+
<script>
|
|
224
|
+
import { getContext } from "svelte";
|
|
225
|
+
|
|
226
|
+
const { binding } = getContext("nil.xit");
|
|
227
|
+
const str_binding = binding.json(
|
|
228
|
+
"binding-id",
|
|
229
|
+
{}, // default value
|
|
230
|
+
{
|
|
231
|
+
encode: (o) => new TextEncoder().encode(JSON.stringify(o)),
|
|
232
|
+
decode: (o) => JSON.parse(new TextDecoder().decode(o))
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
</script>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Frame Composition
|
|
239
|
+
|
|
240
|
+
Composing multiple frames into one is possible.
|
|
241
|
+
|
|
242
|
+
### C++ Code
|
|
243
|
+
|
|
244
|
+
```cpp
|
|
245
|
+
auto& group = add_frame(
|
|
246
|
+
core,
|
|
247
|
+
"group",
|
|
248
|
+
std::filesystem::path(__FILE__).parent_path() / "gui/GroupUp.svelte"
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
bind<JSON>(
|
|
252
|
+
group,
|
|
253
|
+
"frames",
|
|
254
|
+
JSON{.buffer = R"({ "frames": ["frame-1", "frame-2"] })"}
|
|
255
|
+
);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Svelte UI
|
|
259
|
+
|
|
260
|
+
```html
|
|
261
|
+
<script>
|
|
262
|
+
import { getContext } from "svelte";
|
|
263
|
+
const { binding, loader } = getContext("nil.xit");
|
|
264
|
+
|
|
265
|
+
const links = binding.json(
|
|
266
|
+
"links",
|
|
267
|
+
{ links: [] },
|
|
268
|
+
{
|
|
269
|
+
encode: (o) => new TextEncoder().encode(JSON.stringify(o)),
|
|
270
|
+
decode: (o) => JSON.parse(new TextDecoder().decode(o))
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
</script>
|
|
274
|
+
|
|
275
|
+
<div class="wrapper">
|
|
276
|
+
{#each $links.links as link} {@const frame = loader.one(link)}
|
|
277
|
+
<div use:frame />
|
|
278
|
+
{/each}
|
|
279
|
+
</div>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
TODO:
|
|
283
|
+
|
|
284
|
+
- Implement signal/event binding
|
|
285
|
+
- Develop a usable component library
|
|
286
|
+
- Create a more intuitive way to specify the connection URL for the page
|
|
287
|
+
- Enable bundle caching
|
|
288
|
+
- Optimize json type support
|
package/index.d.ts
CHANGED
|
@@ -1 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import { type Action } from "svelte/action";
|
|
2
|
+
import { type Writable } from "svelte/store";
|
|
3
|
+
export type CoDec = {
|
|
4
|
+
encode: (o: object) => Uint8Array;
|
|
5
|
+
decode: (a: Uint8Array) => object;
|
|
6
|
+
};
|
|
7
|
+
export type Loader = {
|
|
8
|
+
one: (f: string) => Action<HTMLDivElement>;
|
|
9
|
+
all: (f: string[]) => Action<HTMLDivElement>;
|
|
10
|
+
};
|
|
11
|
+
export type Xit = {
|
|
12
|
+
binding: {
|
|
13
|
+
string: (t: string, v: string) => Writable<string>;
|
|
14
|
+
number: (t: string, v: number) => Writable<number>;
|
|
15
|
+
buffer: (t: string, v: Uint8Array) => Writable<Uint8Array>;
|
|
16
|
+
json: (t: string, v: object, codec: CoDec) => Writable<object>;
|
|
17
|
+
};
|
|
18
|
+
loader: Loader;
|
|
19
|
+
};
|
|
20
|
+
export declare const nil_xit: () => Xit;
|
|
21
|
+
export declare const json_string_codec: CoDec;
|
package/index.js
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { getContext } from "svelte";
|
|
2
|
+
import {} from "svelte/action";
|
|
3
|
+
import {} from "svelte/store";
|
|
4
|
+
export const nil_xit = () => {
|
|
5
|
+
return getContext("@nil-/xit");
|
|
6
|
+
};
|
|
7
|
+
export const json_string_codec = {
|
|
8
|
+
encode: (o) => new TextEncoder().encode(JSON.stringify(o)),
|
|
9
|
+
decode: (o) => JSON.parse(new TextDecoder().decode(o))
|
|
10
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nil-/xit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"@rollup/browser": "^4.21.2",
|
|
6
|
+
"acorn": "^8.12.1",
|
|
7
|
+
"estree-walker": "^3.0.3",
|
|
8
|
+
"mdsvex": "^0.12.3",
|
|
9
|
+
"remark-admonitions": "^1.2.1",
|
|
10
|
+
"resolve.exports": "^2.0.2"
|
|
11
|
+
},
|
|
4
12
|
"type": "module",
|
|
5
13
|
"exports": {
|
|
6
14
|
"./package.json": "./package.json",
|
package/message.proto
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
syntax = "proto3";
|
|
2
|
-
|
|
3
|
-
option optimize_for = LITE_RUNTIME;
|
|
4
|
-
|
|
5
|
-
package nil.xit.proto;
|
|
6
|
-
|
|
7
|
-
enum MessageType {
|
|
8
|
-
MessageType_MarkupRequest = 0;
|
|
9
|
-
MessageType_MarkupResponse = 1;
|
|
10
|
-
MessageType_BindingRequest = 2;
|
|
11
|
-
MessageType_BindingResponse = 3;
|
|
12
|
-
MessageType_BindingUpdate = 4;
|
|
13
|
-
MessageType_FileRequest = 5;
|
|
14
|
-
MessageType_FileResponse = 6;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
message MarkupResponse
|
|
18
|
-
{
|
|
19
|
-
repeated string components = 1;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
message Binding
|
|
23
|
-
{
|
|
24
|
-
string tag = 1;
|
|
25
|
-
oneof value {
|
|
26
|
-
int64 value_i64 = 2;
|
|
27
|
-
string value_str = 3;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
message BindingGroup
|
|
32
|
-
{
|
|
33
|
-
repeated Binding bindings = 1;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
message BindingResponse
|
|
37
|
-
{
|
|
38
|
-
repeated BindingGroup info = 1;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
message FileRequest
|
|
42
|
-
{
|
|
43
|
-
string target = 1;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
message FileResponse {
|
|
47
|
-
string target = 1;
|
|
48
|
-
string content = 3;
|
|
49
|
-
}
|
package/src/binding.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const binding: <T>(tag: string, default_value: T) => {};
|
package/src/binding.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { getContext } from "svelte";
|
|
2
|
-
import { writable } from 'svelte/store';
|
|
3
|
-
const create_writable = (value) => {
|
|
4
|
-
const w = writable(value);
|
|
5
|
-
w.subscribe((v) => console.log("not used", v));
|
|
6
|
-
return w;
|
|
7
|
-
};
|
|
8
|
-
export const binding = (tag, default_value) => {
|
|
9
|
-
return getContext(tag) ?? create_writable(default_value);
|
|
10
|
-
};
|