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:
@@ -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")]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
@@ -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[];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user