Include shared/ dir in repo (gitignore allowlist)
The previous commit referenced shared/ansi.ts, shared/format.ts, and shared/ctx.ts but those files were filtered by the default-deny .gitignore. Adding !/shared/ to the allowlist so the imports actually resolve in a fresh clone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,5 +18,6 @@
|
||||
!/mechanicus-status-line.ts
|
||||
!/mechanicus-thinking-label.ts
|
||||
!/scripts/
|
||||
!/shared/
|
||||
!/tests/
|
||||
!/themes/
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* ANSI escape-sequence helpers shared across extensions.
|
||||
*
|
||||
* Kept free of relative imports so Node's --experimental-strip-types can load
|
||||
* this directly in tests.
|
||||
*/
|
||||
|
||||
export const ANSI_RESET_FG = "\x1b[39m";
|
||||
export const ANSI_RESET_ALL = "\x1b[0m";
|
||||
|
||||
/**
|
||||
* Convert `#rrggbb` (or `#rgb`) to [r, g, b] bytes. Throws on malformed input
|
||||
* rather than silently producing NaN — previously the callers built broken
|
||||
* ANSI codes out of `\x1b[38;2;NaN;NaN;NaNm` which some terminals render as
|
||||
* bright cyan, giving mysterious color bugs.
|
||||
*/
|
||||
export function hexToRgb(hex: string): [number, number, number] {
|
||||
const m = hex.trim().replace(/^#/, "");
|
||||
const expanded =
|
||||
m.length === 3 ? m.split("").map((c) => c + c).join("") : m;
|
||||
if (!/^[0-9a-fA-F]{6}$/.test(expanded)) {
|
||||
throw new Error(`Invalid hex color: ${hex}`);
|
||||
}
|
||||
const n = parseInt(expanded, 16);
|
||||
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
||||
}
|
||||
|
||||
/** Build a 24-bit foreground ANSI escape for an `#rrggbb` hex color. */
|
||||
export function fgFromHex(hex: string): string {
|
||||
const [r, g, b] = hexToRgb(hex);
|
||||
return `\x1b[38;2;${r};${g};${b}m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip every ANSI CSI escape sequence from a string. Useful when the
|
||||
* renderer needs to know the visible width of a line that contains color
|
||||
* codes.
|
||||
*/
|
||||
export function stripAnsi(s: string): string {
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: CSI stripping is the point
|
||||
return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
||||
}
|
||||
|
||||
/** Count visible grapheme-less characters. Mirrors pi-tui's visibleWidth. */
|
||||
export function visibleWidth(s: string): number {
|
||||
return stripAnsi(s).length;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Helpers for dealing with pi's ExtensionContext in the face of its
|
||||
* "stale after session replacement or reload" semantics.
|
||||
*
|
||||
* Pi's ExtensionRunner throws when any captured ctx field is accessed after
|
||||
* the runner has been torn down (on /quit, /reload, new session). This
|
||||
* module provides a single guarded wrapper so every extension doesn't have
|
||||
* to re-implement try/catch + null-on-shutdown patterns.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Run `fn` with the given ctx, swallowing any "stale context" errors that
|
||||
* occur when pi's runner has gone away between the event that captured ctx
|
||||
* and the time we're now using it (TUI render timers, setInterval ticks,
|
||||
* etc.). Returns `undefined` on failure.
|
||||
*/
|
||||
export function safely<T>(fn: () => T): T | undefined {
|
||||
try {
|
||||
return fn();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async variant of `safely`.
|
||||
*/
|
||||
export async function safelyAsync<T>(fn: () => Promise<T>): Promise<T | undefined> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Minimal shape we rely on — avoids coupling every caller to ExtensionContext. */
|
||||
export interface UiLike {
|
||||
hasUI: boolean;
|
||||
}
|
||||
|
||||
export function hasUI(ctx: UiLike | null | undefined): ctx is UiLike & { hasUI: true } {
|
||||
return !!ctx && ctx.hasUI === true;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Small formatting helpers. Pure, no side effects, no external imports.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format a token count as a compact human-readable string.
|
||||
* formatTokens(42) = "42"
|
||||
* formatTokens(1234) = "1.2k"
|
||||
* formatTokens(12345) = "12k"
|
||||
*/
|
||||
export function formatTokens(n: number): string {
|
||||
if (n < 1000) return String(n);
|
||||
if (n < 10_000) return `${(n / 1000).toFixed(1)}k`;
|
||||
return `${Math.round(n / 1000)}k`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a millisecond duration for the working indicator / timers.
|
||||
* formatElapsed(450) = "0s"
|
||||
* formatElapsed(42_000) = "42s"
|
||||
* formatElapsed(90_500) = "1m 30s"
|
||||
*/
|
||||
export function formatElapsed(ms: number): string {
|
||||
const total = Math.max(0, Math.floor(ms / 1000));
|
||||
const m = Math.floor(total / 60);
|
||||
const s = total % 60;
|
||||
if (m > 0) return `${m}m ${s}s`;
|
||||
return `${s}s`;
|
||||
}
|
||||
Reference in New Issue
Block a user