feat(deps): upgraded brush-core to 0.4.0 and refactored shell execution with new types

- Upgraded brush-core from 0.3.5 to 0.4.0 and added brush-builtins 0.1.0 dependency to support new builtin command framework.
- Refactored shell execution to use new ExecutionResult and ExecutionControlFlow types from brush-core 0.4.0, replacing previous ExitCode-based approach.
- Updated system_info disk refresh to use new sysinfo 0.38 API with single refresh(true) call instead of separate refresh_list() and refresh() calls.
- Added tokio full-featured runtime dependency to support async operations in pi-natives.
- Updated phf from 0.11 to 0.13 with macros feature for improved perfect hash function support.
- Fixed UTF-16 string handling in text.rs to properly strip trailing null bytes before conversion.
This commit is contained in:
can1357
2026-02-01 16:51:06 +01:00
parent 89df02edbc
commit feb3a51d1e
9 changed files with 713 additions and 264 deletions
Generated
+368 -145
View File
@@ -217,13 +217,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "brush-core"
version = "0.3.5"
name = "bon"
version = "3.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb3a9488ef37cd8b951748d861362af5b1d5a815ea325f83c1c4bbb7da5e328"
checksum = "234655ec178edd82b891e262ea7cf71f6584bcd09eff94db786be23f1821825c"
dependencies = [
"bon-macros",
"rustversion",
]
[[package]]
name = "bon-macros"
version = "3.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ec27229c38ed0eb3c0feee3d2c1d6a4379ae44f418a29a658890e062d8f365"
dependencies = [
"darling",
"ident_case",
"prettyplease",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "brush-builtins"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "454219f460ee4308ae5b8befc47936b4ef7ac3a94347052270e59b6c6cfe14ec"
dependencies = [
"brush-core",
"brush-parser",
"cfg-if",
"chrono",
"clap",
"fancy-regex",
"futures",
"itertools",
"nix 0.30.1",
"procfs",
"rlimit",
"strum",
"strum_macros",
"thiserror",
"tokio",
"tracing",
"uucore",
]
[[package]]
name = "brush-core"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a3ad2f2d4eb45ef7f11e74bc1a0816c8f3dce298072352c6b3b41a2c4bfdb74"
dependencies = [
"async-recursion",
"async-trait",
"bon",
"brush-parser",
"cached",
"cfg-if",
@@ -237,12 +288,9 @@ dependencies = [
"hostname",
"indexmap",
"itertools",
"nix",
"nix 0.30.1",
"normalize-path",
"os_pipe",
"procfs",
"rand 0.9.2",
"rlimit",
"rpds",
"strum",
"strum_macros",
@@ -250,7 +298,6 @@ dependencies = [
"thiserror",
"tokio",
"tracing",
"uucore",
"uuid",
"uzers",
"whoami",
@@ -258,10 +305,11 @@ dependencies = [
[[package]]
name = "brush-parser"
version = "0.2.20"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6924d0efd702b4fe3d65f3a63c03520edfe53b67d0955b9d3f8e66b192a410ff"
checksum = "f7367124d4f38fdcd65f4b815bda7caeb3de377b9cd95ffa1b23627989c93718"
dependencies = [
"bon",
"cached",
"indenter",
"peg",
@@ -370,7 +418,7 @@ dependencies = [
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link 0.2.1",
"windows-link",
]
[[package]]
@@ -441,7 +489,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f849b92c694fe237ecd8fafd1ba0df7ae0d45c1df6daeb7f68ed4220d51640bd"
dependencies = [
"nix",
"nix 0.30.1",
"thiserror",
]
@@ -863,8 +911,8 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
"rustix 1.1.3",
"windows-link 0.2.1",
"rustix",
"windows-link",
]
[[package]]
@@ -994,14 +1042,14 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "homedir"
version = "0.3.6"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68df315d2857b2d8d2898be54a85e1d001bbbe0dbb5f8ef847b48dd3a23c4527"
checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2"
dependencies = [
"cfg-if",
"nix",
"nix 0.29.0",
"widestring",
"windows 0.61.3",
"windows 0.57.0",
]
[[package]]
@@ -1012,7 +1060,7 @@ checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
dependencies = [
"cfg-if",
"libc",
"windows-link 0.2.1",
"windows-link",
]
[[package]]
@@ -1076,6 +1124,71 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
dependencies = [
"displaydoc",
"potential_utf",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60"
dependencies = [
"icu_collections",
"icu_locale_core",
"icu_locale_data",
"icu_provider",
"potential_utf",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
dependencies = [
"displaydoc",
"litemap",
"serde",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locale_data"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831"
[[package]]
name = "icu_provider"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
dependencies = [
"displaydoc",
"icu_locale_core",
"serde",
"stable_deref_trait",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -1200,7 +1313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
dependencies = [
"cfg-if",
"windows-link 0.2.1",
"windows-link",
]
[[package]]
@@ -1220,18 +1333,18 @@ dependencies = [
"redox_syscall 0.7.0",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -1401,6 +1514,18 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nix"
version = "0.30.1"
@@ -1540,6 +1665,16 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.2"
@@ -1602,7 +1737,7 @@ dependencies = [
"libc",
"redox_syscall 0.5.18",
"smallvec",
"windows-link 0.2.1",
"windows-link",
]
[[package]]
@@ -1644,7 +1779,6 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_macros",
"phf_shared 0.11.3",
]
@@ -1654,6 +1788,7 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
"phf_macros",
"phf_shared 0.13.1",
"serde",
]
@@ -1700,12 +1835,12 @@ dependencies = [
[[package]]
name = "phf_macros"
version = "0.11.3"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
"phf_generator 0.13.1",
"phf_shared 0.13.1",
"proc-macro2",
"quote",
"syn",
@@ -1734,6 +1869,7 @@ name = "pi-natives"
version = "9.7.0"
dependencies = [
"arboard",
"brush-builtins",
"brush-core",
"bstr",
"clap",
@@ -1750,10 +1886,11 @@ dependencies = [
"napi-derive",
"os_pipe",
"parking_lot",
"phf 0.11.3",
"phf 0.13.1",
"rayon",
"syntect",
"sysinfo",
"tokio",
"unicode-segmentation",
"unicode-width",
]
@@ -1783,6 +1920,17 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "potential_utf"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
dependencies = [
"serde_core",
"writeable",
"zerovec",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1798,6 +1946,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -1809,23 +1967,22 @@ dependencies = [
[[package]]
name = "procfs"
version = "0.17.0"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f"
checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7"
dependencies = [
"bitflags",
"chrono",
"flate2",
"hex",
"procfs-core",
"rustix 0.38.44",
"rustix",
]
[[package]]
name = "procfs-core"
version = "0.17.0"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec"
checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405"
dependencies = [
"bitflags",
"chrono",
@@ -1997,19 +2154,6 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.1.3"
@@ -2019,7 +2163,7 @@ dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys 0.11.0",
"linux-raw-sys",
"windows-sys 0.61.2",
]
@@ -2126,6 +2270,22 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "string_cache"
version = "0.9.0"
@@ -2185,6 +2345,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syntect"
version = "5.3.0"
@@ -2205,17 +2376,16 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.30.13"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
checksum = "fe840c5b1afe259a5657392a4dbb74473a14c8db999c3ec2f4ae812e028a94da"
dependencies = [
"cfg-if",
"core-foundation-sys",
"libc",
"memchr",
"ntapi",
"once_cell",
"rayon",
"windows 0.52.0",
"objc2-core-foundation",
"objc2-io-kit",
"windows 0.62.2",
]
[[package]]
@@ -2235,7 +2405,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
dependencies = [
"rustix 1.1.3",
"rustix",
"windows-sys 0.60.2",
]
@@ -2305,8 +2475,10 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
@@ -2433,20 +2605,25 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uucore"
version = "0.1.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9032bf981784f22fcc5ddc7e74b7cf3bae3d5f44a48d2054138ed38068b9f4e0"
checksum = "2003164a38a7f39da1de103a70fa66b745f572f0045ec261481539516c0a8a0e"
dependencies = [
"bigdecimal",
"bstr",
"clap",
"fluent",
"fluent-bundle",
"fluent-syntax",
"glob",
"icu_locale",
"itertools",
"nix",
"nix 0.30.1",
"num-traits",
"number_prefix",
"os_display",
"phf 0.13.1",
"procfs",
"thiserror",
"unic-langid",
"uucore_procs",
@@ -2455,21 +2632,14 @@ dependencies = [
[[package]]
name = "uucore_procs"
version = "0.1.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c933945fdac5b7779eae1fc746146e61f5b0298deb6ede002ce0b6e93e1b3bfc"
checksum = "c76f0308f7810d915246a39748e7f5d64e43e6bb9d6c8107224f9d741aefc375"
dependencies = [
"proc-macro2",
"quote",
"uuhelp_parser",
]
[[package]]
name = "uuhelp_parser"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beda381dd5c7927f8682f50b055b0903bb694ba5a4b27fad1b4934bc4fbf7b8d"
[[package]]
name = "uuid"
version = "1.20.0"
@@ -2669,82 +2839,82 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.52.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.52.0",
"windows-core 0.57.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.3"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core 0.61.2",
"windows-core 0.62.2",
"windows-future",
"windows-link 0.1.3",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core 0.61.2",
"windows-core 0.62.2",
]
[[package]]
name = "windows-core"
version = "0.52.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement 0.57.0",
"windows-interface 0.57.0",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.2.1",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
"windows-link",
"windows-result 0.4.1",
"windows-strings 0.5.1",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.2.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
"windows-core 0.62.2",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
@@ -2756,6 +2926,17 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
@@ -2767,12 +2948,6 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.1"
@@ -2781,21 +2956,21 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.2.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
"windows-core 0.62.2",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.3.4"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-link 0.1.3",
"windows-targets 0.52.6",
]
[[package]]
@@ -2804,16 +2979,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link 0.1.3",
"windows-link",
]
[[package]]
@@ -2822,16 +2988,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
"windows-link",
]
[[package]]
@@ -2849,7 +3006,7 @@ version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link 0.2.1",
"windows-link",
]
[[package]]
@@ -2874,7 +3031,7 @@ version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link 0.2.1",
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
@@ -2887,11 +3044,11 @@ dependencies = [
[[package]]
name = "windows-threading"
version = "0.1.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link 0.1.3",
"windows-link",
]
[[package]]
@@ -2996,6 +3153,12 @@ version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "writeable"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "x11rb"
version = "0.13.2"
@@ -3003,7 +3166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
"rustix 1.1.3",
"rustix",
"x11rb-protocol",
]
@@ -3023,6 +3186,29 @@ dependencies = [
"markup5ever",
]
[[package]]
name = "yoke"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
dependencies = [
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.8.37"
@@ -3048,6 +3234,30 @@ name = "zerofrom"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerotrie"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
dependencies = [
"displaydoc",
]
[[package]]
name = "zerovec"
@@ -3056,7 +3266,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
dependencies = [
"serde",
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
+5 -3
View File
@@ -13,13 +13,15 @@ crate-type = ["cdylib"]
workspace = true
[dependencies]
tokio = { version = "1", features = ["full"] }
napi = { version = "3", features = ["napi10", "tokio_rt", "tokio_time"] }
napi-derive = "3"
brush-core = "0.3.5"
brush-core = "0.4.0"
brush-builtins = "0.1.0"
parking_lot = "0.12.5"
clap = { version = "4", features = ["derive"] }
os_pipe = "1"
sysinfo = "0.30"
sysinfo = "0.38"
grep-regex = "0.1"
grep-searcher = "0.1"
grep-matcher = "0.1"
@@ -42,7 +44,7 @@ syntect = { version = "5.3", default-features = false, features = [
"regex-fancy",
] }
html-to-markdown-rs = { version = "2.24", default-features = false }
phf = { version = "0.11", features = ["macros"] }
phf = { version = "0.13", features = ["macros"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
+57 -35
View File
@@ -24,9 +24,12 @@ use std::{
};
use brush_core::{
CreateOptions, ExecutionContext, OpenFile, OpenFiles, ProcessGroupPolicy, Shell as BrushShell,
ShellValue, ShellVariable, builtins, env::EnvironmentScope,
CreateOptions, ExecutionContext, ExecutionControlFlow, ExecutionExitCode, ExecutionResult,
ProcessGroupPolicy, Shell as BrushShell, ShellValue, ShellVariable, builtins,
env::EnvironmentScope,
openfiles::{self, OpenFile, OpenFiles},
};
use brush_builtins::{BuiltinSet, default_builtins};
use clap::Parser;
use napi::{
bindgen_prelude::*,
@@ -357,7 +360,25 @@ async fn execute_shell_with_options(
remove_session(&options.session_key);
}
Ok(ShellExecuteResult { exit_code: Some(i32::from(run_result.exit_code)), cancelled, timed_out })
Ok(ShellExecuteResult { exit_code: exit_code(&run_result), cancelled, timed_out })
}
fn null_file() -> Result<OpenFile> {
Ok(openfiles::null()
.map_err(|err| Error::from_reason(format!("Failed to create null file: {err}")))?)
}
fn exit_code(result: &ExecutionResult) -> Option<i32> {
match result.exit_code {
ExecutionExitCode::Success => Some(0),
ExecutionExitCode::GeneralError => Some(1),
ExecutionExitCode::InvalidUsage => Some(2),
ExecutionExitCode::Unimplemented => Some(99),
ExecutionExitCode::CannotExecute => Some(126),
ExecutionExitCode::NotFound => Some(127),
ExecutionExitCode::Interrupted => Some(130),
ExecutionExitCode::Custom(code) => Some(code as i32),
}
}
/// Abort a running shell execution by ID.
@@ -397,17 +418,18 @@ async fn create_session(options: &ShellExecuteOptions) -> Result<ShellSession> {
no_profile: true,
no_rc: true,
do_not_inherit_env: true,
builtins: default_builtins(BuiltinSet::BashMode),
..Default::default()
};
let mut shell = BrushShell::new(&create_options)
let mut shell = BrushShell::new(create_options)
.await
.map_err(|err| Error::from_reason(format!("Failed to initialize shell: {err}")))?;
if let Some(exec_builtin) = shell.builtins.get_mut("exec") {
if let Some(exec_builtin) = shell.builtin_mut("exec") {
exec_builtin.disabled = true;
}
if let Some(suspend_builtin) = shell.builtins.get_mut("suspend") {
if let Some(suspend_builtin) = shell.builtin_mut("suspend") {
suspend_builtin.disabled = true;
}
shell.register_builtin("sleep", builtins::builtin::<SleepCommand>());
@@ -436,11 +458,9 @@ async fn create_session(options: &ShellExecuteOptions) -> Result<ShellSession> {
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);
open_files.set(OpenFiles::STDOUT_FD, OpenFile::Null);
open_files.set(OpenFiles::STDERR_FD, OpenFile::Null);
params.open_files = open_files;
params.set_fd(OpenFiles::STDIN_FD, null_file()?);
params.set_fd(OpenFiles::STDOUT_FD, null_file()?);
params.set_fd(OpenFiles::STDERR_FD, null_file()?);
let escaped = snapshot_path.replace('\'', "'\\''");
let command = format!("source '{escaped}'");
@@ -455,7 +475,7 @@ async fn run_shell_command(
session: &mut ShellSession,
options: &ShellExecuteOptions,
on_chunk: Option<ThreadsafeFunction<String>>,
) -> Result<brush_core::ExecutionResult> {
) -> Result<ExecutionResult> {
if let Some(cwd) = options.cwd.as_deref() {
session
.shell
@@ -472,13 +492,10 @@ async fn run_shell_command(
);
let stderr_file = OpenFile::from(writer_file);
let mut open_files = session.shell.open_files.clone();
open_files.set(OpenFiles::STDIN_FD, OpenFile::Null);
open_files.set(OpenFiles::STDOUT_FD, stdout_file);
open_files.set(OpenFiles::STDERR_FD, stderr_file);
let mut params = session.shell.default_exec_params();
params.open_files = open_files;
params.set_fd(OpenFiles::STDIN_FD, null_file()?);
params.set_fd(OpenFiles::STDOUT_FD, stdout_file);
params.set_fd(OpenFiles::STDERR_FD, stderr_file);
params.process_group_policy = ProcessGroupPolicy::NewProcessGroup;
if let Some(env) = options.env.as_ref() {
@@ -516,7 +533,7 @@ async fn run_shell_command(
drop(params);
let _ = reader_handle.wait().await;
let _: Result<()> = reader_handle.wait().await;
result.map_err(|err| Error::from_reason(format!("Shell execution failed: {err}")))
}
@@ -589,11 +606,14 @@ fn next_execution_id() -> String {
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
|| result.break_loop.is_some()
|| result.continue_loop.is_some()
const fn should_reset_session(result: &ExecutionResult) -> bool {
match result.next_control_flow {
ExecutionControlFlow::Normal => false,
ExecutionControlFlow::BreakLoop { .. } => true,
ExecutionControlFlow::ContinueLoop { .. } => true,
ExecutionControlFlow::ReturnFromFunctionOrScript => true,
ExecutionControlFlow::ExitShell => true,
}
}
fn remove_session(session_key: &str) {
@@ -762,23 +782,24 @@ struct SleepCommand {
}
impl builtins::Command for SleepCommand {
type Error = brush_core::Error;
fn execute(
&self,
context: ExecutionContext<'_>,
) -> impl std::future::Future<Output = std::result::Result<builtins::ExitCode, brush_core::Error>>
+ std::marker::Send {
) -> impl Future<Output = std::result::Result<ExecutionResult, brush_core::Error>> + Send {
let durations = self.durations.clone();
async move {
let mut total = Duration::from_millis(0);
for duration in &durations {
let Some(parsed) = parse_duration(duration) else {
let _ = writeln!(context.stderr(), "sleep: invalid time interval '{duration}'");
return Ok(builtins::ExitCode::Custom(1));
return Ok(ExecutionResult::new(1));
};
total += parsed;
}
time::sleep(total).await;
Ok(builtins::ExitCode::Success)
Ok(ExecutionResult::success())
}
}
}
@@ -793,21 +814,22 @@ struct TimeoutCommand {
}
impl builtins::Command for TimeoutCommand {
type Error = brush_core::Error;
fn execute(
&self,
context: ExecutionContext<'_>,
) -> impl std::future::Future<Output = std::result::Result<builtins::ExitCode, brush_core::Error>>
+ std::marker::Send {
) -> impl Future<Output = std::result::Result<ExecutionResult, brush_core::Error>> + Send {
let duration = self.duration.clone();
let command = self.command.clone();
async move {
let Some(timeout) = parse_duration(&duration) else {
let _ = writeln!(context.stderr(), "timeout: invalid time interval '{duration}'");
return Ok(builtins::ExitCode::Custom(125));
return Ok(ExecutionResult::new(125));
};
if command.is_empty() {
let _ = writeln!(context.stderr(), "timeout: missing command");
return Ok(builtins::ExitCode::Custom(125));
return Ok(ExecutionResult::new(125));
}
let mut params = context.params.clone();
@@ -844,18 +866,18 @@ impl builtins::Command for TimeoutCommand {
let _ = time::timeout(Duration::from_millis(1500), &mut run_future).await;
tracker_done.store(true, Ordering::Release);
let _ = tracker_handle.await;
return Ok(builtins::ExitCode::Custom(124));
return Ok(ExecutionResult::new(124));
}
tracker_done.store(true, Ordering::Release);
let _ = tracker_handle.await;
if timed_out {
return Ok(builtins::ExitCode::Custom(124));
return Ok(ExecutionResult::new(124));
}
let result = result.expect("result ensured")?;
Ok(builtins::ExitCode::from(result))
Ok(result)
}
}
}
+1 -2
View File
@@ -100,8 +100,7 @@ fn strip_quotes(value: &str) -> String {
fn get_disk_info() -> Option<String> {
let mut disks = Disks::new_with_refreshed_list();
disks.refresh_list();
disks.refresh();
disks.refresh(true);
if cfg!(target_os = "windows") {
let mut entries = Vec::new();
+12 -5
View File
@@ -18,8 +18,11 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
const TAB_WIDTH: usize = 3;
const ESC: u16 = 0x1b;
fn build_utf16_string(data: Vec<u16>) -> Utf16String {
fn build_utf16_string(mut data: Vec<u16>) -> Utf16String {
// SAFETY: we know Utf16String == struct(Vec<u16>)
while data.last() == Some(&0) {
data.pop();
}
unsafe { std::mem::transmute(data) }
}
@@ -725,7 +728,8 @@ 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")]
@@ -741,7 +745,8 @@ 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); pads with spaces when requested.
/// `ellipsis_kind`: 0 = "…", 1 = "...", 2 = "" (omit); pads with spaces when
/// requested.
#[napi(js_name = "truncateToWidth")]
pub fn truncate_to_width(
text: JsString<'_>,
@@ -1000,7 +1005,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.
/// Counts terminal cells, skipping ANSI escapes, and optionally enforces strict
/// width.
#[napi(js_name = "sliceWithWidth")]
pub fn slice_with_width(
line: JsString,
@@ -1155,7 +1161,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.
/// Preserves ANSI state so the `after` segment renders correctly after
/// truncation.
#[napi(js_name = "extractSegments")]
pub fn extract_segments(
line: JsString,
+2 -1
View File
@@ -424,7 +424,8 @@ export async function executePython(code: string, options?: PythonExecutorOption
await ensureKernelAvailable(cwd);
const kernelMode = options?.kernelMode ?? "session";
const useSharedGateway = options?.useSharedGateway;
const isTestEnv = process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test";
const useSharedGateway = isTestEnv ? false : options?.useSharedGateway;
const sessionFile = options?.sessionFile;
const artifactsDir = options?.artifactsDir;
+250 -47
View File
@@ -1,11 +1,24 @@
import { describe, expect, it } from "bun:test";
import { Markdown } from "@oh-my-pi/pi-tui/components/markdown";
import type { Terminal as XtermTerminalType } from "@xterm/headless";
import { Chalk } from "chalk";
import { defaultMarkdownTheme } from "./test-themes";
import { Markdown } from "../src/components/markdown.js";
import { type Component, TUI } from "../src/tui.js";
import { defaultMarkdownTheme } from "./test-themes.js";
import { VirtualTerminal } from "./virtual-terminal.js";
// Force full color in CI so ANSI assertions are deterministic
const chalk = new Chalk({ level: 3 });
function getCellItalic(terminal: VirtualTerminal, row: number, col: number): number {
const xterm = (terminal as unknown as { xterm: XtermTerminalType }).xterm;
const buffer = xterm.buffer.active;
const line = buffer.getLine(buffer.viewportY + row);
expect(line, `Missing buffer line at row ${row}`).toBeTruthy();
const cell = line!.getCell(col);
expect(cell, `Missing cell at row ${row} col ${col}`).toBeTruthy();
return cell!.isItalic();
}
describe("Markdown component", () => {
describe("Nested lists", () => {
it("should render simple nested list", () => {
@@ -94,6 +107,43 @@ describe("Markdown component", () => {
expect(plainLines.some(line => line.includes(" - Unordered nested"))).toBeTruthy();
expect(plainLines.some(line => line.includes("2. Second ordered"))).toBeTruthy();
});
it("should maintain numbering when code blocks are not indented (LLM output)", () => {
// When code blocks aren't indented, marked parses each item as a separate list.
// We use token.start to preserve the original numbering.
const markdown = new Markdown(
`1. First item
\`\`\`typescript
// code block
\`\`\`
2. Second item
\`\`\`typescript
// another code block
\`\`\`
3. Third item`,
0,
0,
defaultMarkdownTheme,
);
const lines = markdown.render(80);
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, "").trim());
// Find all lines that start with a number and period
const numberedLines = plainLines.filter(line => /^\d+\./.test(line));
// Should have 3 numbered items
expect(numberedLines.length, `Expected 3 numbered items, got: ${numberedLines.join(", ")}`).toBe(3);
// Check the actual numbers
expect(numberedLines[0].startsWith("1."), `First item should be "1.", got: ${numberedLines[0]}`).toBeTruthy();
expect(numberedLines[1].startsWith("2."), `Second item should be "2.", got: ${numberedLines[1]}`).toBeTruthy();
expect(numberedLines[2].startsWith("3."), `Third item should be "3.", got: ${numberedLines[2]}`).toBeTruthy();
});
});
describe("Tables", () => {
@@ -121,6 +171,52 @@ describe("Markdown component", () => {
expect(plainLines.some(line => line.includes("-"))).toBeTruthy();
});
it("should render row dividers between data rows", () => {
const markdown = new Markdown(
`| Name | Age |
| --- | --- |
| Alice | 30 |
| Bob | 25 |`,
0,
0,
defaultMarkdownTheme,
);
const lines = markdown.render(80);
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, ""));
const dividerLines = plainLines.filter(line => line.includes("+"));
expect(dividerLines.length >= 2, "Expected header + row divider").toBeTruthy();
});
it("should keep column width at least the longest word", () => {
const longestWord = "superlongword";
const markdown = new Markdown(
`| Column One | Column Two |
| --- | --- |
| ${longestWord} short | otherword |
| small | tiny |`,
0,
0,
defaultMarkdownTheme,
);
const lines = markdown.render(32);
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, ""));
const dataLine = plainLines.find(line => line.includes(longestWord));
expect(dataLine, "Expected data row containing longest word").toBeTruthy();
const segments = dataLine!.split("|").slice(1, -1);
const [firstSegment] = segments;
expect(firstSegment, "Expected first column segment").toBeTruthy();
const firstColumnWidth = firstSegment.length - 2;
expect(
firstColumnWidth >= longestWord.length,
`Expected first column width >= ${longestWord.length}, got ${firstColumnWidth}`,
).toBeTruthy();
});
it("should render table with alignment", () => {
const markdown = new Markdown(
`| Left | Center | Right |
@@ -181,15 +277,15 @@ describe("Markdown component", () => {
// All lines should fit within width
for (const line of plainLines) {
expect(line.length <= 50, `Line exceeds width 50: "${line}" (length: ${line.length}).toBeTruthy()`);
expect(line.length <= 50, `Line exceeds width 50: "${line}" (length: ${line.length})`).toBeTruthy();
}
// Content should still be present (possibly wrapped across lines)
const allText = plainLines.join(" ");
expect(allText.includes("Command")).toBeTruthy();
expect(allText.includes("Description")).toBeTruthy();
expect(allText.includes("npm install")).toBeTruthy();
expect(allText.includes("Install")).toBeTruthy();
expect(allText.includes("Command"), "Should contain 'Command'").toBeTruthy();
expect(allText.includes("Description"), "Should contain 'Description'").toBeTruthy();
expect(allText.includes("npm install"), "Should contain 'npm install'").toBeTruthy();
expect(allText.includes("Install"), "Should contain 'Install'").toBeTruthy();
});
it("should wrap long cell content to multiple lines", () => {
@@ -208,13 +304,13 @@ describe("Markdown component", () => {
// Should have multiple data rows due to wrapping
const dataRows = plainLines.filter(line => line.startsWith("|") && !line.includes("-"));
expect(dataRows.length > 2).toBeTruthy();
expect(dataRows.length > 2, `Expected wrapped rows, got ${dataRows.length} rows`).toBeTruthy();
// All content should be preserved (may be split across lines)
const allText = plainLines.join(" ");
expect(allText.includes("very long")).toBeTruthy();
expect(allText.includes("cell content")).toBeTruthy();
expect(allText.includes("should wrap")).toBeTruthy();
expect(allText.includes("very long"), "Should preserve 'very long'").toBeTruthy();
expect(allText.includes("cell content"), "Should preserve 'cell content'").toBeTruthy();
expect(allText.includes("should wrap"), "Should preserve 'should wrap'").toBeTruthy();
});
it("should wrap long unbroken tokens inside table cells (not only at line start)", () => {
@@ -235,22 +331,23 @@ describe("Markdown component", () => {
for (const line of plainLines) {
expect(
line.length <= width,
`Line exceeds width ${width}: "${line}" (length: ${line.length}).toBeTruthy()`,
);
`Line exceeds width ${width}: "${line}" (length: ${line.length})`,
).toBeTruthy();
}
// Borders should stay intact (exactly 2 vertical borders for a 1-col table)
const tableLines = plainLines.filter(line => line.startsWith("|"));
expect(tableLines.length > 0, "Expected table rows to render").toBeTruthy();
for (const line of tableLines) {
const borderCount = line.split("|").length - 1;
expect(borderCount).toBe(2);
expect(borderCount, `Expected 2 borders, got ${borderCount}: "${line}"`).toBe(2);
}
// Strip box drawing characters + whitespace so we can assert the URL is preserved
// even if it was split across multiple wrapped lines.
const extracted = plainLines.join("").replace(/[|++-\s]/g, "");
expect(extracted.includes("prefix")).toBeTruthy();
expect(extracted.includes(url)).toBeTruthy();
const extracted = plainLines.join("").replace(/[|+\-\s]/g, "");
expect(extracted.includes("prefix"), "Should preserve 'prefix'").toBeTruthy();
expect(extracted.includes(url), "Should preserve URL").toBeTruthy();
});
it("should wrap styled inline code inside table cells without breaking borders", () => {
@@ -266,20 +363,20 @@ describe("Markdown component", () => {
const width = 20;
const lines = markdown.render(width);
const joinedOutput = lines.join("\n");
expect(joinedOutput.includes("\x1b[33m")).toBeTruthy();
expect(joinedOutput.includes("\x1b[33m"), "Inline code should be styled (yellow)").toBeTruthy();
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd());
for (const line of plainLines) {
expect(
line.length <= width,
`Line exceeds width ${width}: "${line}" (length: ${line.length}).toBeTruthy()`,
);
`Line exceeds width ${width}: "${line}" (length: ${line.length})`,
).toBeTruthy();
}
const tableLines = plainLines.filter(line => line.startsWith("|"));
for (const line of tableLines) {
const borderCount = line.split("|").length - 1;
expect(borderCount).toBe(2);
expect(borderCount, `Expected 2 borders, got ${borderCount}: "${line}"`).toBe(2);
}
});
@@ -298,11 +395,11 @@ describe("Markdown component", () => {
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd());
// Should not crash and should produce output
expect(lines.length > 0).toBeTruthy();
expect(lines.length > 0, "Should produce output").toBeTruthy();
// Lines should not exceed width
for (const line of plainLines) {
expect(line.length <= 15, `Line exceeds width 15: "${line}" (length: ${line.length}).toBeTruthy()`);
expect(line.length <= 15, `Line exceeds width 15: "${line}" (length: ${line.length})`).toBeTruthy();
}
});
@@ -322,14 +419,14 @@ describe("Markdown component", () => {
// Should have proper table structure
const headerLine = plainLines.find(line => line.includes("A") && line.includes("B"));
expect(headerLine).toBeTruthy();
expect(headerLine?.includes("|")).toBeTruthy();
expect(headerLine, "Should have header row").toBeTruthy();
expect(headerLine?.includes("|"), "Header should have borders").toBeTruthy();
const separatorLine = plainLines.find(line => line.includes("+") && line.includes("+"));
expect(separatorLine).toBeTruthy();
const separatorLine = plainLines.find(line => line.includes("+") && line.includes("-"));
expect(separatorLine, "Should have separator row").toBeTruthy();
const dataLine = plainLines.find(line => line.includes("1") && line.includes("2"));
expect(dataLine).toBeTruthy();
expect(dataLine, "Should have data row").toBeTruthy();
});
it("should respect paddingX when calculating table width", () => {
@@ -348,12 +445,12 @@ describe("Markdown component", () => {
// All lines should respect width
for (const line of plainLines) {
expect(line.length <= 40, `Line exceeds width 40: "${line}" (length: ${line.length}).toBeTruthy()`);
expect(line.length <= 40, `Line exceeds width 40: "${line}" (length: ${line.length})`).toBeTruthy();
}
// Table rows should have left padding
const tableRow = plainLines.find(line => line.includes("|"));
expect(tableRow?.startsWith(" ")).toBeTruthy();
expect(tableRow?.startsWith(" "), "Table should have left padding").toBeTruthy();
});
});
@@ -409,12 +506,12 @@ describe("Markdown component", () => {
expect(joinedOutput.includes("inline code")).toBeTruthy();
// The output should have ANSI codes for gray (90) and italic (3)
expect(joinedOutput.includes("\x1b[90m")).toBeTruthy();
expect(joinedOutput.includes("\x1b[3m")).toBeTruthy();
expect(joinedOutput.includes("\x1b[90m"), "Should have gray color code").toBeTruthy();
expect(joinedOutput.includes("\x1b[3m"), "Should have italic code").toBeTruthy();
// Verify that inline code is styled (theme uses yellow)
const hasCodeColor = joinedOutput.includes("\x1b[33m");
expect(hasCodeColor).toBeTruthy();
expect(hasCodeColor, "Should style inline code").toBeTruthy();
});
it("should preserve gray italic styling after bold text", () => {
@@ -436,11 +533,46 @@ describe("Markdown component", () => {
expect(joinedOutput.includes("bold text")).toBeTruthy();
// The output should have ANSI codes for gray (90) and italic (3)
expect(joinedOutput.includes("\x1b[90m")).toBeTruthy();
expect(joinedOutput.includes("\x1b[3m")).toBeTruthy();
expect(joinedOutput.includes("\x1b[90m"), "Should have gray color code").toBeTruthy();
expect(joinedOutput.includes("\x1b[3m"), "Should have italic code").toBeTruthy();
// Should have bold codes (1 or 22 for bold on/off)
expect(joinedOutput.includes("\x1b[1m")).toBeTruthy();
expect(joinedOutput.includes("\x1b[1m"), "Should have bold code").toBeTruthy();
});
it("should not leak styles into following lines when rendered in TUI", async () => {
class MarkdownWithInput implements Component {
public markdownLineCount = 0;
constructor(private readonly markdown: Markdown) {}
render(width: number): string[] {
const lines = this.markdown.render(width);
this.markdownLineCount = lines.length;
return [...lines, "INPUT"];
}
invalidate(): void {
this.markdown.invalidate();
}
}
const markdown = new Markdown("This is thinking with `inline code`", 1, 0, defaultMarkdownTheme, {
color: text => chalk.gray(text),
italic: true,
});
const terminal = new VirtualTerminal(80, 6);
const tui = new TUI(terminal);
const component = new MarkdownWithInput(markdown);
tui.addChild(component);
tui.start();
await terminal.flush();
expect(component.markdownLineCount > 0).toBeTruthy();
const inputRow = component.markdownLineCount;
expect(getCellItalic(terminal, inputRow, 0)).toBe(0);
tui.stop();
});
});
@@ -463,12 +595,15 @@ again, hello world`,
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd());
const closingBackticksIndex = plainLines.indexOf("```");
expect(closingBackticksIndex !== -1).toBeTruthy();
expect(closingBackticksIndex !== -1, "Should have closing backticks").toBeTruthy();
const afterBackticks = plainLines.slice(closingBackticksIndex + 1);
const emptyLineCount = afterBackticks.findIndex(line => line !== "");
expect(emptyLineCount).toBe(1);
expect(
emptyLineCount,
`Expected 1 empty line after code block, but found ${emptyLineCount}. Lines after backticks: ${JSON.stringify(afterBackticks.slice(0, 5))}`,
).toBe(1);
});
});
@@ -488,13 +623,16 @@ again, hello world`,
const lines = markdown.render(80);
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd());
const dividerIndex = plainLines.findIndex(line => line.includes("-"));
expect(dividerIndex !== -1).toBeTruthy();
const dividerIndex = plainLines.findIndex(line => /^-+$/.test(line.trim()));
expect(dividerIndex !== -1, "Should have divider").toBeTruthy();
const afterDivider = plainLines.slice(dividerIndex + 1);
const emptyLineCount = afterDivider.findIndex(line => line !== "");
expect(emptyLineCount).toBe(1);
expect(
emptyLineCount,
`Expected 1 empty line after divider, but found ${emptyLineCount}. Lines after divider: ${JSON.stringify(afterDivider.slice(0, 5))}`,
).toBe(1);
});
});
@@ -513,12 +651,15 @@ This is a paragraph`,
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd());
const headingIndex = plainLines.findIndex(line => line.includes("Hello"));
expect(headingIndex !== -1).toBeTruthy();
expect(headingIndex !== -1, "Should have heading").toBeTruthy();
const afterHeading = plainLines.slice(headingIndex + 1);
const emptyLineCount = afterHeading.findIndex(line => line !== "");
expect(emptyLineCount).toBe(1);
expect(
emptyLineCount,
`Expected 1 empty line after heading, but found ${emptyLineCount}. Lines after heading: ${JSON.stringify(afterHeading.slice(0, 5))}`,
).toBe(1);
});
});
@@ -539,12 +680,68 @@ again, hello world`,
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd());
const quoteIndex = plainLines.findIndex(line => line.includes("This is a quote"));
expect(quoteIndex !== -1).toBeTruthy();
expect(quoteIndex !== -1, "Should have blockquote").toBeTruthy();
const afterQuote = plainLines.slice(quoteIndex + 1);
const emptyLineCount = afterQuote.findIndex(line => line !== "");
expect(emptyLineCount).toBe(1);
expect(
emptyLineCount,
`Expected 1 empty line after blockquote, but found ${emptyLineCount}. Lines after quote: ${JSON.stringify(afterQuote.slice(0, 5))}`,
).toBe(1);
});
});
describe("Links", () => {
it("should not duplicate URL for autolinked emails", () => {
const markdown = new Markdown("Contact user@example.com for help", 0, 0, defaultMarkdownTheme);
const lines = markdown.render(80);
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, ""));
const joinedPlain = plainLines.join(" ");
// Should contain the email once, not duplicated with mailto:
expect(joinedPlain.includes("user@example.com"), "Should contain email").toBeTruthy();
expect(!joinedPlain.includes("mailto:"), "Should not show mailto: prefix for autolinked emails").toBeTruthy();
});
it("should not duplicate URL for bare URLs", () => {
const markdown = new Markdown("Visit https://example.com for more", 0, 0, defaultMarkdownTheme);
const lines = markdown.render(80);
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, ""));
const joinedPlain = plainLines.join(" ");
// URL should appear only once
const urlCount = (joinedPlain.match(/https:\/\/example\.com/g) || []).length;
expect(urlCount, "URL should appear exactly once").toBe(1);
});
it("should show URL for explicit markdown links with different text", () => {
const markdown = new Markdown("[click here](https://example.com)", 0, 0, defaultMarkdownTheme);
const lines = markdown.render(80);
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, ""));
const joinedPlain = plainLines.join(" ");
// Should show both link text and URL
expect(joinedPlain.includes("click here"), "Should contain link text").toBeTruthy();
expect(joinedPlain.includes("(https://example.com)"), "Should show URL in parentheses").toBeTruthy();
});
it("should show URL for explicit mailto links with different text", () => {
const markdown = new Markdown("[Email me](mailto:test@example.com)", 0, 0, defaultMarkdownTheme);
const lines = markdown.render(80);
const plainLines = lines.map(line => line.replace(/\x1b\[[0-9;]*m/g, ""));
const joinedPlain = plainLines.join(" ");
// Should show both link text and mailto URL
expect(joinedPlain.includes("Email me"), "Should contain link text").toBeTruthy();
expect(
joinedPlain.includes("(mailto:test@example.com)"),
"Should show mailto URL in parentheses",
).toBeTruthy();
});
});
@@ -564,7 +761,10 @@ again, hello world`,
const joinedPlain = plainLines.join(" ");
// The content inside the tags should be visible
expect(joinedPlain.includes("hidden content") || joinedPlain.includes("<thinking>")).toBeTruthy();
expect(
joinedPlain.includes("hidden content") || joinedPlain.includes("<thinking>"),
"Should render HTML-like tags or their content as text, not hide them",
).toBeTruthy();
});
it("should render HTML tags in code blocks correctly", () => {
@@ -575,7 +775,10 @@ again, hello world`,
const joinedPlain = plainLines.join("\n");
// HTML in code blocks should be visible
expect(joinedPlain.includes("<div>") && joinedPlain.includes("</div>")).toBeTruthy();
expect(
joinedPlain.includes("<div>") && joinedPlain.includes("</div>"),
"Should render HTML in code blocks",
).toBeTruthy();
});
});
});
+18 -25
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test";
import { visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui/utils";
import { visibleWidth, wrapTextWithAnsi } from "../src/utils";
describe("wrapTextWithAnsi", () => {
describe("underline styling", () => {
@@ -11,29 +11,22 @@ describe("wrapTextWithAnsi", () => {
const wrapped = wrapTextWithAnsi(text, 40);
const prefix = "read this thread ";
expect(wrapped[0].startsWith(prefix)).toBe(true);
const underlineIndex = wrapped[0].indexOf(underlineOn);
if (underlineIndex !== -1) {
expect(underlineIndex).toBeGreaterThanOrEqual(prefix.length);
expect(wrapped[0].endsWith(underlineOff)).toBe(true);
}
// First line should NOT contain underline code - it's just "read this thread"
expect(wrapped[0]).toBe("read this thread");
// Second line should start with underline
expect(wrapped[1].startsWith(underlineOn)).toBe(true);
const plain = wrapped.join("").replace(/\x1b\[[0-9;]*m/g, "");
expect(plain.includes(url)).toBe(true);
// Second line should start with underline, have URL content
expect(wrapped[1].startsWith(underlineOn)).toStrictEqual(true);
expect(wrapped[1].includes("https://")).toBe(true);
});
it("should preserve whitespace before underline reset code", () => {
it("should not have whitespace before underline reset code", () => {
const underlineOn = "\x1b[4m";
const underlineOff = "\x1b[24m";
const textWithUnderlinedTrailingSpace = `${underlineOn}underlined text here ${underlineOff}more`;
const wrapped = wrapTextWithAnsi(textWithUnderlinedTrailingSpace, 18);
expect(wrapped[1].includes(` ${underlineOff}`)).toBe(true);
expect(wrapped[0].includes(` ${underlineOff}`)).toBe(false);
});
it("should not bleed underline to padding - each line should end with reset for underline only", () => {
@@ -67,7 +60,7 @@ describe("wrapTextWithAnsi", () => {
// Each line should have background color
for (const line of wrapped) {
expect(line.includes(bgBlue)).toBeTruthy();
expect(line.includes(bgBlue)).toBe(true);
}
// Middle lines should NOT end with full reset (kills background for padding)
@@ -76,7 +69,7 @@ describe("wrapTextWithAnsi", () => {
}
});
it("should reset underline without preserving background after wrap", () => {
it("should reset underline but preserve background when wrapping underlined text inside background", () => {
const underlineOn = "\x1b[4m";
const underlineOff = "\x1b[24m";
const reset = "\x1b[0m";
@@ -85,11 +78,11 @@ describe("wrapTextWithAnsi", () => {
const wrapped = wrapTextWithAnsi(text, 20);
const lineHasBg = (line: string) => line.includes("[41m") || line.includes(";41m") || line.includes("[41;");
expect(lineHasBg(wrapped[0])).toBeTruthy();
expect(lineHasBg(wrapped[1])).toBeFalsy();
expect(lineHasBg(wrapped[2])).toBeFalsy();
// All lines should have background color 41 (either as \x1b[41m or combined like \x1b[4;41m)
for (const line of wrapped) {
const hasBgColor = line.includes("[41m") || line.includes(";41m") || line.includes("[41;");
expect(hasBgColor).toBe(true);
}
// Lines with underlined content should use underline-off at end, not full reset
for (let i = 0; i < wrapped.length - 1; i++) {
@@ -111,15 +104,15 @@ describe("wrapTextWithAnsi", () => {
const text = "hello world this is a test";
const wrapped = wrapTextWithAnsi(text, 10);
expect(wrapped.length > 1).toBeTruthy();
expect(wrapped.length > 1).toBe(true);
for (const line of wrapped) {
expect(visibleWidth(line) <= 10).toBeTruthy();
expect(visibleWidth(line) <= 10).toBe(true);
}
});
it("should truncate trailing whitespace that exceeds width", () => {
const twoSpacesWrappedToWidth1 = wrapTextWithAnsi(" ", 1);
expect(visibleWidth(twoSpacesWrappedToWidth1[0]) <= 1).toBeTruthy();
expect(visibleWidth(twoSpacesWrappedToWidth1[0]) <= 1).toBe(true);
});
it("should preserve color codes across wraps", () => {
-1
View File
@@ -1,4 +1,3 @@
[toolchain]
channel = "nightly"
components = ["rustfmt", "clippy", "rust-analyzer"]
targets = ["wasm32-unknown-unknown"]