mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-12 04:12:55 +00:00
docs: document terminal core helpers
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { splitGraphemes } from "./ansi.js";
|
||||
|
||||
// Decorative emoji helpers that degrade cleanly on terminals without reliable emoji support.
|
||||
|
||||
/** Environment and terminal facts used to decide decorative emoji support. */
|
||||
export type DecorativeEmojiOptions = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
isTty?: boolean;
|
||||
@@ -9,6 +12,7 @@ export type DecorativeEmojiOptions = {
|
||||
|
||||
const EMOJI_GRAPHEME_PATTERN = /[\p{Extended_Pictographic}\p{Regional_Indicator}\u20e3]/u;
|
||||
|
||||
/** Detect terminals with known emoji rendering support. */
|
||||
function isKnownEmojiTerminal(env: NodeJS.ProcessEnv): boolean {
|
||||
const termProgram = (env.TERM_PROGRAM ?? "").toLowerCase();
|
||||
const term = (env.TERM ?? "").toLowerCase();
|
||||
@@ -24,6 +28,7 @@ function isKnownEmojiTerminal(env: NodeJS.ProcessEnv): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Return true when locale variables indicate UTF-8 output support. */
|
||||
function hasUtf8Locale(env: NodeJS.ProcessEnv): boolean {
|
||||
const locale = [env.LC_ALL, env.LC_CTYPE, env.LANG].find(
|
||||
(value) => typeof value === "string" && value.trim().length > 0,
|
||||
@@ -34,6 +39,7 @@ function hasUtf8Locale(env: NodeJS.ProcessEnv): boolean {
|
||||
return /utf-?8/i.test(locale);
|
||||
}
|
||||
|
||||
/** Return true when decorative emoji should be emitted for the target terminal. */
|
||||
export function supportsDecorativeEmoji(options: DecorativeEmojiOptions = {}): boolean {
|
||||
const env = options.env ?? process.env;
|
||||
const platform = options.platform ?? process.platform;
|
||||
@@ -57,10 +63,12 @@ export function supportsDecorativeEmoji(options: DecorativeEmojiOptions = {}): b
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Return the emoji only when decorative emoji output is supported. */
|
||||
export function decorativeEmoji(emoji: string, options: DecorativeEmojiOptions = {}): string {
|
||||
return supportsDecorativeEmoji(options) ? emoji : "";
|
||||
}
|
||||
|
||||
/** Prefix text with a decorative emoji when supported. */
|
||||
export function decorativePrefix(
|
||||
emoji: string,
|
||||
text: string,
|
||||
@@ -70,6 +78,7 @@ export function decorativePrefix(
|
||||
return prefix ? `${prefix} ${text}` : text;
|
||||
}
|
||||
|
||||
/** Strip decorative emoji for terminals that should not receive them. */
|
||||
export function stripDecorativeEmojiForTerminal(
|
||||
text: string,
|
||||
options: DecorativeEmojiOptions = {},
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
// Display-safe string helpers for shortening user home paths.
|
||||
|
||||
/** Normalize env/home values and reject shell placeholder strings. */
|
||||
function normalize(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed && trimmed !== "undefined" && trimmed !== "null" ? trimmed : undefined;
|
||||
}
|
||||
|
||||
/** Run a home resolver defensively because some runtimes throw for missing passwd data. */
|
||||
function normalizeSafe(fn: () => string | undefined): string | undefined {
|
||||
try {
|
||||
return normalize(fn());
|
||||
@@ -14,6 +18,7 @@ function normalizeSafe(fn: () => string | undefined): string | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve Termux home from its Android prefix layout. */
|
||||
function resolveTermuxHome(env: NodeJS.ProcessEnv): string | undefined {
|
||||
const prefix = normalize(env.PREFIX);
|
||||
if (!prefix || !normalize(env.ANDROID_DATA)) {
|
||||
@@ -25,6 +30,7 @@ function resolveTermuxHome(env: NodeJS.ProcessEnv): string | undefined {
|
||||
return path.resolve(prefix, "..", "home");
|
||||
}
|
||||
|
||||
/** Resolve the underlying OS home before applying OpenClaw overrides. */
|
||||
function resolveRawOsHomeDir(env: NodeJS.ProcessEnv, homedir: () => string): string | undefined {
|
||||
return (
|
||||
normalize(env.HOME) ??
|
||||
@@ -34,6 +40,7 @@ function resolveRawOsHomeDir(env: NodeJS.ProcessEnv, homedir: () => string): str
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve raw home with OPENCLAW_HOME tilde expansion. */
|
||||
function resolveRawHomeDir(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
homedir: () => string = os.homedir,
|
||||
@@ -46,6 +53,7 @@ function resolveRawHomeDir(
|
||||
return resolveRawOsHomeDir(env, homedir);
|
||||
}
|
||||
|
||||
/** Resolve the effective absolute home directory for display replacement. */
|
||||
function resolveEffectiveHomeDir(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
homedir: () => string = os.homedir,
|
||||
@@ -54,6 +62,7 @@ function resolveEffectiveHomeDir(
|
||||
return raw ? path.resolve(raw) : undefined;
|
||||
}
|
||||
|
||||
/** Resolve the display prefix that should replace the effective home path. */
|
||||
function resolveHomeDisplayPrefix(): { home: string; prefix: string } | undefined {
|
||||
const home = resolveEffectiveHomeDir();
|
||||
if (!home) {
|
||||
@@ -63,6 +72,7 @@ function resolveHomeDisplayPrefix(): { home: string; prefix: string } | undefine
|
||||
return explicitHome ? { home, prefix: "$OPENCLAW_HOME" } : { home, prefix: "~" };
|
||||
}
|
||||
|
||||
/** Replace the effective home path with "~" or "$OPENCLAW_HOME" for terminal display. */
|
||||
export function displayString(input: string): string {
|
||||
if (!input) {
|
||||
return input;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string.js";
|
||||
import { theme } from "./theme.js";
|
||||
|
||||
// Styles the status word in health output lines.
|
||||
|
||||
/** Highlight known health status prefixes in a "label: detail" line. */
|
||||
export function styleHealthChannelLine(line: string, rich: boolean): string {
|
||||
if (!rich) {
|
||||
return line;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Public barrel for shared terminal formatting helpers.
|
||||
|
||||
export * from "./ansi.js";
|
||||
export * from "./decorative-emoji.js";
|
||||
export * from "./health-style.js";
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
// OSC 9;4 progress reporting for terminals that support shell integration progress.
|
||||
|
||||
const OSC_PROGRESS_PREFIX = "\u001b]9;4;";
|
||||
const OSC_PROGRESS_ST = "\u001b\\";
|
||||
const OSC_PROGRESS_BEL = "\u0007";
|
||||
const OSC_PROGRESS_C1_ST = "\u009c";
|
||||
|
||||
/** Controller for terminal progress state. */
|
||||
export type OscProgressController = {
|
||||
setIndeterminate: (label: string) => void;
|
||||
setPercent: (label: string, percent: number) => void;
|
||||
clear: () => void;
|
||||
};
|
||||
|
||||
/** Return true when the terminal is known to support OSC progress messages. */
|
||||
export function supportsOscProgress(env: NodeJS.ProcessEnv, isTty: boolean): boolean {
|
||||
if (!isTty) {
|
||||
return false;
|
||||
@@ -19,6 +23,7 @@ export function supportsOscProgress(env: NodeJS.ProcessEnv, isTty: boolean): boo
|
||||
);
|
||||
}
|
||||
|
||||
/** Remove OSC terminators and escape introducers from progress labels. */
|
||||
function sanitizeOscProgressLabel(label: string): string {
|
||||
return label
|
||||
.replaceAll(OSC_PROGRESS_ST, "")
|
||||
@@ -30,6 +35,7 @@ function sanitizeOscProgressLabel(label: string): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Format one OSC progress control sequence. */
|
||||
function formatOscProgress(state: number, percent: number | null, label: string): string {
|
||||
const cleanLabel = sanitizeOscProgressLabel(label);
|
||||
if (percent === null) {
|
||||
@@ -39,6 +45,7 @@ function formatOscProgress(state: number, percent: number | null, label: string)
|
||||
return `${OSC_PROGRESS_PREFIX}${state};${normalizedPercent};${cleanLabel}${OSC_PROGRESS_ST}`;
|
||||
}
|
||||
|
||||
/** Create a progress controller, returning no-op methods on unsupported terminals. */
|
||||
export function createOscProgressController(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
isTty: boolean;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Tracks the active terminal progress line so callers can clear it before other output.
|
||||
|
||||
let activeStream: NodeJS.WriteStream | null = null;
|
||||
|
||||
/** Register the stream that currently owns an inline progress line. */
|
||||
export function registerActiveProgressLine(stream: NodeJS.WriteStream): void {
|
||||
if (!stream.isTTY) {
|
||||
return;
|
||||
@@ -7,6 +10,7 @@ export function registerActiveProgressLine(stream: NodeJS.WriteStream): void {
|
||||
activeStream = stream;
|
||||
}
|
||||
|
||||
/** Clear the active progress line when it is attached to a TTY stream. */
|
||||
export function clearActiveProgressLine(): void {
|
||||
if (!activeStream?.isTTY) {
|
||||
return;
|
||||
@@ -14,6 +18,7 @@ export function clearActiveProgressLine(): void {
|
||||
activeStream.write("\r\x1b[2K");
|
||||
}
|
||||
|
||||
/** Unregister the active progress line, optionally only for a matching stream. */
|
||||
export function unregisterActiveProgressLine(stream?: NodeJS.WriteStream): void {
|
||||
if (!activeStream) {
|
||||
return;
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { stylePromptHint, stylePromptMessage } from "./prompt-style.js";
|
||||
|
||||
// Pure prompt parameter styler used by interactive prompts and tests.
|
||||
|
||||
/** Minimal select-like params accepted by the prompt styler. */
|
||||
type SelectParamsLike = {
|
||||
message: string;
|
||||
options: readonly object[];
|
||||
};
|
||||
|
||||
/** Styling callbacks for prompt messages and hints. */
|
||||
type PromptSelectStylers = {
|
||||
message: (value: string) => string;
|
||||
hint: (value: string) => string | undefined;
|
||||
};
|
||||
|
||||
/** Default terminal stylers for select prompts. */
|
||||
const defaultStylers: PromptSelectStylers = {
|
||||
message: stylePromptMessage,
|
||||
hint: stylePromptHint,
|
||||
};
|
||||
|
||||
/** Return select params with styled prompt message and per-option hints. */
|
||||
export function styleSelectParams<TParams extends SelectParamsLike>(
|
||||
params: TParams,
|
||||
stylers: PromptSelectStylers = defaultStylers,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { select } from "@clack/prompts";
|
||||
import { styleSelectParams } from "./prompt-select-styled-params.js";
|
||||
|
||||
// Clack select wrapper that applies OpenClaw prompt styling.
|
||||
|
||||
/** Run a clack select prompt with styled message and hints. */
|
||||
export function selectStyled<T>(params: Parameters<typeof select<T>>[0]) {
|
||||
return select(styleSelectParams(params));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { isRich, theme } from "./theme.js";
|
||||
|
||||
// Shared styling helpers for interactive prompt copy.
|
||||
|
||||
/** Style a prompt message when rich terminal output is active. */
|
||||
export const stylePromptMessage = (message: string): string =>
|
||||
isRich() ? theme.accent(message) : message;
|
||||
|
||||
/** Style a prompt title when rich terminal output is active. */
|
||||
export const stylePromptTitle = (title?: string): string | undefined =>
|
||||
title && isRich() ? theme.heading(title) : title;
|
||||
|
||||
/** Style a prompt hint when rich terminal output is active. */
|
||||
export const stylePromptHint = (hint?: string): string | undefined =>
|
||||
hint && isRich() ? theme.muted(hint) : hint;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// Safe terminal stream writer that treats broken pipes as closed output.
|
||||
|
||||
/** Hooks for safe stream writes. */
|
||||
export type SafeStreamWriterOptions = {
|
||||
beforeWrite?: () => void;
|
||||
onBrokenPipe?: (err: NodeJS.ErrnoException, stream: NodeJS.WriteStream) => void;
|
||||
};
|
||||
|
||||
/** Writer facade that tracks closed/broken-pipe state. */
|
||||
export type SafeStreamWriter = {
|
||||
write: (stream: NodeJS.WriteStream, text: string) => boolean;
|
||||
writeLine: (stream: NodeJS.WriteStream, text: string) => boolean;
|
||||
@@ -10,11 +14,13 @@ export type SafeStreamWriter = {
|
||||
isClosed: () => boolean;
|
||||
};
|
||||
|
||||
/** Detect broken pipe style stream errors. */
|
||||
function isBrokenPipeError(err: unknown): err is NodeJS.ErrnoException {
|
||||
const code = (err as NodeJS.ErrnoException)?.code;
|
||||
return code === "EPIPE" || code === "EIO";
|
||||
}
|
||||
|
||||
/** Create a stream writer that stops writing after EPIPE/EIO. */
|
||||
export function createSafeStreamWriter(options: SafeStreamWriterOptions = {}): SafeStreamWriter {
|
||||
let closed = false;
|
||||
let notified = false;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Shared terminal string normalization helpers.
|
||||
|
||||
/** Normalize string input to lowercase, returning empty string for non-strings. */
|
||||
export function normalizeLowercaseStringOrEmpty(value: unknown): string {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// OSC 8 terminal hyperlink formatting with plain-text fallback.
|
||||
|
||||
/** Format a clickable terminal link when supported, otherwise return a readable fallback. */
|
||||
export function formatTerminalLink(
|
||||
label: string,
|
||||
url: string,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import chalk, { Chalk } from "chalk";
|
||||
import { LOBSTER_PALETTE } from "./palette.js";
|
||||
|
||||
// Shared terminal color theme that respects NO_COLOR and FORCE_COLOR.
|
||||
|
||||
const hasForceColor =
|
||||
typeof process.env.FORCE_COLOR === "string" &&
|
||||
process.env.FORCE_COLOR.trim().length > 0 &&
|
||||
@@ -10,6 +12,7 @@ const baseChalk = process.env.NO_COLOR && !hasForceColor ? new Chalk({ level: 0
|
||||
|
||||
const hex = (value: string) => baseChalk.hex(value);
|
||||
|
||||
/** Shared terminal theme color functions. */
|
||||
export const theme = {
|
||||
accent: hex(LOBSTER_PALETTE.accent),
|
||||
accentBright: hex(LOBSTER_PALETTE.accentBright),
|
||||
@@ -24,7 +27,9 @@ export const theme = {
|
||||
option: hex(LOBSTER_PALETTE.warn),
|
||||
} as const;
|
||||
|
||||
/** Return true when color styling is active. */
|
||||
export const isRich = () => baseChalk.level > 0;
|
||||
|
||||
/** Conditionally apply a color function based on caller rich-output state. */
|
||||
export const colorize = (rich: boolean, color: (value: string) => string, value: string) =>
|
||||
rich ? color(value) : value;
|
||||
|
||||
Reference in New Issue
Block a user