@volter/supercode-teams 0.2.81 → 0.2.83

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.81",
3
+ "version": "0.2.83",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",
@@ -4,10 +4,10 @@
4
4
  // daemon's ConPTY host on Windows, holds it); the pane ending ends the client, and the terminal closes the window as
5
5
  // its command exits. Adapters are tried in rank order; SUPERCODE_POPUP_TERMINAL names one (or `none`).
6
6
  import { spawn, spawnSync } from 'node:child_process';
7
- import { chmodSync, existsSync, readdirSync, writeFileSync } from 'node:fs';
7
+ import { chmodSync, existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
8
8
  import { homedir, tmpdir } from 'node:os';
9
9
  import { join } from 'node:path';
10
- import { randomBytes } from 'node:crypto';
10
+ import { createHash, randomBytes } from 'node:crypto';
11
11
  import { weztermConfigFile } from './terminal-look.mjs';
12
12
 
13
13
  const onPath = (name) => {
@@ -31,11 +31,12 @@ function selfDeletingScript(argv, extension) {
31
31
  // running process is on screen in about 0.3 s, where a process started per window took 0.7 to 2 s (measured on macOS
32
32
  // 26 with WezTerm 20240203). The process is known by its class; it listens on ~/.local/share/wezterm/gui-sock-<pid>.
33
33
  // A window goes into it with `wezterm cli spawn --new-window`, and when there is none, or the spawn fails, the window
34
- // starts it. The fallback runs in the launched shell, so the daemon never waits on WezTerm.
34
+ // starts it. The fallback runs in the launched shell, so the daemon never waits on WezTerm. The class carries the
35
+ // config's content: WezTerm does not reload it (terminal-look.mjs), so a process holding an older look is never reused.
35
36
  const WEZTERM_CLASS = 'dev.volter.supercode';
36
37
 
37
- /** The socket of a running supercode WezTerm process, or null. */
38
- function weztermSocket() {
38
+ /** The socket of a running supercode WezTerm process of class `cls`, or null. */
39
+ function weztermSocket(cls) {
39
40
  const dir = join(homedir(), '.local', 'share', 'wezterm');
40
41
  let names;
41
42
  try { names = readdirSync(dir); } catch { return null; }
@@ -44,15 +45,17 @@ function weztermSocket() {
44
45
  if (!pid) continue;
45
46
  try { process.kill(pid, 0); } catch { continue; }
46
47
  const command = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8', timeout: 2_000 }).stdout ?? '';
47
- if (command.includes(`--class ${WEZTERM_CLASS}`)) return join(dir, name);
48
+ if (command.includes(`--class ${cls} `)) return join(dir, name);
48
49
  }
49
50
  return null;
50
51
  }
51
52
 
52
53
  /** WezTerm's launch: a window in supercode's running WezTerm, else a new one with supercode's look (terminal-look.mjs). */
53
54
  function weztermLaunch(program, argv) {
54
- const start = [program, '--config-file', weztermConfigFile(), 'start', '--always-new-process', '--class', WEZTERM_CLASS, '--', ...argv];
55
- const socket = process.platform === 'win32' ? null : weztermSocket();
55
+ const config = weztermConfigFile();
56
+ const cls = `${WEZTERM_CLASS}.${createHash('sha256').update(readFileSync(config)).digest('hex').slice(0, 12)}`;
57
+ const start = [program, '--config-file', config, 'start', '--always-new-process', '--class', cls, '--', ...argv];
58
+ const socket = process.platform === 'win32' ? null : weztermSocket(cls);
56
59
  if (!socket) return [start[0], start.slice(1)];
57
60
  const quote = (arg) => `'${String(arg).replaceAll("'", `'"'"'`)}'`;
58
61
  const spawnWindow = [program, 'cli', '--no-auto-start', 'spawn', '--new-window', '--', ...argv].map(quote).join(' ');
package/terminal-look.mjs CHANGED
@@ -53,6 +53,38 @@ function run() {
53
53
  });
54
54
  }`;
55
55
 
56
+ // AppKit's conversion of sRGB colours into each screen's colour space, keyed by the screen's scale. WezTerm writes its
57
+ // colours to the screen unconverted, where Terminal.app's are colour-managed: on the owner's external monitor Clear
58
+ // Dark's ground #212734 reached the glass as #212734 from WezTerm and as #1e242d from Terminal.app (#1d232d is AppKit's
59
+ // conversion), so every colour looked lighter and bluer. Two screens at one scale share the main screen's conversion.
60
+ const CONVERT_COLOURS = `
61
+ ObjC.import('AppKit');
62
+ function run(argv) {
63
+ const hexes = JSON.parse(argv[0]);
64
+ const two = (v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, '0');
65
+ const screens = $.NSScreen.screens, out = [];
66
+ for (let i = 0; i < screens.count; i++) {
67
+ const screen = screens.objectAtIndex(i), space = screen.colorSpace, map = {};
68
+ for (const hex of hexes) {
69
+ const [r, g, b] = [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255);
70
+ const c = $.NSColor.colorWithSRGBRedGreenBlueAlpha(r, g, b, 1).colorUsingColorSpace(space);
71
+ map[hex] = '#' + two(c.redComponent) + two(c.greenComponent) + two(c.blueComponent);
72
+ }
73
+ out.push({ scale: screen.backingScaleFactor, map });
74
+ }
75
+ return JSON.stringify(out);
76
+ }`;
77
+
78
+ /** Each screen's conversion of `hexes` ([{ scale, map: { hex: hex } }], the main screen first); [] off macOS. */
79
+ export function screenColours(hexes, platform = process.platform) {
80
+ if (platform !== 'darwin' || !hexes.length) return [];
81
+ try {
82
+ return JSON.parse(execFileSync('osascript', ['-l', 'JavaScript', '-e', CONVERT_COLOURS, JSON.stringify(hexes)], { encoding: 'utf8', timeout: 5_000, stdio: ['ignore', 'pipe', 'ignore'] }));
83
+ } catch {
84
+ return [];
85
+ }
86
+ }
87
+
56
88
  /** This machine's Terminal.app default profile, as read by AppKit; null off macOS or when it cannot be read. */
57
89
  export function terminalAppProfile(platform = process.platform) {
58
90
  if (platform !== 'darwin') return null;
@@ -67,7 +99,7 @@ const hex = ([r, g, b]) => `#${[r, g, b].map((part) => Math.round(Math.min(1, Ma
67
99
  const lua = (value) => (typeof value === 'string' ? JSON.stringify(value) : String(value));
68
100
 
69
101
  /** WezTerm's config (Lua) for a window that looks like Terminal.app with `profile` (from terminalAppProfile, or null). */
70
- export function weztermConfig(profile, { fontDirs = existsSync(TERMINAL_FONTS) ? [TERMINAL_FONTS] : [] } = {}) {
102
+ export function weztermConfig(profile, { fontDirs = existsSync(TERMINAL_FONTS) ? [TERMINAL_FONTS] : [], screens = [] } = {}) {
71
103
  const mac = fontDirs.length > 0;
72
104
  const stored = profile?.colors ?? {};
73
105
  // One colour table per appearance: the profile's colour where it stores one, else Basic's for that appearance.
@@ -93,8 +125,8 @@ export function weztermConfig(profile, { fontDirs = existsSync(TERMINAL_FONTS) ?
93
125
  // Terminal.app's cell is whole points: the advance rounded down (7 pt for SF Mono Terminal 12's 7.418) and the line
94
126
  // (ascent + descent + leading) rounded up (15 pt for 14.32), measured as 14 x 30 px on a Retina display. WezTerm rounds
95
127
  // its cell up to whole pixels before and after these factors, so the width factor is floor/ceil of the advance (14 px
96
- // at 2x) and the height factor lands on the doubled line at 2x (30 px). Evidence stops at 2x: at 1x the same factors
97
- // predict 7 x 16 px where Terminal.app draws 7 x 15.
128
+ // at 2x) and the height factor lands on the doubled line at 2x (30 px). Measured on both of the owner's displays: 14 x
129
+ // 30 px at 2x and 7 x 15 px at 1x, the same as Terminal.app's.
98
130
  const font = profile?.font;
99
131
  const advance = (font?.advance ?? 0) * (profile?.widthSpacing ?? 1);
100
132
  const cellWidth = advance > 0 ? Math.floor(advance) / Math.ceil(advance) : 1;
@@ -107,13 +139,32 @@ export function weztermConfig(profile, { fontDirs = existsSync(TERMINAL_FONTS) ?
107
139
  const opacity = alpha >= 0.9 ? 1 : alpha;
108
140
  const cursorShape = ['Block', 'Underline', 'Bar'][profile?.cursorType ?? 0] ?? 'Block';
109
141
  const composed = !profile?.optionIsMeta;
142
+ const light = scheme(TERMINAL_BASIC.light), dark = scheme(TERMINAL_BASIC.dark);
143
+ const convert = (colors, map) => Object.fromEntries(Object.entries(colors).map(([key, value]) => [key, Array.isArray(value) ? value.map((v) => map[v] ?? v) : map[value] ?? value]));
144
+ const entry = (key, map) => ` ${key} = { light = ${table(convert(light, map))}, dark = ${table(convert(dark, map))} },`;
145
+ const byScale = new Map();
146
+ for (const screen of screens) if (!byScale.has(screen.scale)) byScale.set(screen.scale, screen.map);
147
+ const palettes = [entry('main', screens[0]?.map ?? {}), ...[...byScale].map(([scale, map]) => entry(`[${scale}]`, map))].join('\n');
110
148
  const lines = [
111
149
  '-- Generated by supercode (sdk/teams/terminal-look.mjs): this window looks like Terminal.app' + (profile?.name ? ` with this machine's "${profile.name}" profile.` : "'s Basic profile."),
112
150
  "local wezterm = require 'wezterm'",
113
151
  'local config = wezterm.config_builder()',
114
- `local schemes = {\n light = ${table(scheme(TERMINAL_BASIC.light))},\n dark = ${table(scheme(TERMINAL_BASIC.dark))},\n}`,
152
+ `local palettes = {\n${palettes}\n}`,
115
153
  "local appearance = wezterm.gui and wezterm.gui.get_appearance() or 'Dark'",
116
- "config.colors = appearance:find('Dark') and schemes.dark or schemes.light",
154
+ "local function colours(scale) local p = palettes[scale] or palettes.main; return appearance:find('Dark') and p.dark or p.light end",
155
+ 'config.colors = colours()',
156
+ // A window takes the palette converted for the screen it is on (by its scale), when it opens and when it moves.
157
+ 'local shown = {}',
158
+ 'local function fit(window)',
159
+ ' local scale = math.floor(window:get_dimensions().dpi / 72 + 0.5)',
160
+ ' if shown[window:window_id()] == scale then return end',
161
+ ' shown[window:window_id()] = scale',
162
+ ' local overrides = window:get_config_overrides() or {}',
163
+ ' overrides.colors = colours(scale)',
164
+ ' window:set_config_overrides(overrides)',
165
+ 'end',
166
+ "wezterm.on('window-config-reloaded', fit)",
167
+ "wezterm.on('window-resized', fit)",
117
168
  `config.font_dirs = { ${fontDirs.map(lua).join(', ')} }`,
118
169
  ...(families.length ? [`config.font = wezterm.font_with_fallback({ ${families.join(', ')} })`] : []),
119
170
  // CoreText draws glyphs unhinted; FreeType's default hinting snaps their outlines to the pixel grid.
@@ -164,7 +215,10 @@ export function weztermConfig(profile, { fontDirs = existsSync(TERMINAL_FONTS) ?
164
215
  /** Writes the config for this machine, when it differs from the one on disk, and returns its path. */
165
216
  export function weztermConfigFile(platform = process.platform) {
166
217
  const path = join(tmpdir(), `supercode-wezterm-${process.getuid?.() ?? 'user'}.lua`);
167
- const config = weztermConfig(terminalAppProfile(platform));
218
+ const profile = terminalAppProfile(platform);
219
+ const plain = weztermConfig(profile);
220
+ const hexes = [...new Set(plain.match(/#[0-9a-f]{6}/g) ?? [])];
221
+ const config = weztermConfig(profile, { screens: screenColours(hexes, platform) });
168
222
  let current = null;
169
223
  try { current = readFileSync(path, 'utf8'); } catch { /* not written yet */ }
170
224
  if (current === config) return path;