@telepath-computer/television-desktop 0.1.107

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 ADDED
@@ -0,0 +1,29 @@
1
+ # @telepath-computer/television-desktop
2
+
3
+ Electron desktop wrapper for [Television](https://www.npmjs.com/package/@telepath-computer/television).
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm i -g @telepath-computer/television-desktop
9
+ ```
10
+
11
+ ## Run
12
+
13
+ ```sh
14
+ tv-desktop
15
+ ```
16
+
17
+ A connect window opens; point it at a running Television server (URL +
18
+ token, or a `--public` server with an empty token).
19
+
20
+ ## macOS notes
21
+
22
+ The first install runs a postinstall script that renames the bundled
23
+ `Electron.app` to `Television.app` (bundle directory + inner executable +
24
+ `Info.plist` + bundle icon). This is what gives the Dock and menu bar
25
+ "Television" instead of "Electron".
26
+
27
+ If you install with `--ignore-scripts`, the rename is skipped — the app
28
+ still works but will be labelled "Electron" everywhere macOS reads the
29
+ bundle.
Binary file
Binary file
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ const { spawn } = require("node:child_process");
3
+ const path = require("node:path");
4
+ const fs = require("node:fs");
5
+
6
+ const appDir = path.join(__dirname, "..");
7
+
8
+ function resolveExecutable() {
9
+ const electronPkgDir = path.dirname(require.resolve("electron/package.json"));
10
+ const distDir = path.join(electronPkgDir, "dist");
11
+ if (process.platform === "darwin") {
12
+ const renamed = path.join(distDir, "Television.app", "Contents", "MacOS", "Television");
13
+ if (fs.existsSync(renamed)) return renamed;
14
+ }
15
+ return require("electron");
16
+ }
17
+
18
+ const env = { ...process.env };
19
+ delete env.ELECTRON_RUN_AS_NODE;
20
+
21
+ const child = spawn(resolveExecutable(), [appDir, ...process.argv.slice(2)], {
22
+ stdio: "inherit",
23
+ env,
24
+ });
25
+
26
+ child.on("exit", (code, signal) => {
27
+ if (signal) process.kill(process.pid, signal);
28
+ else process.exit(code ?? 0);
29
+ });
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ // src/connect-preload.ts
4
+ var import_electron = require("electron");
5
+ if (window.location.protocol === "file:") {
6
+ import_electron.contextBridge.exposeInMainWorld("television", {
7
+ setConnection: (serverURL, token) => import_electron.ipcRenderer.invoke("television:set-connection", { serverURL, token })
8
+ });
9
+ }
@@ -0,0 +1,128 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'" />
6
+ <title>Television</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: dark;
10
+ }
11
+ html, body {
12
+ margin: 0;
13
+ padding: 0;
14
+ height: 100%;
15
+ background: #000;
16
+ color: #fff;
17
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
18
+ }
19
+ body {
20
+ display: flex;
21
+ flex-direction: column;
22
+ }
23
+ .drag-region {
24
+ height: 36px;
25
+ flex-shrink: 0;
26
+ -webkit-app-region: drag;
27
+ }
28
+ main {
29
+ flex: 1;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ min-height: 0;
34
+ }
35
+ form {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 14px;
39
+ width: 360px;
40
+ }
41
+ label {
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: 6px;
45
+ font-size: 11px;
46
+ color: #888;
47
+ text-transform: uppercase;
48
+ letter-spacing: 0.08em;
49
+ }
50
+ input {
51
+ background: transparent;
52
+ border: 1px solid #2a2a2a;
53
+ color: #fff;
54
+ padding: 10px 12px;
55
+ font-size: 14px;
56
+ font-family: inherit;
57
+ border-radius: 6px;
58
+ outline: none;
59
+ transition: border-color 120ms;
60
+ }
61
+ input:focus {
62
+ border-color: #fff;
63
+ }
64
+ button {
65
+ margin-top: 6px;
66
+ background: #fff;
67
+ color: #000;
68
+ border: none;
69
+ padding: 10px 12px;
70
+ font-size: 14px;
71
+ font-weight: 500;
72
+ font-family: inherit;
73
+ border-radius: 6px;
74
+ cursor: default;
75
+ }
76
+ button:disabled {
77
+ opacity: 0.5;
78
+ }
79
+ .error {
80
+ color: #ff6b6b;
81
+ font-size: 12px;
82
+ min-height: 1em;
83
+ }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <div class="drag-region" data-drag-region></div>
88
+ <main>
89
+ <form id="connect-form">
90
+ <label>
91
+ Server URL
92
+ <input id="serverURL" type="url" placeholder="https://example.com" required autofocus />
93
+ </label>
94
+ <label>
95
+ Token <span style="text-transform: none; letter-spacing: 0; color: #555;">(leave blank for --public servers)</span>
96
+ <input id="token" type="password" />
97
+ </label>
98
+ <div class="error" id="error"></div>
99
+ <button type="submit">Connect</button>
100
+ </form>
101
+ </main>
102
+ <script>
103
+ const form = document.getElementById("connect-form");
104
+ const errorEl = document.getElementById("error");
105
+ const submitButton = form.querySelector("button");
106
+ form.addEventListener("submit", async (event) => {
107
+ event.preventDefault();
108
+ errorEl.textContent = "";
109
+ const serverURL = document.getElementById("serverURL").value.trim();
110
+ const token = document.getElementById("token").value.trim();
111
+ if (!serverURL) return;
112
+ try {
113
+ new URL(serverURL);
114
+ } catch {
115
+ errorEl.textContent = "Invalid URL";
116
+ return;
117
+ }
118
+ submitButton.disabled = true;
119
+ try {
120
+ await window.television.setConnection(serverURL, token);
121
+ } catch (err) {
122
+ errorEl.textContent = err instanceof Error ? err.message : "Failed to connect";
123
+ submitButton.disabled = false;
124
+ }
125
+ });
126
+ </script>
127
+ </body>
128
+ </html>
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ App: () => App
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var import_electron2 = require("electron");
37
+ var import_node_path2 = __toESM(require("node:path"), 1);
38
+
39
+ // src/connection-store.ts
40
+ var import_electron = require("electron");
41
+ var import_node_fs = require("node:fs");
42
+ var import_node_path = __toESM(require("node:path"), 1);
43
+ function storePath() {
44
+ return import_node_path.default.join(import_electron.app.getPath("userData"), "connection.json");
45
+ }
46
+ function loadConnection() {
47
+ const file = storePath();
48
+ if (!(0, import_node_fs.existsSync)(file)) return null;
49
+ try {
50
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
51
+ if (typeof parsed.serverURL !== "string" || typeof parsed.token !== "string") return null;
52
+ return { serverURL: parsed.serverURL, token: parsed.token };
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ function saveConnection(connection) {
58
+ (0, import_node_fs.writeFileSync)(storePath(), JSON.stringify(connection, null, 2), "utf8");
59
+ }
60
+
61
+ // src/index.ts
62
+ import_electron2.app.setName("Television");
63
+ var WINDOW_WIDTH = 1400;
64
+ var WINDOW_HEIGHT = 1e3;
65
+ var TRAFFIC_LIGHT_POSITION = { x: 12, y: 12 };
66
+ var SET_CONNECTION_CHANNEL = "television:set-connection";
67
+ var TEST_FIXTURE_FLAG = "--test-fixture";
68
+ function buildRemoteURL({ serverURL, token }) {
69
+ const url = new URL(serverURL);
70
+ url.searchParams.set("mode", "electron");
71
+ if (token) url.searchParams.set("token", token);
72
+ return url.toString();
73
+ }
74
+ function readTestFixtureURL(argv) {
75
+ const idx = argv.indexOf(TEST_FIXTURE_FLAG);
76
+ if (idx === -1 || idx === argv.length - 1) return null;
77
+ return argv[idx + 1] ?? null;
78
+ }
79
+ var App = class {
80
+ window = null;
81
+ async start() {
82
+ await import_electron2.app.whenReady();
83
+ if (process.platform === "darwin" && import_electron2.app.dock) {
84
+ import_electron2.app.dock.setIcon(import_node_path2.default.join(__dirname, "..", "assets", "icon.png"));
85
+ }
86
+ this.installMenu();
87
+ import_electron2.ipcMain.handle(SET_CONNECTION_CHANNEL, async (_event, connection) => {
88
+ saveConnection(connection);
89
+ await this.loadRemote(connection);
90
+ });
91
+ await this.createWindow();
92
+ import_electron2.app.on("window-all-closed", () => {
93
+ if (process.platform !== "darwin") import_electron2.app.quit();
94
+ });
95
+ import_electron2.app.on("activate", () => {
96
+ if (import_electron2.BrowserWindow.getAllWindows().length === 0) void this.createWindow();
97
+ });
98
+ }
99
+ async createWindow() {
100
+ this.window = new import_electron2.BrowserWindow({
101
+ title: "Television",
102
+ icon: import_node_path2.default.join(__dirname, "..", "assets", "icon.png"),
103
+ width: WINDOW_WIDTH,
104
+ height: WINDOW_HEIGHT,
105
+ show: false,
106
+ backgroundColor: "#000000",
107
+ titleBarStyle: "hidden",
108
+ trafficLightPosition: TRAFFIC_LIGHT_POSITION,
109
+ webPreferences: {
110
+ contextIsolation: true,
111
+ preload: import_node_path2.default.join(__dirname, "connect-preload.cjs"),
112
+ webviewTag: true
113
+ }
114
+ });
115
+ this.window.webContents.on("will-attach-webview", (_event, webPreferences) => {
116
+ webPreferences.preload = import_node_path2.default.join(__dirname, "webview-bridge-preload.cjs");
117
+ });
118
+ this.window.once("ready-to-show", () => this.window?.show());
119
+ this.window.on("closed", () => {
120
+ this.window = null;
121
+ });
122
+ const testFixtureURL = readTestFixtureURL(process.argv);
123
+ if (testFixtureURL) {
124
+ await this.window.loadURL(testFixtureURL);
125
+ return;
126
+ }
127
+ const connection = loadConnection();
128
+ if (connection) {
129
+ await this.loadRemote(connection);
130
+ } else {
131
+ await this.loadConnectScreen();
132
+ }
133
+ }
134
+ async loadConnectScreen() {
135
+ if (!this.window) return;
136
+ await this.window.loadFile(import_node_path2.default.join(__dirname, "connect.html"));
137
+ }
138
+ async loadRemote(connection) {
139
+ if (!this.window) return;
140
+ await this.window.loadURL(buildRemoteURL(connection));
141
+ }
142
+ installMenu() {
143
+ const isMac = process.platform === "darwin";
144
+ const connectItem = {
145
+ label: "Connect to server\u2026",
146
+ accelerator: "CmdOrCtrl+,",
147
+ click: () => void this.loadConnectScreen()
148
+ };
149
+ const template = [
150
+ ...isMac ? [
151
+ {
152
+ label: import_electron2.app.name,
153
+ submenu: [
154
+ { role: "about" },
155
+ { type: "separator" },
156
+ connectItem,
157
+ { type: "separator" },
158
+ { role: "services" },
159
+ { type: "separator" },
160
+ { role: "hide" },
161
+ { role: "hideOthers" },
162
+ { role: "unhide" },
163
+ { type: "separator" },
164
+ { role: "quit" }
165
+ ]
166
+ }
167
+ ] : [],
168
+ {
169
+ label: "File",
170
+ submenu: [
171
+ ...isMac ? [] : [connectItem],
172
+ isMac ? { role: "close" } : { role: "quit" }
173
+ ]
174
+ },
175
+ { role: "editMenu" },
176
+ { role: "viewMenu" },
177
+ { role: "windowMenu" }
178
+ ];
179
+ import_electron2.Menu.setApplicationMenu(import_electron2.Menu.buildFromTemplate(template));
180
+ }
181
+ };
182
+ if (process.env.VITEST !== "true") {
183
+ void new App().start();
184
+ }
185
+ // Annotate the CommonJS export names for ESM import in node:
186
+ 0 && (module.exports = {
187
+ App
188
+ });
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+
3
+ // src/webview-bridge-preload.ts
4
+ var import_electron = require("electron");
5
+ var BRIDGE_CHANNEL = "television-artifact-bridge";
6
+ var lastWheelClientX = 0;
7
+ var lastWheelClientY = 0;
8
+ window.addEventListener(
9
+ "wheel",
10
+ (event) => {
11
+ lastWheelClientX = event.clientX;
12
+ lastWheelClientY = event.clientY;
13
+ const payload = {
14
+ type: "wheel",
15
+ deltaX: event.deltaX,
16
+ deltaY: event.deltaY,
17
+ shiftKey: event.shiftKey
18
+ };
19
+ import_electron.ipcRenderer.sendToHost(BRIDGE_CHANNEL, payload);
20
+ event.preventDefault();
21
+ },
22
+ { passive: false }
23
+ );
24
+ import_electron.ipcRenderer.on(BRIDGE_CHANNEL, (_event, payload) => {
25
+ if (payload?.type !== "scroll") return;
26
+ const deltaX = payload.deltaX ?? 0;
27
+ const deltaY = payload.deltaY ?? 0;
28
+ const target = resolveScrollTarget(lastWheelClientX, lastWheelClientY, deltaX, deltaY);
29
+ target.scrollBy(deltaX, deltaY);
30
+ });
31
+ function resolveScrollTarget(x, y, deltaX, deltaY) {
32
+ const fallback = document.scrollingElement ?? document.documentElement;
33
+ if (typeof document.elementFromPoint !== "function") return fallback;
34
+ let node = document.elementFromPoint(x, y);
35
+ while (node && node !== document.documentElement) {
36
+ if (canScroll(node, deltaX, deltaY)) return node;
37
+ node = node.parentElement;
38
+ }
39
+ return fallback;
40
+ }
41
+ function canScroll(node, deltaX, deltaY) {
42
+ const style = getComputedStyle(node);
43
+ if (deltaY !== 0) {
44
+ if (isScrollableOverflow(style.overflowY) && node.scrollHeight > node.clientHeight) {
45
+ return true;
46
+ }
47
+ }
48
+ if (deltaX !== 0) {
49
+ if (isScrollableOverflow(style.overflowX) && node.scrollWidth > node.clientWidth) {
50
+ return true;
51
+ }
52
+ }
53
+ return false;
54
+ }
55
+ function isScrollableOverflow(value) {
56
+ return value === "auto" || value === "scroll" || value === "overlay";
57
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@telepath-computer/television-desktop",
3
+ "productName": "Television",
4
+ "version": "0.1.107",
5
+ "type": "module",
6
+ "main": "dist/electron.cjs",
7
+ "bin": {
8
+ "tv-desktop": "bin/tv-desktop.cjs"
9
+ },
10
+ "files": [
11
+ "dist/**",
12
+ "bin/**",
13
+ "assets/**",
14
+ "scripts/postinstall.cjs"
15
+ ],
16
+ "exports": {
17
+ ".": "./src/index.ts"
18
+ },
19
+ "scripts": {
20
+ "build": "node build.mjs",
21
+ "postinstall": "node scripts/postinstall.cjs",
22
+ "prepublishOnly": "node build.mjs",
23
+ "type-check": "tsc -p tsconfig.json --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "electron": "35.7.5"
27
+ }
28
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // Renames the bundled Electron.app to Television.app (bundle + inner
3
+ // executable + plist) so macOS labels the Dock and menu bar as
4
+ // "Television" instead of "Electron". macOS reads the running-app name
5
+ // from the bundle's filesystem identity; just editing Info.plist isn't
6
+ // enough — the .app directory and Contents/MacOS/<exe> have to match.
7
+ // Scoped to this package's resolved electron install.
8
+
9
+ const { execFileSync } = require("node:child_process");
10
+ const path = require("node:path");
11
+ const fs = require("node:fs");
12
+
13
+ if (process.platform !== "darwin") process.exit(0);
14
+
15
+ let electronEntry;
16
+ try {
17
+ electronEntry = require.resolve("electron");
18
+ } catch {
19
+ process.exit(0);
20
+ }
21
+
22
+ const NAME = "Television";
23
+ const BUNDLE_ID = "computer.telepath.television";
24
+ const distDir = path.join(path.dirname(electronEntry), "dist");
25
+ const oldApp = path.join(distDir, "Electron.app");
26
+ const newApp = path.join(distDir, `${NAME}.app`);
27
+ const targetApp = fs.existsSync(newApp) ? newApp : oldApp;
28
+
29
+ if (!fs.existsSync(targetApp)) process.exit(0);
30
+
31
+ try {
32
+ if (targetApp === oldApp) fs.renameSync(oldApp, newApp);
33
+
34
+ const macosDir = path.join(newApp, "Contents", "MacOS");
35
+ const oldExe = path.join(macosDir, "Electron");
36
+ const newExe = path.join(macosDir, NAME);
37
+ if (fs.existsSync(oldExe) && !fs.existsSync(newExe)) {
38
+ fs.renameSync(oldExe, newExe);
39
+ }
40
+
41
+ const ourIcon = path.join(__dirname, "..", "assets", "icon.icns");
42
+ const bundleIcon = path.join(newApp, "Contents", "Resources", "electron.icns");
43
+ if (fs.existsSync(ourIcon) && fs.existsSync(path.dirname(bundleIcon))) {
44
+ fs.copyFileSync(ourIcon, bundleIcon);
45
+ }
46
+
47
+ const plist = path.join(newApp, "Contents", "Info.plist");
48
+ execFileSync("plutil", ["-replace", "CFBundleName", "-string", NAME, plist]);
49
+ execFileSync("plutil", ["-replace", "CFBundleDisplayName", "-string", NAME, plist]);
50
+ execFileSync("plutil", ["-replace", "CFBundleExecutable", "-string", NAME, plist]);
51
+ execFileSync("plutil", ["-replace", "CFBundleIdentifier", "-string", BUNDLE_ID, plist]);
52
+
53
+ execFileSync(
54
+ "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",
55
+ ["-f", newApp],
56
+ );
57
+ } catch (err) {
58
+ console.warn(`[television-desktop] could not rename Electron bundle: ${err.message}`);
59
+ }