gui/pkg/display/preload.go

940 lines
31 KiB
Go
Raw Normal View History

package display
import (
2026-04-15 19:20:58 +01:00
"net"
"net/url"
"strings"
core "dappco.re/go/core"
)
type PreloadTarget interface {
ExecJS(string)
}
2026-04-15 14:31:11 +01:00
// InjectPreload injects the page preload script into a live webview.
// Use: _ = display.InjectPreload(webview, "https://example.com")
func (s *Service) InjectPreload(webview PreloadTarget, origin string) error {
script, err := s.BuildPreloadScript(origin)
if err != nil {
return err
}
if strings.TrimSpace(script) == "" {
return nil
}
webview.ExecJS(script)
return nil
}
2026-04-15 14:31:11 +01:00
// BuildPreloadScript returns the JavaScript bootstrap that CoreGUI injects
// before page code runs.
// Use: script, _ := display.BuildPreloadScript("https://example.com")
func (s *Service) BuildPreloadScript(pageURL string) (string, error) {
trustedOrigin := trustedPreloadOrigin(pageURL)
2026-04-15 19:00:02 +01:00
storageBootstrap := map[string]map[string]string{}
if s.storage != nil {
storageBootstrap = s.storage.Snapshot(pageURL)
}
parts := []string{
s.injectStoragePolyfills(pageURL, storageBootstrap, trustedOrigin),
s.injectCoreMLShim(trustedOrigin),
2026-04-15 14:48:12 +01:00
s.buildHLCRFComponents(pageURL),
2026-04-15 17:38:58 +01:00
}
if trustedOrigin {
parts = append(parts,
s.injectBackgroundServiceShims(),
s.injectElectronShim(),
)
}
2026-04-15 19:20:58 +01:00
if appPreloads, err := s.injectAppPreloads(pageURL); err != nil {
if !strings.Contains(err.Error(), "view manifest not found") {
return "", err
}
} else if strings.TrimSpace(appPreloads) != "" {
2026-04-15 17:38:58 +01:00
parts = append(parts, appPreloads)
}
return strings.Join(parts, "\n"), nil
}
func trustedPreloadOrigin(pageURL string) bool {
trimmed := strings.TrimSpace(pageURL)
if trimmed == "" {
return false
}
parsed, err := url.Parse(trimmed)
if err != nil {
return false
}
switch strings.ToLower(parsed.Scheme) {
case "core", "file", "wails", "app":
return true
case "http", "https":
host := strings.TrimSpace(parsed.Host)
if host == "" {
return false
}
name := host
if parsedHost, _, err := net.SplitHostPort(host); err == nil {
name = parsedHost
}
name = strings.Trim(strings.ToLower(name), "[]")
switch name {
case "localhost", "127.0.0.1", "::1":
return true
default:
return false
}
default:
return false
}
}
2026-04-15 19:20:58 +01:00
func validatedLocalMLAPIURL(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return "http://localhost:8090"
}
parsed, err := url.Parse(trimmed)
if err != nil {
return "http://localhost:8090"
}
switch strings.ToLower(parsed.Scheme) {
case "http", "https":
default:
return "http://localhost:8090"
}
host := strings.TrimSpace(parsed.Host)
if host == "" {
return "http://localhost:8090"
}
name := host
if parsedHost, _, err := net.SplitHostPort(host); err == nil {
name = parsedHost
}
name = strings.Trim(strings.ToLower(name), "[]")
switch name {
case "localhost", "127.0.0.1", "::1":
return strings.TrimRight(parsed.String(), "/")
default:
return "http://localhost:8090"
}
}
func (s *Service) injectStoragePolyfills(pageOrigin string, bootstrap map[string]map[string]string, trustedOrigin bool) string {
return `(function() {
2026-04-15 19:00:02 +01:00
const __corePageURL = ` + core.JSONMarshalString(pageOrigin) + `;
const __coreOrigin = ` + core.JSONMarshalString(storageOriginForPageURL(pageOrigin)) + ` || __corePageURL;
const __coreCanInvoke = ` + core.JSONMarshalString(trustedOrigin) + `;
2026-04-15 19:00:02 +01:00
const __coreBootstrapStorage = ` + core.JSONMarshalString(bootstrap) + `;
const __coreScopes = globalThis.__coreStorageScopes || (globalThis.__coreStorageScopes = {});
const __scope = __coreScopes[__coreOrigin] || (__coreScopes[__coreOrigin] = { localStorage: {}, sessionStorage: {}, cookies: {}, indexedDB: {}, caches: {}, buckets: {}, opfs: {} });
2026-04-15 14:48:12 +01:00
const __coreBridge = globalThis.__coreBridge || (globalThis.__coreBridge = {
invoke(route, payload) {
if (!__coreCanInvoke) {
return Promise.reject(new Error("Core bridge unavailable for this origin"));
}
2026-04-15 14:48:12 +01:00
if (typeof globalThis.__CORE_GUI_INVOKE__ === 'function') {
return Promise.resolve(globalThis.__CORE_GUI_INVOKE__(route, payload));
}
return Promise.resolve({ route, payload, bridged: false });
}
});
2026-04-15 19:00:02 +01:00
const hydrateBucket = (bucketName, target, source) => {
if (!source || typeof source !== "object") {
return;
}
const parseMaybeJSON = (value) => {
if (typeof value !== "string") {
return value;
}
try {
return JSON.parse(value);
} catch (_) {
return value;
}
};
for (const [key, value] of Object.entries(source)) {
if (bucketName === "cookies") {
try {
target[key] = typeof value === "string" ? JSON.parse(value) : value;
} catch (_) {
target[key] = { value: String(value ?? "") };
}
continue;
}
if (bucketName.startsWith("cache:")) {
const cacheName = bucketName.slice("cache:".length);
target[cacheName] = target[cacheName] || {};
target[cacheName][key] = parseMaybeJSON(value);
continue;
}
if (bucketName.startsWith("indexeddb:")) {
const databaseName = bucketName.slice("indexeddb:".length);
const [storeName, recordKey] = key.split(":", 2);
target[databaseName] = target[databaseName] || { stores: {} };
target[databaseName].stores = target[databaseName].stores || {};
target[databaseName].stores[storeName] = target[databaseName].stores[storeName] || {};
target[databaseName].stores[storeName][recordKey] = parseMaybeJSON(value);
continue;
}
if (bucketName.startsWith("storageBucket:")) {
const bucketNameValue = bucketName.slice("storageBucket:".length);
target[bucketNameValue] = target[bucketNameValue] || { kv: {}, files: {} };
target[bucketNameValue].kv = target[bucketNameValue].kv || {};
target[bucketNameValue].kv[key] = value;
continue;
}
if (bucketName === "opfs") {
target[key] = { contents: value };
continue;
}
target[key] = value;
}
};
Object.entries(__coreBootstrapStorage || {}).forEach(([bucketName, bucket]) => {
if (bucketName.startsWith("cache:")) {
hydrateBucket(bucketName, __scope.caches, bucket);
return;
}
if (bucketName.startsWith("indexeddb:")) {
hydrateBucket(bucketName, __scope.indexedDB, bucket);
return;
}
if (bucketName.startsWith("storageBucket:")) {
hydrateBucket(bucketName, __scope.buckets, bucket);
return;
}
if (bucketName === "opfs") {
hydrateBucket(bucketName, __scope.opfs, bucket);
return;
}
if (!__scope[bucketName]) {
__scope[bucketName] = {};
}
hydrateBucket(bucketName, __scope[bucketName], bucket);
});
2026-04-15 14:48:12 +01:00
const persist = (bucket, key, value) => {
if (!__coreCanInvoke) {
return;
}
2026-04-15 19:00:02 +01:00
if (bucket === "sessionStorage") {
return;
}
2026-04-15 14:48:12 +01:00
__coreBridge.invoke('display.storage.set', { origin: __coreOrigin, bucket, key, value }).catch(() => undefined);
};
const createStorage = (bucketName, bucket) => ({
getItem(key) { return Object.prototype.hasOwnProperty.call(bucket, key) ? String(bucket[key]) : null; },
setItem(key, value) { bucket[key] = String(value); persist(bucketName, key, bucket[key]); },
removeItem(key) { delete bucket[key]; persist(bucketName, key, ''); },
clear() { Object.keys(bucket).forEach((key) => { delete bucket[key]; persist(bucketName, key, ''); }); },
key(index) { return Object.keys(bucket)[index] ?? null; },
get length() { return Object.keys(bucket).length; }
});
globalThis.core = globalThis.core || {};
globalThis.core.storage = globalThis.core.storage || {};
globalThis.core.storage.local = createStorage('localStorage', __scope.localStorage);
globalThis.core.storage.session = createStorage('sessionStorage', __scope.sessionStorage);
globalThis.core.storage.cookies = createStorage('cookies', __scope.cookies);
2026-04-15 19:00:02 +01:00
const currentLocation = () => globalThis.location || {};
const cookieValue = (record) => {
if (record && typeof record === "object" && !Array.isArray(record)) {
return String(record.value ?? "");
}
return String(record ?? "");
};
const cookieIsExpired = (record) => {
if (!record || typeof record !== "object" || Array.isArray(record)) {
return false;
}
const expiresAt = record.expires ? Date.parse(record.expires) : NaN;
return Number.isFinite(expiresAt) && expiresAt <= Date.now();
};
const cookieMatchesPath = (record) => {
const path = String(currentLocation().pathname || "/");
const cookiePath = (record && typeof record === "object" && record.path) ? String(record.path) : "/";
return path === cookiePath || path.startsWith(cookiePath.endsWith("/") ? cookiePath : cookiePath + "/") || cookiePath === "/";
};
const cookieMatchesDomain = (record) => {
const hostname = String(currentLocation().hostname || "");
const domain = (record && typeof record === "object" && record.domain) ? String(record.domain).replace(/^\./, "") : "";
if (!domain) {
return true;
}
return hostname === domain || hostname.endsWith("." + domain);
};
const cookieMatchesSecure = (record) => {
if (!record || typeof record !== "object" || Array.isArray(record) || !record.secure) {
return true;
}
const protocol = String(currentLocation().protocol || "");
return protocol === "https:" || protocol === "wss:" || currentLocation().hostname === "localhost";
};
const cookieEntries = () => Object.entries(__scope.cookies)
.filter(([, record]) => !cookieIsExpired(record) && cookieMatchesPath(record) && cookieMatchesDomain(record) && cookieMatchesSecure(record))
.map(([name, record]) => name + "=" + cookieValue(record))
.join("; ");
2026-04-15 14:31:11 +01:00
const setCookie = (value) => {
const rawCookie = String(value ?? "");
2026-04-15 19:00:02 +01:00
const parts = rawCookie.split(";").map((part) => part.trim()).filter(Boolean);
const [pair, ...attributes] = parts;
if (!pair) {
return;
}
2026-04-15 14:31:11 +01:00
const separatorIndex = pair.indexOf("=");
if (separatorIndex < 0) {
return;
}
const name = pair.slice(0, separatorIndex).trim();
if (!name) {
return;
}
2026-04-15 19:00:02 +01:00
const record = {
value: pair.slice(separatorIndex + 1).trim(),
path: "/",
domain: String(currentLocation().hostname || ""),
secure: false,
sameSite: "Lax"
};
attributes.forEach((attribute) => {
const [rawKey, ...rawValue] = attribute.split("=");
const key = String(rawKey || "").trim().toLowerCase();
const value = rawValue.join("=").trim();
switch (key) {
case "expires":
if (value) {
const expiresAt = new Date(value);
if (!Number.isNaN(expiresAt.getTime())) {
record.expires = expiresAt.toISOString();
}
}
break;
case "max-age": {
const seconds = Number.parseInt(value, 10);
if (Number.isFinite(seconds)) {
record.expires = new Date(Date.now() + (seconds * 1000)).toISOString();
}
break;
}
case "path":
record.path = value || "/";
break;
case "domain":
record.domain = value.replace(/^\./, "");
break;
case "secure":
record.secure = true;
break;
case "samesite":
record.sameSite = value || "Lax";
break;
}
});
if (cookieIsExpired(record)) {
delete __scope.cookies[name];
persist('cookies', name, '');
return;
}
__scope.cookies[name] = record;
persist('cookies', name, JSON.stringify(record));
2026-04-15 14:48:12 +01:00
};
const asyncResult = (value) => Promise.resolve(value);
const createIDBRequest = (result) => {
const request = { result, error: null, onsuccess: null, onerror: null, onupgradeneeded: null };
queueMicrotask(() => {
request.onupgradeneeded?.({ target: request });
request.onsuccess?.({ target: request });
});
return request;
};
2026-04-15 19:00:02 +01:00
const createObjectStore = (databaseName, database, storeName) => ({
2026-04-15 14:48:12 +01:00
put(value, key) {
database.stores[storeName] = database.stores[storeName] || {};
const resolvedKey = key ?? value?.id ?? crypto.randomUUID?.() ?? String(Date.now());
database.stores[storeName][resolvedKey] = value;
2026-04-15 19:00:02 +01:00
persist('indexeddb:' + databaseName, storeName + ':' + resolvedKey, JSON.stringify(value));
2026-04-15 14:48:12 +01:00
return createIDBRequest(resolvedKey);
},
get(key) {
return createIDBRequest(database.stores?.[storeName]?.[key]);
},
getAll() {
return createIDBRequest(Object.values(database.stores?.[storeName] || {}));
},
delete(key) {
if (database.stores?.[storeName]) {
delete database.stores[storeName][key];
}
2026-04-15 19:00:02 +01:00
persist('indexeddb:' + databaseName, storeName + ':' + key, '');
2026-04-15 14:48:12 +01:00
return createIDBRequest(undefined);
},
clear() {
database.stores[storeName] = {};
return createIDBRequest(undefined);
},
createIndex() {
return this;
}
});
const createDB = (name) => {
const database = __scope.indexedDB[name] || (__scope.indexedDB[name] = { stores: {} });
return {
name,
createObjectStore(storeName) {
database.stores[storeName] = database.stores[storeName] || {};
2026-04-15 19:00:02 +01:00
return createObjectStore(name, database, storeName);
2026-04-15 14:48:12 +01:00
},
transaction(storeNames) {
const names = Array.isArray(storeNames) ? storeNames : [storeNames];
return {
objectStore(storeName) {
const target = names.includes(storeName) ? storeName : names[0];
2026-04-15 19:00:02 +01:00
return createObjectStore(name, database, target);
2026-04-15 14:48:12 +01:00
}
};
},
close() {}
};
};
const cachesAPI = {
async open(name) {
const bucket = __scope.caches[name] || (__scope.caches[name] = {});
return {
async match(request) {
return bucket[typeof request === 'string' ? request : request?.url] ?? undefined;
},
async put(request, response) {
const key = typeof request === 'string' ? request : request?.url;
bucket[key] = response;
2026-04-15 19:00:02 +01:00
persist('cache:' + name, key, JSON.stringify(response));
2026-04-15 14:48:12 +01:00
},
async delete(request) {
const key = typeof request === 'string' ? request : request?.url;
delete bucket[key];
2026-04-15 19:00:02 +01:00
persist('cache:' + name, key, '');
2026-04-15 14:48:12 +01:00
return true;
},
async keys() {
return Object.keys(bucket);
}
};
},
async keys() {
return Object.keys(__scope.caches);
}
};
const bucketAPI = {
async open(name) {
const bucket = __scope.buckets[name] || (__scope.buckets[name] = { kv: {}, files: {} });
return {
name,
persisted() { return asyncResult(true); },
durability: 'strict',
async getDirectory() { return bucket.files; },
storage: createStorage('storageBucket:' + name, bucket.kv)
2026-04-15 14:48:12 +01:00
};
}
};
const opfsRoot = {
async getDirectoryHandle(name, options) {
if (!__scope.opfs[name] || options?.create) {
__scope.opfs[name] = __scope.opfs[name] || {};
}
return __scope.opfs[name];
},
async getFileHandle(name, options) {
if (!__scope.opfs[name] || options?.create) {
__scope.opfs[name] = __scope.opfs[name] || { contents: '' };
}
return {
async createWritable() {
return {
async write(contents) {
__scope.opfs[name].contents = String(contents);
persist('opfs', name, __scope.opfs[name].contents);
},
async close() {}
};
},
async getFile() {
return new File([__scope.opfs[name].contents || ''], name);
}
};
}
2026-04-15 14:31:11 +01:00
};
try {
if (!globalThis.localStorage) {
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
enumerable: true,
get() { return globalThis.core.storage.local; }
});
}
} catch (_) {}
try {
if (!globalThis.sessionStorage) {
Object.defineProperty(globalThis, 'sessionStorage', {
configurable: true,
enumerable: true,
get() { return globalThis.core.storage.session; }
});
}
} catch (_) {}
2026-04-15 14:31:11 +01:00
try {
if (typeof Document !== 'undefined') {
Object.defineProperty(Document.prototype, 'cookie', {
configurable: true,
enumerable: true,
get() { return cookieEntries(); },
set(value) { setCookie(value); }
});
}
} catch (_) {}
2026-04-15 14:48:12 +01:00
try {
if (!globalThis.indexedDB) {
globalThis.indexedDB = {
open(name) { return createIDBRequest(createDB(name)); },
deleteDatabase(name) {
delete __scope.indexedDB[name];
return createIDBRequest(undefined);
}
};
}
} catch (_) {}
try {
if (!globalThis.caches) {
globalThis.caches = cachesAPI;
}
} catch (_) {}
try {
globalThis.navigator = globalThis.navigator || {};
globalThis.navigator.storageBuckets = globalThis.navigator.storageBuckets || bucketAPI;
globalThis.navigator.storage = globalThis.navigator.storage || {};
if (!globalThis.navigator.storage.getDirectory) {
globalThis.navigator.storage.getDirectory = () => asyncResult(opfsRoot);
}
} catch (_) {}
})();`
}
func (s *Service) injectElectronShim() string {
return `(function() {
if (globalThis.electron) {
return;
}
const listeners = new Map();
const toEventName = (channel) => "__core_electron__:" + channel;
2026-04-15 19:25:16 +01:00
const invokeBridge = (route, payload) => (globalThis.__coreBridge?.invoke?.(route, payload) ?? Promise.resolve({ route, payload }));
const toInteger = (value) => {
const number = Number(value);
return Number.isFinite(number) ? Math.trunc(number) : 0;
};
const toBase64 = (value) => {
if (typeof value === "string") {
if (value.startsWith("data:")) {
const commaIndex = value.indexOf(",");
return commaIndex >= 0 ? value.slice(commaIndex + 1) : value;
}
return value;
}
if (value instanceof Uint8Array) {
let binary = "";
for (let i = 0; i < value.length; i++) {
binary += String.fromCharCode(value[i]);
}
return btoa(binary);
}
if (value instanceof ArrayBuffer) {
return toBase64(new Uint8Array(value));
}
if (ArrayBuffer.isView && ArrayBuffer.isView(value)) {
return toBase64(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
}
return "";
};
const roleMap = {
appmenu: 0,
filemenu: 1,
editmenu: 2,
viewmenu: 3,
windowmenu: 4,
helpmenu: 5
};
const menuRoleToCore = (role) => {
const key = String(role ?? "").toLowerCase();
return Object.prototype.hasOwnProperty.call(roleMap, key) ? roleMap[key] : undefined;
};
const menuChildren = (item) => {
const rawChildren = Array.isArray(item?.children) ? item.children : Array.isArray(item?.submenu) ? item.submenu : [];
return rawChildren.map((child) => menuItemToCore(child)).filter(Boolean);
};
const menuItemToCore = (item) => {
if (!item || typeof item !== "object") {
return null;
}
const mapped = {
label: String(item.label ?? ""),
accelerator: String(item.accelerator ?? ""),
type: String(item.type ?? "normal"),
checked: !!item.checked,
disabled: !!item.disabled,
tooltip: String(item.tooltip ?? "")
};
const role = menuRoleToCore(item.role);
if (role !== undefined) {
mapped.role = role;
}
const children = menuChildren(item);
if (children.length > 0) {
mapped.children = children;
}
return mapped;
};
const trayItemToCore = (item) => {
if (!item || typeof item !== "object") {
return null;
}
const mapped = {
label: String(item.label ?? ""),
type: String(item.type ?? "normal"),
checked: !!item.checked,
disabled: !!item.disabled,
tooltip: String(item.tooltip ?? "")
};
const actionId = String(item.actionId ?? item.action_id ?? item.id ?? "");
if (actionId) {
mapped.action_id = actionId;
}
const submenu = Array.isArray(item.submenu) ? item.submenu.map((child) => trayItemToCore(child)).filter(Boolean) : [];
if (submenu.length > 0) {
mapped.submenu = submenu;
}
return mapped;
};
const normalizeMenuTemplate = (template) => Array.isArray(template) ? template.map((item) => menuItemToCore(item)).filter(Boolean) : [];
const normalizeTrayTemplate = (template) => Array.isArray(template) ? template.map((item) => trayItemToCore(item)).filter(Boolean) : [];
const createMenu = (template) => {
const normalized = normalizeMenuTemplate(template);
return {
template: normalized,
items: normalized,
toJSON() {
return normalized;
}
};
};
const ipcRenderer = {
send(channel, ...args) {
globalThis.dispatchEvent(new CustomEvent(toEventName(channel), { detail: args }));
},
invoke(channel, ...args) {
globalThis.dispatchEvent(new CustomEvent(toEventName(channel), { detail: args }));
return Promise.resolve({ channel, args });
},
on(channel, listener) {
const handler = (event) => listener(event, ...(event.detail || []));
listeners.set(listener, handler);
globalThis.addEventListener(toEventName(channel), handler);
return () => ipcRenderer.removeListener(channel, listener);
},
once(channel, listener) {
const off = ipcRenderer.on(channel, (event, ...args) => {
off();
listener(event, ...args);
});
return off;
},
removeListener(channel, listener) {
const handler = listeners.get(listener);
if (handler) {
globalThis.removeEventListener(toEventName(channel), handler);
listeners.delete(listener);
}
}
};
const shell = {
openExternal(url) {
2026-04-15 16:50:14 +01:00
return invokeBridge('gui.browser.open', { url }).then(() => undefined);
},
openPath(path) {
2026-04-15 16:50:14 +01:00
return invokeBridge('gui.browser.openFile', { path }).then(() => "");
}
};
const clipboard = {
readText() {
2026-04-15 16:50:14 +01:00
return invokeBridge('gui.clipboard.read', {}).then((value) => {
if (typeof value === "string") {
return value;
}
return value?.text ?? value?.Text ?? "";
});
},
writeText(text) {
2026-04-15 16:50:14 +01:00
return invokeBridge('gui.clipboard.write', { text }).then(() => undefined);
}
};
2026-04-15 14:48:12 +01:00
const invokeBridge = (route, payload) => (globalThis.__coreBridge?.invoke?.(route, payload) ?? Promise.resolve({ route, payload }));
const dialog = {
showOpenDialog(options) {
return invokeBridge('gui.dialog.open', options);
},
showSaveDialog(options) {
return invokeBridge('gui.dialog.save', options);
},
showMessageBox(options) {
return invokeBridge('gui.dialog.message', options);
}
};
class CoreNotification {
constructor(title, options = {}) {
this.title = title;
this.options = options;
}
show() {
invokeBridge('gui.notification.send', { title: this.title, ...this.options });
}
close() {}
static requestPermission() {
return Promise.resolve('granted');
}
}
2026-04-15 19:25:16 +01:00
class Menu {
constructor(template = []) {
this.template = normalizeMenuTemplate(template);
this.items = this.template;
}
append(item) {
const mapped = menuItemToCore(item);
if (mapped) {
this.template.push(mapped);
this.items = this.template;
}
return this;
}
popup() {
return Promise.resolve(this);
}
toJSON() {
return this.template;
}
static buildFromTemplate(template = []) {
return createMenu(template);
}
static setApplicationMenu(menu) {
return invokeBridge('menu.setAppMenu', { task: { items: normalizeMenuTemplate(menu?.template ?? menu?.items ?? menu) } });
}
}
class Tray {
constructor(image) {
this.image = image ?? null;
if (image !== undefined) {
this.setImage(image);
}
}
setImage(image) {
this.image = image;
const data = toBase64(image);
if (data) {
return invokeBridge('systray.setIcon', { task: { data } });
}
return Promise.resolve(undefined);
}
setToolTip(tooltip) {
this.tooltip = String(tooltip ?? "");
return invokeBridge('systray.setTooltip', { task: { tooltip: this.tooltip } });
}
setTitle(label) {
this.title = String(label ?? "");
return invokeBridge('systray.setLabel', { task: { label: this.title } });
}
setContextMenu(menu) {
const normalized = normalizeTrayTemplate(menu?.template ?? menu?.items ?? menu);
this.menu = normalized;
return invokeBridge('systray.setMenu', { task: { items: normalized } });
}
destroy() {}
}
2026-04-15 14:48:12 +01:00
class BrowserWindow {
constructor(options = {}) {
this.options = options;
this.id = options.id || ('core-window-' + Math.random().toString(36).slice(2));
2026-04-15 19:25:16 +01:00
const backgroundColor = String(options.backgroundColor ?? options.backgroundColour ?? "");
const parsedColour = (() => {
if (!backgroundColor) {
return undefined;
}
const hex = backgroundColor.replace(/^#/, "");
if (hex.length === 6 || hex.length === 8) {
const offset = hex.length === 8 ? 2 : 0;
const alpha = hex.length === 8 ? parseInt(hex.slice(0, 2), 16) : 255;
const red = parseInt(hex.slice(offset, offset + 2), 16);
const green = parseInt(hex.slice(offset + 2, offset + 4), 16);
const blue = parseInt(hex.slice(offset + 4, offset + 6), 16);
if ([red, green, blue, alpha].every((value) => Number.isFinite(value))) {
return [red, green, blue, alpha];
}
}
return undefined;
})();
const windowSpec = {
Name: String(options.name ?? this.id ?? ""),
Title: String(options.title ?? options.name ?? ""),
URL: String(options.url ?? ""),
HTML: String(options.html ?? ""),
JS: String(options.js ?? ""),
Width: toInteger(options.width),
Height: toInteger(options.height),
X: toInteger(options.x),
Y: toInteger(options.y),
MinWidth: toInteger(options.minWidth),
MinHeight: toInteger(options.minHeight),
MaxWidth: toInteger(options.maxWidth),
MaxHeight: toInteger(options.maxHeight),
Frameless: options.frame === false || !!options.frameless,
Hidden: options.show === false || !!options.hidden,
AlwaysOnTop: !!options.alwaysOnTop,
DisableResize: options.resizable === false || !!options.disableResize,
EnableFileDrop: !!options.enableFileDrop
};
if (parsedColour) {
windowSpec.BackgroundColour = parsedColour;
}
invokeBridge('window.open', { task: { window: windowSpec } });
2026-04-15 14:48:12 +01:00
}
loadURL(url) { return invokeBridge('webview.navigate', { name: this.id, url }); }
show() { return invokeBridge('window.setVisibility', { name: this.id, visible: true }); }
hide() { return invokeBridge('window.setVisibility', { name: this.id, visible: false }); }
2026-04-15 14:48:12 +01:00
close() { return invokeBridge('window.close', { name: this.id }); }
}
globalThis.Notification = globalThis.Notification || CoreNotification;
2026-04-15 19:25:16 +01:00
globalThis.electron = { ipcRenderer, shell, clipboard, dialog, Menu, Tray, BrowserWindow, Notification: CoreNotification };
globalThis.require = (name) => name === "electron" ? globalThis.electron : undefined;
})();`
}
2026-04-15 14:48:12 +01:00
func (s *Service) injectBackgroundServiceShims() string {
return `(function() {
const invokeBridge = (route, payload) => (globalThis.__coreBridge?.invoke?.(route, payload) ?? Promise.resolve({ route, payload }));
if (!globalThis.navigator) {
globalThis.navigator = {};
}
globalThis.navigator.serviceWorker = globalThis.navigator.serviceWorker || {
register(scriptURL, options) {
return invokeBridge('core.background.serviceWorker.register', { scriptURL, options });
},
ready: Promise.resolve({ active: true })
};
globalThis.BackgroundFetchManager = globalThis.BackgroundFetchManager || class {
fetch(id, requests, options) { return invokeBridge('core.background.fetch', { id, requests, options }); }
};
globalThis.registration = globalThis.registration || {
sync: {
register(tag) { return invokeBridge('core.background.sync', { tag }); }
},
periodicSync: {
register(tag, options) { return invokeBridge('core.background.periodicSync', { tag, options }); }
},
pushManager: {
subscribe(options) { return invokeBridge('core.background.push.subscribe', options); }
},
paymentManager: {
instruments: {
set(key, details) { return invokeBridge('core.payment.instrument.set', { key, details }); }
}
}
};
})();`
}
func (s *Service) injectCoreMLShim(trustedOrigin bool) string {
return `(function() {
2026-04-15 19:20:58 +01:00
const __coreMLApiURL = ` + core.JSONMarshalString(validatedLocalMLAPIURL(core.Env("CORE_ML_API_URL"))) + ` || "http://localhost:8090";
const __coreCanInvoke = ` + core.JSONMarshalString(trustedOrigin) + `;
globalThis.core = globalThis.core || {};
globalThis.core.ml = globalThis.core.ml || {
async generate(input) {
const payload = typeof input === "string"
? { messages: [{ role: "user", content: input }], stream: false }
: { ...input, stream: false };
2026-04-15 14:31:11 +01:00
const response = await fetch((__coreMLApiURL || "http://localhost:8090") + "/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error("Core ML request failed: " + response.status + " " + response.statusText);
}
const body = await response.text();
try {
const parsed = JSON.parse(body);
const content = parsed?.choices?.[0]?.message?.content;
if (typeof content === "string") {
return content;
}
if (Array.isArray(content)) {
return content.map((part) => {
if (typeof part === "string") {
return part;
}
return part?.text ?? "";
}).join("");
}
if (typeof parsed?.content === "string") {
return parsed.content;
}
if (typeof parsed === "string") {
return parsed;
}
return body;
} catch (_) {
return body;
}
},
async stream(input) {
const payload = typeof input === "string"
? { messages: [{ role: "user", content: input }], stream: true }
: { ...input, stream: true };
2026-04-15 14:31:11 +01:00
return fetch((__coreMLApiURL || "http://localhost:8090") + "/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
2026-04-15 14:48:12 +01:00
},
async state() {
if (!__coreCanInvoke) {
return { available: false, models: [] };
}
return invokeBridge('display.models.state', {}).then((value) => value);
2026-04-15 14:48:12 +01:00
},
async models() {
const state = await this.state();
return state.available || state.models || [];
}
};
})();`
}
2026-04-15 17:38:58 +01:00
func (s *Service) injectAppPreloads(pageURL string) (string, error) {
2026-04-15 14:48:12 +01:00
loaded, err := s.loadManifestForOrigin(pageURL)
if err != nil || loaded == nil {
2026-04-15 17:38:58 +01:00
return "", err
2026-04-15 14:48:12 +01:00
}
scripts := make([]string, 0, len(loaded.Manifest.Preloads))
for _, preload := range loaded.Manifest.Preloads {
if preload.Enabled != nil && !*preload.Enabled {
continue
}
if inline := strings.TrimSpace(preload.Inline); inline != "" {
scripts = append(scripts, inline)
continue
}
if path := strings.TrimSpace(preload.Path); path != "" {
2026-04-15 17:38:58 +01:00
body, readErr := s.readManifestPreload(loaded.BaseDir, path)
if readErr != nil {
return "", readErr
2026-04-15 14:48:12 +01:00
}
2026-04-15 17:38:58 +01:00
scripts = append(scripts, string(body))
2026-04-15 14:48:12 +01:00
}
}
2026-04-15 17:38:58 +01:00
return strings.Join(scripts, "\n"), nil
}