@camstack/addon-terminal 0.1.13 → 0.1.15
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.
|
Binary file
|
|
Binary file
|
package/dist/addon.js
CHANGED
|
@@ -22,9 +22,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
enumerable: true
|
|
23
23
|
}) : target, mod));
|
|
24
24
|
//#endregion
|
|
25
|
+
let node_child_process = require("node:child_process");
|
|
25
26
|
let node_fs = require("node:fs");
|
|
26
|
-
let node_module = require("node:module");
|
|
27
27
|
let node_path = require("node:path");
|
|
28
|
+
node_path = __toESM(node_path);
|
|
29
|
+
let node_url = require("node:url");
|
|
30
|
+
let node_module = require("node:module");
|
|
28
31
|
let sharp = require("sharp");
|
|
29
32
|
sharp = __toESM(sharp);
|
|
30
33
|
let node_http = require("node:http");
|
|
@@ -33377,6 +33380,46 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
|
|
|
33377
33380
|
DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
33378
33381
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
33379
33382
|
//#endregion
|
|
33383
|
+
//#region src/curses-shim.ts
|
|
33384
|
+
/**
|
|
33385
|
+
* macOS repair for the managed Python's curses.
|
|
33386
|
+
*
|
|
33387
|
+
* The python-build-standalone distribution the dependency resolver installs on
|
|
33388
|
+
* macOS links ncurses statically with the terminfo database compiled OUT
|
|
33389
|
+
* (`--disable-database`) and an ABI mismatch that misparses every numeric
|
|
33390
|
+
* capability of its five built-in fallback terminals (`colors=-1`,
|
|
33391
|
+
* `cols=524368`). `has_colors()` is therefore False under every TERM value and
|
|
33392
|
+
* `init_pair` is hard-blocked, so glances takes its B&W branch and paints the
|
|
33393
|
+
* whole screen with SGR conceal/reverse/alt-charset — the "text missing,
|
|
33394
|
+
* light bars instead" terminal camera frames. No environment variable can fix
|
|
33395
|
+
* it: the code that would read TERMINFO/TERMINFO_DIRS was never compiled in.
|
|
33396
|
+
*
|
|
33397
|
+
* The repair ships prebuilt `_curses` / `_curses_panel` extension modules
|
|
33398
|
+
* linked against the system `/usr/lib/libncurses.5.4.dylib` (universal2) and
|
|
33399
|
+
* puts their directory on PYTHONPATH at PTY spawn — PYTHONPATH precedes
|
|
33400
|
+
* lib-dynload on sys.path, so the working modules shadow the broken ones with
|
|
33401
|
+
* no change to the managed distribution. The filename is ABI-locked to
|
|
33402
|
+
* cpython-312: a managed-python bump to 3.13 makes the shim silently inert
|
|
33403
|
+
* (python ignores a mismatched ABI tag), which is why the addon probes
|
|
33404
|
+
* `tigetnum("colors")` at startup and logs loudly when colours are missing.
|
|
33405
|
+
*/
|
|
33406
|
+
/** `<addonRoot>/assets/curses-shim/py312`, valid from both src/ and dist/. */
|
|
33407
|
+
function cursesShimDir() {
|
|
33408
|
+
return node_path.default.join(node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), "..", "assets", "curses-shim", "py312");
|
|
33409
|
+
}
|
|
33410
|
+
function cursesShimDirExists(shimDir) {
|
|
33411
|
+
return (0, node_fs.existsSync)(shimDir);
|
|
33412
|
+
}
|
|
33413
|
+
/**
|
|
33414
|
+
* The env to merge into the glances profile, or undefined when the shim does
|
|
33415
|
+
* not apply (non-macOS, or the assets were not shipped).
|
|
33416
|
+
*/
|
|
33417
|
+
function resolveGlancesCursesShimEnv(options) {
|
|
33418
|
+
if (options.platform !== "darwin" || !options.shimDirExists) return void 0;
|
|
33419
|
+
const existing = options.existingPythonPath?.trim();
|
|
33420
|
+
return { PYTHONPATH: existing ? `${options.shimDir}:${existing}` : options.shimDir };
|
|
33421
|
+
}
|
|
33422
|
+
//#endregion
|
|
33380
33423
|
//#region src/pty.ts
|
|
33381
33424
|
/**
|
|
33382
33425
|
* Minimal pty abstraction. The manager depends on this interface, never on
|
|
@@ -33507,6 +33550,198 @@ function buildTerminalInstanceCameraDeclarations(instances) {
|
|
|
33507
33550
|
function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
|
|
33508
33551
|
return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
|
|
33509
33552
|
}
|
|
33553
|
+
//#endregion
|
|
33554
|
+
//#region src/terminal-cell-runs.ts
|
|
33555
|
+
var TERMINAL_DEFAULT_FG = "#d7dce2";
|
|
33556
|
+
var TERMINAL_DEFAULT_BG = "#0b0d10";
|
|
33557
|
+
/**
|
|
33558
|
+
* The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
|
|
33559
|
+
* frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
|
|
33560
|
+
* whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
|
|
33561
|
+
* near-black background at 13px. Index 7 IS the default foreground, so plain
|
|
33562
|
+
* `CSI 37m` text renders identically to unstyled text.
|
|
33563
|
+
*/
|
|
33564
|
+
var TERMINAL_ANSI_PALETTE = [
|
|
33565
|
+
"#282c34",
|
|
33566
|
+
"#e06c75",
|
|
33567
|
+
"#98c379",
|
|
33568
|
+
"#e5c07b",
|
|
33569
|
+
"#61afef",
|
|
33570
|
+
"#c678dd",
|
|
33571
|
+
"#56b6c2",
|
|
33572
|
+
TERMINAL_DEFAULT_FG,
|
|
33573
|
+
"#5c6370",
|
|
33574
|
+
"#ef596f",
|
|
33575
|
+
"#89ca78",
|
|
33576
|
+
"#f0c674",
|
|
33577
|
+
"#6cb6ff",
|
|
33578
|
+
"#d55fde",
|
|
33579
|
+
"#2bbac5",
|
|
33580
|
+
"#ffffff"
|
|
33581
|
+
];
|
|
33582
|
+
/** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
|
|
33583
|
+
var TERMINAL_CUBE_LEVELS = [
|
|
33584
|
+
0,
|
|
33585
|
+
95,
|
|
33586
|
+
135,
|
|
33587
|
+
175,
|
|
33588
|
+
215,
|
|
33589
|
+
255
|
|
33590
|
+
];
|
|
33591
|
+
var TERMINAL_CUBE_FIRST = 16;
|
|
33592
|
+
var TERMINAL_GRAYSCALE_FIRST = 232;
|
|
33593
|
+
var TERMINAL_GRAYSCALE_BASE = 8;
|
|
33594
|
+
var TERMINAL_GRAYSCALE_STEP = 10;
|
|
33595
|
+
/** SGR 2 keeps the foreground legible; it must not become the background. */
|
|
33596
|
+
var TERMINAL_DIM_WEIGHT = .6;
|
|
33597
|
+
function channel(value) {
|
|
33598
|
+
return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
|
|
33599
|
+
}
|
|
33600
|
+
function hex(red, green, blue) {
|
|
33601
|
+
return `#${channel(red)}${channel(green)}${channel(blue)}`;
|
|
33602
|
+
}
|
|
33603
|
+
function parseHex(color) {
|
|
33604
|
+
return [
|
|
33605
|
+
Number.parseInt(color.slice(1, 3), 16),
|
|
33606
|
+
Number.parseInt(color.slice(3, 5), 16),
|
|
33607
|
+
Number.parseInt(color.slice(5, 7), 16)
|
|
33608
|
+
];
|
|
33609
|
+
}
|
|
33610
|
+
/** Resolve an xterm palette index (0-255) to a hex colour. */
|
|
33611
|
+
function terminalPaletteColor(index) {
|
|
33612
|
+
const ansi = TERMINAL_ANSI_PALETTE[index];
|
|
33613
|
+
if (ansi !== void 0) return ansi;
|
|
33614
|
+
if (index >= TERMINAL_GRAYSCALE_FIRST) {
|
|
33615
|
+
const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
|
|
33616
|
+
return hex(level, level, level);
|
|
33617
|
+
}
|
|
33618
|
+
if (index >= TERMINAL_CUBE_FIRST) {
|
|
33619
|
+
const offset = index - TERMINAL_CUBE_FIRST;
|
|
33620
|
+
return hex(TERMINAL_CUBE_LEVELS[Math.floor(offset / 36) % 6] ?? 0, TERMINAL_CUBE_LEVELS[Math.floor(offset / 6) % 6] ?? 0, TERMINAL_CUBE_LEVELS[offset % 6] ?? 0);
|
|
33621
|
+
}
|
|
33622
|
+
return TERMINAL_DEFAULT_FG;
|
|
33623
|
+
}
|
|
33624
|
+
/** Resolve a 0xRRGGBB truecolor value to a hex colour. */
|
|
33625
|
+
function terminalRgbColor(value) {
|
|
33626
|
+
return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
|
|
33627
|
+
}
|
|
33628
|
+
function blend(color, toward, weight) {
|
|
33629
|
+
const [red, green, blue] = parseHex(color);
|
|
33630
|
+
const [targetRed, targetGreen, targetBlue] = parseHex(toward);
|
|
33631
|
+
return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
|
|
33632
|
+
}
|
|
33633
|
+
function resolveForeground(cell) {
|
|
33634
|
+
if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
|
|
33635
|
+
if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
|
|
33636
|
+
return TERMINAL_DEFAULT_FG;
|
|
33637
|
+
}
|
|
33638
|
+
function resolveBackground(cell) {
|
|
33639
|
+
if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
|
|
33640
|
+
if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
|
|
33641
|
+
return TERMINAL_DEFAULT_BG;
|
|
33642
|
+
}
|
|
33643
|
+
/**
|
|
33644
|
+
* Resolve one cell's attributes into concrete colours.
|
|
33645
|
+
*
|
|
33646
|
+
* Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
|
|
33647
|
+
* defaults is still a visible swap rather than a no-op — that is how a selected
|
|
33648
|
+
* or highlighted row in Glances reads. Invisible is then conceal-by-equality
|
|
33649
|
+
* (foreground painted in its own background): the cell keeps its columns, which
|
|
33650
|
+
* a dropped cell would not, and dropping it would shift the whole rest of the
|
|
33651
|
+
* row left.
|
|
33652
|
+
*/
|
|
33653
|
+
function resolveCellStyle(cell) {
|
|
33654
|
+
const inverse = cell.isInverse() !== 0;
|
|
33655
|
+
const plainFg = resolveForeground(cell);
|
|
33656
|
+
const plainBg = resolveBackground(cell);
|
|
33657
|
+
const background = inverse ? plainFg : plainBg;
|
|
33658
|
+
let foreground = inverse ? plainBg : plainFg;
|
|
33659
|
+
if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
|
|
33660
|
+
if (cell.isInvisible() !== 0) foreground = background;
|
|
33661
|
+
return {
|
|
33662
|
+
fg: foreground === "#d7dce2" ? null : foreground,
|
|
33663
|
+
bg: background === "#0b0d10" ? null : background,
|
|
33664
|
+
bold: cell.isBold() !== 0
|
|
33665
|
+
};
|
|
33666
|
+
}
|
|
33667
|
+
function sameStyle(left, right) {
|
|
33668
|
+
return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
|
|
33669
|
+
}
|
|
33670
|
+
/**
|
|
33671
|
+
* Merge adjacent same-style cells into runs, then drop the trailing run of
|
|
33672
|
+
* default-styled whitespace so a row costs what it draws — the same trim
|
|
33673
|
+
* `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
|
|
33674
|
+
* a green bar of spaces out to the right margin is a pixel Glances drew.
|
|
33675
|
+
*/
|
|
33676
|
+
function buildCellRuns(cells) {
|
|
33677
|
+
const runs = [];
|
|
33678
|
+
let text = "";
|
|
33679
|
+
let style = null;
|
|
33680
|
+
for (const cell of cells) {
|
|
33681
|
+
if (style !== null && sameStyle(style, cell.style)) {
|
|
33682
|
+
text += cell.text;
|
|
33683
|
+
continue;
|
|
33684
|
+
}
|
|
33685
|
+
if (style !== null) runs.push({
|
|
33686
|
+
text,
|
|
33687
|
+
...style
|
|
33688
|
+
});
|
|
33689
|
+
text = cell.text;
|
|
33690
|
+
style = cell.style;
|
|
33691
|
+
}
|
|
33692
|
+
if (style !== null) runs.push({
|
|
33693
|
+
text,
|
|
33694
|
+
...style
|
|
33695
|
+
});
|
|
33696
|
+
while (runs.length > 0) {
|
|
33697
|
+
const last = runs[runs.length - 1];
|
|
33698
|
+
if (last === void 0 || last.bg !== null) break;
|
|
33699
|
+
const trimmed = last.text.replace(/\s+$/u, "");
|
|
33700
|
+
if (trimmed === last.text) break;
|
|
33701
|
+
if (trimmed === "") {
|
|
33702
|
+
runs.pop();
|
|
33703
|
+
continue;
|
|
33704
|
+
}
|
|
33705
|
+
runs[runs.length - 1] = {
|
|
33706
|
+
...last,
|
|
33707
|
+
text: trimmed
|
|
33708
|
+
};
|
|
33709
|
+
break;
|
|
33710
|
+
}
|
|
33711
|
+
return runs;
|
|
33712
|
+
}
|
|
33713
|
+
/**
|
|
33714
|
+
* Monospace families to try, in order — NOT one family and a generic.
|
|
33715
|
+
*
|
|
33716
|
+
* A terminal screen is mostly box-drawing and block characters, and a font
|
|
33717
|
+
* without them renders the frame as noise rather than as missing detail.
|
|
33718
|
+
* `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
|
|
33719
|
+
* DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
|
|
33720
|
+
* coverage is not, and its Glances camera came out unreadable while the hub's
|
|
33721
|
+
* was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
|
|
33722
|
+
*
|
|
33723
|
+
* `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
|
|
33724
|
+
* present on every install, and derived from DejaVu Sans Mono — the same glyph
|
|
33725
|
+
* coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
|
|
33726
|
+
* generic stays last so a host with none of them still draws something.
|
|
33727
|
+
*/
|
|
33728
|
+
var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
|
|
33729
|
+
var TERMINAL_FONT_SIZE = 13;
|
|
33730
|
+
var TERMINAL_TEXT_MARGIN_X = 8;
|
|
33731
|
+
var TERMINAL_ROW_HEIGHT = 15;
|
|
33732
|
+
var TERMINAL_BASELINE_Y = 18;
|
|
33733
|
+
/**
|
|
33734
|
+
* Distance from a row's baseline up to the top of its cell box. Chosen so
|
|
33735
|
+
* consecutive rows tile exactly: row N's box runs from `baseline - this` for
|
|
33736
|
+
* `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
|
|
33737
|
+
* bar that stopped short would draw as stripes across a `CSI 42m` panel.
|
|
33738
|
+
*/
|
|
33739
|
+
var TERMINAL_CELL_ASCENT = 11.5;
|
|
33740
|
+
var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
|
|
33741
|
+
/** Two decimals is under a tenth of a pixel and keeps the SVG small. */
|
|
33742
|
+
function coordinate(value) {
|
|
33743
|
+
return String(Number(value.toFixed(2)));
|
|
33744
|
+
}
|
|
33510
33745
|
function escapeXml(value) {
|
|
33511
33746
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
33512
33747
|
}
|
|
@@ -33521,11 +33756,47 @@ function escapeXml(value) {
|
|
|
33521
33756
|
* looked perfect — which is exactly how the operator reported it. Measured in
|
|
33522
33757
|
* the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
|
|
33523
33758
|
* collapsed against 178 px preserved.
|
|
33524
|
-
|
|
33525
|
-
|
|
33526
|
-
|
|
33527
|
-
|
|
33528
|
-
|
|
33759
|
+
*
|
|
33760
|
+
* Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
|
|
33761
|
+
* never appended to the one before it, so the background rects and the glyphs
|
|
33762
|
+
* are placed off the same grid and cannot drift apart. `textLength` is emitted
|
|
33763
|
+
* with it because it is the correct declaration and renderers that honour it
|
|
33764
|
+
* get an exact grid — but it is not what makes this work: librsvg, which sharp
|
|
33765
|
+
* uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
|
|
33766
|
+
* 600 px still drew its natural 937 px). The anchoring is the guarantee.
|
|
33767
|
+
*/
|
|
33768
|
+
function renderTerminalSvg(rows) {
|
|
33769
|
+
const backgrounds = [];
|
|
33770
|
+
const texts = [];
|
|
33771
|
+
rows.slice(0, 40).forEach((row, index) => {
|
|
33772
|
+
const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
|
|
33773
|
+
const top = baseline - TERMINAL_CELL_ASCENT;
|
|
33774
|
+
let column = 0;
|
|
33775
|
+
for (const run of row) {
|
|
33776
|
+
if (column >= 120) break;
|
|
33777
|
+
const clipped = clipRun(run, 120 - column);
|
|
33778
|
+
const columns = [...clipped].length;
|
|
33779
|
+
if (columns === 0) continue;
|
|
33780
|
+
const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
|
|
33781
|
+
const width = columns * TERMINAL_CELL_WIDTH;
|
|
33782
|
+
if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
|
|
33783
|
+
if (clipped.trim() !== "") {
|
|
33784
|
+
const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
|
|
33785
|
+
const weight = run.bold ? " font-weight=\"bold\"" : "";
|
|
33786
|
+
texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
|
|
33787
|
+
}
|
|
33788
|
+
column += columns;
|
|
33789
|
+
}
|
|
33790
|
+
});
|
|
33791
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${String(960)}" height="${String(640)}"><rect width="100%" height="100%" fill="${TERMINAL_DEFAULT_BG}"/>${backgrounds.join("")}<g fill="${TERMINAL_DEFAULT_FG}" font-family="${TERMINAL_FONT_STACK}" font-size="${String(TERMINAL_FONT_SIZE)}">${texts.join("")}</g></svg>`;
|
|
33792
|
+
}
|
|
33793
|
+
/** Cut a run to the columns still left in the row, by code point not unit. */
|
|
33794
|
+
function clipRun(run, remaining) {
|
|
33795
|
+
const points = [...run.text];
|
|
33796
|
+
return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
|
|
33797
|
+
}
|
|
33798
|
+
async function renderTerminalJpeg(rows) {
|
|
33799
|
+
return (0, sharp.default)(Buffer.from(renderTerminalSvg(rows))).jpeg({
|
|
33529
33800
|
quality: 82,
|
|
33530
33801
|
chromaSubsampling: "4:2:0"
|
|
33531
33802
|
}).toBuffer();
|
|
@@ -38431,6 +38702,40 @@ function createXtermScreen(cols, rows) {
|
|
|
38431
38702
|
for (let row = 0; row < term.rows; row += 1) lines.push(buffer.getLine(buffer.viewportY + row)?.translateToString(true) ?? "");
|
|
38432
38703
|
return lines;
|
|
38433
38704
|
},
|
|
38705
|
+
/**
|
|
38706
|
+
* The colour-bearing read. `translateToString` — the only cell read this
|
|
38707
|
+
* screen used to offer — keeps characters and discards every attribute,
|
|
38708
|
+
* which is why the terminal camera rendered a fully-coloured Glances in
|
|
38709
|
+
* grey. Walking cells is the only way to get the attributes back.
|
|
38710
|
+
*
|
|
38711
|
+
* A zero-width cell is the tail half of a wide glyph (CJK, some emoji);
|
|
38712
|
+
* its characters already came back with the leading cell, so skipping it
|
|
38713
|
+
* keeps the text right. `getNullCell()` is reused across the whole screen
|
|
38714
|
+
* because `getCell` allocates otherwise — 4800 objects per frame.
|
|
38715
|
+
*/
|
|
38716
|
+
styledLines: () => {
|
|
38717
|
+
const buffer = term.buffer.active;
|
|
38718
|
+
const scratch = buffer.getNullCell();
|
|
38719
|
+
const styled = [];
|
|
38720
|
+
for (let row = 0; row < term.rows; row += 1) {
|
|
38721
|
+
const line = buffer.getLine(buffer.viewportY + row);
|
|
38722
|
+
if (!line) {
|
|
38723
|
+
styled.push([]);
|
|
38724
|
+
continue;
|
|
38725
|
+
}
|
|
38726
|
+
const cells = [];
|
|
38727
|
+
for (let column = 0; column < line.length; column += 1) {
|
|
38728
|
+
const cell = line.getCell(column, scratch);
|
|
38729
|
+
if (!cell || cell.getWidth() === 0) continue;
|
|
38730
|
+
cells.push({
|
|
38731
|
+
text: cell.getChars() || " ",
|
|
38732
|
+
style: resolveCellStyle(cell)
|
|
38733
|
+
});
|
|
38734
|
+
}
|
|
38735
|
+
styled.push(buildCellRuns(cells));
|
|
38736
|
+
}
|
|
38737
|
+
return styled;
|
|
38738
|
+
},
|
|
38434
38739
|
dispose: () => {
|
|
38435
38740
|
term.dispose();
|
|
38436
38741
|
}
|
|
@@ -38441,6 +38746,9 @@ function createXtermScreen(cols, rows) {
|
|
|
38441
38746
|
var MJPEG_BOUNDARY = "camstack-terminal-frame";
|
|
38442
38747
|
var SESSION_IDLE_MS = 3e4;
|
|
38443
38748
|
var SNAPSHOT_STARTUP_WAIT_MS = 1500;
|
|
38749
|
+
var SNAPSHOT_SETTLE_QUIET_MS = 300;
|
|
38750
|
+
var SNAPSHOT_SETTLE_BUDGET_MS = 3e3;
|
|
38751
|
+
var SNAPSHOT_SETTLE_MAX_PULLS = 20;
|
|
38444
38752
|
var CLOSE_RETRY_BASE_MS = 50;
|
|
38445
38753
|
var CLOSE_RETRY_MAX_MS = 1e3;
|
|
38446
38754
|
var CLOSE_ATTEMPTS_PER_PASS = 3;
|
|
@@ -38565,20 +38873,36 @@ var TerminalCameraRelay = class {
|
|
|
38565
38873
|
if (state.openPromise === opening) state.openPromise = null;
|
|
38566
38874
|
}
|
|
38567
38875
|
}
|
|
38876
|
+
/** Replay one output batch into the relay screen; true = the session exited. */
|
|
38877
|
+
applyBatch(state, batch) {
|
|
38878
|
+
if (batch.reset) {
|
|
38879
|
+
state.screen.dispose();
|
|
38880
|
+
state.screen = createXtermScreen(120, 40);
|
|
38881
|
+
if (batch.snapshot) state.screen.write(batch.snapshot);
|
|
38882
|
+
}
|
|
38883
|
+
let exited = false;
|
|
38884
|
+
for (const event of batch.events) if (event.kind === "data") state.screen.write(event.data);
|
|
38885
|
+
else {
|
|
38886
|
+
state.sessionId = null;
|
|
38887
|
+
exited = true;
|
|
38888
|
+
}
|
|
38889
|
+
state.cursor = exited ? 0 : batch.cursor;
|
|
38890
|
+
return exited;
|
|
38891
|
+
}
|
|
38568
38892
|
async nextFrame(state, forceRender = false, waitForInitialOutput = false) {
|
|
38569
38893
|
if (state.framePromise) {
|
|
38570
38894
|
await state.framePromise;
|
|
38571
38895
|
return forceRender ? this.nextFrame(state, true, waitForInitialOutput) : state.jpeg ?? this.nextFrame(state, false, waitForInitialOutput);
|
|
38572
38896
|
}
|
|
38573
38897
|
const render = async () => {
|
|
38574
|
-
const
|
|
38898
|
+
const initialPull = state.sessionId === null && waitForInitialOutput;
|
|
38575
38899
|
const sessionId = await this.ensureSession(state);
|
|
38576
38900
|
let batch;
|
|
38577
38901
|
try {
|
|
38578
38902
|
batch = await this.api.pullOutput(state.nodeId, {
|
|
38579
38903
|
sessionId,
|
|
38580
38904
|
afterSeq: state.cursor,
|
|
38581
|
-
...
|
|
38905
|
+
...initialPull ? {
|
|
38582
38906
|
waitMs: SNAPSHOT_STARTUP_WAIT_MS,
|
|
38583
38907
|
waitForOutput: true
|
|
38584
38908
|
} : {}
|
|
@@ -38600,21 +38924,29 @@ var TerminalCameraRelay = class {
|
|
|
38600
38924
|
state.cursor = 0;
|
|
38601
38925
|
throw error;
|
|
38602
38926
|
}
|
|
38603
|
-
|
|
38604
|
-
|
|
38605
|
-
|
|
38606
|
-
|
|
38607
|
-
|
|
38608
|
-
|
|
38609
|
-
|
|
38610
|
-
|
|
38611
|
-
|
|
38612
|
-
|
|
38927
|
+
let exited = this.applyBatch(state, batch);
|
|
38928
|
+
if (initialPull && !exited) {
|
|
38929
|
+
const deadline = Date.now() + SNAPSHOT_SETTLE_BUDGET_MS;
|
|
38930
|
+
for (let pulls = 0; pulls < SNAPSHOT_SETTLE_MAX_PULLS && Date.now() < deadline; pulls += 1) {
|
|
38931
|
+
let settle;
|
|
38932
|
+
try {
|
|
38933
|
+
settle = await this.api.pullOutput(state.nodeId, {
|
|
38934
|
+
sessionId,
|
|
38935
|
+
afterSeq: state.cursor,
|
|
38936
|
+
waitMs: SNAPSHOT_SETTLE_QUIET_MS,
|
|
38937
|
+
waitForOutput: true
|
|
38938
|
+
});
|
|
38939
|
+
} catch {
|
|
38940
|
+
break;
|
|
38941
|
+
}
|
|
38942
|
+
const active = settle.reset || settle.events.some((event) => event.kind === "data");
|
|
38943
|
+
exited = this.applyBatch(state, settle);
|
|
38944
|
+
if (exited || !active) break;
|
|
38945
|
+
}
|
|
38613
38946
|
}
|
|
38614
|
-
state.cursor = exited ? 0 : batch.cursor;
|
|
38615
38947
|
await state.screen.flush();
|
|
38616
38948
|
if (forceRender || state.jpeg === null || state.renderedCursor !== state.cursor) {
|
|
38617
|
-
state.jpeg = await renderTerminalJpeg(state.screen.
|
|
38949
|
+
state.jpeg = await renderTerminalJpeg(state.screen.styledLines());
|
|
38618
38950
|
state.renderedCursor = state.cursor;
|
|
38619
38951
|
}
|
|
38620
38952
|
return state.jpeg;
|
|
@@ -39063,7 +39395,8 @@ function buildProfiles(options) {
|
|
|
39063
39395
|
"-m",
|
|
39064
39396
|
"glances",
|
|
39065
39397
|
...options.glancesArgs ?? []
|
|
39066
|
-
]
|
|
39398
|
+
],
|
|
39399
|
+
...options.glancesEnv && Object.keys(options.glancesEnv).length > 0 ? { env: options.glancesEnv } : {}
|
|
39067
39400
|
});
|
|
39068
39401
|
}
|
|
39069
39402
|
if (options.allowShell) profiles.push({
|
|
@@ -39148,6 +39481,7 @@ var TerminalSessionManager = class {
|
|
|
39148
39481
|
glancesPath: opts.glancesPath,
|
|
39149
39482
|
glancesArgs: opts.glancesArgs,
|
|
39150
39483
|
glancesPythonPath: opts.glancesPythonPath,
|
|
39484
|
+
glancesEnv: opts.glancesEnv,
|
|
39151
39485
|
allowShell: opts.allowShell,
|
|
39152
39486
|
shellPath: opts.shellPath,
|
|
39153
39487
|
customProfiles: opts.customProfiles,
|
|
@@ -39445,6 +39779,18 @@ var TerminalSessionManager = class {
|
|
|
39445
39779
|
};
|
|
39446
39780
|
//#endregion
|
|
39447
39781
|
//#region src/addon.ts
|
|
39782
|
+
/**
|
|
39783
|
+
* Terminal addon — hosts interactive pty sessions running an allowlisted
|
|
39784
|
+
* profile (`monitor` → `btm`), streamed to the Admin UI over the data plane.
|
|
39785
|
+
*
|
|
39786
|
+
* Phase 1 of docs/design/2026-07-26-terminal-session-design.md: a real TTY good
|
|
39787
|
+
* enough to run a full-screen program. Phase 2 (streaming a session as a camera)
|
|
39788
|
+
* is a separate device-provider concern and does not change this addon's cap.
|
|
39789
|
+
*
|
|
39790
|
+
* Its own package (not a builtin) because it carries a native dependency
|
|
39791
|
+
* (`node-pty`) and should update via `camstack deploy` independently of the
|
|
39792
|
+
* framework, exactly like the node-av decoder addon.
|
|
39793
|
+
*/
|
|
39448
39794
|
var DEFAULTS = {
|
|
39449
39795
|
btmPath: "",
|
|
39450
39796
|
btmEnabled: true,
|
|
@@ -39477,12 +39823,21 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39477
39823
|
instanceMutationQueue = new TerminalInstanceMutationQueue();
|
|
39478
39824
|
terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
|
|
39479
39825
|
glancesPythonPath = "";
|
|
39826
|
+
glancesEnv;
|
|
39480
39827
|
constructor() {
|
|
39481
39828
|
super({ ...DEFAULTS });
|
|
39482
39829
|
}
|
|
39483
39830
|
async onInitialize() {
|
|
39484
39831
|
await warmNodePty();
|
|
39485
39832
|
this.glancesPythonPath = await this.ctx.deps.ensurePython() ?? "";
|
|
39833
|
+
const shimDir = cursesShimDir();
|
|
39834
|
+
this.glancesEnv = resolveGlancesCursesShimEnv({
|
|
39835
|
+
platform: process.platform,
|
|
39836
|
+
shimDir,
|
|
39837
|
+
shimDirExists: cursesShimDirExists(shimDir),
|
|
39838
|
+
existingPythonPath: process.env["PYTHONPATH"]
|
|
39839
|
+
});
|
|
39840
|
+
this.probeGlancesCursesColours();
|
|
39486
39841
|
const manager = new TerminalSessionManager({
|
|
39487
39842
|
spawn: createNodePtySpawner(),
|
|
39488
39843
|
screenFactory: createXtermScreen,
|
|
@@ -39498,6 +39853,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39498
39853
|
glancesPath: this.config.glancesPath,
|
|
39499
39854
|
glancesArgs: this.config.glancesArgs,
|
|
39500
39855
|
glancesPythonPath: this.glancesPythonPath,
|
|
39856
|
+
glancesEnv: this.glancesEnv,
|
|
39501
39857
|
allowShell: this.config.allowShell,
|
|
39502
39858
|
shellPath: this.config.shellPath,
|
|
39503
39859
|
maxSessions: this.config.maxSessions,
|
|
@@ -39543,6 +39899,39 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39543
39899
|
provider: manager
|
|
39544
39900
|
}];
|
|
39545
39901
|
}
|
|
39902
|
+
/**
|
|
39903
|
+
* Fire-and-forget canary: the managed python must see a colour-capable
|
|
39904
|
+
* curses, or glances paints its B&W theme (SGR conceal/reverse) and the
|
|
39905
|
+
* terminal camera frames lose their text. The shim filename is ABI-locked
|
|
39906
|
+
* (cpython-312), so a managed-python bump silently disables it — this probe
|
|
39907
|
+
* is what makes that failure loud instead of a garbled camera.
|
|
39908
|
+
*/
|
|
39909
|
+
probeGlancesCursesColours() {
|
|
39910
|
+
if (!this.glancesPythonPath) return;
|
|
39911
|
+
const probeEnv = {
|
|
39912
|
+
...process.env,
|
|
39913
|
+
...this.glancesEnv,
|
|
39914
|
+
TERM: "xterm-256color"
|
|
39915
|
+
};
|
|
39916
|
+
(0, node_child_process.execFile)(this.glancesPythonPath, ["-c", "import curses\ncurses.setupterm('xterm-256color')\nprint(curses.tigetnum('colors'))"], {
|
|
39917
|
+
env: probeEnv,
|
|
39918
|
+
timeout: 1e4
|
|
39919
|
+
}, (error, stdout) => {
|
|
39920
|
+
const colours = error ? NaN : Number.parseInt(stdout.trim(), 10);
|
|
39921
|
+
if (!Number.isFinite(colours) || colours <= 0) {
|
|
39922
|
+
this.ctx.logger.warn("glances curses reports no colours — terminal camera frames will render the B&W theme", { meta: {
|
|
39923
|
+
colours: Number.isFinite(colours) ? colours : null,
|
|
39924
|
+
shimApplied: this.glancesEnv !== void 0,
|
|
39925
|
+
error: error ? error instanceof Error ? error.message : String(error) : void 0
|
|
39926
|
+
} });
|
|
39927
|
+
return;
|
|
39928
|
+
}
|
|
39929
|
+
this.ctx.logger.debug("glances curses colours available", { meta: {
|
|
39930
|
+
colours,
|
|
39931
|
+
shimApplied: this.glancesEnv !== void 0
|
|
39932
|
+
} });
|
|
39933
|
+
});
|
|
39934
|
+
}
|
|
39546
39935
|
async onConfigChanged() {
|
|
39547
39936
|
this.replaceTerminalCameraTombstones(this.config.terminalCameraTombstones);
|
|
39548
39937
|
this.manager?.reconfigureProfiles({
|
|
@@ -39556,6 +39945,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39556
39945
|
glancesPath: this.config.glancesPath,
|
|
39557
39946
|
glancesArgs: this.config.glancesArgs,
|
|
39558
39947
|
glancesPythonPath: this.glancesPythonPath,
|
|
39948
|
+
glancesEnv: this.glancesEnv,
|
|
39559
39949
|
allowShell: this.config.allowShell,
|
|
39560
39950
|
shellPath: this.config.shellPath,
|
|
39561
39951
|
maxSessions: this.config.maxSessions,
|
|
@@ -40002,11 +40392,17 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
40002
40392
|
}
|
|
40003
40393
|
};
|
|
40004
40394
|
//#endregion
|
|
40395
|
+
exports.TERMINAL_DEFAULT_BG = TERMINAL_DEFAULT_BG;
|
|
40396
|
+
exports.TERMINAL_DEFAULT_FG = TERMINAL_DEFAULT_FG;
|
|
40005
40397
|
exports.TerminalAddon = TerminalAddon;
|
|
40006
40398
|
exports.TerminalSessionManager = TerminalSessionManager;
|
|
40399
|
+
exports.buildCellRuns = buildCellRuns;
|
|
40007
40400
|
exports.buildProfiles = buildProfiles;
|
|
40008
40401
|
exports.createNodePtySpawner = createNodePtySpawner;
|
|
40009
40402
|
exports.createTerminalDataPlaneHandler = createTerminalDataPlaneHandler;
|
|
40010
40403
|
exports.createXtermScreen = createXtermScreen;
|
|
40011
40404
|
exports.findProfile = findProfile;
|
|
40405
|
+
exports.resolveCellStyle = resolveCellStyle;
|
|
40406
|
+
exports.terminalPaletteColor = terminalPaletteColor;
|
|
40407
|
+
exports.terminalRgbColor = terminalRgbColor;
|
|
40012
40408
|
exports.warmNodePty = warmNodePty;
|
package/dist/addon.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
2
3
|
import { accessSync, chmodSync, constants, existsSync, statSync } from "node:fs";
|
|
3
|
-
import { delimiter, dirname, join } from "node:path";
|
|
4
|
+
import path, { delimiter, dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
import sharp from "sharp";
|
|
5
7
|
import { createServer } from "node:http";
|
|
6
8
|
//#region \0rolldown/runtime.js
|
|
@@ -33355,6 +33357,46 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
|
|
|
33355
33357
|
DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
33356
33358
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
33357
33359
|
//#endregion
|
|
33360
|
+
//#region src/curses-shim.ts
|
|
33361
|
+
/**
|
|
33362
|
+
* macOS repair for the managed Python's curses.
|
|
33363
|
+
*
|
|
33364
|
+
* The python-build-standalone distribution the dependency resolver installs on
|
|
33365
|
+
* macOS links ncurses statically with the terminfo database compiled OUT
|
|
33366
|
+
* (`--disable-database`) and an ABI mismatch that misparses every numeric
|
|
33367
|
+
* capability of its five built-in fallback terminals (`colors=-1`,
|
|
33368
|
+
* `cols=524368`). `has_colors()` is therefore False under every TERM value and
|
|
33369
|
+
* `init_pair` is hard-blocked, so glances takes its B&W branch and paints the
|
|
33370
|
+
* whole screen with SGR conceal/reverse/alt-charset — the "text missing,
|
|
33371
|
+
* light bars instead" terminal camera frames. No environment variable can fix
|
|
33372
|
+
* it: the code that would read TERMINFO/TERMINFO_DIRS was never compiled in.
|
|
33373
|
+
*
|
|
33374
|
+
* The repair ships prebuilt `_curses` / `_curses_panel` extension modules
|
|
33375
|
+
* linked against the system `/usr/lib/libncurses.5.4.dylib` (universal2) and
|
|
33376
|
+
* puts their directory on PYTHONPATH at PTY spawn — PYTHONPATH precedes
|
|
33377
|
+
* lib-dynload on sys.path, so the working modules shadow the broken ones with
|
|
33378
|
+
* no change to the managed distribution. The filename is ABI-locked to
|
|
33379
|
+
* cpython-312: a managed-python bump to 3.13 makes the shim silently inert
|
|
33380
|
+
* (python ignores a mismatched ABI tag), which is why the addon probes
|
|
33381
|
+
* `tigetnum("colors")` at startup and logs loudly when colours are missing.
|
|
33382
|
+
*/
|
|
33383
|
+
/** `<addonRoot>/assets/curses-shim/py312`, valid from both src/ and dist/. */
|
|
33384
|
+
function cursesShimDir() {
|
|
33385
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets", "curses-shim", "py312");
|
|
33386
|
+
}
|
|
33387
|
+
function cursesShimDirExists(shimDir) {
|
|
33388
|
+
return existsSync(shimDir);
|
|
33389
|
+
}
|
|
33390
|
+
/**
|
|
33391
|
+
* The env to merge into the glances profile, or undefined when the shim does
|
|
33392
|
+
* not apply (non-macOS, or the assets were not shipped).
|
|
33393
|
+
*/
|
|
33394
|
+
function resolveGlancesCursesShimEnv(options) {
|
|
33395
|
+
if (options.platform !== "darwin" || !options.shimDirExists) return void 0;
|
|
33396
|
+
const existing = options.existingPythonPath?.trim();
|
|
33397
|
+
return { PYTHONPATH: existing ? `${options.shimDir}:${existing}` : options.shimDir };
|
|
33398
|
+
}
|
|
33399
|
+
//#endregion
|
|
33358
33400
|
//#region src/pty.ts
|
|
33359
33401
|
/**
|
|
33360
33402
|
* Minimal pty abstraction. The manager depends on this interface, never on
|
|
@@ -33485,6 +33527,198 @@ function buildTerminalInstanceCameraDeclarations(instances) {
|
|
|
33485
33527
|
function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
|
|
33486
33528
|
return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
|
|
33487
33529
|
}
|
|
33530
|
+
//#endregion
|
|
33531
|
+
//#region src/terminal-cell-runs.ts
|
|
33532
|
+
var TERMINAL_DEFAULT_FG = "#d7dce2";
|
|
33533
|
+
var TERMINAL_DEFAULT_BG = "#0b0d10";
|
|
33534
|
+
/**
|
|
33535
|
+
* The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
|
|
33536
|
+
* frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
|
|
33537
|
+
* whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
|
|
33538
|
+
* near-black background at 13px. Index 7 IS the default foreground, so plain
|
|
33539
|
+
* `CSI 37m` text renders identically to unstyled text.
|
|
33540
|
+
*/
|
|
33541
|
+
var TERMINAL_ANSI_PALETTE = [
|
|
33542
|
+
"#282c34",
|
|
33543
|
+
"#e06c75",
|
|
33544
|
+
"#98c379",
|
|
33545
|
+
"#e5c07b",
|
|
33546
|
+
"#61afef",
|
|
33547
|
+
"#c678dd",
|
|
33548
|
+
"#56b6c2",
|
|
33549
|
+
TERMINAL_DEFAULT_FG,
|
|
33550
|
+
"#5c6370",
|
|
33551
|
+
"#ef596f",
|
|
33552
|
+
"#89ca78",
|
|
33553
|
+
"#f0c674",
|
|
33554
|
+
"#6cb6ff",
|
|
33555
|
+
"#d55fde",
|
|
33556
|
+
"#2bbac5",
|
|
33557
|
+
"#ffffff"
|
|
33558
|
+
];
|
|
33559
|
+
/** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
|
|
33560
|
+
var TERMINAL_CUBE_LEVELS = [
|
|
33561
|
+
0,
|
|
33562
|
+
95,
|
|
33563
|
+
135,
|
|
33564
|
+
175,
|
|
33565
|
+
215,
|
|
33566
|
+
255
|
|
33567
|
+
];
|
|
33568
|
+
var TERMINAL_CUBE_FIRST = 16;
|
|
33569
|
+
var TERMINAL_GRAYSCALE_FIRST = 232;
|
|
33570
|
+
var TERMINAL_GRAYSCALE_BASE = 8;
|
|
33571
|
+
var TERMINAL_GRAYSCALE_STEP = 10;
|
|
33572
|
+
/** SGR 2 keeps the foreground legible; it must not become the background. */
|
|
33573
|
+
var TERMINAL_DIM_WEIGHT = .6;
|
|
33574
|
+
function channel(value) {
|
|
33575
|
+
return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
|
|
33576
|
+
}
|
|
33577
|
+
function hex(red, green, blue) {
|
|
33578
|
+
return `#${channel(red)}${channel(green)}${channel(blue)}`;
|
|
33579
|
+
}
|
|
33580
|
+
function parseHex(color) {
|
|
33581
|
+
return [
|
|
33582
|
+
Number.parseInt(color.slice(1, 3), 16),
|
|
33583
|
+
Number.parseInt(color.slice(3, 5), 16),
|
|
33584
|
+
Number.parseInt(color.slice(5, 7), 16)
|
|
33585
|
+
];
|
|
33586
|
+
}
|
|
33587
|
+
/** Resolve an xterm palette index (0-255) to a hex colour. */
|
|
33588
|
+
function terminalPaletteColor(index) {
|
|
33589
|
+
const ansi = TERMINAL_ANSI_PALETTE[index];
|
|
33590
|
+
if (ansi !== void 0) return ansi;
|
|
33591
|
+
if (index >= TERMINAL_GRAYSCALE_FIRST) {
|
|
33592
|
+
const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
|
|
33593
|
+
return hex(level, level, level);
|
|
33594
|
+
}
|
|
33595
|
+
if (index >= TERMINAL_CUBE_FIRST) {
|
|
33596
|
+
const offset = index - TERMINAL_CUBE_FIRST;
|
|
33597
|
+
return hex(TERMINAL_CUBE_LEVELS[Math.floor(offset / 36) % 6] ?? 0, TERMINAL_CUBE_LEVELS[Math.floor(offset / 6) % 6] ?? 0, TERMINAL_CUBE_LEVELS[offset % 6] ?? 0);
|
|
33598
|
+
}
|
|
33599
|
+
return TERMINAL_DEFAULT_FG;
|
|
33600
|
+
}
|
|
33601
|
+
/** Resolve a 0xRRGGBB truecolor value to a hex colour. */
|
|
33602
|
+
function terminalRgbColor(value) {
|
|
33603
|
+
return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
|
|
33604
|
+
}
|
|
33605
|
+
function blend(color, toward, weight) {
|
|
33606
|
+
const [red, green, blue] = parseHex(color);
|
|
33607
|
+
const [targetRed, targetGreen, targetBlue] = parseHex(toward);
|
|
33608
|
+
return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
|
|
33609
|
+
}
|
|
33610
|
+
function resolveForeground(cell) {
|
|
33611
|
+
if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
|
|
33612
|
+
if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
|
|
33613
|
+
return TERMINAL_DEFAULT_FG;
|
|
33614
|
+
}
|
|
33615
|
+
function resolveBackground(cell) {
|
|
33616
|
+
if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
|
|
33617
|
+
if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
|
|
33618
|
+
return TERMINAL_DEFAULT_BG;
|
|
33619
|
+
}
|
|
33620
|
+
/**
|
|
33621
|
+
* Resolve one cell's attributes into concrete colours.
|
|
33622
|
+
*
|
|
33623
|
+
* Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
|
|
33624
|
+
* defaults is still a visible swap rather than a no-op — that is how a selected
|
|
33625
|
+
* or highlighted row in Glances reads. Invisible is then conceal-by-equality
|
|
33626
|
+
* (foreground painted in its own background): the cell keeps its columns, which
|
|
33627
|
+
* a dropped cell would not, and dropping it would shift the whole rest of the
|
|
33628
|
+
* row left.
|
|
33629
|
+
*/
|
|
33630
|
+
function resolveCellStyle(cell) {
|
|
33631
|
+
const inverse = cell.isInverse() !== 0;
|
|
33632
|
+
const plainFg = resolveForeground(cell);
|
|
33633
|
+
const plainBg = resolveBackground(cell);
|
|
33634
|
+
const background = inverse ? plainFg : plainBg;
|
|
33635
|
+
let foreground = inverse ? plainBg : plainFg;
|
|
33636
|
+
if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
|
|
33637
|
+
if (cell.isInvisible() !== 0) foreground = background;
|
|
33638
|
+
return {
|
|
33639
|
+
fg: foreground === "#d7dce2" ? null : foreground,
|
|
33640
|
+
bg: background === "#0b0d10" ? null : background,
|
|
33641
|
+
bold: cell.isBold() !== 0
|
|
33642
|
+
};
|
|
33643
|
+
}
|
|
33644
|
+
function sameStyle(left, right) {
|
|
33645
|
+
return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
|
|
33646
|
+
}
|
|
33647
|
+
/**
|
|
33648
|
+
* Merge adjacent same-style cells into runs, then drop the trailing run of
|
|
33649
|
+
* default-styled whitespace so a row costs what it draws — the same trim
|
|
33650
|
+
* `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
|
|
33651
|
+
* a green bar of spaces out to the right margin is a pixel Glances drew.
|
|
33652
|
+
*/
|
|
33653
|
+
function buildCellRuns(cells) {
|
|
33654
|
+
const runs = [];
|
|
33655
|
+
let text = "";
|
|
33656
|
+
let style = null;
|
|
33657
|
+
for (const cell of cells) {
|
|
33658
|
+
if (style !== null && sameStyle(style, cell.style)) {
|
|
33659
|
+
text += cell.text;
|
|
33660
|
+
continue;
|
|
33661
|
+
}
|
|
33662
|
+
if (style !== null) runs.push({
|
|
33663
|
+
text,
|
|
33664
|
+
...style
|
|
33665
|
+
});
|
|
33666
|
+
text = cell.text;
|
|
33667
|
+
style = cell.style;
|
|
33668
|
+
}
|
|
33669
|
+
if (style !== null) runs.push({
|
|
33670
|
+
text,
|
|
33671
|
+
...style
|
|
33672
|
+
});
|
|
33673
|
+
while (runs.length > 0) {
|
|
33674
|
+
const last = runs[runs.length - 1];
|
|
33675
|
+
if (last === void 0 || last.bg !== null) break;
|
|
33676
|
+
const trimmed = last.text.replace(/\s+$/u, "");
|
|
33677
|
+
if (trimmed === last.text) break;
|
|
33678
|
+
if (trimmed === "") {
|
|
33679
|
+
runs.pop();
|
|
33680
|
+
continue;
|
|
33681
|
+
}
|
|
33682
|
+
runs[runs.length - 1] = {
|
|
33683
|
+
...last,
|
|
33684
|
+
text: trimmed
|
|
33685
|
+
};
|
|
33686
|
+
break;
|
|
33687
|
+
}
|
|
33688
|
+
return runs;
|
|
33689
|
+
}
|
|
33690
|
+
/**
|
|
33691
|
+
* Monospace families to try, in order — NOT one family and a generic.
|
|
33692
|
+
*
|
|
33693
|
+
* A terminal screen is mostly box-drawing and block characters, and a font
|
|
33694
|
+
* without them renders the frame as noise rather than as missing detail.
|
|
33695
|
+
* `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
|
|
33696
|
+
* DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
|
|
33697
|
+
* coverage is not, and its Glances camera came out unreadable while the hub's
|
|
33698
|
+
* was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
|
|
33699
|
+
*
|
|
33700
|
+
* `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
|
|
33701
|
+
* present on every install, and derived from DejaVu Sans Mono — the same glyph
|
|
33702
|
+
* coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
|
|
33703
|
+
* generic stays last so a host with none of them still draws something.
|
|
33704
|
+
*/
|
|
33705
|
+
var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
|
|
33706
|
+
var TERMINAL_FONT_SIZE = 13;
|
|
33707
|
+
var TERMINAL_TEXT_MARGIN_X = 8;
|
|
33708
|
+
var TERMINAL_ROW_HEIGHT = 15;
|
|
33709
|
+
var TERMINAL_BASELINE_Y = 18;
|
|
33710
|
+
/**
|
|
33711
|
+
* Distance from a row's baseline up to the top of its cell box. Chosen so
|
|
33712
|
+
* consecutive rows tile exactly: row N's box runs from `baseline - this` for
|
|
33713
|
+
* `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
|
|
33714
|
+
* bar that stopped short would draw as stripes across a `CSI 42m` panel.
|
|
33715
|
+
*/
|
|
33716
|
+
var TERMINAL_CELL_ASCENT = 11.5;
|
|
33717
|
+
var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
|
|
33718
|
+
/** Two decimals is under a tenth of a pixel and keeps the SVG small. */
|
|
33719
|
+
function coordinate(value) {
|
|
33720
|
+
return String(Number(value.toFixed(2)));
|
|
33721
|
+
}
|
|
33488
33722
|
function escapeXml(value) {
|
|
33489
33723
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
33490
33724
|
}
|
|
@@ -33499,11 +33733,47 @@ function escapeXml(value) {
|
|
|
33499
33733
|
* looked perfect — which is exactly how the operator reported it. Measured in
|
|
33500
33734
|
* the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
|
|
33501
33735
|
* collapsed against 178 px preserved.
|
|
33502
|
-
|
|
33503
|
-
|
|
33504
|
-
|
|
33505
|
-
|
|
33506
|
-
|
|
33736
|
+
*
|
|
33737
|
+
* Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
|
|
33738
|
+
* never appended to the one before it, so the background rects and the glyphs
|
|
33739
|
+
* are placed off the same grid and cannot drift apart. `textLength` is emitted
|
|
33740
|
+
* with it because it is the correct declaration and renderers that honour it
|
|
33741
|
+
* get an exact grid — but it is not what makes this work: librsvg, which sharp
|
|
33742
|
+
* uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
|
|
33743
|
+
* 600 px still drew its natural 937 px). The anchoring is the guarantee.
|
|
33744
|
+
*/
|
|
33745
|
+
function renderTerminalSvg(rows) {
|
|
33746
|
+
const backgrounds = [];
|
|
33747
|
+
const texts = [];
|
|
33748
|
+
rows.slice(0, 40).forEach((row, index) => {
|
|
33749
|
+
const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
|
|
33750
|
+
const top = baseline - TERMINAL_CELL_ASCENT;
|
|
33751
|
+
let column = 0;
|
|
33752
|
+
for (const run of row) {
|
|
33753
|
+
if (column >= 120) break;
|
|
33754
|
+
const clipped = clipRun(run, 120 - column);
|
|
33755
|
+
const columns = [...clipped].length;
|
|
33756
|
+
if (columns === 0) continue;
|
|
33757
|
+
const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
|
|
33758
|
+
const width = columns * TERMINAL_CELL_WIDTH;
|
|
33759
|
+
if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
|
|
33760
|
+
if (clipped.trim() !== "") {
|
|
33761
|
+
const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
|
|
33762
|
+
const weight = run.bold ? " font-weight=\"bold\"" : "";
|
|
33763
|
+
texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
|
|
33764
|
+
}
|
|
33765
|
+
column += columns;
|
|
33766
|
+
}
|
|
33767
|
+
});
|
|
33768
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${String(960)}" height="${String(640)}"><rect width="100%" height="100%" fill="${TERMINAL_DEFAULT_BG}"/>${backgrounds.join("")}<g fill="${TERMINAL_DEFAULT_FG}" font-family="${TERMINAL_FONT_STACK}" font-size="${String(TERMINAL_FONT_SIZE)}">${texts.join("")}</g></svg>`;
|
|
33769
|
+
}
|
|
33770
|
+
/** Cut a run to the columns still left in the row, by code point not unit. */
|
|
33771
|
+
function clipRun(run, remaining) {
|
|
33772
|
+
const points = [...run.text];
|
|
33773
|
+
return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
|
|
33774
|
+
}
|
|
33775
|
+
async function renderTerminalJpeg(rows) {
|
|
33776
|
+
return sharp(Buffer.from(renderTerminalSvg(rows))).jpeg({
|
|
33507
33777
|
quality: 82,
|
|
33508
33778
|
chromaSubsampling: "4:2:0"
|
|
33509
33779
|
}).toBuffer();
|
|
@@ -38409,6 +38679,40 @@ function createXtermScreen(cols, rows) {
|
|
|
38409
38679
|
for (let row = 0; row < term.rows; row += 1) lines.push(buffer.getLine(buffer.viewportY + row)?.translateToString(true) ?? "");
|
|
38410
38680
|
return lines;
|
|
38411
38681
|
},
|
|
38682
|
+
/**
|
|
38683
|
+
* The colour-bearing read. `translateToString` — the only cell read this
|
|
38684
|
+
* screen used to offer — keeps characters and discards every attribute,
|
|
38685
|
+
* which is why the terminal camera rendered a fully-coloured Glances in
|
|
38686
|
+
* grey. Walking cells is the only way to get the attributes back.
|
|
38687
|
+
*
|
|
38688
|
+
* A zero-width cell is the tail half of a wide glyph (CJK, some emoji);
|
|
38689
|
+
* its characters already came back with the leading cell, so skipping it
|
|
38690
|
+
* keeps the text right. `getNullCell()` is reused across the whole screen
|
|
38691
|
+
* because `getCell` allocates otherwise — 4800 objects per frame.
|
|
38692
|
+
*/
|
|
38693
|
+
styledLines: () => {
|
|
38694
|
+
const buffer = term.buffer.active;
|
|
38695
|
+
const scratch = buffer.getNullCell();
|
|
38696
|
+
const styled = [];
|
|
38697
|
+
for (let row = 0; row < term.rows; row += 1) {
|
|
38698
|
+
const line = buffer.getLine(buffer.viewportY + row);
|
|
38699
|
+
if (!line) {
|
|
38700
|
+
styled.push([]);
|
|
38701
|
+
continue;
|
|
38702
|
+
}
|
|
38703
|
+
const cells = [];
|
|
38704
|
+
for (let column = 0; column < line.length; column += 1) {
|
|
38705
|
+
const cell = line.getCell(column, scratch);
|
|
38706
|
+
if (!cell || cell.getWidth() === 0) continue;
|
|
38707
|
+
cells.push({
|
|
38708
|
+
text: cell.getChars() || " ",
|
|
38709
|
+
style: resolveCellStyle(cell)
|
|
38710
|
+
});
|
|
38711
|
+
}
|
|
38712
|
+
styled.push(buildCellRuns(cells));
|
|
38713
|
+
}
|
|
38714
|
+
return styled;
|
|
38715
|
+
},
|
|
38412
38716
|
dispose: () => {
|
|
38413
38717
|
term.dispose();
|
|
38414
38718
|
}
|
|
@@ -38419,6 +38723,9 @@ function createXtermScreen(cols, rows) {
|
|
|
38419
38723
|
var MJPEG_BOUNDARY = "camstack-terminal-frame";
|
|
38420
38724
|
var SESSION_IDLE_MS = 3e4;
|
|
38421
38725
|
var SNAPSHOT_STARTUP_WAIT_MS = 1500;
|
|
38726
|
+
var SNAPSHOT_SETTLE_QUIET_MS = 300;
|
|
38727
|
+
var SNAPSHOT_SETTLE_BUDGET_MS = 3e3;
|
|
38728
|
+
var SNAPSHOT_SETTLE_MAX_PULLS = 20;
|
|
38422
38729
|
var CLOSE_RETRY_BASE_MS = 50;
|
|
38423
38730
|
var CLOSE_RETRY_MAX_MS = 1e3;
|
|
38424
38731
|
var CLOSE_ATTEMPTS_PER_PASS = 3;
|
|
@@ -38543,20 +38850,36 @@ var TerminalCameraRelay = class {
|
|
|
38543
38850
|
if (state.openPromise === opening) state.openPromise = null;
|
|
38544
38851
|
}
|
|
38545
38852
|
}
|
|
38853
|
+
/** Replay one output batch into the relay screen; true = the session exited. */
|
|
38854
|
+
applyBatch(state, batch) {
|
|
38855
|
+
if (batch.reset) {
|
|
38856
|
+
state.screen.dispose();
|
|
38857
|
+
state.screen = createXtermScreen(120, 40);
|
|
38858
|
+
if (batch.snapshot) state.screen.write(batch.snapshot);
|
|
38859
|
+
}
|
|
38860
|
+
let exited = false;
|
|
38861
|
+
for (const event of batch.events) if (event.kind === "data") state.screen.write(event.data);
|
|
38862
|
+
else {
|
|
38863
|
+
state.sessionId = null;
|
|
38864
|
+
exited = true;
|
|
38865
|
+
}
|
|
38866
|
+
state.cursor = exited ? 0 : batch.cursor;
|
|
38867
|
+
return exited;
|
|
38868
|
+
}
|
|
38546
38869
|
async nextFrame(state, forceRender = false, waitForInitialOutput = false) {
|
|
38547
38870
|
if (state.framePromise) {
|
|
38548
38871
|
await state.framePromise;
|
|
38549
38872
|
return forceRender ? this.nextFrame(state, true, waitForInitialOutput) : state.jpeg ?? this.nextFrame(state, false, waitForInitialOutput);
|
|
38550
38873
|
}
|
|
38551
38874
|
const render = async () => {
|
|
38552
|
-
const
|
|
38875
|
+
const initialPull = state.sessionId === null && waitForInitialOutput;
|
|
38553
38876
|
const sessionId = await this.ensureSession(state);
|
|
38554
38877
|
let batch;
|
|
38555
38878
|
try {
|
|
38556
38879
|
batch = await this.api.pullOutput(state.nodeId, {
|
|
38557
38880
|
sessionId,
|
|
38558
38881
|
afterSeq: state.cursor,
|
|
38559
|
-
...
|
|
38882
|
+
...initialPull ? {
|
|
38560
38883
|
waitMs: SNAPSHOT_STARTUP_WAIT_MS,
|
|
38561
38884
|
waitForOutput: true
|
|
38562
38885
|
} : {}
|
|
@@ -38578,21 +38901,29 @@ var TerminalCameraRelay = class {
|
|
|
38578
38901
|
state.cursor = 0;
|
|
38579
38902
|
throw error;
|
|
38580
38903
|
}
|
|
38581
|
-
|
|
38582
|
-
|
|
38583
|
-
|
|
38584
|
-
|
|
38585
|
-
|
|
38586
|
-
|
|
38587
|
-
|
|
38588
|
-
|
|
38589
|
-
|
|
38590
|
-
|
|
38904
|
+
let exited = this.applyBatch(state, batch);
|
|
38905
|
+
if (initialPull && !exited) {
|
|
38906
|
+
const deadline = Date.now() + SNAPSHOT_SETTLE_BUDGET_MS;
|
|
38907
|
+
for (let pulls = 0; pulls < SNAPSHOT_SETTLE_MAX_PULLS && Date.now() < deadline; pulls += 1) {
|
|
38908
|
+
let settle;
|
|
38909
|
+
try {
|
|
38910
|
+
settle = await this.api.pullOutput(state.nodeId, {
|
|
38911
|
+
sessionId,
|
|
38912
|
+
afterSeq: state.cursor,
|
|
38913
|
+
waitMs: SNAPSHOT_SETTLE_QUIET_MS,
|
|
38914
|
+
waitForOutput: true
|
|
38915
|
+
});
|
|
38916
|
+
} catch {
|
|
38917
|
+
break;
|
|
38918
|
+
}
|
|
38919
|
+
const active = settle.reset || settle.events.some((event) => event.kind === "data");
|
|
38920
|
+
exited = this.applyBatch(state, settle);
|
|
38921
|
+
if (exited || !active) break;
|
|
38922
|
+
}
|
|
38591
38923
|
}
|
|
38592
|
-
state.cursor = exited ? 0 : batch.cursor;
|
|
38593
38924
|
await state.screen.flush();
|
|
38594
38925
|
if (forceRender || state.jpeg === null || state.renderedCursor !== state.cursor) {
|
|
38595
|
-
state.jpeg = await renderTerminalJpeg(state.screen.
|
|
38926
|
+
state.jpeg = await renderTerminalJpeg(state.screen.styledLines());
|
|
38596
38927
|
state.renderedCursor = state.cursor;
|
|
38597
38928
|
}
|
|
38598
38929
|
return state.jpeg;
|
|
@@ -39041,7 +39372,8 @@ function buildProfiles(options) {
|
|
|
39041
39372
|
"-m",
|
|
39042
39373
|
"glances",
|
|
39043
39374
|
...options.glancesArgs ?? []
|
|
39044
|
-
]
|
|
39375
|
+
],
|
|
39376
|
+
...options.glancesEnv && Object.keys(options.glancesEnv).length > 0 ? { env: options.glancesEnv } : {}
|
|
39045
39377
|
});
|
|
39046
39378
|
}
|
|
39047
39379
|
if (options.allowShell) profiles.push({
|
|
@@ -39126,6 +39458,7 @@ var TerminalSessionManager = class {
|
|
|
39126
39458
|
glancesPath: opts.glancesPath,
|
|
39127
39459
|
glancesArgs: opts.glancesArgs,
|
|
39128
39460
|
glancesPythonPath: opts.glancesPythonPath,
|
|
39461
|
+
glancesEnv: opts.glancesEnv,
|
|
39129
39462
|
allowShell: opts.allowShell,
|
|
39130
39463
|
shellPath: opts.shellPath,
|
|
39131
39464
|
customProfiles: opts.customProfiles,
|
|
@@ -39423,6 +39756,18 @@ var TerminalSessionManager = class {
|
|
|
39423
39756
|
};
|
|
39424
39757
|
//#endregion
|
|
39425
39758
|
//#region src/addon.ts
|
|
39759
|
+
/**
|
|
39760
|
+
* Terminal addon — hosts interactive pty sessions running an allowlisted
|
|
39761
|
+
* profile (`monitor` → `btm`), streamed to the Admin UI over the data plane.
|
|
39762
|
+
*
|
|
39763
|
+
* Phase 1 of docs/design/2026-07-26-terminal-session-design.md: a real TTY good
|
|
39764
|
+
* enough to run a full-screen program. Phase 2 (streaming a session as a camera)
|
|
39765
|
+
* is a separate device-provider concern and does not change this addon's cap.
|
|
39766
|
+
*
|
|
39767
|
+
* Its own package (not a builtin) because it carries a native dependency
|
|
39768
|
+
* (`node-pty`) and should update via `camstack deploy` independently of the
|
|
39769
|
+
* framework, exactly like the node-av decoder addon.
|
|
39770
|
+
*/
|
|
39426
39771
|
var DEFAULTS = {
|
|
39427
39772
|
btmPath: "",
|
|
39428
39773
|
btmEnabled: true,
|
|
@@ -39455,12 +39800,21 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39455
39800
|
instanceMutationQueue = new TerminalInstanceMutationQueue();
|
|
39456
39801
|
terminalReconcileCoordinator = new TerminalInstanceReconcileCoordinator(this.instanceMutationQueue);
|
|
39457
39802
|
glancesPythonPath = "";
|
|
39803
|
+
glancesEnv;
|
|
39458
39804
|
constructor() {
|
|
39459
39805
|
super({ ...DEFAULTS });
|
|
39460
39806
|
}
|
|
39461
39807
|
async onInitialize() {
|
|
39462
39808
|
await warmNodePty();
|
|
39463
39809
|
this.glancesPythonPath = await this.ctx.deps.ensurePython() ?? "";
|
|
39810
|
+
const shimDir = cursesShimDir();
|
|
39811
|
+
this.glancesEnv = resolveGlancesCursesShimEnv({
|
|
39812
|
+
platform: process.platform,
|
|
39813
|
+
shimDir,
|
|
39814
|
+
shimDirExists: cursesShimDirExists(shimDir),
|
|
39815
|
+
existingPythonPath: process.env["PYTHONPATH"]
|
|
39816
|
+
});
|
|
39817
|
+
this.probeGlancesCursesColours();
|
|
39464
39818
|
const manager = new TerminalSessionManager({
|
|
39465
39819
|
spawn: createNodePtySpawner(),
|
|
39466
39820
|
screenFactory: createXtermScreen,
|
|
@@ -39476,6 +39830,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39476
39830
|
glancesPath: this.config.glancesPath,
|
|
39477
39831
|
glancesArgs: this.config.glancesArgs,
|
|
39478
39832
|
glancesPythonPath: this.glancesPythonPath,
|
|
39833
|
+
glancesEnv: this.glancesEnv,
|
|
39479
39834
|
allowShell: this.config.allowShell,
|
|
39480
39835
|
shellPath: this.config.shellPath,
|
|
39481
39836
|
maxSessions: this.config.maxSessions,
|
|
@@ -39521,6 +39876,39 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39521
39876
|
provider: manager
|
|
39522
39877
|
}];
|
|
39523
39878
|
}
|
|
39879
|
+
/**
|
|
39880
|
+
* Fire-and-forget canary: the managed python must see a colour-capable
|
|
39881
|
+
* curses, or glances paints its B&W theme (SGR conceal/reverse) and the
|
|
39882
|
+
* terminal camera frames lose their text. The shim filename is ABI-locked
|
|
39883
|
+
* (cpython-312), so a managed-python bump silently disables it — this probe
|
|
39884
|
+
* is what makes that failure loud instead of a garbled camera.
|
|
39885
|
+
*/
|
|
39886
|
+
probeGlancesCursesColours() {
|
|
39887
|
+
if (!this.glancesPythonPath) return;
|
|
39888
|
+
const probeEnv = {
|
|
39889
|
+
...process.env,
|
|
39890
|
+
...this.glancesEnv,
|
|
39891
|
+
TERM: "xterm-256color"
|
|
39892
|
+
};
|
|
39893
|
+
execFile(this.glancesPythonPath, ["-c", "import curses\ncurses.setupterm('xterm-256color')\nprint(curses.tigetnum('colors'))"], {
|
|
39894
|
+
env: probeEnv,
|
|
39895
|
+
timeout: 1e4
|
|
39896
|
+
}, (error, stdout) => {
|
|
39897
|
+
const colours = error ? NaN : Number.parseInt(stdout.trim(), 10);
|
|
39898
|
+
if (!Number.isFinite(colours) || colours <= 0) {
|
|
39899
|
+
this.ctx.logger.warn("glances curses reports no colours — terminal camera frames will render the B&W theme", { meta: {
|
|
39900
|
+
colours: Number.isFinite(colours) ? colours : null,
|
|
39901
|
+
shimApplied: this.glancesEnv !== void 0,
|
|
39902
|
+
error: error ? error instanceof Error ? error.message : String(error) : void 0
|
|
39903
|
+
} });
|
|
39904
|
+
return;
|
|
39905
|
+
}
|
|
39906
|
+
this.ctx.logger.debug("glances curses colours available", { meta: {
|
|
39907
|
+
colours,
|
|
39908
|
+
shimApplied: this.glancesEnv !== void 0
|
|
39909
|
+
} });
|
|
39910
|
+
});
|
|
39911
|
+
}
|
|
39524
39912
|
async onConfigChanged() {
|
|
39525
39913
|
this.replaceTerminalCameraTombstones(this.config.terminalCameraTombstones);
|
|
39526
39914
|
this.manager?.reconfigureProfiles({
|
|
@@ -39534,6 +39922,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39534
39922
|
glancesPath: this.config.glancesPath,
|
|
39535
39923
|
glancesArgs: this.config.glancesArgs,
|
|
39536
39924
|
glancesPythonPath: this.glancesPythonPath,
|
|
39925
|
+
glancesEnv: this.glancesEnv,
|
|
39537
39926
|
allowShell: this.config.allowShell,
|
|
39538
39927
|
shellPath: this.config.shellPath,
|
|
39539
39928
|
maxSessions: this.config.maxSessions,
|
|
@@ -39980,4 +40369,4 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
39980
40369
|
}
|
|
39981
40370
|
};
|
|
39982
40371
|
//#endregion
|
|
39983
|
-
export { TerminalAddon, createXtermScreen as a, createTerminalDataPlaneHandler as i, buildProfiles as n,
|
|
40372
|
+
export { TerminalAddon, createXtermScreen as a, buildCellRuns as c, terminalRgbColor as d, createNodePtySpawner as f, createTerminalDataPlaneHandler as i, resolveCellStyle as l, buildProfiles as n, TERMINAL_DEFAULT_BG as o, warmNodePty as p, findProfile as r, TERMINAL_DEFAULT_FG as s, TerminalSessionManager as t, terminalPaletteColor as u };
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,18 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_addon = require("./addon.js");
|
|
6
|
+
exports.TERMINAL_DEFAULT_BG = require_addon.TERMINAL_DEFAULT_BG;
|
|
7
|
+
exports.TERMINAL_DEFAULT_FG = require_addon.TERMINAL_DEFAULT_FG;
|
|
6
8
|
exports.TerminalAddon = require_addon.TerminalAddon;
|
|
7
9
|
exports.TerminalSessionManager = require_addon.TerminalSessionManager;
|
|
10
|
+
exports.buildCellRuns = require_addon.buildCellRuns;
|
|
8
11
|
exports.buildProfiles = require_addon.buildProfiles;
|
|
9
12
|
exports.createNodePtySpawner = require_addon.createNodePtySpawner;
|
|
10
13
|
exports.createTerminalDataPlaneHandler = require_addon.createTerminalDataPlaneHandler;
|
|
11
14
|
exports.createXtermScreen = require_addon.createXtermScreen;
|
|
12
15
|
exports.default = require_addon.TerminalAddon;
|
|
13
16
|
exports.findProfile = require_addon.findProfile;
|
|
17
|
+
exports.resolveCellStyle = require_addon.resolveCellStyle;
|
|
18
|
+
exports.terminalPaletteColor = require_addon.terminalPaletteColor;
|
|
19
|
+
exports.terminalRgbColor = require_addon.terminalRgbColor;
|
|
14
20
|
exports.warmNodePty = require_addon.warmNodePty;
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { TerminalAddon, a as createXtermScreen, i as createTerminalDataPlaneHandler, n as buildProfiles, o as
|
|
2
|
-
export { TerminalAddon, TerminalAddon as default, TerminalSessionManager, buildProfiles, createNodePtySpawner, createTerminalDataPlaneHandler, createXtermScreen, findProfile, warmNodePty };
|
|
1
|
+
import { TerminalAddon, a as createXtermScreen, c as buildCellRuns, d as terminalRgbColor, f as createNodePtySpawner, i as createTerminalDataPlaneHandler, l as resolveCellStyle, n as buildProfiles, o as TERMINAL_DEFAULT_BG, p as warmNodePty, r as findProfile, s as TERMINAL_DEFAULT_FG, t as TerminalSessionManager, u as terminalPaletteColor } from "./addon.mjs";
|
|
2
|
+
export { TERMINAL_DEFAULT_BG, TERMINAL_DEFAULT_FG, TerminalAddon, TerminalAddon as default, TerminalSessionManager, buildCellRuns, buildProfiles, createNodePtySpawner, createTerminalDataPlaneHandler, createXtermScreen, findProfile, resolveCellStyle, terminalPaletteColor, terminalRgbColor, warmNodePty };
|