@tishlang/tish-desktop-api 1.0.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/LICENSE +13 -0
- package/package.json +51 -0
- package/src/appApi.tish +66 -0
- package/src/bridge.js +74 -0
- package/src/desktopHost.tish +15 -0
- package/src/web-bridge.js +157 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Pay It Forward License (PIF)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present The Tish Project Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
This license includes a perpetual, worldwide, non-exclusive, royalty-free, irrevocable (except as stated below) grant for patent claims necessarily infringed by the use of the Software or any contributions submitted to it; terminating automatically when an entity initiates patent litigation (including a cross-claim or counterclaim) alleging that the Software or any portion thereof infringes a patent.
|
|
10
|
+
|
|
11
|
+
The software is provided "as is," without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
|
|
12
|
+
|
|
13
|
+
Recipients of the Software should pay it forward by contributing to the open-source community through actions including but not limited to providing code, documentation, tutorials, guides, support, feedback, or promoting open-source projects through advocacy or related efforts that advance innovation and community growth.
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tishlang/tish-desktop-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core UI bridge and app-api façade (invoke, listen, state.*) — no UI kit dependency",
|
|
5
|
+
"license": "PIF",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/tishlang/tish-desktop.git",
|
|
10
|
+
"directory": "packages/desktop-api"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"tish": {
|
|
20
|
+
"module": "./src/appApi.tish"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"tish": "./src/appApi.tish"
|
|
25
|
+
},
|
|
26
|
+
"./desktopHost": {
|
|
27
|
+
"tish": "./src/desktopHost.tish"
|
|
28
|
+
},
|
|
29
|
+
"./appApi": {
|
|
30
|
+
"tish": "./src/appApi.tish"
|
|
31
|
+
},
|
|
32
|
+
"./bridge": {
|
|
33
|
+
"import": "./src/bridge.js"
|
|
34
|
+
},
|
|
35
|
+
"./web": {
|
|
36
|
+
"import": "./src/web-bridge.js"
|
|
37
|
+
},
|
|
38
|
+
"./web-bridge": {
|
|
39
|
+
"import": "./src/web-bridge.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@tishlang/tish-desktop-shared": "1.0.0"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"tish",
|
|
47
|
+
"desktop",
|
|
48
|
+
"tauri",
|
|
49
|
+
"app-api"
|
|
50
|
+
]
|
|
51
|
+
}
|
package/src/appApi.tish
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Core app API — invoke / listen / state.* / Platform. No lattish dependency.
|
|
2
|
+
|
|
3
|
+
import { Platform, platformSelect, setPlatformFacts } from "@tishlang/tish-desktop-shared"
|
|
4
|
+
import { EVT_STATE_CHANGED } from "@tishlang/tish-desktop-shared"
|
|
5
|
+
|
|
6
|
+
fn bridge() {
|
|
7
|
+
let g = globalThis
|
|
8
|
+
let b = g.__TISH_APP__
|
|
9
|
+
if (b === null || b === undefined) b = g.__TISH_DESKTOP__
|
|
10
|
+
if (b === null || b === undefined) {
|
|
11
|
+
console.error("tish-app bridge not installed — import bridge.js or web-bridge.js first")
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
return b
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export fn getCurrentWindowLabel() {
|
|
18
|
+
let b = bridge()
|
|
19
|
+
if (b === null) return "main"
|
|
20
|
+
return b.getCurrentWindowLabel()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export fn invoke(cmd, args) {
|
|
24
|
+
let b = bridge()
|
|
25
|
+
let a = args === null || args === undefined ? {} : args
|
|
26
|
+
return b.invoke(cmd, a)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export fn listen(eventName, handler) {
|
|
30
|
+
let b = bridge()
|
|
31
|
+
return b.listen(eventName, handler)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export fn emit(eventName, payload) {
|
|
35
|
+
let b = bridge()
|
|
36
|
+
return b.emit(eventName, payload)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Plan contract: state.get({ path }) */
|
|
40
|
+
export fn stateGet(path) {
|
|
41
|
+
return invoke("state.get", { path: path })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export fn stateSet(path, value) {
|
|
45
|
+
return invoke("state.set", { path: path, value: value })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export fn statePatch(path, value) {
|
|
49
|
+
return invoke("state.patch", { path: path, value: value })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Subscribe to `state:changed` for `path` (or all paths if null).
|
|
54
|
+
* Returns an unsubscribe promise/handle from listen.
|
|
55
|
+
*/
|
|
56
|
+
export fn useSharedState(path, onChange) {
|
|
57
|
+
return listen(EVT_STATE_CHANGED, (payload) => {
|
|
58
|
+
let p = payload.path
|
|
59
|
+
if (p === undefined) p = payload.key
|
|
60
|
+
if (path !== null && path !== undefined && p !== path) return
|
|
61
|
+
if (onChange) onChange(payload.value, p, payload.revision, payload.source)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { Platform, platformSelect, setPlatformFacts }
|
|
66
|
+
export const PROTOCOL = "desktop/v1"
|
package/src/bridge.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injects window.__TISH_APP__ (and compat __TISH_DESKTOP__).
|
|
3
|
+
* Rebind-safe for Vite HMR — call installBridge() from UI boot and hot.accept.
|
|
4
|
+
*
|
|
5
|
+
* Prefer an existing **host** bridge (tish-macos / tish-ios WK bootstrap with
|
|
6
|
+
* `webkit.messageHandlers.tish` + `__dispatch`) over the Tauri transport. Nested
|
|
7
|
+
* `<webview bridge>` pages load this module after document-start injection; overwriting
|
|
8
|
+
* with Tauri broke `state:changed` sync and `invoke` in hybrid SC4.
|
|
9
|
+
*/
|
|
10
|
+
export function installBridge() {
|
|
11
|
+
const g = typeof window !== "undefined" ? window : globalThis;
|
|
12
|
+
|
|
13
|
+
const existing = g.__TISH_APP__ || g.__TISH_DESKTOP__;
|
|
14
|
+
const hostWk =
|
|
15
|
+
typeof g.webkit !== "undefined" &&
|
|
16
|
+
g.webkit?.messageHandlers?.tish != null;
|
|
17
|
+
// Nested host WK (tish-macos/ios bootstrap): never replace with Tauri.
|
|
18
|
+
// Bootstrap has `__dispatch` for broker → pane events (`state:changed`).
|
|
19
|
+
if (hostWk) {
|
|
20
|
+
if (existing && typeof existing.__dispatch === "function") {
|
|
21
|
+
g.__TISH_APP__ = existing;
|
|
22
|
+
g.__TISH_DESKTOP__ = existing;
|
|
23
|
+
return existing;
|
|
24
|
+
}
|
|
25
|
+
console.warn(
|
|
26
|
+
"[tish-app] WK host bridge present but __dispatch missing — skip Tauri install (reload if sync is broken)"
|
|
27
|
+
);
|
|
28
|
+
if (existing) return existing;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!g.__TAURI__?.core?.invoke) {
|
|
32
|
+
console.warn("[tish-app] Tauri core.invoke not available yet");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const api = {
|
|
36
|
+
protocol: "desktop/v1",
|
|
37
|
+
surface: "webview",
|
|
38
|
+
getCurrentWindowLabel() {
|
|
39
|
+
try {
|
|
40
|
+
return g.__TAURI__?.webviewWindow?.getCurrentWebviewWindow?.()?.label ?? "main";
|
|
41
|
+
} catch {
|
|
42
|
+
return "main";
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async invoke(cmd, args = {}) {
|
|
46
|
+
const core = g.__TAURI__?.core;
|
|
47
|
+
if (!core?.invoke) {
|
|
48
|
+
throw new Error("Tauri core.invoke unavailable");
|
|
49
|
+
}
|
|
50
|
+
return core.invoke("desktop_invoke", { cmd, args });
|
|
51
|
+
},
|
|
52
|
+
async listen(eventName, handler) {
|
|
53
|
+
const event = g.__TAURI__?.event;
|
|
54
|
+
if (!event?.listen) {
|
|
55
|
+
throw new Error("Tauri event.listen unavailable");
|
|
56
|
+
}
|
|
57
|
+
return event.listen(eventName, (e) => handler(e.payload));
|
|
58
|
+
},
|
|
59
|
+
async emit(eventName, payload) {
|
|
60
|
+
const event = g.__TAURI__?.event;
|
|
61
|
+
if (!event?.emit) return;
|
|
62
|
+
return event.emit(eventName, payload);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
g.__TISH_APP__ = api;
|
|
67
|
+
g.__TISH_DESKTOP__ = api; // compat alias
|
|
68
|
+
return api;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getBridge() {
|
|
72
|
+
const g = typeof window !== "undefined" ? window : globalThis;
|
|
73
|
+
return g.__TISH_APP__ || g.__TISH_DESKTOP__ || installBridge();
|
|
74
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// UI façade over window.__TISH_APP__ / __TISH_DESKTOP__ (injected by bridge.js).
|
|
2
|
+
// Re-exports app-api helpers for backward compatibility.
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
getCurrentWindowLabel,
|
|
6
|
+
invoke,
|
|
7
|
+
listen,
|
|
8
|
+
emit,
|
|
9
|
+
stateGet,
|
|
10
|
+
stateSet,
|
|
11
|
+
statePatch,
|
|
12
|
+
useSharedState,
|
|
13
|
+
Platform,
|
|
14
|
+
PROTOCOL,
|
|
15
|
+
} from "./appApi.tish"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-browser stub bridge (`--surface web`).
|
|
3
|
+
* Plan contract: state.* uses `path` + revision; unsupported → `{ code, capability, platform }`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const memory = new Map();
|
|
7
|
+
let rev = 0;
|
|
8
|
+
const listeners = new Map();
|
|
9
|
+
|
|
10
|
+
function unsupported(capability) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
code: "unsupported",
|
|
14
|
+
capability,
|
|
15
|
+
platform: "web",
|
|
16
|
+
message: `${capability} is not supported on web`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function emitLocal(eventName, payload) {
|
|
21
|
+
const set = listeners.get(eventName);
|
|
22
|
+
if (set) {
|
|
23
|
+
for (const h of set) {
|
|
24
|
+
try {
|
|
25
|
+
h(payload);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error("[tish-app web-bridge]", e);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (typeof window !== "undefined") {
|
|
32
|
+
window.dispatchEvent(new CustomEvent(`tish:${eventName}`, { detail: payload }));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pathOf(args) {
|
|
37
|
+
return args.path ?? args.key;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function invokeState(cmd, args = {}) {
|
|
41
|
+
const path = pathOf(args);
|
|
42
|
+
if (cmd === "state.get") {
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
path,
|
|
46
|
+
value: memory.has(path) ? memory.get(path) : null,
|
|
47
|
+
revision: rev,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (cmd === "state.set") {
|
|
51
|
+
memory.set(path, args.value);
|
|
52
|
+
rev += 1;
|
|
53
|
+
const payload = {
|
|
54
|
+
path,
|
|
55
|
+
value: args.value,
|
|
56
|
+
revision: rev,
|
|
57
|
+
source: "web",
|
|
58
|
+
};
|
|
59
|
+
emitLocal("state:changed", payload);
|
|
60
|
+
return { ok: true, path, value: args.value, revision: rev };
|
|
61
|
+
}
|
|
62
|
+
if (cmd === "state.patch") {
|
|
63
|
+
const prev = memory.get(path) ?? {};
|
|
64
|
+
const patch = args.value ?? args.patch ?? {};
|
|
65
|
+
const next =
|
|
66
|
+
typeof prev === "object" && prev && typeof patch === "object" && patch
|
|
67
|
+
? { ...prev, ...patch }
|
|
68
|
+
: patch;
|
|
69
|
+
memory.set(path, next);
|
|
70
|
+
rev += 1;
|
|
71
|
+
emitLocal("state:changed", { path, value: next, revision: rev, source: "web" });
|
|
72
|
+
return { ok: true, path, value: next, revision: rev };
|
|
73
|
+
}
|
|
74
|
+
if (cmd === "state.keys") {
|
|
75
|
+
return { ok: true, keys: [...memory.keys()] };
|
|
76
|
+
}
|
|
77
|
+
if (cmd === "state.delete") {
|
|
78
|
+
const deleted = memory.delete(path);
|
|
79
|
+
if (deleted) {
|
|
80
|
+
rev += 1;
|
|
81
|
+
emitLocal("state:changed", { path, value: null, revision: rev, source: "web" });
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, deleted, revision: rev };
|
|
84
|
+
}
|
|
85
|
+
if (cmd === "state.surfaces") {
|
|
86
|
+
// Pure web has no native surface registry; expose the logical web surface.
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
surfaces: [{ id: "web", kind: "web", label: "web" }],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function invokeNotification(cmd, args = {}) {
|
|
96
|
+
if (cmd === "notification.permissionState") {
|
|
97
|
+
const perm =
|
|
98
|
+
typeof Notification !== "undefined" ? Notification.permission : "denied";
|
|
99
|
+
return { ok: true, state: perm };
|
|
100
|
+
}
|
|
101
|
+
if (cmd === "notification.requestPermission") {
|
|
102
|
+
if (typeof Notification === "undefined") return unsupported("notification");
|
|
103
|
+
const state = await Notification.requestPermission();
|
|
104
|
+
return { ok: true, state };
|
|
105
|
+
}
|
|
106
|
+
if (cmd === "notification.show") {
|
|
107
|
+
if (typeof Notification === "undefined") return unsupported("notification");
|
|
108
|
+
if (Notification.permission !== "granted") {
|
|
109
|
+
await Notification.requestPermission();
|
|
110
|
+
}
|
|
111
|
+
if (Notification.permission !== "granted") {
|
|
112
|
+
return { ok: false, code: "denied", capability: "notification", message: "permission denied" };
|
|
113
|
+
}
|
|
114
|
+
new Notification(args.title || "", { body: args.body || "" });
|
|
115
|
+
return { ok: true };
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function installWebBridge() {
|
|
121
|
+
const g = typeof window !== "undefined" ? window : globalThis;
|
|
122
|
+
|
|
123
|
+
const api = {
|
|
124
|
+
protocol: "desktop/v1",
|
|
125
|
+
surface: "web",
|
|
126
|
+
getCurrentWindowLabel() {
|
|
127
|
+
return "web";
|
|
128
|
+
},
|
|
129
|
+
async invoke(cmd, args = {}) {
|
|
130
|
+
const stateRes = await invokeState(cmd, args);
|
|
131
|
+
if (stateRes) return stateRes;
|
|
132
|
+
const noteRes = await invokeNotification(cmd, args);
|
|
133
|
+
if (noteRes) return noteRes;
|
|
134
|
+
if (cmd === "ping") {
|
|
135
|
+
return { ok: true, protocol: "desktop/v1", surface: "web", ts: Date.now() };
|
|
136
|
+
}
|
|
137
|
+
return unsupported(cmd.split(".")[0] || cmd);
|
|
138
|
+
},
|
|
139
|
+
async listen(eventName, handler) {
|
|
140
|
+
if (!listeners.has(eventName)) listeners.set(eventName, new Set());
|
|
141
|
+
listeners.get(eventName).add(handler);
|
|
142
|
+
return () => listeners.get(eventName)?.delete(handler);
|
|
143
|
+
},
|
|
144
|
+
async emit(eventName, payload) {
|
|
145
|
+
emitLocal(eventName, payload);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
g.__TISH_APP__ = api;
|
|
150
|
+
g.__TISH_DESKTOP__ = api;
|
|
151
|
+
return api;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getBridge() {
|
|
155
|
+
const g = typeof window !== "undefined" ? window : globalThis;
|
|
156
|
+
return g.__TISH_APP__ || g.__TISH_DESKTOP__ || installWebBridge();
|
|
157
|
+
}
|