feat: refactored native bindings into modular types with persistent shell sessions

- Added Shell class to persistent shell session management with session caching and abort capabilities.
- Added find() function for file discovery with glob pattern matching and optional streaming callbacks.
- Exported ImageFormat enum from main natives package for image encoding format selection.
- Reorganized native bindings into modular type files with declaration merging pattern for better maintainability.
- Refactored shell execution to use persistent Shell instances instead of stateless executeShell function calls.
- Enhanced SystemInfo type with additional fields for distro, kernel, CPU, and disk information.
This commit is contained in:
can1357
2026-02-01 15:48:05 +01:00
parent e042823697
commit 96b83875f4
38 changed files with 1054 additions and 472 deletions
+7
View File
@@ -24,8 +24,10 @@ use napi_derive::napi;
/// Clipboard image payload encoded as PNG bytes.
#[napi(object)]
pub struct ClipboardImage {
/// PNG-encoded image bytes.
pub data: Uint8Array,
#[napi(js_name = "mimeType")]
/// MIME type for the encoded image payload.
pub mime_type: String,
}
@@ -47,6 +49,9 @@ fn encode_png(image: ImageData<'_>) -> Result<Vec<u8>> {
/// Copy plain text to the system clipboard.
///
/// # Parameters
/// - `text`: UTF-8 text to place on the clipboard.
///
/// # Errors
/// Returns an error if clipboard access fails.
#[napi(js_name = "copyToClipboard")]
@@ -66,6 +71,8 @@ pub async fn copy_to_clipboard(text: String) -> Result<()> {
/// Read an image from the system clipboard.
///
/// Returns `Ok(None)` when no image data is available.
///
/// # Errors
/// Returns an error if clipboard access fails or image encoding fails.
#[napi(js_name = "readImageFromClipboard")]
+7
View File
@@ -50,7 +50,9 @@ pub struct FindOptions {
#[derive(Clone)]
#[napi(object)]
pub struct FindMatch {
/// Relative path from the search root, using forward slashes.
pub path: String,
/// Resolved filesystem type for the match.
#[napi(js_name = "fileType")]
pub file_type: String,
/// Modification time in milliseconds since epoch (if available).
@@ -60,7 +62,9 @@ pub struct FindMatch {
/// Result of a find operation.
#[napi(object)]
pub struct FindResult {
/// Matched filesystem entries.
pub matches: Vec<FindMatch>,
/// Number of matches returned after limits are applied.
#[napi(js_name = "totalMatches")]
pub total_matches: u32,
}
@@ -285,6 +289,9 @@ fn run_find(
/// Find filesystem entries matching a glob pattern.
///
/// Uses the provided options to resolve the search root, apply glob
/// matching, and optionally stream matches to a callback.
///
/// # Errors
/// Returns an error if the glob is invalid or the search path is missing.
#[napi(js_name = "find")]
+39 -6
View File
@@ -101,6 +101,7 @@ pub struct GrepOptions {
pub struct ContextLine {
#[napi(js_name = "lineNumber")]
pub line_number: u32,
/// Raw line content (trimmed line ending).
pub line: String,
}
@@ -141,15 +142,22 @@ pub struct SearchResult {
#[derive(Clone)]
#[napi(object)]
pub struct GrepMatch {
/// File path for the match (relative for directory searches).
pub path: String,
/// 1-indexed line number (0 for count-only entries).
#[napi(js_name = "lineNumber")]
pub line_number: u32,
/// The matched line content (empty for count-only entries).
pub line: String,
/// Context lines before the match.
#[napi(js_name = "contextBefore")]
pub context_before: Option<Vec<ContextLine>>,
/// Context lines after the match.
#[napi(js_name = "contextAfter")]
pub context_after: Option<Vec<ContextLine>>,
/// Whether the line was truncated.
pub truncated: Option<bool>,
/// Per-file match count (count mode only).
#[napi(js_name = "matchCount")]
pub match_count: Option<u32>,
}
@@ -157,13 +165,18 @@ pub struct GrepMatch {
/// Result of searching files.
#[napi(object)]
pub struct GrepResult {
/// Matches or per-file counts, depending on output mode.
pub matches: Vec<GrepMatch>,
/// Total matches across all files.
#[napi(js_name = "totalMatches")]
pub total_matches: u32,
/// Number of files with at least one match.
#[napi(js_name = "filesWithMatches")]
pub files_with_matches: u32,
/// Number of files searched.
#[napi(js_name = "filesSearched")]
pub files_searched: u32,
/// Whether the limit/offset stopped the search early.
#[napi(js_name = "limitReached")]
pub limit_reached: Option<bool>,
}
@@ -939,8 +952,12 @@ fn grep_sync(
/// Search content for a pattern (one-shot, compiles pattern each time).
/// For repeated searches with the same pattern, use [`grep`] with file filters.
///
/// Accepts either a `Uint8Array`/`Buffer` (zero-copy) or a `string` (transcoded
/// to UTF-8).
/// # Arguments
/// - `content`: `Uint8Array`/`Buffer` (zero-copy) or `string` (UTF-8).
/// - `options`: Regex settings, context, and output mode.
///
/// # Returns
/// Match list plus counts/limit status; errors are surfaced in `error`.
#[napi(js_name = "search")]
pub fn search(content: Either<JsString, Uint8Array>, options: SearchOptions) -> SearchResult {
match &content {
@@ -957,8 +974,14 @@ pub fn search(content: Either<JsString, Uint8Array>, options: SearchOptions) ->
/// Quick check if content matches a pattern.
///
/// Accepts either `Uint8Array`/`Buffer` (zero-copy) or `string` for both
/// content and pattern.
/// # Arguments
/// - `content`: `Uint8Array`/`Buffer` (zero-copy) or `string` (UTF-8).
/// - `pattern`: `Uint8Array`/`Buffer` (zero-copy) or `string` (UTF-8).
/// - `ignore_case`: Case-insensitive matching.
/// - `multiline`: Enable multiline regex mode.
///
/// # Returns
/// True if any match exists; false on no match.
#[napi(js_name = "hasMatch")]
pub fn has_match(
content: Either<JsString, Uint8Array>,
@@ -996,6 +1019,13 @@ pub fn has_match(
}
/// Search files for a regex pattern.
///
/// # Arguments
/// - `options`: Pattern, path, filters, and output mode.
/// - `on_match`: Optional callback invoked per match/result.
///
/// # Returns
/// Aggregated results across matching files.
#[napi(js_name = "grep")]
pub async fn grep(
options: GrepOptions,
@@ -1118,8 +1148,11 @@ fn fuzzy_find_sync(options: FuzzyFindOptions) -> Result<FuzzyFindResult> {
/// Fuzzy file path search for autocomplete.
///
/// Searches for files and directories whose paths contain the query substring
/// (case-insensitive). Respects .gitignore by default.
/// # Arguments
/// - `options`: Query substring, root path, and limits.
///
/// # Returns
/// Matching file and directory entries.
#[napi(js_name = "fuzzyFind")]
pub async fn fuzzy_find(options: FuzzyFindOptions) -> Result<FuzzyFindResult> {
task::spawn_blocking(move || fuzzy_find_sync(options))
+11
View File
@@ -125,18 +125,29 @@ fn get_scope_matchers() -> &'static ScopeMatchers {
#[derive(Debug)]
#[napi(object)]
pub struct HighlightColors {
/// ANSI color for comments.
pub comment: String,
/// ANSI color for keywords.
pub keyword: String,
/// ANSI color for function names.
pub function: String,
/// ANSI color for variables and identifiers.
pub variable: String,
/// ANSI color for string literals.
pub string: String,
/// ANSI color for numeric literals.
pub number: String,
/// ANSI color for type identifiers.
#[napi(js_name = "type")]
pub r#type: String,
/// ANSI color for operators.
pub operator: String,
/// ANSI color for punctuation tokens.
pub punctuation: String,
/// ANSI color for diff inserted lines.
#[napi(js_name = "inserted")]
pub inserted: Option<String>,
/// ANSI color for diff deleted lines.
#[napi(js_name = "deleted")]
pub deleted: Option<String>,
}
+4 -1
View File
@@ -16,7 +16,10 @@ pub struct HtmlToMarkdownOptions {
pub skip_images: Option<bool>,
}
/// Convert HTML to Markdown.
/// Convert HTML source to Markdown with optional preprocessing.
///
/// # Errors
/// Returns an error if the conversion fails or the worker task aborts.
#[napi(js_name = "htmlToMarkdown")]
pub async fn html_to_markdown(
html: String,
+13 -6
View File
@@ -19,10 +19,15 @@ use napi_derive::napi;
/// Sampling filter for resize operations.
#[napi]
pub enum SamplingFilter {
/// Nearest-neighbor sampling (fast, low quality).
Nearest = 1,
/// Triangle filter (linear interpolation).
Triangle = 2,
/// Catmull-Rom filter with sharper edges.
CatmullRom = 3,
/// Gaussian filter for smoother results.
Gaussian = 4,
/// Lanczos3 filter for high-quality downscaling.
Lanczos3 = 5,
}
@@ -41,13 +46,14 @@ impl From<SamplingFilter> for FilterType {
/// Image container for native interop.
#[napi]
pub struct PhotonImage {
/// Shared decoded image data.
img: Arc<DynamicImage>,
}
#[napi]
impl PhotonImage {
/// Create a new `PhotonImage` from encoded image bytes (PNG, JPEG, WebP,
/// GIF).
/// Create a new `PhotonImage` from encoded image bytes (PNG, JPEG, WebP, GIF).
/// Returns the decoded image handle on success.
///
/// # Errors
/// Returns an error if the image format cannot be detected or decoded.
@@ -71,19 +77,19 @@ impl PhotonImage {
Ok(Self { img: Arc::new(img) })
}
/// Get the width of the image.
/// Get the image width in pixels.
#[napi(getter, js_name = "width")]
pub fn get_width(&self) -> u32 {
self.img.width()
}
/// Get the height of the image.
/// Get the image height in pixels.
#[napi(getter, js_name = "height")]
pub fn get_height(&self) -> u32 {
self.img.height()
}
/// Encode image to bytes in the specified format.
/// Encode the image to bytes in the specified format.
///
/// Format values (matching `ImageFormat` enum in TS):
/// - 0: PNG (quality ignored)
@@ -102,7 +108,8 @@ impl PhotonImage {
Ok(Uint8Array::from(buffer))
}
/// Resize the image to the specified dimensions.
/// Resize the image to the specified pixel dimensions using the filter.
/// Returns a new `PhotonImage` containing the resized image.
#[napi(js_name = "resize")]
pub async fn resize(&self, width: u32, height: u32, filter: SamplingFilter) -> Result<Self> {
let img = Arc::clone(&self.img);
+19 -4
View File
@@ -94,14 +94,18 @@ struct ParsedKittySequence {
event_type: Option<u32>,
}
/// Parsed Kitty keyboard protocol sequence result.
/// Parsed Kitty keyboard protocol sequence result for a Kitty input sequence.
#[napi(object)]
pub struct ParsedKittyResult {
/// Primary codepoint associated with the key.
pub codepoint: i32,
/// Optional shifted key codepoint from the sequence.
pub shifted_key: Option<i32>,
/// Optional base layout key codepoint from the sequence.
pub base_layout_key: Option<i32>,
/// Modifier bitmask (shift/alt/ctrl), excluding lock bits.
pub modifier: u32,
/// 1 = press, 2 = repeat, 3 = release
/// Optional event type (1 = press, 2 = repeat, 3 = release).
pub event_type: Option<u32>,
}
@@ -203,7 +207,10 @@ static LETTERS: [&str; 26] = [
// Public API
// =============================================================================
/// Matches Kitty protocol keyboard sequences against a codepoint and modifier.
/// Match Kitty protocol input against a codepoint and modifier mask.
///
/// Returns true when the parsed sequence matches the expected codepoint (or
/// base layout key) and modifier bits.
#[napi(js_name = "matchesKittySequence")]
pub fn matches_kitty_sequence(
data: String,
@@ -224,12 +231,16 @@ pub fn matches_kitty_sequence(
}
/// Parse terminal input and return a normalized key identifier.
///
/// Returns a key id like "escape" or "ctrl+c", or None if unrecognized.
#[napi(js_name = "parseKey")]
pub fn parse_key(data: String, kitty_protocol_active: bool) -> Option<String> {
parse_key_inner(data.as_bytes(), kitty_protocol_active).map(|s| s.into_owned())
}
/// Check if input matches a legacy escape sequence.
/// Check if input matches a legacy escape sequence for the given key name.
///
/// Returns true only when the byte sequence maps to the exact key identifier.
#[napi(js_name = "matchesLegacySequence")]
pub fn matches_legacy_sequence(data: String, key_name: String) -> bool {
LEGACY_SEQUENCES
@@ -238,12 +249,16 @@ pub fn matches_legacy_sequence(data: String, key_name: String) -> bool {
}
/// Match input data against a key identifier string.
///
/// Returns true when the bytes represent the specified key with modifiers.
#[napi(js_name = "matchesKey")]
pub fn matches_key(data: String, key_id: String, kitty_protocol_active: bool) -> bool {
matches_key_inner(data.as_bytes(), &key_id, kitty_protocol_active)
}
/// Parse a Kitty keyboard protocol sequence.
///
/// Returns a structured parse result when the input is a valid Kitty sequence.
#[napi(js_name = "parseKittySequence")]
pub fn parse_kitty_sequence_napi(data: String) -> Option<ParsedKittyResult> {
parse_kitty_sequence(data.as_bytes()).map(|p| ParsedKittyResult {
+30 -13
View File
@@ -23,7 +23,8 @@ use napi_derive::napi;
mod platform {
use std::fs;
/// Recursively collect all descendant PIDs by reading /proc/{pid}/children.
/// Collect all descendant PIDs of `pid` into `pids`.
/// Skips branches when `/proc/{pid}/children` cannot be read.
pub fn collect_descendants(pid: i32, pids: &mut Vec<i32>) {
let children_path = format!("/proc/{pid}/task/{pid}/children");
let Ok(content) = fs::read_to_string(&children_path) else {
@@ -38,19 +39,23 @@ mod platform {
}
}
/// Kill a process with the given signal.
/// Send `signal` to `pid`.
/// Returns true when the signal is delivered successfully.
pub fn kill_pid(pid: i32, signal: i32) -> bool {
// SAFETY: libc::kill is safe to call with any pid/signal combination
unsafe { libc::kill(pid, signal) == 0 }
}
/// Get the process group id for a pid.
/// Get the process group id for `pid`.
/// Returns `None` when the process does not exist or is inaccessible.
pub fn process_group_id(pid: i32) -> Option<i32> {
// SAFETY: `libc::getpgid` is safe to call with any pid
let pgid = unsafe { libc::getpgid(pid) };
if pgid < 0 { None } else { Some(pgid) }
}
/// Kill a process group with the given signal.
/// Send `signal` to the process group `pgid`.
/// Returns true when the signal is delivered successfully.
pub fn kill_process_group(pgid: i32, signal: i32) -> bool {
// SAFETY: libc::kill is safe to call with any pid/signal combination
unsafe { libc::kill(-pgid, signal) == 0 }
@@ -66,7 +71,8 @@ mod platform {
fn proc_listchildpids(ppid: i32, buffer: *mut i32, buffersize: i32) -> i32;
}
/// Recursively collect all descendant PIDs using libproc.
/// Collect all descendant PIDs of `pid` into `pids` using libproc.
/// Skips branches when libproc returns no children.
pub fn collect_descendants(pid: i32, pids: &mut Vec<i32>) {
// First call to get count
let count = unsafe { proc_listchildpids(pid, ptr::null_mut(), 0) };
@@ -92,19 +98,23 @@ mod platform {
}
}
/// Kill a process with the given signal.
/// Send `signal` to `pid`.
/// Returns true when the signal is delivered successfully.
pub fn kill_pid(pid: i32, signal: i32) -> bool {
// SAFETY: libc::kill is safe to call with any pid/signal combination
unsafe { libc::kill(pid, signal) == 0 }
}
/// Get the process group id for a pid.
/// Get the process group id for `pid`.
/// Returns `None` when the process does not exist or is inaccessible.
pub fn process_group_id(pid: i32) -> Option<i32> {
// SAFETY: libc::getpgid is safe to call with any pid
let pgid = unsafe { libc::getpgid(pid) };
if pgid < 0 { None } else { Some(pgid) }
}
/// Kill a process group with the given signal.
/// Send `signal` to the process group `pgid`.
/// Returns true when the signal is delivered successfully.
pub fn kill_process_group(pgid: i32, signal: i32) -> bool {
// SAFETY: libc::kill is safe to call with any pid/signal combination
unsafe { libc::kill(-pgid, signal) == 0 }
@@ -177,7 +187,8 @@ mod platform {
tree
}
/// Recursively collect all descendant PIDs.
/// Collect all descendant PIDs of `pid` into `pids`.
/// Uses a snapshot of the current process table.
pub fn collect_descendants(pid: i32, pids: &mut Vec<i32>) {
let tree = build_process_tree();
collect_descendants_from_tree(pid as u32, &tree, pids);
@@ -192,7 +203,8 @@ mod platform {
}
}
/// Kill a process (signal is ignored on Windows, always terminates).
/// Terminate `pid` (Windows ignores `signal`).
/// Returns true when the process is terminated.
pub fn kill_pid(pid: i32, _signal: i32) -> bool {
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, 0, pid as u32);
@@ -206,11 +218,13 @@ mod platform {
}
/// Process groups are not exposed on Windows.
/// Always returns `None`.
pub fn process_group_id(_pid: i32) -> Option<i32> {
None
}
/// Process groups are not exposed on Windows.
/// Always returns `false`.
pub fn kill_process_group(_pgid: i32, _signal: i32) -> bool {
false
}
@@ -218,6 +232,7 @@ mod platform {
/// Kill a process tree (the process and all its descendants).
///
/// Arguments: `pid` is the root process and `signal` is the kill signal.
/// Kills children first (bottom-up) to prevent orphan re-parenting issues.
/// Returns the number of processes successfully killed.
#[napi]
@@ -242,17 +257,19 @@ pub fn kill_tree(pid: i32, signal: i32) -> u32 {
killed
}
/// Get the process group id for a pid.
/// Get the process group id for `pid`.
/// Returns `None` when the process is missing or unsupported on the platform.
pub fn process_group_id(pid: i32) -> Option<i32> {
platform::process_group_id(pid)
}
/// Kill a process group by pgid.
/// Send `signal` to the process group `pgid`.
/// Returns false when process groups are unsupported on the platform.
pub fn kill_process_group(pgid: i32, signal: i32) -> bool {
platform::kill_process_group(pgid, signal)
}
/// List all descendant PIDs of a process.
/// List all descendant PIDs of `pid`.
///
/// Returns an empty array if the process has no children or doesn't exist.
#[napi]
+168 -19
View File
@@ -6,7 +6,9 @@
//!
//! # Example
//! ```ignore
//! const result = await natives.executeShell({ command: "ls" }, (chunk) => {
//! const shell = new natives.Shell();
//! const result = await shell.run({ command: "ls" }, (err, chunk) => {
//! if (err) return;
//! console.log(chunk);
//! });
//! ```
@@ -16,14 +18,14 @@ use std::{
io::{Read, Write},
sync::{
Arc, LazyLock,
atomic::{AtomicBool, AtomicI32, Ordering},
atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering},
},
time::Duration,
};
use brush_core::{
CreateOptions, ExecutionContext, OpenFile, OpenFiles, ProcessGroupPolicy, Shell, ShellValue,
ShellVariable, builtins, env::EnvironmentScope,
CreateOptions, ExecutionContext, OpenFile, OpenFiles, ProcessGroupPolicy, Shell as BrushShell,
ShellValue, ShellVariable, builtins, env::EnvironmentScope,
};
use clap::Parser;
use napi::{
@@ -40,7 +42,8 @@ type ExecutionMap = HashMap<String, ExecutionControl>;
type SessionMap = HashMap<String, Arc<TokioMutex<ShellSession>>>;
struct ExecutionControl {
cancel: tokio::sync::oneshot::Sender<()>,
cancel: tokio::sync::oneshot::Sender<()>,
session_key: String,
}
struct ExecutionTarget {
@@ -94,40 +97,172 @@ impl Drop for ExecutionGuard {
}
struct ShellSession {
shell: Shell,
shell: BrushShell,
}
static EXECUTIONS: LazyLock<Mutex<ExecutionMap>> = LazyLock::new(|| Mutex::new(HashMap::new()));
static SESSIONS: LazyLock<Mutex<SessionMap>> = LazyLock::new(|| Mutex::new(HashMap::new()));
static SESSION_COUNTER: AtomicU64 = AtomicU64::new(1);
static EXECUTION_COUNTER: AtomicU64 = AtomicU64::new(1);
/// Options for configuring a persistent shell session.
#[napi(object)]
pub struct ShellOptions {
/// Environment variables to apply once per session.
pub session_env: Option<HashMap<String, String>>,
/// Optional snapshot file to source on session creation.
pub snapshot_path: Option<String>,
}
/// Options for running a shell command.
#[napi(object)]
pub struct ShellRunOptions {
/// Command string to execute in the shell.
pub command: String,
/// Working directory for the command.
pub cwd: Option<String>,
/// Environment variables to apply for this command only.
pub env: Option<HashMap<String, String>>,
/// Timeout in milliseconds before cancelling the command.
pub timeout_ms: Option<u32>,
}
/// Result of running a shell command.
#[napi(object)]
pub struct ShellRunResult {
/// Exit code when the command completes normally.
pub exit_code: Option<i32>,
/// Whether the command was cancelled via abort.
pub cancelled: bool,
/// Whether the command timed out before completion.
pub timed_out: bool,
}
/// Persistent brush-core shell session.
#[napi]
pub struct Shell {
session_key: String,
session_env: Option<HashMap<String, String>>,
snapshot_path: Option<String>,
}
#[napi]
impl Shell {
#[napi(constructor)]
/// Create a new shell session from optional configuration.
///
/// The options set session-scoped environment variables and a snapshot path.
pub fn new(options: Option<ShellOptions>) -> Self {
let session_key = next_session_key();
let (session_env, snapshot_path) =
options.map_or((None, None), |opt| (opt.session_env, opt.snapshot_path));
Self { session_key, session_env, snapshot_path }
}
/// Run a shell command using the provided options.
///
/// The `on_chunk` callback receives streamed stdout/stderr output. Returns the
/// exit code when the command completes, or flags when cancelled or timed out.
#[napi]
pub async fn run(
&self,
options: ShellRunOptions,
#[napi(ts_arg_type = "((error: Error | null, chunk: string) => void) | undefined | null")]
on_chunk: Option<ThreadsafeFunction<String>>,
) -> Result<ShellRunResult> {
let execution_id = next_execution_id();
let execute_options = ShellExecuteOptions {
command: options.command,
cwd: options.cwd,
env: options.env,
session_env: self.session_env.clone(),
timeout_ms: options.timeout_ms,
execution_id,
session_key: self.session_key.clone(),
snapshot_path: self.snapshot_path.clone(),
};
execute_shell_with_options(execute_options, on_chunk)
.await
.map(|result| ShellRunResult {
exit_code: result.exit_code,
cancelled: result.cancelled,
timed_out: result.timed_out,
})
}
/// Abort all running commands for this shell session.
///
/// Returns `Ok(())` even when no commands are running.
#[napi]
pub fn abort(&self) -> Result<()> {
let execution_ids: Vec<String> = {
let executions = EXECUTIONS.lock();
executions
.iter()
.filter(|(_, control)| control.session_key == self.session_key)
.map(|(execution_id, _)| execution_id.clone())
.collect()
};
for execution_id in execution_ids {
abort_shell_execution(execution_id)?;
}
Ok(())
}
}
/// Options for executing a shell command via brush-core.
#[napi(object)]
pub struct ShellExecuteOptions {
/// Command string to execute in the shell.
pub command: String,
/// Working directory for the command.
pub cwd: Option<String>,
/// Environment variables to apply for this command only.
pub env: Option<HashMap<String, String>>,
/// Environment variables to apply once per session.
pub session_env: Option<HashMap<String, String>>,
/// Timeout in milliseconds before cancelling the command.
pub timeout_ms: Option<u32>,
/// Unique identifier for this execution.
pub execution_id: String,
/// Session key for a persistent brush shell instance.
pub session_key: String,
/// Optional snapshot file to source on session creation.
pub snapshot_path: Option<String>,
}
/// Result of executing a shell command via brush-core.
#[napi(object)]
pub struct ShellExecuteResult {
/// Exit code when the command completes normally.
pub exit_code: Option<i32>,
/// Whether the command was cancelled via abort.
pub cancelled: bool,
/// Whether the command timed out before completion.
pub timed_out: bool,
}
/// Execute a brush shell command.
/// Execute a brush shell command with explicit session metadata.
///
/// The `on_chunk` callback receives streamed stdout/stderr output. Returns the
/// exit code when the command completes, or flags when cancelled or timed out.
#[napi]
pub async fn execute_shell(
options: ShellExecuteOptions,
#[napi(ts_arg_type = "((chunk: string) => void) | undefined | null")] on_chunk: Option<
ThreadsafeFunction<String>,
>,
) -> Result<ShellExecuteResult> {
execute_shell_with_options(options, on_chunk).await
}
async fn execute_shell_with_options(
options: ShellExecuteOptions,
on_chunk: Option<ThreadsafeFunction<String>>,
) -> Result<ShellExecuteResult> {
let execution_id = options.execution_id.clone();
let timeout_ms = options.timeout_ms;
@@ -138,7 +273,10 @@ pub async fn execute_shell(
if executions.contains_key(&execution_id) {
return Err(Error::from_reason("Execution already running"));
}
executions.insert(execution_id.clone(), ExecutionControl { cancel: cancel_tx });
executions.insert(execution_id.clone(), ExecutionControl {
cancel: cancel_tx,
session_key: options.session_key.clone(),
});
}
let _guard = ExecutionGuard { execution_id };
@@ -185,7 +323,12 @@ pub async fn execute_shell(
}
};
if run_result.is_none() {
if let Some(run_result) = run_result {
Some(
run_result
.map_err(|err| Error::from_reason(format!("Shell execution failed: {err}")))?,
)
} else {
wait_for_execution_target(&execution_target, Duration::from_millis(200)).await;
terminate_execution_processes(&execution_target).await;
if time::timeout(Duration::from_millis(1500), &mut run_future)
@@ -195,12 +338,6 @@ pub async fn execute_shell(
tainted = true;
}
None
} else {
Some(
run_result
.expect("run_result ensured")
.map_err(|err| Error::from_reason(format!("Shell execution failed: {err}")))?,
)
}
};
@@ -222,7 +359,9 @@ pub async fn execute_shell(
Ok(ShellExecuteResult { exit_code: Some(i32::from(run_result.exit_code)), cancelled, timed_out })
}
/// Abort a running shell execution.
/// Abort a running shell execution by ID.
///
/// Returns `Ok(())` even when the execution ID is not active.
#[napi]
pub fn abort_shell_execution(execution_id: String) -> Result<()> {
let mut executions = EXECUTIONS.lock();
@@ -235,7 +374,7 @@ pub fn abort_shell_execution(execution_id: String) -> Result<()> {
async fn get_or_create_session(
options: &ShellExecuteOptions,
) -> Result<Arc<TokioMutex<ShellSession>>> {
if let Some(session) = SESSIONS.lock().get(&options.session_key).cloned() {
if let Some(session) = { SESSIONS.lock().get(&options.session_key).cloned() } {
return Ok(session);
}
@@ -260,7 +399,7 @@ async fn create_session(options: &ShellExecuteOptions) -> Result<ShellSession> {
..Default::default()
};
let mut shell = Shell::new(&create_options)
let mut shell = BrushShell::new(&create_options)
.await
.map_err(|err| Error::from_reason(format!("Failed to initialize shell: {err}")))?;
@@ -294,7 +433,7 @@ async fn create_session(options: &ShellExecuteOptions) -> Result<ShellSession> {
Ok(ShellSession { shell })
}
async fn source_snapshot(shell: &mut Shell, snapshot_path: &str) -> Result<()> {
async fn source_snapshot(shell: &mut BrushShell, snapshot_path: &str) -> Result<()> {
let mut params = shell.default_exec_params();
let mut open_files = shell.open_files.clone();
open_files.set(OpenFiles::STDIN_FD, OpenFile::Null);
@@ -436,6 +575,16 @@ fn should_skip_env_var(key: &str) -> bool {
)
}
fn next_session_key() -> String {
let counter = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("shell-{}-{counter}", std::process::id())
}
fn next_execution_id() -> String {
let counter = EXECUTION_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("exec-{}-{counter}", std::process::id())
}
const fn should_reset_session(result: &brush_core::ExecutionResult) -> bool {
result.exit_shell
|| result.return_from_function_or_script
+5
View File
@@ -18,12 +18,17 @@ use sysinfo::{Disks, System};
/// Basic system info without shelling out.
#[napi(object)]
pub struct SystemInfo {
/// Linux distro or OS name when available.
pub distro: Option<String>,
/// Kernel version string (if reported by the OS).
pub kernel: Option<String>,
/// Primary CPU brand/model string.
pub cpu: Option<String>,
/// Disk usage summary (used/total) for primary mount.
pub disk: Option<String>,
}
/// Collect system info with native APIs (no shell commands).
#[napi(js_name = "getSystemInfo")]
pub fn get_system_info() -> SystemInfo {
let mut system = System::new_all();
+16 -3
View File
@@ -29,17 +29,23 @@ fn build_utf16_string(data: Vec<u16>) -> Utf16String {
#[napi(object)]
pub struct SliceResult {
/// UTF-16 slice containing the selected text.
pub text: Utf16String,
/// Visible width of the slice in terminal cells.
pub width: u32,
}
#[napi(object)]
pub struct ExtractSegmentsResult {
/// UTF-16 content before the overlay region.
pub before: Utf16String,
#[napi(js_name = "beforeWidth")]
/// Visible width of the `before` segment.
pub before_width: u32,
/// UTF-16 content after the overlay region.
pub after: Utf16String,
#[napi(js_name = "afterWidth")]
/// Visible width of the `after` segment.
pub after_width: u32,
}
@@ -719,8 +725,9 @@ fn wrap_text_with_ansi_impl(text: &[u16], width: usize) -> Vec<Vec<u16>> {
result
}
/// Wrap text to a visible width, preserving ANSI escape codes across line
/// breaks.
/// Wrap text to a visible width, preserving ANSI escape codes across line breaks.
///
/// Returns UTF-16 lines with active SGR codes carried across line boundaries.
#[napi(js_name = "wrapTextWithAnsi")]
pub fn wrap_text_with_ansi(text: JsString, width: u32) -> Result<Vec<Utf16String>> {
let text_u16 = text.into_utf16()?;
@@ -734,7 +741,7 @@ pub fn wrap_text_with_ansi(text: JsString, width: u32) -> Result<Vec<Utf16String
/// Truncate text to a visible width, preserving ANSI codes.
///
/// `ellipsis_kind`: 0 = "…", 1 = "...", 2 = "" (omit)
/// `ellipsis_kind`: 0 = "…", 1 = "...", 2 = "" (omit); pads with spaces when requested.
#[napi(js_name = "truncateToWidth")]
pub fn truncate_to_width(
text: JsString<'_>,
@@ -992,6 +999,8 @@ fn slice_with_width_impl(
}
/// Slice a range of visible columns from a line.
///
/// Counts terminal cells, skipping ANSI escapes, and optionally enforces strict width.
#[napi(js_name = "sliceWithWidth")]
pub fn slice_with_width(
line: JsString,
@@ -1145,6 +1154,8 @@ fn extract_segments_impl(
}
/// Extract the before/after slices around an overlay region.
///
/// Preserves ANSI state so the `after` segment renders correctly after truncation.
#[napi(js_name = "extractSegments")]
pub fn extract_segments(
line: JsString,
@@ -1177,6 +1188,8 @@ pub fn extract_segments(
// ============================================================================
/// Calculate visible width of text, excluding ANSI escape sequences.
///
/// Tabs count as a fixed-width cell.
#[napi(js_name = "visibleWidth")]
pub fn visible_width_napi(text: JsString) -> Result<u32> {
let text_u16 = text.into_utf16()?;
+31 -28
View File
@@ -3,8 +3,7 @@
*
* Uses brush-core via native bindings for shell execution.
*/
import * as crypto from "node:crypto";
import { abortShellExecution, executeShell } from "@oh-my-pi/pi-natives";
import { Shell } from "@oh-my-pi/pi-natives";
import { Settings } from "../config/settings";
import { OutputSink } from "../session/streaming-output";
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
@@ -35,14 +34,13 @@ export interface BashResult {
artifactId?: string;
}
const shellSessions = new Map<string, Shell>();
export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
const settings = await Settings.init();
const { shell, env: shellEnv, prefix } = settings.getShellConfig();
const snapshotPath = shell.includes("bash") ? await getOrCreateSnapshot(shell, shellEnv) : null;
// Generate unique execution ID for abort support
const executionId = crypto.randomUUID();
// Apply command prefix if configured
const prefixedCommand = prefix ? `${prefix} ${command}` : command;
const finalCommand = prefixedCommand;
@@ -59,37 +57,43 @@ export async function executeBash(command: string, options?: BashExecutorOptions
pendingChunks = pendingChunks.then(() => sink.push(chunk)).catch(() => {});
};
// Set up abort handling
let abortListener: (() => void) | undefined;
if (options?.signal) {
const signal = options.signal;
if (signal.aborted) {
// Already aborted
return {
exitCode: undefined,
cancelled: true,
...(await sink.dump("Command cancelled")),
};
}
abortListener = () => {
abortShellExecution(executionId);
if (options?.signal?.aborted) {
return {
exitCode: undefined,
cancelled: true,
...(await sink.dump("Command cancelled")),
};
signal.addEventListener("abort", abortListener, { once: true });
}
let abortListener: (() => void) | undefined;
try {
const result = await executeShell(
const sessionKey = buildSessionKey(shell, prefix, snapshotPath, shellEnv, options?.sessionKey);
let shellSession = shellSessions.get(sessionKey);
if (!shellSession) {
shellSession = new Shell({ sessionEnv: shellEnv, snapshotPath: snapshotPath ?? undefined });
shellSessions.set(sessionKey, shellSession);
}
if (options?.signal) {
abortListener = () => {
shellSession?.abort();
};
options.signal.addEventListener("abort", abortListener, { once: true });
}
const result = await shellSession.run(
{
command: finalCommand,
cwd: options?.cwd,
env: options?.env,
sessionEnv: shellEnv,
timeoutMs: options?.timeout,
executionId,
sessionKey: options?.sessionKey ?? "singleton",
snapshotPath: snapshotPath ?? undefined,
},
enqueueChunk,
(err, chunk) => {
if (!err) {
enqueueChunk(chunk);
}
},
);
await pendingChunks;
@@ -123,8 +127,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
};
} finally {
await pendingChunks;
// Clean up abort listener
if (abortListener && options?.signal) {
if (options?.signal && abortListener) {
options.signal.removeEventListener("abort", abortListener);
}
}
@@ -1,5 +1,4 @@
import { PhotonImage } from "@oh-my-pi/pi-natives";
import { ImageFormat } from "@oh-my-pi/pi-natives/native";
import { ImageFormat, PhotonImage } from "@oh-my-pi/pi-natives";
/**
* Convert image to PNG format for terminal display.
@@ -1,6 +1,5 @@
import type { ImageContent } from "@oh-my-pi/pi-ai";
import { PhotonImage, SamplingFilter } from "@oh-my-pi/pi-natives";
import { ImageFormat } from "@oh-my-pi/pi-natives/native";
import { ImageFormat, PhotonImage, SamplingFilter } from "@oh-my-pi/pi-natives";
export interface ImageResizeOptions {
maxWidth?: number; // Default: 2000
+10
View File
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
### Breaking Changes
- Removed `resize()` function; use `PhotonImage.resize()` method instead
@@ -13,6 +14,11 @@
### Added
- Exported `Shell` class for creating persistent shell sessions with `run()` method and session options
- Exported `ShellOptions`, `ShellRunOptions`, and `ShellRunResult` types for shell session management
- Exported `find()` function for file discovery with glob patterns and .gitignore support
- Exported `FindOptions`, `FindMatch`, and `FindResult` types for file search operations
- Exported `ImageFormat` enum for specifying output formats (PNG, JPEG, WEBP, GIF) in image encoding
- Added `ImageFormat` enum for specifying output format (PNG, JPEG, WEBP, GIF) in `encode()` method
- Added `SamplingFilter` as exported enum instead of object
- Added `Shell` class with persistent session options (`sessionEnv`, `snapshotPath`) and a `run()` command API
@@ -24,6 +30,10 @@
### Changed
- Reorganized native bindings into modular type files with declaration merging via `NativeBindings` interface
- Moved type definitions from implementation files to dedicated `types.ts` modules for better separation of concerns
- Enhanced `SystemInfo` type with additional fields: `os`, `arch`, `hostname`, `shell`, `terminal`, `de`, `wm`, and `gpu`
- Refactored module exports to use direct destructuring from native bindings instead of wrapper functions
- Changed `PhotonImage` API to use instance methods (`resize()`, `encode()`) instead of standalone functions
- Changed `PhotonImage` to use property accessors for `width` and `height` instead of getter methods
+13
View File
@@ -0,0 +1,13 @@
/**
* Base types for native bindings.
* Modules extend this interface via declaration merging.
*/
/** Callback type for threadsafe functions from N-API. */
export type TsFunc<T> = (error: Error | null, value: T) => void;
/**
* Native bindings interface.
* Extended by each module via declaration merging.
*/
export interface NativeBindings {}
+1 -14
View File
@@ -3,20 +3,7 @@
*/
import { native } from "../native";
import type { ClipboardImage } from "./types";
export type { ClipboardImage } from "./types";
/**
* Copy plain text to the system clipboard.
*/
export async function copyToClipboard(text: string): Promise<void> {
await native.copyToClipboard(text);
}
/**
* Read an image from the clipboard, if available.
*/
export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
return native.readImageFromClipboard();
}
export const { copyToClipboard, readImageFromClipboard } = native;
+23
View File
@@ -1,4 +1,27 @@
/**
* Types for clipboard operations.
*/
/** PNG-encoded clipboard image payload. */
export interface ClipboardImage {
/** PNG image bytes. */
data: Uint8Array;
/** MIME type for the PNG payload. */
mimeType: string;
}
declare module "../bindings" {
/** Native clipboard operations exposed by the bindings layer. */
interface NativeBindings {
/**
* Copy text to the system clipboard.
* @param text - UTF-8 text to place on the clipboard.
*/
copyToClipboard(text: string): Promise<void>;
/**
* Read an image from the clipboard.
* @returns PNG payload or null when no image is available.
*/
readImageFromClipboard(): Promise<ClipboardImage | null>;
}
}
+35
View File
@@ -0,0 +1,35 @@
/**
* File discovery API powered by globset + ignore crate.
*/
import * as path from "node:path";
import { native } from "../native";
import type { FindMatch, FindOptions, FindResult } from "./types";
export type { FindMatch, FindOptions, FindResult } from "./types";
/**
* Find files matching a glob pattern.
* Respects .gitignore by default.
*/
export async function find(options: FindOptions, onMatch?: (match: FindMatch) => void): Promise<FindResult> {
const searchPath = path.resolve(options.path);
const pattern = options.pattern || "*";
// Convert simple patterns to recursive globs if needed
const globPattern = pattern.includes("/") || pattern.startsWith("**") ? pattern : `**/${pattern}`;
// napi-rs ThreadsafeFunction passes (error, value) - skip callback on error
const cb = onMatch ? (err: Error | null, m: FindMatch) => !err && onMatch(m) : undefined;
return native.find(
{
...options,
path: searchPath,
pattern: globPattern,
hidden: options.hidden ?? false,
gitignore: options.gitignore ?? true,
},
cb,
);
}
+28 -7
View File
@@ -2,30 +2,51 @@
* Types for native find API.
*/
import type { TsFunc } from "../bindings";
/** Options for discovering files and directories. */
export interface FindOptions {
/** Glob pattern to match (e.g., `*.ts`) */
/** Glob pattern to match (e.g., `*.ts`). */
pattern: string;
/** Directory to search */
/** Directory to search. */
path: string;
/** Filter by file type: "file", "dir", or "symlink" */
/** Filter by file type: "file", "dir", or "symlink". */
fileType?: "file" | "dir" | "symlink";
/** Include hidden files (default: false) */
/** Include hidden files (default: false). */
hidden?: boolean;
/** Maximum number of results */
/** Maximum number of results to return. */
maxResults?: number;
/** Respect .gitignore files (default: true) */
/** Respect .gitignore files (default: true). */
gitignore?: boolean;
/** Sort results by mtime (most recent first) before applying limit */
/** Sort results by mtime (most recent first) before applying limit. */
sortByMtime?: boolean;
}
/** A single filesystem match. */
export interface FindMatch {
/** Relative path from the search root. */
path: string;
/** Resolved filesystem type for the match. */
fileType: "file" | "dir" | "symlink";
/** Modification time in milliseconds since epoch, if available. */
mtime?: number;
}
/** Result of a find operation. */
export interface FindResult {
/** Matched filesystem entries. */
matches: FindMatch[];
/** Number of matches returned after limits are applied. */
totalMatches: number;
}
declare module "../bindings" {
interface NativeBindings {
/**
* Find filesystem entries matching a glob pattern.
* @param options Search options that control globbing and filters.
* @param onMatch Optional callback for streaming matches as they are found.
*/
find(options: FindOptions, onMatch?: TsFunc<FindMatch>): Promise<FindResult>;
}
}
+55
View File
@@ -1,3 +1,8 @@
/**
* Types for grep/search operations.
*/
import type { TsFunc } from "../bindings";
import type { RequestOptions } from "../request-options";
/** Options for searching files. */
@@ -28,32 +33,51 @@ export interface GrepOptions extends RequestOptions {
mode?: "content" | "filesWithMatches" | "count";
}
/** A context line returned around a match. */
export interface ContextLine {
/** 1-indexed line number. */
lineNumber: number;
/** Line content (trimmed line ending). */
line: string;
}
/** A single grep match or per-file count entry. */
export interface GrepMatch {
/** File path for the match (relative for directory searches). */
path: string;
/** 1-indexed line number (0 for count-only entries). */
lineNumber: number;
/** Matched line content (empty for count-only entries). */
line: string;
/** Context lines before the match. */
contextBefore?: ContextLine[];
/** Context lines after the match. */
contextAfter?: ContextLine[];
/** Whether the line was truncated. */
truncated?: boolean;
/** Per-file match count (count mode only). */
matchCount?: number;
}
/** Summary stats for a grep run. */
export interface GrepSummary {
/** Total matches across all files. */
totalMatches: number;
/** Number of files with at least one match. */
filesWithMatches: number;
/** Number of files searched. */
filesSearched: number;
/** Whether the limit/offset stopped the search early. */
limitReached?: boolean;
}
/** Full grep result including matches and summary counts. */
export interface GrepResult extends GrepSummary {
/** Matches or per-file counts, depending on mode. */
matches: GrepMatch[];
}
/** Options for searching in-memory content. */
export interface SearchOptions {
/** Regex pattern to search for */
pattern: string;
@@ -73,22 +97,35 @@ export interface SearchOptions {
mode?: "content" | "count";
}
/** A single content match. */
export interface SearchMatch {
/** 1-indexed line number. */
lineNumber: number;
/** Matched line content. */
line: string;
/** Context lines before the match. */
contextBefore?: ContextLine[];
/** Context lines after the match. */
contextAfter?: ContextLine[];
/** Whether the line was truncated. */
truncated?: boolean;
}
/** Result of searching in-memory content. */
export interface SearchResult {
/** All matches found. */
matches: SearchMatch[];
/** Total number of matches (may exceed `matches.length`). */
matchCount: number;
/** Whether the limit was reached. */
limitReached: boolean;
/** Error message, if any. */
error?: string;
}
/** Legacy alias for WASM match output. */
export type WasmMatch = SearchMatch;
/** Legacy alias for WASM search output. */
export type WasmSearchResult = SearchResult;
/** Options for fuzzy file path search. */
@@ -120,3 +157,21 @@ export interface FuzzyFindResult {
/** Total number of matches found (may exceed `matches.length`). */
totalMatches: number;
}
declare module "../bindings" {
interface NativeBindings {
/** Fuzzy file path search for autocomplete. */
fuzzyFind(options: FuzzyFindOptions): Promise<FuzzyFindResult>;
/** Search files for a regex pattern. */
grep(options: GrepOptions, onMatch?: TsFunc<GrepMatch>): Promise<GrepResult>;
/** Search in-memory content for a regex pattern. */
search(content: string | Uint8Array, options: SearchOptions): SearchResult;
/** Quick check if content matches a pattern. */
hasMatch(
content: string | Uint8Array,
pattern: string | Uint8Array,
ignoreCase: boolean,
multiline: boolean,
): boolean;
}
}
+2 -44
View File
@@ -4,48 +4,6 @@
import { native } from "../native";
/**
* Theme colors for syntax highlighting.
* Each color should be an ANSI escape sequence (e.g., "\x1b[38;2;255;0;0m").
*/
export interface HighlightColors {
comment: string;
keyword: string;
function: string;
variable: string;
string: string;
number: string;
type: string;
operator: string;
punctuation: string;
/** Color for diff inserted lines (+). Optional, defaults to no coloring. */
inserted?: string;
/** Color for diff deleted lines (-). Optional, defaults to no coloring. */
deleted?: string;
}
export type { HighlightColors } from "./types";
/**
* Highlight code with syntax coloring.
*
* @param code - The source code to highlight
* @param lang - Optional language identifier (e.g., "rust", "typescript", "python")
* @param colors - Theme colors as ANSI escape sequences
* @returns Highlighted code as a single string with ANSI color codes
*/
export function highlightCode(code: string, lang: string | undefined, colors: HighlightColors): string {
return native.highlightCode(code, lang, colors);
}
/**
* Check if a language is supported for highlighting.
*/
export function supportsLanguage(lang: string): boolean {
return native.supportsLanguage(lang);
}
/**
* Get list of all supported languages.
*/
export function getSupportedLanguages(): string[] {
return native.getSupportedLanguages();
}
export const { highlightCode, supportsLanguage, getSupportedLanguages } = native;
+56
View File
@@ -0,0 +1,56 @@
/**
* Types for syntax highlighting.
*/
/**
* Theme colors for syntax highlighting.
* Each color should be an ANSI escape sequence (e.g., "\x1b[38;2;255;0;0m").
*/
export interface HighlightColors {
/** ANSI color for comments. */
comment: string;
/** ANSI color for keywords. */
keyword: string;
/** ANSI color for function names. */
function: string;
/** ANSI color for variables and identifiers. */
variable: string;
/** ANSI color for string literals. */
string: string;
/** ANSI color for numeric literals. */
number: string;
/** ANSI color for type identifiers. */
type: string;
/** ANSI color for operators. */
operator: string;
/** ANSI color for punctuation tokens. */
punctuation: string;
/** Color for diff inserted lines (+). */
inserted?: string;
/** Color for diff deleted lines (-). */
deleted?: string;
}
declare module "../bindings" {
interface NativeBindings {
/**
* Highlight code with syntax coloring.
* @param code Source code to highlight.
* @param lang Language name, extension, or null for plain text.
* @param colors ANSI color palette for semantic scopes.
* @returns Highlighted code with ANSI color codes.
*/
highlightCode(code: string, lang: string | null | undefined, colors: HighlightColors): string;
/**
* Check if a language is supported for highlighting.
* @param lang Language name or extension to test.
* @returns True when highlighting is available.
*/
supportsLanguage(lang: string): boolean;
/**
* Get list of all supported languages.
* @returns Syntect language names supported by the native highlighter.
*/
getSupportedLanguages(): string[];
}
}
+16 -2
View File
@@ -2,9 +2,23 @@
* Types for HTML to Markdown conversion.
*/
/** Options controlling HTML preprocessing and output. */
export interface HtmlToMarkdownOptions {
/** Remove navigation elements, forms, headers, footers */
/** Remove navigation elements, forms, headers, and footers. */
cleanContent?: boolean;
/** Skip images during conversion */
/** Skip images during conversion. */
skipImages?: boolean;
}
declare module "../bindings" {
/** Native HTML utilities exposed by the Rust bindings. */
interface NativeBindings {
/**
* Convert HTML to Markdown.
* @param html HTML source to convert.
* @param options Optional conversion settings.
* @returns Markdown output.
*/
htmlToMarkdown(html: string, options?: HtmlToMarkdownOptions | null): Promise<string>;
}
}
+7 -3
View File
@@ -2,8 +2,12 @@
* Image processing via native bindings.
*/
import { native, SamplingFilter } from "../native";
import { native } from "../native";
const { PhotonImage } = native;
export { ImageFormat, type PhotonImageConstructor, SamplingFilter } from "./types";
export { PhotonImage, SamplingFilter };
/** PhotonImage class for image manipulation. Use PhotonImage.parse() to create instances. */
export const PhotonImage = native.PhotonImage;
/** PhotonImage instance type. */
export type PhotonImage = import("./types").PhotonImage;
+65
View File
@@ -0,0 +1,65 @@
/**
* Types for image processing.
*/
/** Output format for image encoding. */
export const enum ImageFormat {
/** PNG encoded bytes. */
PNG = 0,
/** JPEG encoded bytes. */
JPEG = 1,
/** WebP encoded bytes. */
WEBP = 2,
/** GIF encoded bytes. */
GIF = 3,
}
/** Sampling filter for resize operations. */
export const enum SamplingFilter {
/** Nearest-neighbor sampling (fast, low quality). */
Nearest = 1,
/** Triangle filter (linear interpolation). */
Triangle = 2,
/** Catmull-Rom filter with sharper edges. */
CatmullRom = 3,
/** Gaussian filter for smoother results. */
Gaussian = 4,
/** Lanczos3 filter for high-quality downscaling. */
Lanczos3 = 5,
}
/** Image container for native image operations. */
export interface PhotonImage {
/** Image width in pixels. */
get width(): number;
/** Image height in pixels. */
get height(): number;
/**
* Encode the image using the requested format and quality.
* Returns the encoded image bytes.
*/
encode(format: ImageFormat, quality: number): Promise<Uint8Array>;
/**
* Resize the image to the requested dimensions with the filter.
* Returns a new image instance.
*/
resize(width: number, height: number, filter: SamplingFilter): Promise<PhotonImage>;
}
/** Static entrypoints for creating `PhotonImage` instances. */
export interface PhotonImageConstructor {
/** Parse image bytes (PNG, JPEG, WebP, GIF) into a native image. */
parse(bytes: Uint8Array): Promise<PhotonImage>;
/** Instance prototype reference. */
prototype: PhotonImage;
}
declare module "../bindings" {
/** Native bindings for image operations. */
interface NativeBindings {
/** Sampling filters exposed by the native module. */
SamplingFilter: typeof SamplingFilter;
/** Photon image constructor exposed by the native module. */
PhotonImage: PhotonImageConstructor;
}
}
+15 -74
View File
@@ -2,20 +2,13 @@
* Native utilities powered by N-API.
*/
import * as path from "node:path";
import { setNativeKillTree } from "@oh-my-pi/pi-utils";
import type { FindMatch, FindOptions, FindResult } from "./find/types";
import { native } from "./native";
export type { RequestOptions } from "./request-options";
setNativeKillTree(native.killTree);
// =============================================================================
// Clipboard
// =============================================================================
export { type ClipboardImage, copyToClipboard, readImageFromClipboard } from "./clipboard/index";
export { type ClipboardImage, copyToClipboard, readImageFromClipboard } from "./clipboard";
// =============================================================================
// Grep (ripgrep-based regex search)
@@ -34,48 +27,19 @@ export {
grep,
hasMatch,
searchContent,
} from "./grep/index";
} from "./grep";
// =============================================================================
// Find (file discovery)
// =============================================================================
export type { FindMatch, FindOptions, FindResult } from "./find/types";
/**
* Find files matching a glob pattern.
* Respects .gitignore by default.
*/
export async function find(options: FindOptions, onMatch?: (match: FindMatch) => void): Promise<FindResult> {
const searchPath = path.resolve(options.path);
const pattern = options.pattern || "*";
// Convert simple patterns to recursive globs if needed
const globPattern = pattern.includes("/") || pattern.startsWith("**") ? pattern : `**/${pattern}`;
// napi-rs ThreadsafeFunction passes (error, value) - skip callback on error
const cb = onMatch ? (err: Error | null, m: FindMatch) => !err && onMatch(m) : undefined;
return native.find(
{
...options,
path: searchPath,
pattern: globPattern,
hidden: options.hidden ?? false,
gitignore: options.gitignore ?? true,
},
cb,
);
}
export { type FindMatch, type FindOptions, type FindResult, find } from "./find";
// =============================================================================
// Image processing (photon-compatible API)
// =============================================================================
export {
PhotonImage,
SamplingFilter,
} from "./image/index";
export { ImageFormat, PhotonImage, SamplingFilter } from "./image";
// =============================================================================
// Text utilities
@@ -90,7 +54,7 @@ export {
truncateToWidth,
visibleWidth,
wrapTextWithAnsi,
} from "./text/index";
} from "./text";
// =============================================================================
// Syntax highlighting
@@ -101,7 +65,7 @@ export {
type HighlightColors,
highlightCode,
supportsLanguage,
} from "./highlight/index";
} from "./highlight";
// =============================================================================
// Keyboard sequence helpers
@@ -115,22 +79,19 @@ export {
type ParsedKittyResult,
parseKey,
parseKittySequence,
} from "./keys/index";
} from "./keys";
// =============================================================================
// HTML to Markdown
// =============================================================================
export {
type HtmlToMarkdownOptions,
htmlToMarkdown,
} from "./html/index";
export { type HtmlToMarkdownOptions, htmlToMarkdown } from "./html";
// =============================================================================
// System info
// =============================================================================
export { getSystemInfo, type SystemInfo } from "./system-info/index";
export { getSystemInfo, type SystemInfo } from "./system-info";
// =============================================================================
// Shell execution (brush-core)
@@ -139,36 +100,16 @@ export { getSystemInfo, type SystemInfo } from "./system-info/index";
export {
abortShellExecution,
executeShell,
Shell,
type ShellExecuteOptions,
type ShellExecuteResult,
} from "./shell/index";
type ShellOptions,
type ShellRunOptions,
type ShellRunResult,
} from "./shell";
// =============================================================================
// Process management
// =============================================================================
/**
* Kill a process and all its descendants.
*
* Uses platform-native APIs for efficiency:
* - Linux: /proc/{pid}/children
* - macOS: libproc (proc_listchildpids)
* - Windows: CreateToolhelp32Snapshot
*
* @param pid - Process ID to kill
* @param signal - Signal number (e.g., 9 for SIGKILL). Ignored on Windows.
* @returns Number of processes successfully killed
*/
export function killTree(pid: number, signal: number): number {
return native.killTree(pid, signal);
}
/**
* List all descendant PIDs of a process.
*
* @param pid - Process ID to query
* @returns Array of descendant PIDs (children, grandchildren, etc.)
*/
export function listDescendants(pid: number): number[] {
return native.listDescendants(pid);
}
export { killTree, listDescendants } from "./ps";
+3 -58
View File
@@ -2,63 +2,8 @@
* Keyboard sequence utilities powered by native bindings.
*/
import { type KeyEventType, native, type ParsedKittyResult } from "../native";
import { native } from "../native";
export type { KeyEventType, ParsedKittyResult };
export type { KeyEventType, ParsedKittyResult } from "./types";
/** Match Kitty protocol sequences for codepoint and modifier. */
export function matchesKittySequence(data: string, expectedCodepoint: number, expectedModifier: number): boolean {
return native.matchesKittySequence(data, expectedCodepoint, expectedModifier);
}
/**
* Parse a Kitty keyboard protocol sequence.
*
* @param data - Raw escape sequence from terminal
* @returns Parsed sequence with codepoint, modifier, and event type, or undefined if not a valid Kitty sequence
*/
export function parseKittySequence(data: string): ParsedKittyResult | undefined {
return native.parseKittySequence(data) ?? undefined;
}
/**
* Parse terminal input and return a normalized key identifier.
*
* Returns key names like "escape", "ctrl+c", "shift+tab", "alt+enter".
* Returns undefined if the input is not a recognized key sequence.
*
* @param data - Raw input data from terminal
* @param kittyProtocolActive - Whether Kitty keyboard protocol is active
*/
export function parseKey(data: string, kittyProtocolActive: boolean): string | undefined {
return native.parseKey(data, kittyProtocolActive) ?? undefined;
}
/**
* Check if input matches a legacy escape sequence for a specific key.
*
* @param data - Raw input data from terminal
* @param keyName - Key name to match (e.g., "up", "f1", "ctrl+up")
*/
export function matchesLegacySequence(data: string, keyName: string): boolean {
return native.matchesLegacySequence(data, keyName);
}
/**
* Match input data against a key identifier string.
*
* Supported key identifiers:
* - Single keys: "escape", "tab", "enter", "backspace", "delete", "home", "end", "space"
* - Arrow keys: "up", "down", "left", "right"
* - Ctrl combinations: "ctrl+c", "ctrl+z", etc.
* - Shift combinations: "shift+tab", "shift+enter"
* - Alt combinations: "alt+enter", "alt+backspace"
* - Combined modifiers: "shift+ctrl+p", "ctrl+alt+x"
*
* @param data - Raw input data from terminal
* @param keyId - Key identifier (e.g., "ctrl+c", "escape")
* @param kittyProtocolActive - Whether Kitty keyboard protocol is active
*/
export function matchesKey(data: string, keyId: string, kittyProtocolActive: boolean): boolean {
return native.matchesKey(data, keyId, kittyProtocolActive);
}
export const { matchesKittySequence, parseKey, matchesLegacySequence, parseKittySequence, matchesKey } = native;
+75
View File
@@ -0,0 +1,75 @@
/**
* Types for keyboard sequence handling.
*/
/**
* Event types from Kitty keyboard protocol (flag 2).
* 1 = key press, 2 = key repeat, 3 = key release.
*/
export const enum KeyEventType {
/** Key press event. */
Press = 1,
/** Key repeat event. */
Repeat = 2,
/** Key release event. */
Release = 3,
}
/** Parsed Kitty keyboard protocol sequence result. */
export interface ParsedKittyResult {
/** Primary codepoint associated with the key. */
codepoint: number;
/** Optional shifted key codepoint from the sequence. */
shiftedKey?: number;
/** Optional base layout key codepoint from the sequence. */
baseLayoutKey?: number;
/** Modifier bitmask (shift/alt/ctrl), excluding lock bits. */
modifier: number;
/** Optional event type from the sequence. */
eventType?: KeyEventType;
}
declare module "../bindings" {
interface NativeBindings {
/**
* Match Kitty protocol sequences for a codepoint and modifier mask.
* @param data Raw terminal input data.
* @param expectedCodepoint Codepoint to compare against the parsed sequence.
* @param expectedModifier Modifier mask (shift/alt/ctrl).
* @returns True when the sequence matches the expected codepoint and modifiers.
*/
matchesKittySequence(data: string, expectedCodepoint: number, expectedModifier: number): boolean;
/**
* Parse terminal input and return a normalized key identifier.
* Returns key names like "escape", "ctrl+c", "shift+tab", "alt+enter".
* Returns null if the input is not a recognized key sequence.
* @param data Raw terminal input data.
* @param kittyProtocolActive Whether Kitty disambiguation is enabled.
* @returns The normalized key id or null when unrecognized.
*/
parseKey(data: string, kittyProtocolActive: boolean): string | null;
/**
* Check if input matches a legacy escape sequence for a specific key.
* @param data Raw terminal input data.
* @param keyName Key identifier to match (e.g. "home").
* @returns True when the sequence maps to the given key name.
*/
matchesLegacySequence(data: string, keyName: string): boolean;
/**
* Parse a Kitty keyboard protocol sequence.
* @param data Raw terminal input data.
* @returns Parsed sequence info or null if not a Kitty sequence.
*/
parseKittySequence(data: string): ParsedKittyResult | null;
/**
* Match input data against a key identifier string.
* Supports: escape, tab, enter, backspace, delete, home, end, space,
* arrows (up/down/left/right), ctrl+X, shift+X, alt+X, combined modifiers.
* @param data Raw terminal input data.
* @param keyId Key identifier string to match (e.g. "ctrl+c").
* @param kittyProtocolActive Whether Kitty disambiguation is enabled.
* @returns True when the input matches the key identifier.
*/
matchesKey(data: string, keyId: string, kittyProtocolActive: boolean): boolean;
}
}
+21 -106
View File
@@ -1,113 +1,27 @@
/**
* Native addon loader and bindings.
*
* Each module extends NativeBindings via declaration merging in its types.ts.
*/
import { createRequire } from "node:module";
import * as path from "node:path";
import type { ClipboardImage } from "./clipboard/types";
import type { FindMatch, FindOptions, FindResult } from "./find/types";
import type {
FuzzyFindOptions,
FuzzyFindResult,
GrepOptions,
GrepResult,
SearchOptions,
SearchResult,
} from "./grep/types";
import type { HighlightColors } from "./highlight/index";
import type { HtmlToMarkdownOptions } from "./html/types";
import type { ShellExecuteOptions, ShellExecuteResult } from "./shell/types";
import type { SystemInfo } from "./system-info/index";
import type { ExtractSegmentsResult, SliceWithWidthResult } from "./text/index";
import type { NativeBindings } from "./bindings";
export type { RequestOptions } from "./request-options";
// Import types to trigger declaration merging
import "./clipboard/types";
import "./find/types";
import "./grep/types";
import "./highlight/types";
import "./html/types";
import "./image/types";
import "./keys/types";
import "./ps/types";
import "./shell/types";
import "./system-info/types";
import "./text/types";
/**
* Event types from Kitty keyboard protocol (flag 2)
* 1 = key press, 2 = key repeat, 3 = key release
*/
export const enum KeyEventType {
Press = 1,
Repeat = 2,
Release = 3,
}
/** Parsed Kitty keyboard protocol sequence result. */
export interface ParsedKittyResult {
codepoint: number;
shiftedKey?: number;
baseLayoutKey?: number;
modifier: number;
eventType?: KeyEventType;
}
export const enum ImageFormat {
PNG = 0,
JPEG = 1,
WEBP = 2,
GIF = 3,
}
export interface PhotonImage {
get width(): number;
get height(): number;
encode(format: ImageFormat, quality: number): Promise<Uint8Array>;
resize(width: number, height: number, filter: number): Promise<PhotonImage>;
}
export interface PhotonImageConstructor {
parse(bytes: Uint8Array): Promise<PhotonImage>;
prototype: PhotonImage;
}
export const enum SamplingFilter {
Nearest = 1,
Triangle = 2,
CatmullRom = 3,
Gaussian = 4,
Lanczos3 = 5,
}
import type { GrepMatch } from "./grep/types";
export type TsFunc<T> = (error: Error | null, value: T) => void;
export interface NativeBindings {
copyToClipboard(text: string): Promise<void>;
readImageFromClipboard(): Promise<ClipboardImage | null>;
find(options: FindOptions, onMatch?: TsFunc<FindMatch>): Promise<FindResult>;
fuzzyFind(options: FuzzyFindOptions): Promise<FuzzyFindResult>;
grep(options: GrepOptions, onMatch?: TsFunc<GrepMatch>): Promise<GrepResult>;
search(content: string | Uint8Array, options: SearchOptions): SearchResult;
hasMatch(
content: string | Uint8Array,
pattern: string | Uint8Array,
ignoreCase: boolean,
multiline: boolean,
): boolean;
htmlToMarkdown(html: string, options?: HtmlToMarkdownOptions | null): Promise<string>;
highlightCode(code: string, lang: string | null | undefined, colors: HighlightColors): string;
supportsLanguage(lang: string): boolean;
getSupportedLanguages(): string[];
SamplingFilter: SamplingFilter;
PhotonImage: PhotonImageConstructor;
truncateToWidth(text: string, maxWidth: number, ellipsisKind: number, pad: boolean): string;
wrapTextWithAnsi(text: string, width: number): string[];
sliceWithWidth(line: string, startCol: number, length: number, strict: boolean): SliceWithWidthResult;
visibleWidth(text: string): number;
extractSegments(
line: string,
beforeEnd: number,
afterStart: number,
afterLen: number,
strictAfter: boolean,
): ExtractSegmentsResult;
matchesKittySequence(data: string, expectedCodepoint: number, expectedModifier: number): boolean;
executeShell(options: ShellExecuteOptions, onChunk?: TsFunc<string>): Promise<ShellExecuteResult>;
abortShellExecution(executionId: string): void;
parseKey(data: string, kittyProtocolActive: boolean): string | null;
matchesLegacySequence(data: string, keyName: string): boolean;
parseKittySequence(data: string): ParsedKittyResult | null;
matchesKey(data: string, keyId: string, kittyProtocolActive: boolean): boolean;
killTree(pid: number, signal: number): number;
listDescendants(pid: number): number[];
getSystemInfo(): SystemInfo;
}
export type { NativeBindings, TsFunc } from "./bindings";
const require = createRequire(import.meta.url);
const platformTag = `${process.platform}-${process.arch}`;
@@ -195,6 +109,7 @@ function validateNative(bindings: NativeBindings, source: string): void {
checkFn("matchesKittySequence");
checkFn("executeShell");
checkFn("abortShellExecution");
checkFn("Shell");
checkFn("parseKey");
checkFn("matchesLegacySequence");
checkFn("parseKittySequence");
+10
View File
@@ -0,0 +1,10 @@
/**
* Process management utilities.
*/
import { setNativeKillTree } from "@oh-my-pi/pi-utils";
import { native } from "../native";
setNativeKillTree(native.killTree);
export const { killTree, listDescendants } = native;
+24
View File
@@ -0,0 +1,24 @@
/**
* Types for process management.
*/
export {};
declare module "../bindings" {
/** Native process-management bindings implemented in pi-natives. */
interface NativeBindings {
/**
* Kill a process and all its descendants using platform-native APIs.
* @param pid Root process id.
* @param signal Signal number (ignored on Windows).
* @returns Number of processes successfully killed.
*/
killTree(pid: number, signal: number): number;
/**
* List all descendant PIDs of a process (children, grandchildren, etc.).
* @param pid Root process id.
* @returns Empty array when the process has no children or doesn't exist.
*/
listDescendants(pid: number): number[];
}
}
+13 -3
View File
@@ -3,9 +3,20 @@
*/
import { native } from "../native";
import type { ShellExecuteOptions, ShellExecuteResult } from "./types";
import type { ShellExecuteOptions, ShellExecuteResult, ShellOptions, ShellRunOptions, ShellRunResult } from "./types";
export type { ShellExecuteOptions, ShellExecuteResult } from "./types";
export type { ShellExecuteOptions, ShellExecuteResult, ShellOptions, ShellRunOptions, ShellRunResult } from "./types";
export interface Shell {
run(options: ShellRunOptions, onChunk?: (error: Error | null, chunk: string) => void): Promise<ShellRunResult>;
abort(): void;
}
export interface ShellConstructor {
new (options?: ShellOptions): Shell;
}
export const Shell = native.Shell as ShellConstructor;
/**
* Execute a shell command using brush-core.
@@ -18,7 +29,6 @@ export async function executeShell(
options: ShellExecuteOptions,
onChunk?: (chunk: string) => void,
): Promise<ShellExecuteResult> {
// napi-rs ThreadsafeFunction passes (error, value) - skip callback on error
const wrappedCallback = onChunk ? (err: Error | null, chunk: string) => !err && onChunk(chunk) : undefined;
return native.executeShell(options, wrappedCallback);
}
+99 -21
View File
@@ -1,33 +1,111 @@
/**
* Options for executing a shell command via brush-core.
* Types for shell execution via brush-core.
*/
export interface ShellExecuteOptions {
/** The command to execute */
command: string;
/** Working directory for command execution */
cwd?: string;
/** Environment variables to apply for this command */
env?: Record<string, string>;
/** Environment variables to set once per session */
import type { TsFunc } from "../bindings";
/**
* Configuration for a persistent brush-core shell session.
*/
export interface ShellOptions {
/** Environment variables to set once per session. */
sessionEnv?: Record<string, string>;
/** Timeout in milliseconds */
timeoutMs?: number;
/** Unique identifier for this execution (used for abort) */
executionId: string;
/** Session key for persistent brush shell instances */
sessionKey: string;
/** Optional snapshot path to source for bash sessions */
/** Optional snapshot path to source for bash sessions. */
snapshotPath?: string;
}
/**
* Result of executing a shell command via brush-core.
* Options for running a single shell command.
*/
export interface ShellExecuteResult {
/** Exit code of the command (undefined if cancelled or timed out) */
export interface ShellRunOptions {
/** The command to execute. */
command: string;
/** Working directory for command execution. */
cwd?: string;
/** Environment variables to apply for this command. */
env?: Record<string, string>;
/** Timeout in milliseconds. */
timeoutMs?: number;
}
/**
* Result of running a shell command via brush-core.
*/
export interface ShellRunResult {
/** Exit code of the command (undefined if cancelled or timed out). */
exitCode?: number;
/** Whether the command was cancelled via abort */
/** Whether the command was cancelled via abort. */
cancelled: boolean;
/** Whether the command timed out */
/** Whether the command timed out. */
timedOut: boolean;
}
/**
* Internal options for the native brush-core binding.
*/
export interface ShellExecuteOptions {
/** The command to execute. */
command: string;
/** Working directory for command execution. */
cwd?: string;
/** Environment variables to apply for this command. */
env?: Record<string, string>;
/** Environment variables to set once per session. */
sessionEnv?: Record<string, string>;
/** Timeout in milliseconds. */
timeoutMs?: number;
/** Unique identifier for this execution (used for abort). */
executionId: string;
/** Session key for persistent brush shell instances. */
sessionKey: string;
/** Optional snapshot path to source for bash sessions. */
snapshotPath?: string;
}
/**
/** Internal result from the native brush-core binding. */
export interface ShellExecuteResult extends ShellRunResult {}
/** Native Shell class instance. */
export interface NativeShell {
/**
* Run a command in the shell.
* @param options Command execution options.
* @param onChunk Optional callback for streamed output.
* @returns Promise resolving to the command result.
*/
run(options: ShellRunOptions, onChunk?: TsFunc<string>): Promise<ShellRunResult>;
/**
* Abort all running commands in this session.
*/
abort(): void;
}
/** Native Shell class constructor. */
export interface NativeShellConstructor {
/**
* Create a new shell session.
* @param options Optional session configuration.
*/
new (options?: ShellOptions): NativeShell;
}
declare module "../bindings" {
/** Native bindings exposed by the shell module. */
interface NativeBindings {
/**
* Execute a shell command with explicit session metadata.
* @param options Execution options including session identifiers.
* @param onChunk Optional callback for streamed output.
* @returns Promise resolving to the command result.
*/
executeShell(options: ShellExecuteOptions, onChunk?: TsFunc<string>): Promise<ShellExecuteResult>;
/**
* Abort a running shell execution by ID.
* @param executionId Execution identifier from ShellExecuteOptions.
*/
abortShellExecution(executionId: string): void;
/** Shell class constructor for creating sessions. */
Shell: NativeShellConstructor;
}
}
+2 -9
View File
@@ -4,13 +4,6 @@
import { native } from "../native";
export interface SystemInfo {
distro?: string;
kernel?: string;
cpu?: string;
disk?: string;
}
export type { SystemInfo } from "./types";
export function getSystemInfo(): SystemInfo {
return native.getSystemInfo();
}
export const { getSystemInfo } = native;
+41
View File
@@ -0,0 +1,41 @@
/**
* Types for system information.
*/
/** Snapshot of system details reported by native probes. */
export interface SystemInfo {
/** Operating system name (e.g. Linux, macOS, Windows). */
os: string;
/** CPU architecture (e.g. x64, arm64). */
arch: string;
/** Linux distro or detailed OS name when available. */
distro?: string;
/** Kernel version string, if the OS reports one. */
kernel?: string;
/** Hostname of the current machine. */
hostname?: string;
/** Active login shell, when detected. */
shell?: string;
/** Terminal program identifier, when available. */
terminal?: string;
/** Desktop environment name, if reported. */
de?: string;
/** Window manager name, if reported. */
wm?: string;
/** Primary CPU brand/model string. */
cpu?: string;
/** Primary GPU identifier, when available. */
gpu?: string;
/** System memory summary (used/total). */
memory?: string;
/** Disk usage summary (used/total) for primary mount. */
disk?: string;
}
declare module "../bindings" {
/** Native bindings that expose system info collection. */
interface NativeBindings {
/** Get system information (OS, CPU, memory, and disk summaries). */
getSystemInfo(): SystemInfo;
}
}
+9 -47
View File
@@ -2,25 +2,11 @@
* ANSI-aware text utilities powered by native bindings.
*/
import { Ellipsis, type SliceWithWidthResult } from "@oh-my-pi/pi-natives";
import { native } from "../native";
export interface SliceWithWidthResult {
text: string;
width: number;
}
export interface ExtractSegmentsResult {
before: string;
beforeWidth: number;
after: string;
afterWidth: number;
}
export const enum Ellipsis {
Unicode = 0, // "…"
Ascii = 1, // "..."
Omit = 2, // ""
}
export type { ExtractSegmentsResult, SliceWithWidthResult } from "./types";
export { Ellipsis } from "./types";
/**
* Truncate text to fit within a maximum visible width, adding ellipsis if needed.
@@ -42,41 +28,17 @@ export function truncateToWidth(
return native.truncateToWidth(text, maxWidth, ellipsis, pad);
}
/**
* Wrap text to a visible width, preserving ANSI escape codes across line breaks.
*
* @param text - Text to wrap (may contain ANSI codes and newlines)
* @param width - Maximum visible width per line
* @returns Array of wrapped lines (NOT padded to width)
*/
export function wrapTextWithAnsi(text: string, width: number): string[] {
return native.wrapTextWithAnsi(text, width);
}
/**
* Measure the visible width of text (excluding ANSI codes).
*/
export function visibleWidth(text: string): number {
return native.visibleWidth(text);
}
/**
* Slice a range of visible columns from a line.
* @param line - The line to slice
* @param startCol - The starting column
* @param length - The length of the slice
* @param strict - Whether to strictly enforce the length
* @returns The sliced line
*/
export function sliceWithWidth(line: string, startCol: number, length: number, strict = false): SliceWithWidthResult {
if (length <= 0) return { text: "", width: 0 };
return native.sliceWithWidth(line, startCol, length, strict);
}
/**
* Extract before/after segments around an overlay region.
*/
export function extractSegments(
line: string,
beforeEnd: number,
afterStart: number,
afterLen: number,
strictAfter = false,
): ExtractSegmentsResult {
return native.extractSegments(line, beforeEnd, afterStart, afterLen, strictAfter);
}
export const { wrapTextWithAnsi, visibleWidth, extractSegments } = native;
+79
View File
@@ -0,0 +1,79 @@
/**
* Types for ANSI-aware text utilities.
*/
/** Result of slicing a line by visible columns. */
export interface SliceWithWidthResult {
/** UTF-16 slice containing the selected text. */
text: string;
/** Visible width of the slice in terminal cells. */
width: number;
}
/** Result of extracting before/after overlay segments. */
export interface ExtractSegmentsResult {
/** UTF-16 content before the overlay region. */
before: string;
/** Visible width of the `before` segment. */
beforeWidth: number;
/** UTF-16 content after the overlay region. */
after: string;
/** Visible width of the `after` segment. */
afterWidth: number;
}
/** Ellipsis strategy for truncation. */
export const enum Ellipsis {
/** Use a single Unicode ellipsis character ("…"). */
Unicode = 0,
/** Use three ASCII dots ("..."). */
Ascii = 1,
/** Omit ellipsis entirely. */
Omit = 2,
}
declare module "../bindings" {
interface NativeBindings {
/**
* Truncate text to a visible width, optionally padding with spaces.
* @param text UTF-16 input text.
* @param maxWidth Maximum visible width in terminal cells.
* @param ellipsisKind Ellipsis strategy (see {@link Ellipsis}).
* @param pad Whether to pad the output to `maxWidth`.
*/
truncateToWidth(text: string, maxWidth: number, ellipsisKind: number, pad: boolean): string;
/**
* Wrap text to a visible width, preserving ANSI codes across line breaks.
* @param text UTF-16 input text with optional ANSI escapes.
* @param width Maximum visible width per line.
*/
wrapTextWithAnsi(text: string, width: number): string[];
/**
* Slice a range of visible columns from a line.
* @param line UTF-16 input line with optional ANSI escapes.
* @param startCol Starting column in terminal cells.
* @param length Number of visible cells to include.
* @param strict Whether to drop graphemes that overflow the range.
*/
sliceWithWidth(line: string, startCol: number, length: number, strict: boolean): SliceWithWidthResult;
/**
* Measure the visible width of text (excluding ANSI codes).
* @param text UTF-16 input text with optional ANSI escapes.
*/
visibleWidth(text: string): number;
/** Extract before/after segments around an overlay region.
* @param line UTF-16 input line with optional ANSI escapes.
* @param beforeEnd Column where the "before" segment ends.
* @param afterStart Column where the "after" segment starts.
* @param afterLen Visible width of the "after" segment.
* @param strictAfter Whether to drop graphemes that overflow `afterLen`.
*/
extractSegments(
line: string,
beforeEnd: number,
afterStart: number,
afterLen: number,
strictAfter: boolean,
): ExtractSegmentsResult;
}
}