refactor(v2): extract bterminal-core crate with EventSink trait
Create Cargo workspace at v2/ level with members: src-tauri, bterminal-core, bterminal-relay. Extract PtyManager and SidecarManager into shared bterminal-core crate with EventSink trait for abstracting event emission. TauriEventSink wraps AppHandle. src-tauri pty.rs and sidecar.rs become thin re-exports. Move Cargo.lock to workspace root. Add v2/target/ to .gitignore.
This commit is contained in:
parent
250ea17d3e
commit
f894c2862c
13 changed files with 972 additions and 453 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@ __pycache__/
|
|||
*.pyc
|
||||
*.pyo
|
||||
/CLAUDE.md
|
||||
v2/target/
|
||||
|
|
|
|||
369
v2/src-tauri/Cargo.lock → v2/Cargo.lock
generated
369
v2/src-tauri/Cargo.lock → v2/Cargo.lock
generated
|
|
@ -68,7 +68,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
|
||||
dependencies = [
|
||||
"android_log-sys",
|
||||
"env_filter",
|
||||
"env_filter 0.1.4",
|
||||
"log",
|
||||
]
|
||||
|
||||
|
|
@ -81,6 +81,56 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
|
|
@ -242,10 +292,11 @@ dependencies = [
|
|||
name = "bterminal"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bterminal-core",
|
||||
"dirs 5.0.1",
|
||||
"futures-util",
|
||||
"log",
|
||||
"notify",
|
||||
"portable-pty",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -254,6 +305,35 @@ dependencies = [
|
|||
"tauri-plugin-log",
|
||||
"tauri-plugin-updater",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bterminal-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"portable-pty",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bterminal-relay"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bterminal-core",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
|
@ -446,6 +526,52 @@ dependencies = [
|
|||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
|
|
@ -497,7 +623,7 @@ dependencies = [
|
|||
"bitflags 2.11.0",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
|
@ -627,6 +753,12 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
|
|
@ -831,6 +963,29 @@ dependencies = [
|
|||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter 1.0.0",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
|
|
@ -954,6 +1109,15 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
|
|
@ -961,7 +1125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -975,6 +1139,12 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
|
|
@ -1795,6 +1965,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
|
|
@ -1824,6 +2000,30 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
|
|
@ -2153,6 +2353,23 @@ dependencies = [
|
|||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
|
|
@ -2416,12 +2633,56 @@ version = "1.21.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
|
|
@ -2680,6 +2941,21 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-pty"
|
||||
version = "0.8.1"
|
||||
|
|
@ -3595,6 +3871,17 @@ dependencies = [
|
|||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
|
|
@ -3628,6 +3915,16 @@ version = "1.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
|
|
@ -4329,11 +4626,35 @@ dependencies = [
|
|||
"bytes",
|
||||
"libc",
|
||||
"mio 1.1.1",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
|
|
@ -4344,6 +4665,20 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
|
|
@ -4554,6 +4889,26 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"native-tls",
|
||||
"rand 0.8.5",
|
||||
"sha1",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
|
|
@ -4674,6 +5029,12 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.21.0"
|
||||
3
v2/Cargo.toml
Normal file
3
v2/Cargo.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[workspace]
|
||||
members = ["src-tauri", "bterminal-core", "bterminal-relay"]
|
||||
resolver = "2"
|
||||
13
v2/bterminal-core/Cargo.toml
Normal file
13
v2/bterminal-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "bterminal-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Shared PTY and sidecar management for BTerminal"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
log = "0.4"
|
||||
portable-pty = "0.8"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
5
v2/bterminal-core/src/event.rs
Normal file
5
v2/bterminal-core/src/event.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/// Trait for emitting events from PTY and sidecar managers.
|
||||
/// Implemented by Tauri's AppHandle (controller) and WebSocket sender (relay).
|
||||
pub trait EventSink: Send + Sync {
|
||||
fn emit(&self, event: &str, payload: serde_json::Value);
|
||||
}
|
||||
3
v2/bterminal-core/src/lib.rs
Normal file
3
v2/bterminal-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod event;
|
||||
pub mod pty;
|
||||
pub mod sidecar;
|
||||
173
v2/bterminal-core/src/pty.rs
Normal file
173
v2/bterminal-core/src/pty.rs
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{BufReader, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::event::EventSink;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PtyOptions {
|
||||
pub shell: Option<String>,
|
||||
pub cwd: Option<String>,
|
||||
pub args: Option<Vec<String>>,
|
||||
pub cols: Option<u16>,
|
||||
pub rows: Option<u16>,
|
||||
}
|
||||
|
||||
struct PtyInstance {
|
||||
master: Box<dyn MasterPty + Send>,
|
||||
writer: Box<dyn Write + Send>,
|
||||
}
|
||||
|
||||
pub struct PtyManager {
|
||||
instances: Arc<Mutex<HashMap<String, PtyInstance>>>,
|
||||
sink: Arc<dyn EventSink>,
|
||||
}
|
||||
|
||||
impl PtyManager {
|
||||
pub fn new(sink: Arc<dyn EventSink>) -> Self {
|
||||
Self {
|
||||
instances: Arc::new(Mutex::new(HashMap::new())),
|
||||
sink,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn(&self, options: PtyOptions) -> Result<String, String> {
|
||||
let pty_system = native_pty_system();
|
||||
let cols = options.cols.unwrap_or(80);
|
||||
let rows = options.rows.unwrap_or(24);
|
||||
|
||||
let pair = pty_system
|
||||
.openpty(PtySize {
|
||||
rows,
|
||||
cols,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
})
|
||||
.map_err(|e| format!("Failed to open PTY: {e}"))?;
|
||||
|
||||
let shell = options.shell.unwrap_or_else(|| {
|
||||
std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string())
|
||||
});
|
||||
|
||||
let mut cmd = CommandBuilder::new(&shell);
|
||||
if let Some(args) = &options.args {
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
}
|
||||
if let Some(cwd) = &options.cwd {
|
||||
cmd.cwd(cwd);
|
||||
}
|
||||
|
||||
let _child = pair
|
||||
.slave
|
||||
.spawn_command(cmd)
|
||||
.map_err(|e| format!("Failed to spawn command: {e}"))?;
|
||||
|
||||
drop(pair.slave);
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let reader = pair
|
||||
.master
|
||||
.try_clone_reader()
|
||||
.map_err(|e| format!("Failed to clone PTY reader: {e}"))?;
|
||||
let writer = pair
|
||||
.master
|
||||
.take_writer()
|
||||
.map_err(|e| format!("Failed to take PTY writer: {e}"))?;
|
||||
|
||||
let event_id = id.clone();
|
||||
let sink = self.sink.clone();
|
||||
thread::spawn(move || {
|
||||
let mut buf_reader = BufReader::with_capacity(4096, reader);
|
||||
let mut buf = vec![0u8; 4096];
|
||||
loop {
|
||||
match std::io::Read::read(&mut buf_reader, &mut buf) {
|
||||
Ok(0) => {
|
||||
sink.emit(
|
||||
&format!("pty-exit-{event_id}"),
|
||||
serde_json::Value::Null,
|
||||
);
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
let data = String::from_utf8_lossy(&buf[..n]).to_string();
|
||||
sink.emit(
|
||||
&format!("pty-data-{event_id}"),
|
||||
serde_json::Value::String(data),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("PTY read error for {event_id}: {e}");
|
||||
sink.emit(
|
||||
&format!("pty-exit-{event_id}"),
|
||||
serde_json::Value::Null,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let instance = PtyInstance {
|
||||
master: pair.master,
|
||||
writer,
|
||||
};
|
||||
self.instances.lock().unwrap().insert(id.clone(), instance);
|
||||
|
||||
log::info!("Spawned PTY {id} ({shell})");
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn write(&self, id: &str, data: &str) -> Result<(), String> {
|
||||
let mut instances = self.instances.lock().unwrap();
|
||||
let instance = instances
|
||||
.get_mut(id)
|
||||
.ok_or_else(|| format!("PTY {id} not found"))?;
|
||||
instance
|
||||
.writer
|
||||
.write_all(data.as_bytes())
|
||||
.map_err(|e| format!("PTY write error: {e}"))?;
|
||||
instance
|
||||
.writer
|
||||
.flush()
|
||||
.map_err(|e| format!("PTY flush error: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resize(&self, id: &str, cols: u16, rows: u16) -> Result<(), String> {
|
||||
let instances = self.instances.lock().unwrap();
|
||||
let instance = instances
|
||||
.get(id)
|
||||
.ok_or_else(|| format!("PTY {id} not found"))?;
|
||||
instance
|
||||
.master
|
||||
.resize(PtySize {
|
||||
rows,
|
||||
cols,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
})
|
||||
.map_err(|e| format!("PTY resize error: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn kill(&self, id: &str) -> Result<(), String> {
|
||||
let mut instances = self.instances.lock().unwrap();
|
||||
if instances.remove(id).is_some() {
|
||||
log::info!("Killed PTY {id}");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("PTY {id} not found"))
|
||||
}
|
||||
}
|
||||
|
||||
/// List active PTY session IDs.
|
||||
pub fn list_sessions(&self) -> Vec<String> {
|
||||
self.instances.lock().unwrap().keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
261
v2/bterminal-core/src/sidecar.rs
Normal file
261
v2/bterminal-core/src/sidecar.rs
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
// Sidecar lifecycle management (Deno-first, Node.js fallback)
|
||||
// Spawns agent-runner-deno.ts (or agent-runner.mjs), communicates via stdio NDJSON
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
use crate::event::EventSink;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentQueryOptions {
|
||||
pub session_id: String,
|
||||
pub prompt: String,
|
||||
pub cwd: Option<String>,
|
||||
pub max_turns: Option<u32>,
|
||||
pub max_budget_usd: Option<f64>,
|
||||
pub resume_session_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Directories to search for sidecar scripts.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SidecarConfig {
|
||||
pub search_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
struct SidecarCommand {
|
||||
program: String,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct SidecarManager {
|
||||
child: Arc<Mutex<Option<Child>>>,
|
||||
stdin_writer: Arc<Mutex<Option<Box<dyn Write + Send>>>>,
|
||||
ready: Arc<Mutex<bool>>,
|
||||
sink: Arc<dyn EventSink>,
|
||||
config: SidecarConfig,
|
||||
}
|
||||
|
||||
impl SidecarManager {
|
||||
pub fn new(sink: Arc<dyn EventSink>, config: SidecarConfig) -> Self {
|
||||
Self {
|
||||
child: Arc::new(Mutex::new(None)),
|
||||
stdin_writer: Arc::new(Mutex::new(None)),
|
||||
ready: Arc::new(Mutex::new(false)),
|
||||
sink,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Result<(), String> {
|
||||
let mut child_lock = self.child.lock().unwrap();
|
||||
if child_lock.is_some() {
|
||||
return Err("Sidecar already running".to_string());
|
||||
}
|
||||
|
||||
let cmd = self.resolve_sidecar_command()?;
|
||||
|
||||
log::info!("Starting sidecar: {} {}", cmd.program, cmd.args.join(" "));
|
||||
|
||||
let mut child = Command::new(&cmd.program)
|
||||
.args(&cmd.args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start sidecar: {e}"))?;
|
||||
|
||||
let child_stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or("Failed to capture sidecar stdin")?;
|
||||
let child_stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or("Failed to capture sidecar stdout")?;
|
||||
let child_stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or("Failed to capture sidecar stderr")?;
|
||||
|
||||
*self.stdin_writer.lock().unwrap() = Some(Box::new(child_stdin));
|
||||
|
||||
// Stdout reader thread — forwards NDJSON to event sink
|
||||
let sink = self.sink.clone();
|
||||
let ready = self.ready.clone();
|
||||
thread::spawn(move || {
|
||||
let reader = BufReader::new(child_stdout);
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
match serde_json::from_str::<serde_json::Value>(&line) {
|
||||
Ok(msg) => {
|
||||
if msg.get("type").and_then(|t| t.as_str()) == Some("ready") {
|
||||
*ready.lock().unwrap() = true;
|
||||
log::info!("Sidecar ready");
|
||||
}
|
||||
sink.emit("sidecar-message", msg);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Invalid JSON from sidecar: {e}: {line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Sidecar stdout read error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("Sidecar stdout reader exited");
|
||||
sink.emit("sidecar-exited", serde_json::Value::Null);
|
||||
});
|
||||
|
||||
// Stderr reader thread — logs only
|
||||
thread::spawn(move || {
|
||||
let reader = BufReader::new(child_stderr);
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(line) => log::info!("[sidecar stderr] {line}"),
|
||||
Err(e) => {
|
||||
log::error!("Sidecar stderr read error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
*child_lock = Some(child);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_message(&self, msg: &serde_json::Value) -> Result<(), String> {
|
||||
let mut writer_lock = self.stdin_writer.lock().unwrap();
|
||||
let writer = writer_lock.as_mut().ok_or("Sidecar not running")?;
|
||||
|
||||
let line =
|
||||
serde_json::to_string(msg).map_err(|e| format!("JSON serialize error: {e}"))?;
|
||||
|
||||
writer
|
||||
.write_all(line.as_bytes())
|
||||
.map_err(|e| format!("Sidecar write error: {e}"))?;
|
||||
writer
|
||||
.write_all(b"\n")
|
||||
.map_err(|e| format!("Sidecar write error: {e}"))?;
|
||||
writer
|
||||
.flush()
|
||||
.map_err(|e| format!("Sidecar flush error: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn query(&self, options: &AgentQueryOptions) -> Result<(), String> {
|
||||
if !*self.ready.lock().unwrap() {
|
||||
return Err("Sidecar not ready".to_string());
|
||||
}
|
||||
|
||||
let msg = serde_json::json!({
|
||||
"type": "query",
|
||||
"sessionId": options.session_id,
|
||||
"prompt": options.prompt,
|
||||
"cwd": options.cwd,
|
||||
"maxTurns": options.max_turns,
|
||||
"maxBudgetUsd": options.max_budget_usd,
|
||||
"resumeSessionId": options.resume_session_id,
|
||||
});
|
||||
|
||||
self.send_message(&msg)
|
||||
}
|
||||
|
||||
pub fn stop_session(&self, session_id: &str) -> Result<(), String> {
|
||||
let msg = serde_json::json!({
|
||||
"type": "stop",
|
||||
"sessionId": session_id,
|
||||
});
|
||||
self.send_message(&msg)
|
||||
}
|
||||
|
||||
pub fn restart(&self) -> Result<(), String> {
|
||||
log::info!("Restarting sidecar");
|
||||
let _ = self.shutdown();
|
||||
self.start()
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) -> Result<(), String> {
|
||||
let mut child_lock = self.child.lock().unwrap();
|
||||
if let Some(ref mut child) = *child_lock {
|
||||
log::info!("Shutting down sidecar");
|
||||
*self.stdin_writer.lock().unwrap() = None;
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
*child_lock = None;
|
||||
*self.ready.lock().unwrap() = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_ready(&self) -> bool {
|
||||
*self.ready.lock().unwrap()
|
||||
}
|
||||
|
||||
fn resolve_sidecar_command(&self) -> Result<SidecarCommand, String> {
|
||||
let mut checked_deno = Vec::new();
|
||||
let mut checked_node = Vec::new();
|
||||
|
||||
// Try Deno first in each search path
|
||||
for base in &self.config.search_paths {
|
||||
let deno_path = base.join("agent-runner-deno.ts");
|
||||
if deno_path.exists() {
|
||||
if Command::new("deno").arg("--version").output().is_ok() {
|
||||
return Ok(SidecarCommand {
|
||||
program: "deno".to_string(),
|
||||
args: vec![
|
||||
"run".to_string(),
|
||||
"--allow-run".to_string(),
|
||||
"--allow-env".to_string(),
|
||||
"--allow-read".to_string(),
|
||||
deno_path.to_string_lossy().to_string(),
|
||||
],
|
||||
});
|
||||
}
|
||||
log::warn!(
|
||||
"Deno sidecar found at {} but deno not in PATH, falling back to Node.js",
|
||||
deno_path.display()
|
||||
);
|
||||
}
|
||||
checked_deno.push(deno_path);
|
||||
}
|
||||
|
||||
// Fallback to Node.js
|
||||
for base in &self.config.search_paths {
|
||||
let node_path = base.join("dist").join("agent-runner.mjs");
|
||||
if node_path.exists() {
|
||||
return Ok(SidecarCommand {
|
||||
program: "node".to_string(),
|
||||
args: vec![node_path.to_string_lossy().to_string()],
|
||||
});
|
||||
}
|
||||
checked_node.push(node_path);
|
||||
}
|
||||
|
||||
let deno_list: Vec<_> = checked_deno.iter().map(|p| p.display().to_string()).collect();
|
||||
let node_list: Vec<_> = checked_node.iter().map(|p| p.display().to_string()).collect();
|
||||
Err(format!(
|
||||
"Sidecar not found. Checked Deno ({}) and Node.js ({})",
|
||||
deno_list.join(", "),
|
||||
node_list.join(", "),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SidecarManager {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.shutdown();
|
||||
}
|
||||
}
|
||||
|
|
@ -17,17 +17,20 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||
tauri-build = { version = "2.5.6", features = [] }
|
||||
|
||||
[dependencies]
|
||||
bterminal-core = { path = "../bterminal-core" }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.10.3", features = [] }
|
||||
tauri-plugin-log = "2"
|
||||
portable-pty = "0.8"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
dirs = "5"
|
||||
notify = { version = "6", features = ["macos_fsevent"] }
|
||||
tauri-plugin-updater = "2.10.0"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
futures-util = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
|||
11
v2/src-tauri/src/event_sink.rs
Normal file
11
v2/src-tauri/src/event_sink.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
use bterminal_core::event::EventSink;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
/// Bridges bterminal-core's EventSink trait to Tauri's event system.
|
||||
pub struct TauriEventSink(pub AppHandle);
|
||||
|
||||
impl EventSink for TauriEventSink {
|
||||
fn emit(&self, event: &str, payload: serde_json::Value) {
|
||||
let _ = self.0.emit(event, &payload);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,20 @@
|
|||
mod ctx;
|
||||
mod event_sink;
|
||||
mod pty;
|
||||
mod remote;
|
||||
mod sidecar;
|
||||
mod watcher;
|
||||
mod session;
|
||||
mod watcher;
|
||||
|
||||
use ctx::CtxDb;
|
||||
use event_sink::TauriEventSink;
|
||||
use pty::{PtyManager, PtyOptions};
|
||||
use remote::{RemoteManager, RemoteMachineConfig};
|
||||
use session::{Session, SessionDb, LayoutState, SshSession};
|
||||
use sidecar::{AgentQueryOptions, SidecarManager};
|
||||
use sidecar::{AgentQueryOptions, SidecarConfig, SidecarManager};
|
||||
use watcher::FileWatcherManager;
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use tauri::{Manager, State};
|
||||
|
||||
struct AppState {
|
||||
pty_manager: Arc<PtyManager>,
|
||||
|
|
@ -18,17 +22,17 @@ struct AppState {
|
|||
session_db: Arc<SessionDb>,
|
||||
file_watcher: Arc<FileWatcherManager>,
|
||||
ctx_db: Arc<CtxDb>,
|
||||
remote_manager: Arc<RemoteManager>,
|
||||
}
|
||||
|
||||
// --- PTY commands ---
|
||||
|
||||
#[tauri::command]
|
||||
fn pty_spawn(
|
||||
app: tauri::AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
options: PtyOptions,
|
||||
) -> Result<String, String> {
|
||||
state.pty_manager.spawn(&app, options)
|
||||
state.pty_manager.spawn(options)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -72,8 +76,8 @@ fn agent_ready(state: State<'_, AppState>) -> bool {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn agent_restart(app: tauri::AppHandle, state: State<'_, AppState>) -> Result<(), String> {
|
||||
state.sidecar_manager.restart(&app)
|
||||
fn agent_restart(state: State<'_, AppState>) -> Result<(), String> {
|
||||
state.sidecar_manager.restart()
|
||||
}
|
||||
|
||||
// --- File watcher commands ---
|
||||
|
|
@ -201,32 +205,66 @@ fn ctx_search(state: State<'_, AppState>, query: String) -> Result<Vec<ctx::CtxE
|
|||
state.ctx_db.search(&query)
|
||||
}
|
||||
|
||||
// --- Remote machine commands ---
|
||||
|
||||
#[tauri::command]
|
||||
fn remote_list(state: State<'_, AppState>) -> Vec<remote::RemoteMachineInfo> {
|
||||
state.remote_manager.list_machines()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remote_add(state: State<'_, AppState>, config: RemoteMachineConfig) -> Result<String, String> {
|
||||
Ok(state.remote_manager.add_machine(config))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remote_remove(state: State<'_, AppState>, machine_id: String) -> Result<(), String> {
|
||||
state.remote_manager.remove_machine(&machine_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remote_connect(app: tauri::AppHandle, state: State<'_, AppState>, machine_id: String) -> Result<(), String> {
|
||||
state.remote_manager.connect(&app, &machine_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remote_disconnect(state: State<'_, AppState>, machine_id: String) -> Result<(), String> {
|
||||
state.remote_manager.disconnect(&machine_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remote_agent_query(state: State<'_, AppState>, machine_id: String, options: AgentQueryOptions) -> Result<(), String> {
|
||||
state.remote_manager.agent_query(&machine_id, &options).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remote_agent_stop(state: State<'_, AppState>, machine_id: String, session_id: String) -> Result<(), String> {
|
||||
state.remote_manager.agent_stop(&machine_id, &session_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remote_pty_spawn(state: State<'_, AppState>, machine_id: String, options: PtyOptions) -> Result<String, String> {
|
||||
state.remote_manager.pty_spawn(&machine_id, &options).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remote_pty_write(state: State<'_, AppState>, machine_id: String, id: String, data: String) -> Result<(), String> {
|
||||
state.remote_manager.pty_write(&machine_id, &id, &data).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remote_pty_resize(state: State<'_, AppState>, machine_id: String, id: String, cols: u16, rows: u16) -> Result<(), String> {
|
||||
state.remote_manager.pty_resize(&machine_id, &id, cols, rows).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remote_pty_kill(state: State<'_, AppState>, machine_id: String, id: String) -> Result<(), String> {
|
||||
state.remote_manager.pty_kill(&machine_id, &id).await
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let pty_manager = Arc::new(PtyManager::new());
|
||||
let sidecar_manager = Arc::new(SidecarManager::new());
|
||||
|
||||
// Initialize session database in app data directory
|
||||
let data_dir = dirs::data_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join("bterminal");
|
||||
let session_db = Arc::new(
|
||||
SessionDb::open(&data_dir).expect("Failed to open session database")
|
||||
);
|
||||
|
||||
let file_watcher = Arc::new(FileWatcherManager::new());
|
||||
let ctx_db = Arc::new(CtxDb::new());
|
||||
|
||||
let app_state = AppState {
|
||||
pty_manager,
|
||||
sidecar_manager: sidecar_manager.clone(),
|
||||
session_db,
|
||||
file_watcher,
|
||||
ctx_db,
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
.manage(app_state)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
pty_spawn,
|
||||
pty_write,
|
||||
|
|
@ -258,6 +296,17 @@ pub fn run() {
|
|||
ctx_get_shared,
|
||||
ctx_get_summaries,
|
||||
ctx_search,
|
||||
remote_list,
|
||||
remote_add,
|
||||
remote_remove,
|
||||
remote_connect,
|
||||
remote_disconnect,
|
||||
remote_agent_query,
|
||||
remote_agent_stop,
|
||||
remote_pty_spawn,
|
||||
remote_pty_write,
|
||||
remote_pty_resize,
|
||||
remote_pty_kill,
|
||||
])
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.setup(move |app| {
|
||||
|
|
@ -269,12 +318,57 @@ pub fn run() {
|
|||
)?;
|
||||
}
|
||||
|
||||
// Start sidecar on app launch
|
||||
match sidecar_manager.start(app.handle()) {
|
||||
// Create TauriEventSink for core managers
|
||||
let sink: Arc<dyn bterminal_core::event::EventSink> =
|
||||
Arc::new(TauriEventSink(app.handle().clone()));
|
||||
|
||||
// Build sidecar config from Tauri paths
|
||||
let resource_dir = app
|
||||
.handle()
|
||||
.path()
|
||||
.resource_dir()
|
||||
.unwrap_or_default();
|
||||
let dev_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
let sidecar_config = SidecarConfig {
|
||||
search_paths: vec![
|
||||
resource_dir.join("sidecar"),
|
||||
dev_root.join("sidecar"),
|
||||
],
|
||||
};
|
||||
|
||||
let pty_manager = Arc::new(PtyManager::new(sink.clone()));
|
||||
let sidecar_manager = Arc::new(SidecarManager::new(sink, sidecar_config));
|
||||
|
||||
// Initialize session database
|
||||
let data_dir = dirs::data_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join("bterminal");
|
||||
let session_db = Arc::new(
|
||||
SessionDb::open(&data_dir).expect("Failed to open session database"),
|
||||
);
|
||||
|
||||
let file_watcher = Arc::new(FileWatcherManager::new());
|
||||
let ctx_db = Arc::new(CtxDb::new());
|
||||
let remote_manager = Arc::new(RemoteManager::new());
|
||||
|
||||
// Start local sidecar
|
||||
match sidecar_manager.start() {
|
||||
Ok(()) => log::info!("Sidecar startup initiated"),
|
||||
Err(e) => log::warn!("Sidecar startup failed (agent features unavailable): {e}"),
|
||||
}
|
||||
|
||||
app.manage(AppState {
|
||||
pty_manager,
|
||||
sidecar_manager,
|
||||
session_db,
|
||||
file_watcher,
|
||||
ctx_db,
|
||||
remote_manager,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
|
|
|
|||
|
|
@ -1,160 +1,4 @@
|
|||
use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{BufReader, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use uuid::Uuid;
|
||||
// Thin wrapper — re-exports bterminal_core::pty types.
|
||||
// PtyManager is now in bterminal-core; this module only re-exports for lib.rs.
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PtyOptions {
|
||||
pub shell: Option<String>,
|
||||
pub cwd: Option<String>,
|
||||
pub args: Option<Vec<String>>,
|
||||
pub cols: Option<u16>,
|
||||
pub rows: Option<u16>,
|
||||
}
|
||||
|
||||
struct PtyInstance {
|
||||
master: Box<dyn MasterPty + Send>,
|
||||
writer: Box<dyn Write + Send>,
|
||||
}
|
||||
|
||||
pub struct PtyManager {
|
||||
instances: Arc<Mutex<HashMap<String, PtyInstance>>>,
|
||||
}
|
||||
|
||||
impl PtyManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
instances: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
options: PtyOptions,
|
||||
) -> Result<String, String> {
|
||||
let pty_system = native_pty_system();
|
||||
let cols = options.cols.unwrap_or(80);
|
||||
let rows = options.rows.unwrap_or(24);
|
||||
|
||||
let pair = pty_system
|
||||
.openpty(PtySize {
|
||||
rows,
|
||||
cols,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
})
|
||||
.map_err(|e| format!("Failed to open PTY: {e}"))?;
|
||||
|
||||
let shell = options.shell.unwrap_or_else(|| {
|
||||
std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string())
|
||||
});
|
||||
|
||||
let mut cmd = CommandBuilder::new(&shell);
|
||||
if let Some(args) = &options.args {
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
}
|
||||
if let Some(cwd) = &options.cwd {
|
||||
cmd.cwd(cwd);
|
||||
}
|
||||
|
||||
let _child = pair
|
||||
.slave
|
||||
.spawn_command(cmd)
|
||||
.map_err(|e| format!("Failed to spawn command: {e}"))?;
|
||||
|
||||
// Drop the slave side — we only need the master
|
||||
drop(pair.slave);
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let reader = pair
|
||||
.master
|
||||
.try_clone_reader()
|
||||
.map_err(|e| format!("Failed to clone PTY reader: {e}"))?;
|
||||
let writer = pair
|
||||
.master
|
||||
.take_writer()
|
||||
.map_err(|e| format!("Failed to take PTY writer: {e}"))?;
|
||||
|
||||
// Spawn reader thread that emits Tauri events
|
||||
let event_id = id.clone();
|
||||
let app_handle = app.clone();
|
||||
thread::spawn(move || {
|
||||
let mut buf_reader = BufReader::with_capacity(4096, reader);
|
||||
let mut buf = vec![0u8; 4096];
|
||||
loop {
|
||||
match std::io::Read::read(&mut buf_reader, &mut buf) {
|
||||
Ok(0) => {
|
||||
// PTY closed
|
||||
let _ = app_handle.emit(&format!("pty-exit-{event_id}"), ());
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
let data = String::from_utf8_lossy(&buf[..n]).to_string();
|
||||
let _ = app_handle.emit(&format!("pty-data-{event_id}"), &data);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("PTY read error for {event_id}: {e}");
|
||||
let _ = app_handle.emit(&format!("pty-exit-{event_id}"), ());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let instance = PtyInstance { master: pair.master, writer };
|
||||
self.instances.lock().unwrap().insert(id.clone(), instance);
|
||||
|
||||
log::info!("Spawned PTY {id} ({shell})");
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn write(&self, id: &str, data: &str) -> Result<(), String> {
|
||||
let mut instances = self.instances.lock().unwrap();
|
||||
let instance = instances
|
||||
.get_mut(id)
|
||||
.ok_or_else(|| format!("PTY {id} not found"))?;
|
||||
instance
|
||||
.writer
|
||||
.write_all(data.as_bytes())
|
||||
.map_err(|e| format!("PTY write error: {e}"))?;
|
||||
instance
|
||||
.writer
|
||||
.flush()
|
||||
.map_err(|e| format!("PTY flush error: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resize(&self, id: &str, cols: u16, rows: u16) -> Result<(), String> {
|
||||
let instances = self.instances.lock().unwrap();
|
||||
let instance = instances
|
||||
.get(id)
|
||||
.ok_or_else(|| format!("PTY {id} not found"))?;
|
||||
instance
|
||||
.master
|
||||
.resize(PtySize {
|
||||
rows,
|
||||
cols,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
})
|
||||
.map_err(|e| format!("PTY resize error: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn kill(&self, id: &str) -> Result<(), String> {
|
||||
let mut instances = self.instances.lock().unwrap();
|
||||
if instances.remove(id).is_some() {
|
||||
log::info!("Killed PTY {id}");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("PTY {id} not found"))
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use bterminal_core::pty::{PtyManager, PtyOptions};
|
||||
|
|
|
|||
|
|
@ -1,257 +1,4 @@
|
|||
// Sidecar lifecycle management (Deno-first, Node.js fallback)
|
||||
// Spawns agent-runner-deno.ts (or agent-runner.mjs), communicates via stdio NDJSON
|
||||
// Thin wrapper — re-exports bterminal_core::sidecar types.
|
||||
// SidecarManager is now in bterminal-core; this module only re-exports for lib.rs.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentQueryOptions {
|
||||
pub session_id: String,
|
||||
pub prompt: String,
|
||||
pub cwd: Option<String>,
|
||||
pub max_turns: Option<u32>,
|
||||
pub max_budget_usd: Option<f64>,
|
||||
pub resume_session_id: Option<String>,
|
||||
}
|
||||
|
||||
struct SidecarCommand {
|
||||
program: String,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct SidecarManager {
|
||||
child: Arc<Mutex<Option<Child>>>,
|
||||
stdin_writer: Arc<Mutex<Option<Box<dyn Write + Send>>>>,
|
||||
ready: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl SidecarManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
child: Arc::new(Mutex::new(None)),
|
||||
stdin_writer: Arc::new(Mutex::new(None)),
|
||||
ready: Arc::new(Mutex::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&self, app: &AppHandle) -> Result<(), String> {
|
||||
let mut child_lock = self.child.lock().unwrap();
|
||||
if child_lock.is_some() {
|
||||
return Err("Sidecar already running".to_string());
|
||||
}
|
||||
|
||||
// Resolve sidecar command (Deno-first, Node.js fallback)
|
||||
let cmd = Self::resolve_sidecar_command(app)?;
|
||||
|
||||
log::info!("Starting sidecar: {} {}", cmd.program, cmd.args.join(" "));
|
||||
|
||||
let mut child = Command::new(&cmd.program)
|
||||
.args(&cmd.args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start sidecar: {e}"))?;
|
||||
|
||||
let child_stdin = child.stdin.take().ok_or("Failed to capture sidecar stdin")?;
|
||||
let child_stdout = child.stdout.take().ok_or("Failed to capture sidecar stdout")?;
|
||||
let child_stderr = child.stderr.take().ok_or("Failed to capture sidecar stderr")?;
|
||||
|
||||
*self.stdin_writer.lock().unwrap() = Some(Box::new(child_stdin));
|
||||
|
||||
// Stdout reader thread — forwards NDJSON to Tauri events
|
||||
let app_handle = app.clone();
|
||||
let ready = self.ready.clone();
|
||||
thread::spawn(move || {
|
||||
let reader = BufReader::new(child_stdout);
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
match serde_json::from_str::<serde_json::Value>(&line) {
|
||||
Ok(msg) => {
|
||||
// Check for ready signal
|
||||
if msg.get("type").and_then(|t| t.as_str()) == Some("ready") {
|
||||
*ready.lock().unwrap() = true;
|
||||
log::info!("Sidecar ready");
|
||||
}
|
||||
let _ = app_handle.emit("sidecar-message", &msg);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Invalid JSON from sidecar: {e}: {line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Sidecar stdout read error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("Sidecar stdout reader exited");
|
||||
let _ = app_handle.emit("sidecar-exited", ());
|
||||
});
|
||||
|
||||
// Stderr reader thread — logs only
|
||||
thread::spawn(move || {
|
||||
let reader = BufReader::new(child_stderr);
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(line) => log::info!("[sidecar stderr] {line}"),
|
||||
Err(e) => {
|
||||
log::error!("Sidecar stderr read error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
*child_lock = Some(child);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_message(&self, msg: &serde_json::Value) -> Result<(), String> {
|
||||
let mut writer_lock = self.stdin_writer.lock().unwrap();
|
||||
let writer = writer_lock
|
||||
.as_mut()
|
||||
.ok_or("Sidecar not running")?;
|
||||
|
||||
let line = serde_json::to_string(msg)
|
||||
.map_err(|e| format!("JSON serialize error: {e}"))?;
|
||||
|
||||
writer
|
||||
.write_all(line.as_bytes())
|
||||
.map_err(|e| format!("Sidecar write error: {e}"))?;
|
||||
writer
|
||||
.write_all(b"\n")
|
||||
.map_err(|e| format!("Sidecar write error: {e}"))?;
|
||||
writer
|
||||
.flush()
|
||||
.map_err(|e| format!("Sidecar flush error: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn query(&self, options: &AgentQueryOptions) -> Result<(), String> {
|
||||
if !*self.ready.lock().unwrap() {
|
||||
return Err("Sidecar not ready".to_string());
|
||||
}
|
||||
|
||||
let msg = serde_json::json!({
|
||||
"type": "query",
|
||||
"sessionId": options.session_id,
|
||||
"prompt": options.prompt,
|
||||
"cwd": options.cwd,
|
||||
"maxTurns": options.max_turns,
|
||||
"maxBudgetUsd": options.max_budget_usd,
|
||||
"resumeSessionId": options.resume_session_id,
|
||||
});
|
||||
|
||||
self.send_message(&msg)
|
||||
}
|
||||
|
||||
pub fn stop_session(&self, session_id: &str) -> Result<(), String> {
|
||||
let msg = serde_json::json!({
|
||||
"type": "stop",
|
||||
"sessionId": session_id,
|
||||
});
|
||||
self.send_message(&msg)
|
||||
}
|
||||
|
||||
pub fn restart(&self, app: &AppHandle) -> Result<(), String> {
|
||||
log::info!("Restarting sidecar");
|
||||
let _ = self.shutdown();
|
||||
self.start(app)
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) -> Result<(), String> {
|
||||
let mut child_lock = self.child.lock().unwrap();
|
||||
if let Some(ref mut child) = *child_lock {
|
||||
log::info!("Shutting down sidecar");
|
||||
// Drop stdin to signal EOF
|
||||
*self.stdin_writer.lock().unwrap() = None;
|
||||
// Give it a moment, then kill
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
*child_lock = None;
|
||||
*self.ready.lock().unwrap() = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_ready(&self) -> bool {
|
||||
*self.ready.lock().unwrap()
|
||||
}
|
||||
|
||||
fn resolve_sidecar_command(app: &AppHandle) -> Result<SidecarCommand, String> {
|
||||
let resource_dir = app
|
||||
.path()
|
||||
.resource_dir()
|
||||
.map_err(|e| format!("Failed to get resource dir: {e}"))?;
|
||||
|
||||
let dev_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
|
||||
// Try Deno first (runs TypeScript directly, no build step needed)
|
||||
let deno_paths = [
|
||||
resource_dir.join("sidecar").join("agent-runner-deno.ts"),
|
||||
dev_root.join("sidecar").join("agent-runner-deno.ts"),
|
||||
];
|
||||
|
||||
for path in &deno_paths {
|
||||
if path.exists() {
|
||||
// Check if deno is available
|
||||
if Command::new("deno").arg("--version").output().is_ok() {
|
||||
return Ok(SidecarCommand {
|
||||
program: "deno".to_string(),
|
||||
args: vec![
|
||||
"run".to_string(),
|
||||
"--allow-run".to_string(),
|
||||
"--allow-env".to_string(),
|
||||
"--allow-read".to_string(),
|
||||
path.to_string_lossy().to_string(),
|
||||
],
|
||||
});
|
||||
}
|
||||
log::warn!("Deno sidecar found at {} but deno not in PATH, falling back to Node.js", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to Node.js
|
||||
let node_paths = [
|
||||
resource_dir.join("sidecar").join("dist").join("agent-runner.mjs"),
|
||||
dev_root.join("sidecar").join("dist").join("agent-runner.mjs"),
|
||||
];
|
||||
|
||||
for path in &node_paths {
|
||||
if path.exists() {
|
||||
return Ok(SidecarCommand {
|
||||
program: "node".to_string(),
|
||||
args: vec![path.to_string_lossy().to_string()],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"Sidecar not found. Checked Deno ({}, {}) and Node.js ({}, {})",
|
||||
deno_paths[0].display(),
|
||||
deno_paths[1].display(),
|
||||
node_paths[0].display(),
|
||||
node_paths[1].display(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SidecarManager {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.shutdown();
|
||||
}
|
||||
}
|
||||
pub use bterminal_core::sidecar::{AgentQueryOptions, SidecarConfig, SidecarManager};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue