mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-30 18:32:39 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d15a29bc6 | ||
|
|
519c24cc4c |
No files matched your search
@@ -5,6 +5,7 @@ import { createRequire } from "node:module";
|
||||
import type { SerializeAddon as SerializeAddonType } from "@xterm/addon-serialize";
|
||||
import type { Terminal as XTermType } from "@xterm/xterm";
|
||||
|
||||
import { DEFAULT_KEYWORD_HIGHLIGHT_RULES } from "../../domain/models/terminal.ts";
|
||||
import { writeTerminalDataWithLineTimestamps } from "./runtime/terminalLineTimestamps.ts";
|
||||
import {
|
||||
noteTerminalOutputPressureData,
|
||||
@@ -81,6 +82,76 @@ test("new output is colored on the written cells without decorations", async ()
|
||||
term.dispose();
|
||||
});
|
||||
|
||||
test("keyword highlight skips cells whose PTY foreground is already colored", async () => {
|
||||
// Starship directory segments set a truecolor foreground (#3940). Those
|
||||
// cells must keep the PTY color; only default-foreground matches are recolored.
|
||||
const unixPath = DEFAULT_KEYWORD_HIGHLIGHT_RULES.find((entry) => entry.id === "unix-path");
|
||||
assert.ok(unixPath);
|
||||
const keywordRgb = Number.parseInt(unixPath.color.slice(1), 16);
|
||||
const cream = 0xfbf1c7;
|
||||
const yellow = 0xfabd2f;
|
||||
const term = new XTerm({ allowProposedApi: true, cols: 80, rows: 5, scrollback: 20 });
|
||||
const highlighter = new KeywordHighlighter(term);
|
||||
highlighter.setRules([unixPath], true);
|
||||
await write(
|
||||
term,
|
||||
"\x1b[38;2;251;241;199m\x1b[48;2;250;189;47m/opt\x1b[0m"
|
||||
+ " \x1b[38;5;220m/mnt\x1b[0m"
|
||||
+ " \x1b[31m/bin\x1b[0m"
|
||||
+ " /var",
|
||||
);
|
||||
|
||||
const tokenCells = (text: string) => {
|
||||
const line = term.buffer.active.getLine(0);
|
||||
const index = (line?.translateToString(true) ?? "").indexOf(text);
|
||||
assert.ok(index >= 0, text);
|
||||
return Array.from({ length: text.length }, (_, offset) => {
|
||||
const cell = line?.getCell(index + offset);
|
||||
assert.ok(cell, `${text}[${offset}]`);
|
||||
return cell;
|
||||
});
|
||||
};
|
||||
|
||||
for (const cell of tokenCells("/opt")) {
|
||||
assert.equal(cell.isFgRGB(), true);
|
||||
assert.equal(cell.getFgColorMode(), 0x3000000);
|
||||
assert.equal(cell.getFgColor(), cream);
|
||||
assert.equal(cell.isBgRGB(), true);
|
||||
assert.equal(cell.getBgColor(), yellow);
|
||||
}
|
||||
for (const cell of tokenCells("/mnt")) {
|
||||
assert.equal(cell.isFgPalette(), true);
|
||||
assert.equal(cell.getFgColorMode(), 0x2000000);
|
||||
assert.equal(cell.getFgColor(), 220);
|
||||
}
|
||||
for (const cell of tokenCells("/bin")) {
|
||||
assert.equal(cell.isFgPalette(), true);
|
||||
assert.equal(cell.getFgColorMode(), 0x1000000);
|
||||
assert.equal(cell.getFgColor(), 1);
|
||||
}
|
||||
for (const cell of tokenCells("/var")) {
|
||||
assert.equal(cell.isFgRGB(), true);
|
||||
assert.equal(cell.getFgColor(), keywordRgb);
|
||||
}
|
||||
|
||||
highlighter.setRules([{ ...unixPath, color: "#60A5FA" }], true);
|
||||
await highlighter.whenSettled();
|
||||
assert.equal(tokenCells("/opt")[0]?.getFgColor(), cream);
|
||||
assert.equal(tokenCells("/mnt")[0]?.getFgColor(), 220);
|
||||
assert.equal(tokenCells("/bin")[0]?.getFgColor(), 1);
|
||||
assert.equal(tokenCells("/var")[0]?.getFgColor(), BLUE);
|
||||
|
||||
highlighter.setRules([{ ...unixPath, color: "#60A5FA" }], false);
|
||||
await highlighter.whenSettled();
|
||||
assert.equal(tokenCells("/opt")[0]?.getFgColor(), cream);
|
||||
assert.equal(tokenCells("/mnt")[0]?.getFgColor(), 220);
|
||||
assert.equal(tokenCells("/bin")[0]?.getFgColor(), 1);
|
||||
assert.equal(tokenCells("/var")[0]?.getFgColor(), -1);
|
||||
|
||||
highlighter.dispose();
|
||||
term.dispose();
|
||||
});
|
||||
|
||||
test("ordinary Enter output does not revisit existing history", async () => {
|
||||
const term = new XTerm({ allowProposedApi: true, cols: 80, rows: 5, scrollback: 20 });
|
||||
const highlighter = new KeywordHighlighter(term);
|
||||
|
||||
@@ -67,6 +67,7 @@ const CELL_INDICES = 3;
|
||||
const CELL_CONTENT = 0;
|
||||
const CELL_FG = 1;
|
||||
const STYLE_MASK = 0xfc000000;
|
||||
const CM_MASK = 0x3000000;
|
||||
const CM_RGB = 0x3000000;
|
||||
const MAX_PLUGIN_HIGHLIGHT_SCAN_CHARS = 4_096;
|
||||
const MAX_PLUGIN_HIGHLIGHT_MATCHES_PER_WRITE = 256;
|
||||
@@ -1061,7 +1062,13 @@ export class KeywordHighlighter implements IDisposable {
|
||||
const dataIndex = x * CELL_INDICES;
|
||||
const content = internal._data[dataIndex + CELL_CONTENT];
|
||||
const currentFg = internal._data[dataIndex + CELL_FG];
|
||||
if (!originals.mask[x] || originals.content[x] !== content) {
|
||||
// A cell already carrying a PTY palette/truecolor foreground (starship
|
||||
// directory segments, ANSI logs) must keep that color. Keyword RGB is
|
||||
// only for the default foreground.
|
||||
const maskedForContent = originals.mask[x] === 1 && originals.content[x] === content;
|
||||
const ptyFg = maskedForContent ? originals.fg[x] : currentFg;
|
||||
if ((ptyFg & CM_MASK) !== 0) continue;
|
||||
if (!maskedForContent) {
|
||||
originals.fg[x] = currentFg;
|
||||
originals.content[x] = content;
|
||||
originals.mask[x] = 1;
|
||||
|
||||
@@ -84,7 +84,14 @@ function isDefaultCmdPromptLine(line) {
|
||||
|
||||
function isDefaultPosixPromptLine(line) {
|
||||
const normalized = String(line || "").replace(/\s+$/, "");
|
||||
return POSIX_PROMPT_PATTERN.test(normalized) || ALPINE_PROMPT_PATTERN.test(normalized);
|
||||
// Optional virtualenv / conda / BT-panel style env name prefix: "(name) "
|
||||
// before the shell prompt. Recognition only; callers still return the full
|
||||
// prompt line so suffix matching sees the visible prefix (#3947).
|
||||
const withoutEnvPrefix = normalized.replace(/^\([^()\n\r]+\)\s+/, "");
|
||||
return (
|
||||
POSIX_PROMPT_PATTERN.test(withoutEnvPrefix) ||
|
||||
ALPINE_PROMPT_PATTERN.test(withoutEnvPrefix)
|
||||
);
|
||||
}
|
||||
|
||||
function extractTrailingIdlePrompt(output) {
|
||||
|
||||
@@ -219,6 +219,50 @@ test("extracts a trailing RHEL-family bracketed idle prompt", () => {
|
||||
assert.equal(extractTrailingIdlePrompt("welcome\r\nroot@host ~#"), "");
|
||||
});
|
||||
|
||||
test("isDefaultPosixPromptLine accepts virtualenv, conda, and BT-panel env prefixes", () => {
|
||||
// Python venv / conda / BT-panel prepend "(name) " before the real prompt.
|
||||
// Recognition strips that prefix; a missing space after ")" is already
|
||||
// accepted because the username class absorbs the parens (#3947).
|
||||
for (const prompt of [
|
||||
"(3.12.12) root@example:~# ",
|
||||
"(3.12.12) root@example:~#",
|
||||
"(venv) alice@host:~$ ",
|
||||
"(base) [opc@host ~]$ ",
|
||||
"(my-env) localhost:~# ",
|
||||
"(3.12.12)root@example:~# ",
|
||||
]) {
|
||||
assert.equal(isDefaultPosixPromptLine(prompt), true, prompt);
|
||||
}
|
||||
});
|
||||
|
||||
test("extractTrailingIdlePrompt keeps the full env-prefixed POSIX prompt", () => {
|
||||
// Suffix matching compares the visible PTY line, so the captured prompt
|
||||
// must still include the "(name) " prefix rather than the stripped form.
|
||||
for (const prompt of [
|
||||
"(3.12.12) root@example:~# ",
|
||||
"(3.12.12) root@example:~#",
|
||||
"(venv) alice@host:~$ ",
|
||||
"(base) [opc@host ~]$ ",
|
||||
"(my-env) localhost:~# ",
|
||||
]) {
|
||||
assert.equal(extractTrailingIdlePrompt(`Last login\r\n${prompt}`), prompt, prompt);
|
||||
}
|
||||
});
|
||||
|
||||
test("env-prefixed POSIX recognition rejects incomplete prefixes and non-prompts", () => {
|
||||
for (const line of [
|
||||
"(3.12.12 root@example:~# ",
|
||||
"(3.12.12) root@example:~# printf busy",
|
||||
"(venv) not a prompt",
|
||||
"root@host ~#",
|
||||
"(base) [opc@host ~]$ printf busy",
|
||||
"(my-env) localhost:~# printf busy",
|
||||
]) {
|
||||
assert.equal(isDefaultPosixPromptLine(line), false, line);
|
||||
assert.equal(extractTrailingIdlePrompt(`welcome\r\n${line}`), "", line);
|
||||
}
|
||||
});
|
||||
|
||||
test("isPlausibleCliVersionOutput rejects stack traces and file URLs", () => {
|
||||
assert.equal(isPlausibleCliVersionOutput("2.1.123 (Claude Code)"), true);
|
||||
assert.equal(isPlausibleCliVersionOutput("codex-cli 0.125.0"), true);
|
||||
|
||||
Reference in new issue
Block a user