diff --git a/crates/pi-natives/src/clipboard.rs b/crates/pi-natives/src/clipboard.rs index c5143640e..529f13d1c 100644 --- a/crates/pi-natives/src/clipboard.rs +++ b/crates/pi-natives/src/clipboard.rs @@ -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> { /// 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")] diff --git a/crates/pi-natives/src/find.rs b/crates/pi-natives/src/find.rs index f21fe8183..ab422f15d 100644 --- a/crates/pi-natives/src/find.rs +++ b/crates/pi-natives/src/find.rs @@ -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, + /// 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")] diff --git a/crates/pi-natives/src/grep.rs b/crates/pi-natives/src/grep.rs index f7b57bdc5..fae332d45 100644 --- a/crates/pi-natives/src/grep.rs +++ b/crates/pi-natives/src/grep.rs @@ -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>, + /// Context lines after the match. #[napi(js_name = "contextAfter")] pub context_after: Option>, + /// Whether the line was truncated. pub truncated: Option, + /// Per-file match count (count mode only). #[napi(js_name = "matchCount")] pub match_count: Option, } @@ -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, + /// 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, } @@ -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, options: SearchOptions) -> SearchResult { match &content { @@ -957,8 +974,14 @@ pub fn search(content: Either, 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, @@ -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 { /// 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 { task::spawn_blocking(move || fuzzy_find_sync(options)) diff --git a/crates/pi-natives/src/highlight.rs b/crates/pi-natives/src/highlight.rs index 19bde7f53..e2ba692da 100644 --- a/crates/pi-natives/src/highlight.rs +++ b/crates/pi-natives/src/highlight.rs @@ -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, + /// ANSI color for diff deleted lines. #[napi(js_name = "deleted")] pub deleted: Option, } diff --git a/crates/pi-natives/src/html.rs b/crates/pi-natives/src/html.rs index a730a997e..b39c2fa64 100644 --- a/crates/pi-natives/src/html.rs +++ b/crates/pi-natives/src/html.rs @@ -16,7 +16,10 @@ pub struct HtmlToMarkdownOptions { pub skip_images: Option, } -/// 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, diff --git a/crates/pi-natives/src/image.rs b/crates/pi-natives/src/image.rs index 7dade7edc..ea46979fa 100644 --- a/crates/pi-natives/src/image.rs +++ b/crates/pi-natives/src/image.rs @@ -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 for FilterType { /// Image container for native interop. #[napi] pub struct PhotonImage { + /// Shared decoded image data. img: Arc, } #[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 { let img = Arc::clone(&self.img); diff --git a/crates/pi-natives/src/keys.rs b/crates/pi-natives/src/keys.rs index f93ba42e1..f87d620f0 100644 --- a/crates/pi-natives/src/keys.rs +++ b/crates/pi-natives/src/keys.rs @@ -94,14 +94,18 @@ struct ParsedKittySequence { event_type: Option, } -/// 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, + /// Optional base layout key codepoint from the sequence. pub base_layout_key: Option, + /// 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, } @@ -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 { 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 { parse_kitty_sequence(data.as_bytes()).map(|p| ParsedKittyResult { diff --git a/crates/pi-natives/src/ps.rs b/crates/pi-natives/src/ps.rs index 982644a23..f60f9d5f8 100644 --- a/crates/pi-natives/src/ps.rs +++ b/crates/pi-natives/src/ps.rs @@ -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) { 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 { + // 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) { // 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 { + // 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) { 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 { 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 { 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] diff --git a/crates/pi-natives/src/shell.rs b/crates/pi-natives/src/shell.rs index 076ec7123..1fb7dd671 100644 --- a/crates/pi-natives/src/shell.rs +++ b/crates/pi-natives/src/shell.rs @@ -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; type SessionMap = HashMap>>; 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> = LazyLock::new(|| Mutex::new(HashMap::new())); static SESSIONS: LazyLock> = 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>, + /// Optional snapshot file to source on session creation. + pub snapshot_path: Option, +} + +/// 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, + /// Environment variables to apply for this command only. + pub env: Option>, + /// Timeout in milliseconds before cancelling the command. + pub timeout_ms: Option, +} + +/// Result of running a shell command. +#[napi(object)] +pub struct ShellRunResult { + /// Exit code when the command completes normally. + pub exit_code: Option, + /// 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>, + snapshot_path: Option, +} + +#[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) -> 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>, + ) -> Result { + 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 = { + 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, + /// Environment variables to apply for this command only. pub env: Option>, + /// Environment variables to apply once per session. pub session_env: Option>, + /// Timeout in milliseconds before cancelling the command. pub timeout_ms: Option, + /// 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, } /// 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, + /// 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, >, +) -> Result { + execute_shell_with_options(options, on_chunk).await +} + +async fn execute_shell_with_options( + options: ShellExecuteOptions, + on_chunk: Option>, ) -> Result { 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>> { - 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 { ..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 { 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 diff --git a/crates/pi-natives/src/system_info.rs b/crates/pi-natives/src/system_info.rs index 482324176..804b2908c 100644 --- a/crates/pi-natives/src/system_info.rs +++ b/crates/pi-natives/src/system_info.rs @@ -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, + /// Kernel version string (if reported by the OS). pub kernel: Option, + /// Primary CPU brand/model string. pub cpu: Option, + /// Disk usage summary (used/total) for primary mount. pub disk: Option, } +/// 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(); diff --git a/crates/pi-natives/src/text.rs b/crates/pi-natives/src/text.rs index e44c450dc..b694f09f8 100644 --- a/crates/pi-natives/src/text.rs +++ b/crates/pi-natives/src/text.rs @@ -29,17 +29,23 @@ fn build_utf16_string(data: Vec) -> 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> { 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> { let text_u16 = text.into_utf16()?; @@ -734,7 +741,7 @@ pub fn wrap_text_with_ansi(text: JsString, width: u32) -> Result, @@ -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 { let text_u16 = text.into_utf16()?; diff --git a/packages/coding-agent/src/exec/bash-executor.ts b/packages/coding-agent/src/exec/bash-executor.ts index 0f5a53896..e48c63c64 100644 --- a/packages/coding-agent/src/exec/bash-executor.ts +++ b/packages/coding-agent/src/exec/bash-executor.ts @@ -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(); + export async function executeBash(command: string, options?: BashExecutorOptions): Promise { 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); } } diff --git a/packages/coding-agent/src/utils/image-convert.ts b/packages/coding-agent/src/utils/image-convert.ts index b129beb06..e8eb83615 100644 --- a/packages/coding-agent/src/utils/image-convert.ts +++ b/packages/coding-agent/src/utils/image-convert.ts @@ -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. diff --git a/packages/coding-agent/src/utils/image-resize.ts b/packages/coding-agent/src/utils/image-resize.ts index c460b86c0..aff3c0c29 100644 --- a/packages/coding-agent/src/utils/image-resize.ts +++ b/packages/coding-agent/src/utils/image-resize.ts @@ -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 diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index bfe64e229..5b21a0108 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -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 diff --git a/packages/natives/src/bindings.ts b/packages/natives/src/bindings.ts new file mode 100644 index 000000000..12bbddf36 --- /dev/null +++ b/packages/natives/src/bindings.ts @@ -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 = (error: Error | null, value: T) => void; + +/** + * Native bindings interface. + * Extended by each module via declaration merging. + */ +export interface NativeBindings {} diff --git a/packages/natives/src/clipboard/index.ts b/packages/natives/src/clipboard/index.ts index cdc1bdff1..7e862593c 100644 --- a/packages/natives/src/clipboard/index.ts +++ b/packages/natives/src/clipboard/index.ts @@ -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 { - await native.copyToClipboard(text); -} - -/** - * Read an image from the clipboard, if available. - */ -export async function readImageFromClipboard(): Promise { - return native.readImageFromClipboard(); -} +export const { copyToClipboard, readImageFromClipboard } = native; diff --git a/packages/natives/src/clipboard/types.ts b/packages/natives/src/clipboard/types.ts index 2d3f2454e..7cadfd264 100644 --- a/packages/natives/src/clipboard/types.ts +++ b/packages/natives/src/clipboard/types.ts @@ -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; + /** + * Read an image from the clipboard. + * @returns PNG payload or null when no image is available. + */ + readImageFromClipboard(): Promise; + } +} diff --git a/packages/natives/src/find/index.ts b/packages/natives/src/find/index.ts new file mode 100644 index 000000000..013f726d4 --- /dev/null +++ b/packages/natives/src/find/index.ts @@ -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 { + 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, + ); +} diff --git a/packages/natives/src/find/types.ts b/packages/natives/src/find/types.ts index d5785fa87..c5bc44ac5 100644 --- a/packages/natives/src/find/types.ts +++ b/packages/natives/src/find/types.ts @@ -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): Promise; + } +} diff --git a/packages/natives/src/grep/types.ts b/packages/natives/src/grep/types.ts index fe0727d18..34e9e988e 100644 --- a/packages/natives/src/grep/types.ts +++ b/packages/natives/src/grep/types.ts @@ -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; + /** Search files for a regex pattern. */ + grep(options: GrepOptions, onMatch?: TsFunc): Promise; + /** 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; + } +} diff --git a/packages/natives/src/highlight/index.ts b/packages/natives/src/highlight/index.ts index 09cf7bd0b..0a0f99dbf 100644 --- a/packages/natives/src/highlight/index.ts +++ b/packages/natives/src/highlight/index.ts @@ -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; diff --git a/packages/natives/src/highlight/types.ts b/packages/natives/src/highlight/types.ts new file mode 100644 index 000000000..ad7ac6593 --- /dev/null +++ b/packages/natives/src/highlight/types.ts @@ -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[]; + } +} diff --git a/packages/natives/src/html/types.ts b/packages/natives/src/html/types.ts index a3166df2e..f3c69677a 100644 --- a/packages/natives/src/html/types.ts +++ b/packages/natives/src/html/types.ts @@ -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; + } +} diff --git a/packages/natives/src/image/index.ts b/packages/natives/src/image/index.ts index caf572769..4abaea416 100644 --- a/packages/natives/src/image/index.ts +++ b/packages/natives/src/image/index.ts @@ -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; diff --git a/packages/natives/src/image/types.ts b/packages/natives/src/image/types.ts new file mode 100644 index 000000000..2b4522148 --- /dev/null +++ b/packages/natives/src/image/types.ts @@ -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; + /** + * Resize the image to the requested dimensions with the filter. + * Returns a new image instance. + */ + resize(width: number, height: number, filter: SamplingFilter): Promise; +} + +/** Static entrypoints for creating `PhotonImage` instances. */ +export interface PhotonImageConstructor { + /** Parse image bytes (PNG, JPEG, WebP, GIF) into a native image. */ + parse(bytes: Uint8Array): Promise; + /** 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; + } +} diff --git a/packages/natives/src/index.ts b/packages/natives/src/index.ts index bb9f2a37e..27c2e765b 100644 --- a/packages/natives/src/index.ts +++ b/packages/natives/src/index.ts @@ -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 { - 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"; diff --git a/packages/natives/src/keys/index.ts b/packages/natives/src/keys/index.ts index 8d99fe080..cc96254fb 100644 --- a/packages/natives/src/keys/index.ts +++ b/packages/natives/src/keys/index.ts @@ -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; diff --git a/packages/natives/src/keys/types.ts b/packages/natives/src/keys/types.ts new file mode 100644 index 000000000..8accd2cf4 --- /dev/null +++ b/packages/natives/src/keys/types.ts @@ -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; + } +} diff --git a/packages/natives/src/native.ts b/packages/natives/src/native.ts index 9bb055bb2..75277c4b1 100644 --- a/packages/natives/src/native.ts +++ b/packages/natives/src/native.ts @@ -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; - resize(width: number, height: number, filter: number): Promise; -} - -export interface PhotonImageConstructor { - parse(bytes: Uint8Array): Promise; - 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 = (error: Error | null, value: T) => void; - -export interface NativeBindings { - copyToClipboard(text: string): Promise; - readImageFromClipboard(): Promise; - find(options: FindOptions, onMatch?: TsFunc): Promise; - fuzzyFind(options: FuzzyFindOptions): Promise; - grep(options: GrepOptions, onMatch?: TsFunc): Promise; - 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; - 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): Promise; - 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"); diff --git a/packages/natives/src/ps/index.ts b/packages/natives/src/ps/index.ts new file mode 100644 index 000000000..88544b7b7 --- /dev/null +++ b/packages/natives/src/ps/index.ts @@ -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; diff --git a/packages/natives/src/ps/types.ts b/packages/natives/src/ps/types.ts new file mode 100644 index 000000000..a4b9af0b3 --- /dev/null +++ b/packages/natives/src/ps/types.ts @@ -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[]; + } +} diff --git a/packages/natives/src/shell/index.ts b/packages/natives/src/shell/index.ts index 12988eabc..c805ed225 100644 --- a/packages/natives/src/shell/index.ts +++ b/packages/natives/src/shell/index.ts @@ -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; + 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 { - // 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); } diff --git a/packages/natives/src/shell/types.ts b/packages/natives/src/shell/types.ts index 28b0a9ccd..4f70f05d9 100644 --- a/packages/natives/src/shell/types.ts +++ b/packages/natives/src/shell/types.ts @@ -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; - /** 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; - /** 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; + /** 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; + /** Environment variables to set once per session. */ + sessionEnv?: Record; + /** 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): Promise; + /** + * 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): Promise; + /** + * 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; + } +} diff --git a/packages/natives/src/system-info/index.ts b/packages/natives/src/system-info/index.ts index d902685cc..d582b7f99 100644 --- a/packages/natives/src/system-info/index.ts +++ b/packages/natives/src/system-info/index.ts @@ -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; diff --git a/packages/natives/src/system-info/types.ts b/packages/natives/src/system-info/types.ts new file mode 100644 index 000000000..1a0bb2869 --- /dev/null +++ b/packages/natives/src/system-info/types.ts @@ -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; + } +} diff --git a/packages/natives/src/text/index.ts b/packages/natives/src/text/index.ts index 93bc07a76..73d1df1ea 100644 --- a/packages/natives/src/text/index.ts +++ b/packages/natives/src/text/index.ts @@ -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; diff --git a/packages/natives/src/text/types.ts b/packages/natives/src/text/types.ts new file mode 100644 index 000000000..4501a0324 --- /dev/null +++ b/packages/natives/src/text/types.ts @@ -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; + } +}