plusui-native-core 0.1.102 → 0.1.103
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/Core/Features/API/Connect_API.ts +62 -146
- package/Core/Features/API/index.ts +2 -3
- package/Core/Features/Connection/README.md +176 -52
- package/Core/Features/Connection/connect.ts +136 -21
- package/Core/Features/FileDrop/filedrop.ts +27 -10
- package/Core/Features/Keyboard/keyboard.cpp +218 -189
- package/Core/Features/Keyboard/keyboard.ts +100 -31
- package/Core/Features/Keyboard/keyboard_linux.cpp +226 -0
- package/Core/Features/Keyboard/keyboard_macos.cpp +220 -0
- package/Core/Features/Keyboard/keyboard_windows.cpp +56 -0
- package/Core/Features/Window/webview.cpp +238 -52
- package/Core/Features/Window/window.cpp +424 -56
- package/Core/include/plusui/connect.hpp +125 -81
- package/package.json +1 -1
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PlusUI Connection
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* SEMANTIC SYNTAX. ZERO CONFIG. ALL 5 PATTERNS.
|
|
5
|
+
*
|
|
6
|
+
* Just write code using `connect.namespace.method()` syntax for CUSTOM
|
|
7
|
+
* frontend ↔ backend communication, then run `plusui connect` to generate bindings.
|
|
8
|
+
*
|
|
9
|
+
* Patterns are auto-detected from your code:
|
|
10
|
+
*
|
|
11
|
+
* connect.user.fetch(123) → Request/Response (CALL)
|
|
12
|
+
* connect.app.onNotify(cb) → Event Listener (SUBSCRIBE)
|
|
13
|
+
* connect.files.startUpload(file) → Fire & Forget (SIMPLEX)
|
|
14
|
+
* connect.settings.onThemeChange(cb) → Event Listener
|
|
15
|
+
*
|
|
16
|
+
* BUILT-IN FEATURES - use direct imports from 'plusui':
|
|
17
|
+
* import plusui from 'plusui';
|
|
18
|
+
* plusui.window.minimize();
|
|
19
|
+
* plusui.clipboard.setText('hello');
|
|
20
|
+
* plusui.app.quit();
|
|
21
|
+
*
|
|
10
22
|
* Custom channels are auto-generated by `plusui connect`.
|
|
11
23
|
*/
|
|
12
24
|
|
|
@@ -30,10 +42,12 @@ export type ConnectionEnvelope = {
|
|
|
30
42
|
};
|
|
31
43
|
|
|
32
44
|
type MessageCallback = (payload: any) => void;
|
|
45
|
+
type CallHandler = (...args: any[]) => any;
|
|
33
46
|
|
|
34
47
|
class ConnectionClient {
|
|
35
48
|
private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
|
|
36
49
|
private listeners = new Map<string, Set<MessageCallback>>();
|
|
50
|
+
private callHandlers = new Map<string, CallHandler>();
|
|
37
51
|
|
|
38
52
|
constructor() {
|
|
39
53
|
const host = globalThis as any;
|
|
@@ -83,6 +97,7 @@ class ConnectionClient {
|
|
|
83
97
|
const env = this.decode(message);
|
|
84
98
|
if (!env) return;
|
|
85
99
|
|
|
100
|
+
// Handle call results
|
|
86
101
|
if ((env.kind === "result" || env.kind === "error") && env.id) {
|
|
87
102
|
const entry = this.pending.get(env.id);
|
|
88
103
|
if (!entry) return;
|
|
@@ -95,7 +110,31 @@ class ConnectionClient {
|
|
|
95
110
|
return;
|
|
96
111
|
}
|
|
97
112
|
|
|
98
|
-
|
|
113
|
+
// Handle calls FROM backend (backend calling frontend)
|
|
114
|
+
if (env.kind === "call" && env.id) {
|
|
115
|
+
const handler = this.callHandlers.get(env.name);
|
|
116
|
+
if (handler) {
|
|
117
|
+
try {
|
|
118
|
+
const result = handler(env.payload);
|
|
119
|
+
// If the handler returns a Promise, wait for it
|
|
120
|
+
if (result && typeof result.then === 'function') {
|
|
121
|
+
result.then((resolved: any) => {
|
|
122
|
+
this.send({ kind: "result", id: env.id, name: env.name, payload: resolved });
|
|
123
|
+
}).catch((err: Error) => {
|
|
124
|
+
this.send({ kind: "error", id: env.id, name: env.name, error: err.message });
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
this.send({ kind: "result", id: env.id!, name: env.name, payload: result });
|
|
128
|
+
}
|
|
129
|
+
} catch (err: any) {
|
|
130
|
+
this.send({ kind: "error", id: env.id!, name: env.name, error: err?.message || "Handler error" });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle events/streams/publishes
|
|
137
|
+
if (env.kind === "event" || env.kind === "stream" || env.kind === "publish" || env.kind === "fire") {
|
|
99
138
|
const handlers = this.listeners.get(env.name);
|
|
100
139
|
if (!handlers) return;
|
|
101
140
|
for (const handler of handlers) {
|
|
@@ -104,6 +143,9 @@ class ConnectionClient {
|
|
|
104
143
|
}
|
|
105
144
|
}
|
|
106
145
|
|
|
146
|
+
// ---- Public API ----
|
|
147
|
+
|
|
148
|
+
/** Send a call (request/response) to the backend */
|
|
107
149
|
async call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
|
|
108
150
|
const id = this.nextId();
|
|
109
151
|
const promise = new Promise<TOut>((resolve, reject) => {
|
|
@@ -113,10 +155,12 @@ class ConnectionClient {
|
|
|
113
155
|
return promise;
|
|
114
156
|
}
|
|
115
157
|
|
|
158
|
+
/** Fire a one-way message (simplex / fire & forget) */
|
|
116
159
|
fire<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
117
160
|
void this.send({ kind: "fire", name, payload });
|
|
118
161
|
}
|
|
119
162
|
|
|
163
|
+
/** Listen for messages on a channel (events, streams, pub-sub) */
|
|
120
164
|
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
|
|
121
165
|
const existing = this.listeners.get(name) ?? new Set<MessageCallback>();
|
|
122
166
|
existing.add(callback as MessageCallback);
|
|
@@ -130,30 +174,97 @@ class ConnectionClient {
|
|
|
130
174
|
}
|
|
131
175
|
};
|
|
132
176
|
}
|
|
177
|
+
|
|
178
|
+
/** Register a handler for backend-initiated calls (backend calling frontend) */
|
|
179
|
+
handle(name: string, handler: CallHandler): () => void {
|
|
180
|
+
this.callHandlers.set(name, handler);
|
|
181
|
+
return () => {
|
|
182
|
+
this.callHandlers.delete(name);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
133
185
|
}
|
|
134
186
|
|
|
135
187
|
const _client = new ConnectionClient();
|
|
136
188
|
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// Semantic Connect Proxy
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Usage: connect.namespace.method(...args)
|
|
193
|
+
//
|
|
194
|
+
// The proxy automatically routes to the correct wire format:
|
|
195
|
+
// - connect.user.fetch(123) → _client.call('user.fetch', 123)
|
|
196
|
+
// - connect.app.onNotify(cb) → _client.on('app.onNotify', cb)
|
|
197
|
+
// - connect.ui.handlePrompt = fn → _client.handle('ui.handlePrompt', fn)
|
|
198
|
+
// - connect.system.minimize() → _client.fire('system.minimize', {})
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
137
201
|
/**
|
|
138
|
-
* connect
|
|
139
|
-
*
|
|
140
|
-
* Use for
|
|
141
|
-
*
|
|
142
|
-
*
|
|
202
|
+
* connect — Semantic channel proxy
|
|
203
|
+
*
|
|
204
|
+
* Use this for all custom backend ↔ frontend communication.
|
|
205
|
+
* Just write `connect.namespace.method()` and run `plusui connect`.
|
|
206
|
+
*
|
|
207
|
+
* The proxy works even BEFORE running `plusui connect` — it dynamically
|
|
208
|
+
* infers intent from use. Running `plusui connect` generates type
|
|
209
|
+
* declarations for IDE autocomplete and compile-time safety.
|
|
143
210
|
*/
|
|
144
|
-
export const connect = {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
211
|
+
export const connect: Record<string, Record<string, any>> = new Proxy({} as any, {
|
|
212
|
+
get(_target, namespace: string) {
|
|
213
|
+
if (namespace === '__esModule' || namespace === 'then' || typeof namespace === 'symbol') {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Return a proxy for the namespace that intercepts method calls
|
|
218
|
+
return new Proxy({} as any, {
|
|
219
|
+
get(_nsTarget, method: string) {
|
|
220
|
+
if (typeof method === 'symbol') return undefined;
|
|
221
|
+
|
|
222
|
+
const wireName = `${namespace}.${method}`;
|
|
223
|
+
|
|
224
|
+
// If method starts with 'on' + uppercase → event listener
|
|
225
|
+
if (/^on[A-Z]/.test(method)) {
|
|
226
|
+
return <T = unknown>(cb: (data: T) => void) => _client.on<T>(wireName, cb);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// If method starts with 'handle' + uppercase → register a handler for backend calls
|
|
230
|
+
if (/^handle[A-Z]/.test(method)) {
|
|
231
|
+
return (handler: CallHandler) => _client.handle(wireName, handler);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Default: a callable function
|
|
235
|
+
// We return a function that can be either awaited (call) or just invoked (fire).
|
|
236
|
+
// The generated code will be more specific, but this proxy handles the dynamic case.
|
|
237
|
+
return (...args: unknown[]) => {
|
|
238
|
+
const payload = args.length === 1 ? args[0] : args.length === 0 ? {} : args;
|
|
239
|
+
// We use call (request/response) by default so `await` works.
|
|
240
|
+
// If the user doesn't await, the Promise is simply ignored (fire-and-forget).
|
|
241
|
+
return _client.call(wireName, payload);
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
set(_nsTarget, method: string, value: any) {
|
|
246
|
+
if (typeof method === 'symbol') return false;
|
|
247
|
+
const wireName = `${namespace}.${method}`;
|
|
248
|
+
|
|
249
|
+
// Setting a handler: connect.user.handleFetch = (data) => { ... }
|
|
250
|
+
if (typeof value === 'function') {
|
|
251
|
+
_client.handle(wireName, value);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
},
|
|
256
|
+
});
|
|
150
257
|
},
|
|
151
|
-
|
|
152
|
-
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// Feature-scoped connect (for framework internals)
|
|
262
|
+
// ============================================================================
|
|
153
263
|
|
|
154
264
|
export type FeatureConnect = {
|
|
155
265
|
on: <TData = unknown>(name: string, callback: (payload: TData) => void) => (() => void);
|
|
156
266
|
emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
|
|
267
|
+
call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn) => Promise<TOut>;
|
|
157
268
|
};
|
|
158
269
|
|
|
159
270
|
function scopeName(scope: string, name: string): string {
|
|
@@ -183,6 +294,10 @@ export function createFeatureConnect(scope: string): FeatureConnect {
|
|
|
183
294
|
window.removeEventListener(`plusui:${scoped}`, domHandler as EventListener);
|
|
184
295
|
};
|
|
185
296
|
},
|
|
297
|
+
call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> => {
|
|
298
|
+
const scoped = scopeName(scope, name);
|
|
299
|
+
return _client.call<TOut, TIn>(scoped, payload);
|
|
300
|
+
},
|
|
186
301
|
};
|
|
187
302
|
}
|
|
188
303
|
|
|
@@ -25,11 +25,21 @@ g.__response__ = function(id: string, result: any, error?: any) {
|
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
// Called by C++ when files are dropped on a named zone
|
|
28
29
|
g.__plusui_fileDrop__ = function(zoneName: string, files: FileInfo[]) {
|
|
29
30
|
const zone = zoneCallbacks.get(zoneName);
|
|
30
31
|
if (zone && zone.onFiles) zone.onFiles(files);
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
// Called by C++ when the drop point doesn't hit any named zone — deliver to
|
|
35
|
+
// all registered zones so a single-zone app always works regardless of DPI
|
|
36
|
+
// scaling or hit-test accuracy.
|
|
37
|
+
g.__plusui_fileDrop_default__ = function(files: FileInfo[]) {
|
|
38
|
+
for (const zone of zoneCallbacks.values()) {
|
|
39
|
+
if (zone && zone.onFiles) zone.onFiles(files);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
33
43
|
async function invoke<T>(method: string, params: any[] = []): Promise<T> {
|
|
34
44
|
return new Promise((resolve, reject) => {
|
|
35
45
|
const id = String(++callId);
|
|
@@ -55,32 +65,39 @@ export interface DropZone {
|
|
|
55
65
|
|
|
56
66
|
export function createDropZone(name: string, el?: HTMLElement | null): DropZone {
|
|
57
67
|
const element = el || document.querySelector(`[data-dropzone="${name}"]`) as HTMLElement;
|
|
58
|
-
|
|
68
|
+
|
|
59
69
|
if (element) {
|
|
60
70
|
element.setAttribute('data-dropzone', name);
|
|
61
|
-
|
|
62
|
-
// Prevent browser default on drop (file navigation)
|
|
63
|
-
//
|
|
71
|
+
|
|
72
|
+
// Prevent browser default on drop (file navigation).
|
|
73
|
+
// Actual file delivery comes from C++ via WM_DROPFILES → __plusui_fileDrop__.
|
|
64
74
|
element.addEventListener('drop', (e: DragEvent) => {
|
|
65
75
|
e.preventDefault();
|
|
66
76
|
e.stopPropagation();
|
|
67
77
|
element.classList.remove('dropzone-active');
|
|
68
78
|
});
|
|
69
|
-
|
|
70
|
-
// Visual feedback: the injected C++ script manages dropzone-active
|
|
71
|
-
// via document-level listeners, but we also handle cleanup here
|
|
79
|
+
|
|
72
80
|
element.addEventListener('dragleave', (e: DragEvent) => {
|
|
73
|
-
// Only remove if actually leaving the element (not entering a child)
|
|
74
81
|
const related = e.relatedTarget as Node | null;
|
|
75
82
|
if (!related || !element.contains(related)) {
|
|
76
83
|
element.classList.remove('dropzone-active');
|
|
77
84
|
}
|
|
78
85
|
});
|
|
79
|
-
|
|
86
|
+
|
|
87
|
+
element.addEventListener('dragenter', (e: DragEvent) => {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
element.classList.add('dropzone-active');
|
|
90
|
+
if (e.dataTransfer) {
|
|
91
|
+
// 'move' matches the native OS dropzone cursor (arrow + file icon)
|
|
92
|
+
// rather than the browser's '+ Copy' cursor
|
|
93
|
+
try { e.dataTransfer.dropEffect = 'move'; } catch (_) {}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
80
97
|
element.addEventListener('dragover', (e: DragEvent) => {
|
|
81
98
|
e.preventDefault();
|
|
82
99
|
if (e.dataTransfer) {
|
|
83
|
-
try { e.dataTransfer.dropEffect = '
|
|
100
|
+
try { e.dataTransfer.dropEffect = 'move'; } catch (_) {}
|
|
84
101
|
}
|
|
85
102
|
});
|
|
86
103
|
}
|