Merge spacesh M3: status detection sources + UI + native notifications
Claude Code hooks (per-surface CLAUDE_CONFIG_DIR), zsh OSC 133, fallback patterns → set_state; status rings, sidebar aggregate, Event Center, native macOS notifications, auto-unread. 87 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+10
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-notification": "^2",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -1330,6 +1331,15 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-notification": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-notification": "^2",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
Generated
+560
-1
@@ -47,6 +47,137 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-broadcast"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-channel"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||||
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-executor"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||||
|
dependencies = [
|
||||||
|
"async-task",
|
||||||
|
"concurrent-queue",
|
||||||
|
"fastrand",
|
||||||
|
"futures-lite",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-io"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"cfg-if",
|
||||||
|
"concurrent-queue",
|
||||||
|
"futures-io",
|
||||||
|
"futures-lite",
|
||||||
|
"parking",
|
||||||
|
"polling",
|
||||||
|
"rustix",
|
||||||
|
"slab",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-lock"
|
||||||
|
version = "3.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-process"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel",
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"async-signal",
|
||||||
|
"async-task",
|
||||||
|
"blocking",
|
||||||
|
"cfg-if",
|
||||||
|
"event-listener",
|
||||||
|
"futures-lite",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-recursion"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-signal"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
|
||||||
|
dependencies = [
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"atomic-waker",
|
||||||
|
"cfg-if",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"rustix",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"slab",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-task"
|
||||||
|
version = "4.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atk"
|
name = "atk"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
@@ -142,6 +273,19 @@ dependencies = [
|
|||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blocking"
|
||||||
|
version = "1.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel",
|
||||||
|
"async-task",
|
||||||
|
"futures-io",
|
||||||
|
"futures-lite",
|
||||||
|
"piper",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "8.0.3"
|
version = "8.0.3"
|
||||||
@@ -331,6 +475,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "concurrent-queue"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -723,6 +876,33 @@ version = "1.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "endi"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumflags2"
|
||||||
|
version = "0.7.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||||
|
dependencies = [
|
||||||
|
"enumflags2_derive",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumflags2_derive"
|
||||||
|
version = "0.7.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -750,6 +930,27 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener"
|
||||||
|
version = "5.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||||
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener-strategy"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@@ -877,6 +1078,19 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-lite"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -1242,6 +1456,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -1708,6 +1928,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -1729,6 +1955,18 @@ version = "0.4.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac-notification-sys"
|
||||||
|
version = "0.6.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50efa634682b3fc5a1ab6f3dd5b2bce7b848011fc485b53b063dc68f2f74feae"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
@@ -1833,6 +2071,20 @@ version = "1.0.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-rust"
|
||||||
|
version = "4.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50ff2e74231b72c832d82982193b417f230945be6bdb5575b251d941d31adb00"
|
||||||
|
dependencies = [
|
||||||
|
"futures-lite",
|
||||||
|
"log",
|
||||||
|
"mac-notification-sys",
|
||||||
|
"serde",
|
||||||
|
"tauri-winrt-notification",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -1993,6 +2245,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
@@ -2077,6 +2330,16 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-stream"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -2102,6 +2365,12 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -2190,6 +2459,17 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "piper"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"fastrand",
|
||||||
|
"futures-io",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.33"
|
version = "0.3.33"
|
||||||
@@ -2204,7 +2484,7 @@ checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
"quick-xml",
|
"quick-xml 0.39.4",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
@@ -2235,6 +2515,20 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polling"
|
||||||
|
version = "3.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"concurrent-queue",
|
||||||
|
"hermit-abi",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -2250,6 +2544,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "precomputed-hash"
|
name = "precomputed-hash"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2328,6 +2631,15 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.37.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.39.4"
|
version = "0.39.4"
|
||||||
@@ -2358,6 +2670,35 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -2493,6 +2834,19 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -2873,6 +3227,7 @@ dependencies = [
|
|||||||
"spacesh-proto",
|
"spacesh-proto",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-notification",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3159,6 +3514,41 @@ dependencies = [
|
|||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin"
|
||||||
|
version = "2.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"glob",
|
||||||
|
"plist",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri-utils",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-notification"
|
||||||
|
version = "2.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"notify-rust",
|
||||||
|
"rand",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.11.2"
|
version = "2.11.2"
|
||||||
@@ -3259,6 +3649,31 @@ dependencies = [
|
|||||||
"toml 1.1.2+spec-1.1.0",
|
"toml 1.1.2+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-winrt-notification"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
|
||||||
|
dependencies = [
|
||||||
|
"quick-xml 0.37.5",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"windows",
|
||||||
|
"windows-version",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.4.2",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tendril"
|
name = "tendril"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -3578,9 +3993,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.36"
|
version = "0.1.36"
|
||||||
@@ -3630,6 +4057,17 @@ version = "1.20.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uds_windows"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||||
|
dependencies = [
|
||||||
|
"memoffset",
|
||||||
|
"tempfile",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unic-char-property"
|
name = "unic-char-property"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -4656,6 +5094,87 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus"
|
||||||
|
version = "5.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285"
|
||||||
|
dependencies = [
|
||||||
|
"async-broadcast",
|
||||||
|
"async-executor",
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"async-process",
|
||||||
|
"async-recursion",
|
||||||
|
"async-task",
|
||||||
|
"async-trait",
|
||||||
|
"blocking",
|
||||||
|
"enumflags2",
|
||||||
|
"event-listener",
|
||||||
|
"futures-core",
|
||||||
|
"futures-lite",
|
||||||
|
"hex",
|
||||||
|
"libc",
|
||||||
|
"ordered-stream",
|
||||||
|
"rustix",
|
||||||
|
"serde",
|
||||||
|
"serde_repr",
|
||||||
|
"tracing",
|
||||||
|
"uds_windows",
|
||||||
|
"uuid",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
"winnow 1.0.3",
|
||||||
|
"zbus_macros",
|
||||||
|
"zbus_names",
|
||||||
|
"zvariant",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus_macros"
|
||||||
|
version = "5.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate 3.5.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
"zbus_names",
|
||||||
|
"zvariant",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus_names"
|
||||||
|
version = "4.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"winnow 1.0.3",
|
||||||
|
"zvariant",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.50"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.50"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
@@ -4715,3 +5234,43 @@ name = "zmij"
|
|||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant"
|
||||||
|
version = "5.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0"
|
||||||
|
dependencies = [
|
||||||
|
"endi",
|
||||||
|
"enumflags2",
|
||||||
|
"serde",
|
||||||
|
"winnow 1.0.3",
|
||||||
|
"zvariant_derive",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant_derive"
|
||||||
|
version = "5.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate 3.5.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant_utils"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde",
|
||||||
|
"syn 2.0.117",
|
||||||
|
"winnow 1.0.3",
|
||||||
|
]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ tauri-build = { version = "2", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-plugin-notification = "2"
|
||||||
spacesh-proto = { path = "../../crates/spacesh-proto" }
|
spacesh-proto = { path = "../../crates/spacesh-proto" }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"core:app:default",
|
"core:app:default",
|
||||||
"core:resources:default",
|
"core:resources:default",
|
||||||
"core:menu:default",
|
"core:menu:default",
|
||||||
"core:tray:default"
|
"core:tray:default",
|
||||||
|
"notification:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Default capability for spacesh app","local":true,"windows":["main"],"permissions":["core:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default"]}}
|
{"default":{"identifier":"default","description":"Default capability for spacesh app","local":true,"windows":["main"],"permissions":["core:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","notification:default"]}}
|
||||||
@@ -2191,6 +2191,204 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:window:deny-unminimize",
|
"const": "core:window:deny-unminimize",
|
||||||
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:default",
|
||||||
|
"markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the batch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-batch",
|
||||||
|
"markdownDescription": "Enables the batch command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the cancel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-cancel",
|
||||||
|
"markdownDescription": "Enables the cancel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-check-permissions",
|
||||||
|
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the create_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-create-channel",
|
||||||
|
"markdownDescription": "Enables the create_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the delete_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-delete-channel",
|
||||||
|
"markdownDescription": "Enables the delete_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the get_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-get-active",
|
||||||
|
"markdownDescription": "Enables the get_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the get_pending command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-get-pending",
|
||||||
|
"markdownDescription": "Enables the get_pending command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the is_permission_granted command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-is-permission-granted",
|
||||||
|
"markdownDescription": "Enables the is_permission_granted command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the list_channels command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-list-channels",
|
||||||
|
"markdownDescription": "Enables the list_channels command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the notify command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-notify",
|
||||||
|
"markdownDescription": "Enables the notify command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the permission_state command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-permission-state",
|
||||||
|
"markdownDescription": "Enables the permission_state command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_action_types command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-register-action-types",
|
||||||
|
"markdownDescription": "Enables the register_action_types command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-register-listener",
|
||||||
|
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the remove_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-remove-active",
|
||||||
|
"markdownDescription": "Enables the remove_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the request_permission command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-request-permission",
|
||||||
|
"markdownDescription": "Enables the request_permission command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the show command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-show",
|
||||||
|
"markdownDescription": "Enables the show command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the batch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-batch",
|
||||||
|
"markdownDescription": "Denies the batch command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the cancel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-cancel",
|
||||||
|
"markdownDescription": "Denies the cancel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-check-permissions",
|
||||||
|
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the create_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-create-channel",
|
||||||
|
"markdownDescription": "Denies the create_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the delete_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-delete-channel",
|
||||||
|
"markdownDescription": "Denies the delete_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the get_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-get-active",
|
||||||
|
"markdownDescription": "Denies the get_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the get_pending command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-get-pending",
|
||||||
|
"markdownDescription": "Denies the get_pending command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the is_permission_granted command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-is-permission-granted",
|
||||||
|
"markdownDescription": "Denies the is_permission_granted command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the list_channels command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-list-channels",
|
||||||
|
"markdownDescription": "Denies the list_channels command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the notify command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-notify",
|
||||||
|
"markdownDescription": "Denies the notify command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the permission_state command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-permission-state",
|
||||||
|
"markdownDescription": "Denies the permission_state command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_action_types command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-register-action-types",
|
||||||
|
"markdownDescription": "Denies the register_action_types command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-register-listener",
|
||||||
|
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the remove_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-remove-active",
|
||||||
|
"markdownDescription": "Denies the remove_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the request_permission command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-request-permission",
|
||||||
|
"markdownDescription": "Denies the request_permission command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the show command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-show",
|
||||||
|
"markdownDescription": "Denies the show command without any pre-configured scope."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2191,6 +2191,204 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:window:deny-unminimize",
|
"const": "core:window:deny-unminimize",
|
||||||
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:default",
|
||||||
|
"markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the batch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-batch",
|
||||||
|
"markdownDescription": "Enables the batch command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the cancel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-cancel",
|
||||||
|
"markdownDescription": "Enables the cancel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-check-permissions",
|
||||||
|
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the create_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-create-channel",
|
||||||
|
"markdownDescription": "Enables the create_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the delete_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-delete-channel",
|
||||||
|
"markdownDescription": "Enables the delete_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the get_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-get-active",
|
||||||
|
"markdownDescription": "Enables the get_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the get_pending command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-get-pending",
|
||||||
|
"markdownDescription": "Enables the get_pending command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the is_permission_granted command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-is-permission-granted",
|
||||||
|
"markdownDescription": "Enables the is_permission_granted command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the list_channels command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-list-channels",
|
||||||
|
"markdownDescription": "Enables the list_channels command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the notify command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-notify",
|
||||||
|
"markdownDescription": "Enables the notify command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the permission_state command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-permission-state",
|
||||||
|
"markdownDescription": "Enables the permission_state command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_action_types command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-register-action-types",
|
||||||
|
"markdownDescription": "Enables the register_action_types command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-register-listener",
|
||||||
|
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the remove_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-remove-active",
|
||||||
|
"markdownDescription": "Enables the remove_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the request_permission command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-request-permission",
|
||||||
|
"markdownDescription": "Enables the request_permission command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the show command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-show",
|
||||||
|
"markdownDescription": "Enables the show command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the batch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-batch",
|
||||||
|
"markdownDescription": "Denies the batch command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the cancel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-cancel",
|
||||||
|
"markdownDescription": "Denies the cancel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-check-permissions",
|
||||||
|
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the create_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-create-channel",
|
||||||
|
"markdownDescription": "Denies the create_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the delete_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-delete-channel",
|
||||||
|
"markdownDescription": "Denies the delete_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the get_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-get-active",
|
||||||
|
"markdownDescription": "Denies the get_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the get_pending command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-get-pending",
|
||||||
|
"markdownDescription": "Denies the get_pending command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the is_permission_granted command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-is-permission-granted",
|
||||||
|
"markdownDescription": "Denies the is_permission_granted command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the list_channels command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-list-channels",
|
||||||
|
"markdownDescription": "Denies the list_channels command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the notify command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-notify",
|
||||||
|
"markdownDescription": "Denies the notify command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the permission_state command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-permission-state",
|
||||||
|
"markdownDescription": "Denies the permission_state command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_action_types command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-register-action-types",
|
||||||
|
"markdownDescription": "Denies the register_action_types command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-register-listener",
|
||||||
|
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the remove_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-remove-active",
|
||||||
|
"markdownDescription": "Denies the remove_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the request_permission command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-request-permission",
|
||||||
|
"markdownDescription": "Denies the request_permission command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the show command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-show",
|
||||||
|
"markdownDescription": "Denies the show command without any pre-configured scope."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -255,3 +255,8 @@ pub async fn set_group(state: BridgeState<'_>, group_id: String, name: Option<St
|
|||||||
pub async fn delete_group(state: BridgeState<'_>, group_id: String) -> Result<Value, String> {
|
pub async fn delete_group(state: BridgeState<'_>, group_id: String) -> Result<Value, String> {
|
||||||
data_of(state.request(Cmd::DeleteGroup { group_id: GroupId(group_id) }).await.map_err(|e| e.to_string())?)
|
data_of(state.request(Cmd::DeleteGroup { group_id: GroupId(group_id) }).await.map_err(|e| e.to_string())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn focus(state: BridgeState<'_>, surface_id: String) -> Result<Value, String> {
|
||||||
|
data_of(state.request(Cmd::Focus { surface_id: SurfaceId(surface_id) }).await.map_err(|e| e.to_string())?)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use tauri::Manager;
|
|||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
// Connect the bridge on a tokio runtime, then manage it.
|
// Connect the bridge on a tokio runtime, then manage it.
|
||||||
@@ -35,6 +36,7 @@ pub fn run() {
|
|||||||
bridge::create_group,
|
bridge::create_group,
|
||||||
bridge::set_group,
|
bridge::set_group,
|
||||||
bridge::delete_group,
|
bridge::delete_group,
|
||||||
|
bridge::focus,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running spacesh");
|
.expect("error while running spacesh");
|
||||||
|
|||||||
+50
-9
@@ -1,40 +1,80 @@
|
|||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback, useRef } from "react";
|
||||||
import { LayoutEngine } from "./LayoutEngine";
|
import { LayoutEngine } from "./LayoutEngine";
|
||||||
import { Sidebar } from "./Sidebar";
|
import { Sidebar } from "./Sidebar";
|
||||||
import { PresetPicker } from "./PresetPicker";
|
import { PresetPicker } from "./PresetPicker";
|
||||||
import { Wizard } from "./Wizard";
|
import { Wizard } from "./Wizard";
|
||||||
import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent } from "./socketBridge";
|
import { EventCenter, type FeedEntry } from "./EventCenter";
|
||||||
import type { Group, WorkspaceView } from "./layoutTypes";
|
import { maybeNotify } from "./notify";
|
||||||
|
import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface } from "./socketBridge";
|
||||||
|
import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [groups, setGroups] = useState<Group[]>([]);
|
const [groups, setGroups] = useState<Group[]>([]);
|
||||||
const [workspaces, setWorkspaces] = useState<WorkspaceView[]>([]);
|
const [workspaces, setWorkspaces] = useState<WorkspaceView[]>([]);
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
const [running, setRunning] = useState<Record<string, boolean>>({});
|
const [running, setRunning] = useState<Record<string, boolean>>({});
|
||||||
|
const [states, setStates] = useState<Record<string, SurfaceState>>({});
|
||||||
|
const [feed, setFeed] = useState<FeedEntry[]>([]);
|
||||||
const [wizard, setWizard] = useState(false);
|
const [wizard, setWizard] = useState(false);
|
||||||
|
const feedId = useRef(0);
|
||||||
|
const activeRef = useRef<string | null>(null);
|
||||||
|
const wsRef = useRef<WorkspaceView[]>([]);
|
||||||
|
activeRef.current = activeId;
|
||||||
|
wsRef.current = workspaces;
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
const st = await getStatusFull();
|
const st = await getStatusFull();
|
||||||
setGroups(st.groups);
|
setGroups(st.groups);
|
||||||
setWorkspaces(st.workspaces);
|
setWorkspaces(st.workspaces);
|
||||||
const run: Record<string, boolean> = {};
|
const run: Record<string, boolean> = {};
|
||||||
st.workspaces.forEach((w) => Object.entries(w.surfaces).forEach(([id, sv]) => { run[id] = sv.running; }));
|
const stt: Record<string, SurfaceState> = {};
|
||||||
|
st.workspaces.forEach((w) => Object.entries(w.surfaces).forEach(([id, sv]) => { run[id] = sv.running; stt[id] = sv.state; }));
|
||||||
setRunning(run);
|
setRunning(run);
|
||||||
if (!activeId && st.workspaces.length) setActiveId(st.workspaces[0].id);
|
setStates(stt);
|
||||||
}, [activeId]);
|
if (!activeRef.current && st.workspaces.length) setActiveId(st.workspaces[0].id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const wsOf = (surfaceId: string): WorkspaceView | undefined =>
|
||||||
|
wsRef.current.find((w) => surfaceId in w.surfaces);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refresh();
|
void refresh();
|
||||||
const unlisten = onDaemonEvent(() => { void refresh(); });
|
const unlisten = onDaemonEvent((evt) => {
|
||||||
|
if (evt.evt === "state") {
|
||||||
|
const { surface_id, state } = evt.data;
|
||||||
|
setStates((m) => ({ ...m, [surface_id]: state }));
|
||||||
|
const w = wsOf(surface_id);
|
||||||
|
const agent = w?.surfaces[surface_id]?.spec.agent_label ?? "shell";
|
||||||
|
if (["done", "wait", "error"].includes(state)) {
|
||||||
|
const entry: FeedEntry = { id: feedId.current++, surfaceId: surface_id, workspace: w?.name ?? "?", agent, kind: state, time: "now" };
|
||||||
|
setFeed((f) => [entry, ...f].slice(0, 200));
|
||||||
|
if (w && w.id !== activeRef.current) void setWorkspaceMeta(w.id, { unread: true });
|
||||||
|
void maybeNotify(surface_id, agent, w?.name ?? "?", state);
|
||||||
|
}
|
||||||
|
void refresh();
|
||||||
|
} else if (evt.evt === "exit") {
|
||||||
|
const w = wsOf(evt.data.surface_id);
|
||||||
|
const exitEntry: FeedEntry = { id: feedId.current++, surfaceId: evt.data.surface_id, workspace: w?.name ?? "?", agent: w?.surfaces[evt.data.surface_id]?.spec.agent_label ?? "shell", kind: "exit", time: "now" };
|
||||||
|
setFeed((f) => [exitEntry, ...f].slice(0, 200));
|
||||||
|
void refresh();
|
||||||
|
} else {
|
||||||
|
void refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
const reconnect = onDaemonRawEvent("spacesh:disconnected", () => { void refresh(); });
|
const reconnect = onDaemonRawEvent("spacesh:disconnected", () => { void refresh(); });
|
||||||
return () => { void unlisten.then((f) => f()); void reconnect.then((f) => f()); };
|
return () => { void unlisten.then((f) => f()); void reconnect.then((f) => f()); };
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
const active = workspaces.find((w) => w.id === activeId) ?? null;
|
const active = workspaces.find((w) => w.id === activeId) ?? null;
|
||||||
|
|
||||||
|
function selectWorkspace(id: string) {
|
||||||
|
setActiveId(id);
|
||||||
|
void setWorkspaceMeta(id, { unread: false });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", height: "100vh", background: "#0E1116" }}>
|
<div style={{ display: "flex", height: "100vh", background: "#0E1116" }}>
|
||||||
<Sidebar groups={groups} workspaces={workspaces} activeId={activeId} onSelect={setActiveId} onNew={() => setWizard(true)} />
|
<Sidebar groups={groups} workspaces={workspaces} activeId={activeId} onSelect={selectWorkspace} onNew={() => setWizard(true)} />
|
||||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||||
{active && (
|
{active && (
|
||||||
<div style={{ padding: 8, borderBottom: "1px solid #232A33" }}>
|
<div style={{ padding: 8, borderBottom: "1px solid #232A33" }}>
|
||||||
@@ -43,10 +83,11 @@ export function App() {
|
|||||||
)}
|
)}
|
||||||
<div style={{ flex: 1, minHeight: 0 }}>
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
{active
|
{active
|
||||||
? <LayoutEngine workspaceId={active.id} layout={active.layout} running={running} />
|
? <LayoutEngine workspaceId={active.id} layout={active.layout} running={running} states={states} />
|
||||||
: <div style={{ color: "#666", padding: 24 }}>No workspace — create one to begin.</div>}
|
: <div style={{ color: "#666", padding: 24 }}>No workspace — create one to begin.</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<EventCenter feed={feed} onMarkRead={() => setFeed([])} onSelect={(sid) => { void focusSurface(sid); }} />
|
||||||
{wizard && <Wizard onDone={(id) => { setWizard(false); setActiveId(id); void refresh(); }} onCancel={() => setWizard(false)} />}
|
{wizard && <Wizard onDone={(id) => { setWizard(false); setActiveId(id); void refresh(); }} onCancel={() => setWizard(false)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { SurfaceState } from "./layoutTypes";
|
||||||
|
|
||||||
|
export interface FeedEntry {
|
||||||
|
id: number;
|
||||||
|
surfaceId: string;
|
||||||
|
workspace: string;
|
||||||
|
agent: string;
|
||||||
|
kind: SurfaceState | "exit";
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ICON: Record<string, string> = { done: "✓", wait: "⌛", error: "✕", work: "●", idle: "·", exit: "⏻" };
|
||||||
|
const COLOR: Record<string, string> = { done: "#3FB950", wait: "#F2B84B", error: "#F4544E", work: "#4C8DFF", idle: "#5A6573", exit: "#5A6573" };
|
||||||
|
|
||||||
|
export function EventCenter({ feed, onMarkRead, onSelect }: { feed: FeedEntry[]; onMarkRead: () => void; onSelect: (surfaceId: string) => void }) {
|
||||||
|
return (
|
||||||
|
<div style={{ width: 300, background: "#13171F", height: "100%", padding: 14, boxSizing: "border-box", display: "flex", flexDirection: "column", borderLeft: "1px solid #232A33" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", marginBottom: 12 }}>
|
||||||
|
<span style={{ fontFamily: "Inter", fontSize: 13, fontWeight: 700, color: "#E6EDF3", flex: 1 }}>Event Center</span>
|
||||||
|
<span onClick={onMarkRead} style={{ fontSize: 11, color: "#4C8DFF", cursor: "pointer" }}>Mark all read</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
|
{feed.length === 0 && <div style={{ color: "#5A6573", fontSize: 12 }}>No events yet.</div>}
|
||||||
|
{feed.map((e) => (
|
||||||
|
<div key={e.id} onClick={() => onSelect(e.surfaceId)}
|
||||||
|
style={{ display: "flex", gap: 9, padding: 10, borderRadius: 8, border: "1px solid #232A33", cursor: "pointer" }}>
|
||||||
|
<span style={{ color: COLOR[e.kind] }}>{ICON[e.kind]}</span>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontFamily: "monospace", fontSize: 11, color: "#8B97A6" }}>{e.workspace} · {e.agent}</div>
|
||||||
|
<div style={{ fontFamily: "Inter", fontSize: 12, color: "#E6EDF3" }}>{e.kind} <span style={{ color: "#5A6573" }}>{e.time}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { TerminalView } from "./TerminalView";
|
import { TerminalView } from "./TerminalView";
|
||||||
import type { LayoutNode } from "./layoutTypes";
|
import { StatusRing } from "./StatusRing";
|
||||||
|
import type { LayoutNode, SurfaceState } from "./layoutTypes";
|
||||||
import { setRatios, restartSurface } from "./socketBridge";
|
import { setRatios, restartSurface } from "./socketBridge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -8,16 +9,17 @@ interface Props {
|
|||||||
layout: LayoutNode | null;
|
layout: LayoutNode | null;
|
||||||
/** surface_id -> running flag, from the latest status/events. */
|
/** surface_id -> running flag, from the latest status/events. */
|
||||||
running: Record<string, boolean>;
|
running: Record<string, boolean>;
|
||||||
|
states: Record<string, SurfaceState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LayoutEngine({ workspaceId, layout, running }: Props) {
|
export function LayoutEngine({ workspaceId, layout, running, states }: Props) {
|
||||||
if (!layout) {
|
if (!layout) {
|
||||||
return <div style={{ color: "#666", padding: 24 }}>Empty workspace — apply a preset to add panels.</div>;
|
return <div style={{ color: "#666", padding: 24 }}>Empty workspace — apply a preset to add panels.</div>;
|
||||||
}
|
}
|
||||||
return <Node workspaceId={workspaceId} node={layout} path={[]} running={running} />;
|
return <Node workspaceId={workspaceId} node={layout} path={[]} running={running} states={states} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Node({ workspaceId, node, path, running }: { workspaceId: string; node: LayoutNode; path: number[]; running: Record<string, boolean> }) {
|
function Node({ workspaceId, node, path, running, states }: { workspaceId: string; node: LayoutNode; path: number[]; running: Record<string, boolean>; states: Record<string, SurfaceState> }) {
|
||||||
if ("leaf" in node) {
|
if ("leaf" in node) {
|
||||||
const id = node.leaf.surface_id;
|
const id = node.leaf.surface_id;
|
||||||
if (running[id] === false) {
|
if (running[id] === false) {
|
||||||
@@ -28,7 +30,17 @@ function Node({ workspaceId, node, path, running }: { workspaceId: string; node:
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <TerminalView key={id} surfaceId={id} />;
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", width: "100%", height: "100%" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 7, padding: "3px 8px", background: "#0A0D12", borderBottom: "1px solid #232A33" }}>
|
||||||
|
<StatusRing state={states[id] ?? "idle"} running={true} />
|
||||||
|
<span style={{ fontFamily: "monospace", fontSize: 11, color: "#8B97A6" }}>{id}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
|
<TerminalView key={id} surfaceId={id} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orient, ratios, children } = node.split;
|
const { orient, ratios, children } = node.split;
|
||||||
@@ -43,7 +55,7 @@ function Node({ workspaceId, node, path, running }: { workspaceId: string; node:
|
|||||||
next[i + 1] = Math.max(0.05, (next[i + 1] ?? 1) - deltaFrac);
|
next[i + 1] = Math.max(0.05, (next[i + 1] ?? 1) - deltaFrac);
|
||||||
void setRatios(workspaceId, path, next);
|
void setRatios(workspaceId, path, next);
|
||||||
}}>
|
}}>
|
||||||
<Node workspaceId={workspaceId} node={child} path={[...path, i]} running={running} />
|
<Node workspaceId={workspaceId} node={child} path={[...path, i]} running={running} states={states} />
|
||||||
</Pane>
|
</Pane>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+16
-2
@@ -1,4 +1,18 @@
|
|||||||
import type { Group, WorkspaceView } from "./layoutTypes";
|
import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes";
|
||||||
|
|
||||||
|
const RING: Record<SurfaceState | "stopped", string> = {
|
||||||
|
error: "#F4544E", wait: "#F2B84B", work: "#4C8DFF", done: "#3FB950", idle: "#5A6573", stopped: "#5A6573",
|
||||||
|
};
|
||||||
|
|
||||||
|
function aggregate(w: WorkspaceView): SurfaceState | "stopped" {
|
||||||
|
const order: SurfaceState[] = ["error", "wait", "work", "done", "idle"];
|
||||||
|
const running = Object.values(w.surfaces).filter((s) => s.running);
|
||||||
|
if (running.length === 0) return "stopped";
|
||||||
|
for (const st of order) {
|
||||||
|
if (running.some((s) => s.state === st)) return st;
|
||||||
|
}
|
||||||
|
return "idle";
|
||||||
|
}
|
||||||
|
|
||||||
export function Sidebar({
|
export function Sidebar({
|
||||||
groups, workspaces, activeId, onSelect, onNew,
|
groups, workspaces, activeId, onSelect, onNew,
|
||||||
@@ -19,7 +33,7 @@ export function Sidebar({
|
|||||||
background: w.id === activeId ? "#1A2029" : "transparent", fontFamily: "Inter", fontSize: 13,
|
background: w.id === activeId ? "#1A2029" : "transparent", fontFamily: "Inter", fontSize: 13,
|
||||||
color: w.id === activeId ? "#E6EDF3" : "#8B97A6",
|
color: w.id === activeId ? "#E6EDF3" : "#8B97A6",
|
||||||
}}>
|
}}>
|
||||||
<span style={{ width: 10, height: 10, borderRadius: "50%", border: "2px solid #5A6573" }} />
|
<span style={{ width: 10, height: 10, borderRadius: "50%", border: `2px solid ${RING[aggregate(w)]}`, boxSizing: "border-box" }} />
|
||||||
<span style={{ flex: 1 }}>{w.name}</span>
|
<span style={{ flex: 1 }}>{w.name}</span>
|
||||||
{w.unread && <span style={{ width: 7, height: 7, borderRadius: "50%", background: "#4C8DFF" }} />}
|
{w.unread && <span style={{ width: 7, height: 7, borderRadius: "50%", background: "#4C8DFF" }} />}
|
||||||
<span style={{ fontFamily: "monospace", fontSize: 11, color: "#5A6573" }}>{Object.keys(w.surfaces).length}</span>
|
<span style={{ fontFamily: "monospace", fontSize: 11, color: "#5A6573" }}>{Object.keys(w.surfaces).length}</span>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { SurfaceState } from "./layoutTypes";
|
||||||
|
|
||||||
|
const COLOR: Record<SurfaceState, string> = {
|
||||||
|
work: "#4C8DFF",
|
||||||
|
wait: "#F2B84B",
|
||||||
|
done: "#3FB950",
|
||||||
|
error: "#F4544E",
|
||||||
|
idle: "#5A6573",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StatusRing({ state, running }: { state: SurfaceState; running: boolean }) {
|
||||||
|
const color = running ? COLOR[state] : "#5A6573";
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
title={running ? state : "stopped"}
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: `2px solid ${color}`,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
opacity: running ? 1 : 0.5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
export type Orient = "h" | "v";
|
export type Orient = "h" | "v";
|
||||||
|
|
||||||
|
export type SurfaceState = "work" | "wait" | "done" | "error" | "idle";
|
||||||
|
|
||||||
export type LayoutNode =
|
export type LayoutNode =
|
||||||
| { leaf: { surface_id: string } }
|
| { leaf: { surface_id: string } }
|
||||||
| { split: { orient: Orient; ratios: number[]; children: LayoutNode[] } };
|
| { split: { orient: Orient; ratios: number[]; children: LayoutNode[] } };
|
||||||
@@ -15,6 +17,7 @@ export interface SurfaceView {
|
|||||||
autostart: boolean;
|
autostart: boolean;
|
||||||
};
|
};
|
||||||
running: boolean;
|
running: boolean;
|
||||||
|
state: SurfaceState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Group {
|
export interface Group {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { isPermissionGranted, requestPermission, sendNotification } from "@tauri-apps/plugin-notification";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import type { SurfaceState } from "./layoutTypes";
|
||||||
|
|
||||||
|
const NOTIFY_STATES: SurfaceState[] = ["done", "wait", "error"];
|
||||||
|
let lastBySurface: Record<string, SurfaceState> = {};
|
||||||
|
|
||||||
|
/// Fire a native notification for a status change when the window is unfocused.
|
||||||
|
export async function maybeNotify(surfaceId: string, agent: string, workspace: string, state: SurfaceState): Promise<void> {
|
||||||
|
if (!NOTIFY_STATES.includes(state)) return;
|
||||||
|
if (lastBySurface[surfaceId] === state) return; // dedup repeats
|
||||||
|
lastBySurface[surfaceId] = state;
|
||||||
|
|
||||||
|
const focused = await getCurrentWindow().isFocused().catch(() => true);
|
||||||
|
if (focused) return;
|
||||||
|
|
||||||
|
let granted = await isPermissionGranted();
|
||||||
|
if (!granted) granted = (await requestPermission()) === "granted";
|
||||||
|
if (!granted) return;
|
||||||
|
|
||||||
|
sendNotification({ title: `${workspace} · ${agent}`, body: `${state}` });
|
||||||
|
}
|
||||||
@@ -65,12 +65,20 @@ export async function getStatus(): Promise<WorkspaceStatus[]> {
|
|||||||
export type DaemonEvt =
|
export type DaemonEvt =
|
||||||
| { evt: "exit"; data: { surface_id: string; code: number } }
|
| { evt: "exit"; data: { surface_id: string; code: number } }
|
||||||
| { evt: "surface_created"; data: { surface_id: string; workspace_id: string } }
|
| { evt: "surface_created"; data: { surface_id: string; workspace_id: string } }
|
||||||
| { evt: "surface_closed"; data: { surface_id: string } };
|
| { evt: "surface_closed"; data: { surface_id: string } }
|
||||||
|
| { evt: "state"; data: { surface_id: string; state: import("./layoutTypes").SurfaceState } }
|
||||||
|
| { evt: "layout_changed"; data: { workspace_id: string } }
|
||||||
|
| { evt: "workspace_changed"; data: unknown }
|
||||||
|
| { evt: "groups_changed"; data: unknown };
|
||||||
|
|
||||||
export function onDaemonEvent(handler: (evt: DaemonEvt) => void): Promise<() => void> {
|
export function onDaemonEvent(handler: (evt: DaemonEvt) => void): Promise<() => void> {
|
||||||
return listen<DaemonEvt>("spacesh:evt", (e) => handler(e.payload));
|
return listen<DaemonEvt>("spacesh:evt", (e) => handler(e.payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function focusSurface(surfaceId: string): Promise<void> {
|
||||||
|
await invoke("focus", { surfaceId });
|
||||||
|
}
|
||||||
|
|
||||||
export function onDaemonRawEvent(name: string, handler: () => void): Promise<() => void> {
|
export function onDaemonRawEvent(name: string, handler: () => void): Promise<() => void> {
|
||||||
return listen(name, () => handler());
|
return listen(name, () => handler());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
//! Pure status detectors over terminal output. No I/O.
|
||||||
|
use spacesh_proto::status::SurfaceState;
|
||||||
|
|
||||||
|
/// Scans a byte stream for OSC 133 semantic-prompt markers and yields the
|
||||||
|
/// status each marker implies. Robust to escape sequences split across feeds:
|
||||||
|
/// an incomplete trailing marker is buffered until the next feed.
|
||||||
|
///
|
||||||
|
/// Markers: ESC ] 133 ; A ST (prompt) → Idle; ; C ST (command output) → Work;
|
||||||
|
/// ; D [;exit] ST (command end) → Done (exit 0) / Error (exit != 0).
|
||||||
|
/// ST is BEL (0x07) or ESC \ (0x1b 0x5c). The `B` marker (input start) is ignored.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Osc133Scanner {
|
||||||
|
buf: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Osc133Scanner {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn feed(&mut self, bytes: &[u8]) -> Vec<SurfaceState> {
|
||||||
|
self.buf.extend_from_slice(bytes);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let prefix: &[u8] = b"\x1b]133;";
|
||||||
|
loop {
|
||||||
|
// Find the next marker start.
|
||||||
|
let Some(start) = find(&self.buf, prefix) else {
|
||||||
|
// No marker start. Keep only a possible partial prefix at the tail.
|
||||||
|
self.buf = keep_partial_tail(&self.buf, prefix);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
// Drop anything before the marker start.
|
||||||
|
if start > 0 {
|
||||||
|
self.buf.drain(0..start);
|
||||||
|
}
|
||||||
|
// After the prefix, find the terminator (BEL or ESC \).
|
||||||
|
let body_start = prefix.len();
|
||||||
|
let Some((body_end, term_len)) = find_terminator(&self.buf, body_start) else {
|
||||||
|
break; // incomplete marker; wait for more bytes
|
||||||
|
};
|
||||||
|
let body = &self.buf[body_start..body_end];
|
||||||
|
if let Some(state) = classify(body) {
|
||||||
|
out.push(state);
|
||||||
|
}
|
||||||
|
// Consume through the terminator.
|
||||||
|
self.buf.drain(0..body_end + term_len);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify the `133;` body (e.g. `A`, `C`, `D`, `D;0`, `D;1`).
|
||||||
|
fn classify(body: &[u8]) -> Option<SurfaceState> {
|
||||||
|
let s = std::str::from_utf8(body).ok()?;
|
||||||
|
let mut parts = s.split(';');
|
||||||
|
match parts.next()? {
|
||||||
|
"C" => Some(SurfaceState::Work),
|
||||||
|
"A" => Some(SurfaceState::Idle),
|
||||||
|
"D" => {
|
||||||
|
// exit code is the next part, if present.
|
||||||
|
match parts.next() {
|
||||||
|
Some(code) if code != "0" && !code.is_empty() => Some(SurfaceState::Error),
|
||||||
|
_ => Some(SurfaceState::Done),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None, // B and others: no status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
|
||||||
|
if needle.is_empty() || hay.len() < needle.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
hay.windows(needle.len()).position(|w| w == needle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Terminator search from `from`: returns (index_of_terminator, terminator_len).
|
||||||
|
fn find_terminator(hay: &[u8], from: usize) -> Option<(usize, usize)> {
|
||||||
|
let mut i = from;
|
||||||
|
while i < hay.len() {
|
||||||
|
if hay[i] == 0x07 {
|
||||||
|
return Some((i, 1));
|
||||||
|
}
|
||||||
|
if hay[i] == 0x1b && i + 1 < hay.len() && hay[i + 1] == 0x5c {
|
||||||
|
return Some((i, 2));
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep only the longest suffix of `buf` that is a strict prefix of `needle`
|
||||||
|
/// (a possibly-incomplete marker start), so it can complete on the next feed.
|
||||||
|
fn keep_partial_tail(buf: &[u8], needle: &[u8]) -> Vec<u8> {
|
||||||
|
let max = needle.len().saturating_sub(1).min(buf.len());
|
||||||
|
for n in (1..=max).rev() {
|
||||||
|
let tail = &buf[buf.len() - n..];
|
||||||
|
if needle.starts_with(tail) {
|
||||||
|
return tail.to_vec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stateless best-effort heuristics over a window of recent terminal text.
|
||||||
|
pub struct FallbackScanner;
|
||||||
|
|
||||||
|
impl FallbackScanner {
|
||||||
|
/// Returns a status implied by the tail text, or None for "no change".
|
||||||
|
pub fn scan(text: &str) -> Option<SurfaceState> {
|
||||||
|
let tail = text.trim_end();
|
||||||
|
let last_line = tail.lines().last().unwrap_or("");
|
||||||
|
// Confirmation / input prompts → waiting for the user.
|
||||||
|
let wait_markers = ["(y/n)", "(Y/n)", "(y/N)", "[y/N]", "[Y/n]", "Press enter", "press enter", "❯ 1.", "? "];
|
||||||
|
if wait_markers.iter().any(|m| last_line.contains(m)) {
|
||||||
|
return Some(SurfaceState::Wait);
|
||||||
|
}
|
||||||
|
// Spinner glyphs at the tail → working.
|
||||||
|
let spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏', '|', '/', '-', '\\'];
|
||||||
|
if let Some(c) = last_line.chars().rev().find(|c| !c.is_whitespace()) {
|
||||||
|
if spinners.contains(&c) {
|
||||||
|
return Some(SurfaceState::Work);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc133_c_then_d0_gives_work_then_done() {
|
||||||
|
let mut s = Osc133Scanner::new();
|
||||||
|
let evs = s.feed(b"\x1b]133;C\x07hello\x1b]133;D;0\x07");
|
||||||
|
assert_eq!(evs, vec![SurfaceState::Work, SurfaceState::Done]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc133_d_nonzero_is_error() {
|
||||||
|
let mut s = Osc133Scanner::new();
|
||||||
|
assert_eq!(s.feed(b"\x1b]133;D;1\x07"), vec![SurfaceState::Error]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc133_a_is_idle_and_st_can_be_esc_backslash() {
|
||||||
|
let mut s = Osc133Scanner::new();
|
||||||
|
assert_eq!(s.feed(b"\x1b]133;A\x1b\\"), vec![SurfaceState::Idle]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc133_split_across_feeds_is_buffered() {
|
||||||
|
let mut s = Osc133Scanner::new();
|
||||||
|
assert_eq!(s.feed(b"\x1b]133;C"), vec![]); // no terminator yet
|
||||||
|
assert_eq!(s.feed(b"\x07"), vec![SurfaceState::Work]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc133_split_in_prefix_is_buffered() {
|
||||||
|
let mut s = Osc133Scanner::new();
|
||||||
|
assert_eq!(s.feed(b"text\x1b]13"), vec![]); // partial prefix retained
|
||||||
|
assert_eq!(s.feed(b"3;C\x07"), vec![SurfaceState::Work]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc133_ignores_plain_text() {
|
||||||
|
let mut s = Osc133Scanner::new();
|
||||||
|
assert_eq!(s.feed(b"just some output\n"), vec![]);
|
||||||
|
assert!(s.feed(b"more\n").is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fallback_detects_confirmation_and_spinner() {
|
||||||
|
assert_eq!(FallbackScanner::scan("Apply changes? (y/n)"), Some(SurfaceState::Wait));
|
||||||
|
assert_eq!(FallbackScanner::scan("building ⠹"), Some(SurfaceState::Work));
|
||||||
|
assert_eq!(FallbackScanner::scan("normal output"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,6 +59,23 @@ impl GridSurface {
|
|||||||
pub fn term(&self) -> &Term<VoidListener> {
|
pub fn term(&self) -> &Term<VoidListener> {
|
||||||
&self.term
|
&self.term
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The visible grid as text — the last `lines` rows, trailing blanks trimmed.
|
||||||
|
/// Used by the fallback detector.
|
||||||
|
pub fn tail_text(&self, lines: usize) -> String {
|
||||||
|
let size = self.size();
|
||||||
|
let start = size.lines.saturating_sub(lines);
|
||||||
|
let mut out = String::new();
|
||||||
|
for line in start..size.lines {
|
||||||
|
let mut row = String::new();
|
||||||
|
for col in 0..size.cols {
|
||||||
|
row.push(self.char_at(line, col));
|
||||||
|
}
|
||||||
|
out.push_str(row.trim_end());
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
pub mod detect;
|
||||||
pub mod grid;
|
pub mod grid;
|
||||||
pub mod ops;
|
pub mod ops;
|
||||||
pub mod presets;
|
pub mod presets;
|
||||||
pub mod snapshot;
|
pub mod snapshot;
|
||||||
|
|
||||||
|
pub use detect::{FallbackScanner, Osc133Scanner};
|
||||||
pub use grid::GridSurface;
|
pub use grid::GridSurface;
|
||||||
pub use snapshot::Snapshot;
|
pub use snapshot::Snapshot;
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
//! Versioned Claude Code hook adapter. For a Claude agent surface, writes a
|
||||||
|
//! per-surface CLAUDE_CONFIG_DIR with hooks that call `spacesh notify`, and
|
||||||
|
//! returns the env to inject. Isolated so hook-format drift is a local fix.
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use spacesh_proto::ids::SurfaceId;
|
||||||
|
|
||||||
|
/// Is this command a Claude Code agent we should hook? (heuristic)
|
||||||
|
pub fn is_agent(command: &str, agent_label: Option<&str>) -> bool {
|
||||||
|
let base = std::path::Path::new(command)
|
||||||
|
.file_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or(command);
|
||||||
|
base == "claude" || agent_label == Some("claude")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-surface config dir under ~/.spacesh/hooks/<surface_id>.
|
||||||
|
fn dir_for(home: &PathBuf, sid: &SurfaceId) -> PathBuf {
|
||||||
|
home.join(".spacesh").join("hooks").join(&sid.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the settings.json contents wiring Stop/Notification/UserPromptSubmit
|
||||||
|
/// to `spacesh notify`. `spacesh_bin` is the absolute path to the CLI.
|
||||||
|
pub fn settings_json(spacesh_bin: &str) -> String {
|
||||||
|
let line = |state: &str| {
|
||||||
|
format!(
|
||||||
|
"{{\"hooks\":[{{\"type\":\"command\",\"command\":\"{spacesh_bin} notify --surface $SPACESH_SURFACE_ID --state {state}\"}}]}}"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"{{\"hooks\":{{\"Stop\":[{}],\"Notification\":[{}],\"UserPromptSubmit\":[{}]}}}}",
|
||||||
|
line("done"), line("wait"), line("work")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare the per-surface hook config; return env pairs to merge into the spawn.
|
||||||
|
/// Best-effort: on any I/O error returns an empty vec (spawn proceeds without hooks).
|
||||||
|
pub fn prepare(sid: &SurfaceId, spacesh_bin: &str) -> Vec<(String, String)> {
|
||||||
|
let Some(home) = dirs::home_dir() else { return vec![] };
|
||||||
|
let dir = dir_for(&home, sid);
|
||||||
|
if std::fs::create_dir_all(&dir).is_err() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
if std::fs::write(dir.join("settings.json"), settings_json(spacesh_bin)).is_err() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
vec![("CLAUDE_CONFIG_DIR".to_string(), dir.to_string_lossy().to_string())]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the per-surface hook dir (best-effort) on close.
|
||||||
|
pub fn cleanup(sid: &SurfaceId) {
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
let _ = std::fs::remove_dir_all(dir_for(&home, sid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Absolute path to the `spacesh` CLI binary, sibling of the running daemon.
|
||||||
|
pub fn spacesh_bin() -> String {
|
||||||
|
std::env::current_exe()
|
||||||
|
.ok()
|
||||||
|
.map(|p| p.with_file_name("spacesh"))
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| "spacesh".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// zsh OSC 133 integration script, embedded at build time.
|
||||||
|
const ZSH_INTEGRATION: &str = include_str!("shell_integration/spacesh.zsh");
|
||||||
|
|
||||||
|
/// Is this command a zsh shell we can OSC-133-integrate via ZDOTDIR?
|
||||||
|
pub fn is_zsh(command: &str) -> bool {
|
||||||
|
std::path::Path::new(command).file_name().and_then(|s| s.to_str()) == Some("zsh")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare a per-surface ZDOTDIR whose .zshrc sources the user's rc then our
|
||||||
|
/// integration. Returns env pairs (ZDOTDIR, and SPACESH_ZDOTDIR=original) to
|
||||||
|
/// inject. Best-effort: empty vec on I/O failure.
|
||||||
|
pub fn shell_env(sid: &SurfaceId) -> Vec<(String, String)> {
|
||||||
|
let Some(home) = dirs::home_dir() else { return vec![] };
|
||||||
|
let dir = home.join(".spacesh").join("shellint").join(&sid.0);
|
||||||
|
if std::fs::create_dir_all(&dir).is_err() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
if std::fs::write(dir.join("spacesh.zsh"), ZSH_INTEGRATION).is_err() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let orig_zdotdir = std::env::var("ZDOTDIR").unwrap_or_else(|_| home.to_string_lossy().to_string());
|
||||||
|
// .zshrc: source user rc, then our integration.
|
||||||
|
let zshrc = format!(
|
||||||
|
"[ -f \"{orig}/.zshrc\" ] && source \"{orig}/.zshrc\"\nsource \"{dir}/spacesh.zsh\"\n",
|
||||||
|
orig = orig_zdotdir,
|
||||||
|
dir = dir.to_string_lossy(),
|
||||||
|
);
|
||||||
|
if std::fs::write(dir.join(".zshrc"), zshrc).is_err() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
vec![("ZDOTDIR".to_string(), dir.to_string_lossy().to_string())]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the per-surface shellint dir (best-effort).
|
||||||
|
pub fn cleanup_shell(sid: &SurfaceId) {
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
let _ = std::fs::remove_dir_all(home.join(".spacesh").join("shellint").join(&sid.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_agent_matches_claude_by_command_or_label() {
|
||||||
|
assert!(is_agent("claude", None));
|
||||||
|
assert!(is_agent("/usr/local/bin/claude", None));
|
||||||
|
assert!(is_agent("/bin/sh", Some("claude")));
|
||||||
|
assert!(!is_agent("/bin/zsh", None));
|
||||||
|
assert!(!is_agent("node", Some("codex")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_json_has_three_events_with_abs_bin_and_env_var() {
|
||||||
|
let j = settings_json("/abs/spacesh");
|
||||||
|
assert!(j.contains("\"Stop\""));
|
||||||
|
assert!(j.contains("\"Notification\""));
|
||||||
|
assert!(j.contains("\"UserPromptSubmit\""));
|
||||||
|
assert!(j.contains("/abs/spacesh notify --surface $SPACESH_SURFACE_ID --state done"));
|
||||||
|
assert!(j.contains("--state wait"));
|
||||||
|
assert!(j.contains("--state work"));
|
||||||
|
// Valid JSON.
|
||||||
|
let _: serde_json::Value = serde_json::from_str(&j).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prepare_writes_config_and_cleanup_removes_it() {
|
||||||
|
let sid = SurfaceId(format!("s_test_{}", std::process::id()));
|
||||||
|
let env = prepare(&sid, "/abs/spacesh");
|
||||||
|
assert_eq!(env.len(), 1);
|
||||||
|
assert_eq!(env[0].0, "CLAUDE_CONFIG_DIR");
|
||||||
|
let dir = std::path::PathBuf::from(&env[0].1);
|
||||||
|
assert!(dir.join("settings.json").exists());
|
||||||
|
cleanup(&sid);
|
||||||
|
assert!(!dir.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_zsh_detects_zsh() {
|
||||||
|
assert!(is_zsh("/bin/zsh"));
|
||||||
|
assert!(is_zsh("zsh"));
|
||||||
|
assert!(!is_zsh("/bin/bash"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_env_writes_zdotdir_with_integration() {
|
||||||
|
let sid = SurfaceId(format!("s_shtest_{}", std::process::id()));
|
||||||
|
let env = shell_env(&sid);
|
||||||
|
assert_eq!(env.len(), 1);
|
||||||
|
assert_eq!(env[0].0, "ZDOTDIR");
|
||||||
|
let dir = std::path::PathBuf::from(&env[0].1);
|
||||||
|
assert!(dir.join(".zshrc").exists());
|
||||||
|
assert!(dir.join("spacesh.zsh").exists());
|
||||||
|
cleanup_shell(&sid);
|
||||||
|
assert!(!dir.exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod hooks;
|
||||||
mod launchd;
|
mod launchd;
|
||||||
mod lifecycle;
|
mod lifecycle;
|
||||||
mod persist;
|
mod persist;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use anyhow::Result;
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use spacesh_proto::codec::{read_frame, write_frame};
|
use spacesh_proto::codec::{read_frame, write_frame};
|
||||||
use spacesh_proto::{Cmd, Envelope, ErrorBody, Evt, SurfaceId, WorkspaceId};
|
use spacesh_proto::{Cmd, Envelope, ErrorBody, Evt, SurfaceId, WorkspaceId};
|
||||||
|
use spacesh_proto::status::SurfaceState;
|
||||||
use tokio::net::{UnixListener, UnixStream};
|
use tokio::net::{UnixListener, UnixStream};
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
use crate::persist::{self, Persister};
|
use crate::persist::{self, Persister};
|
||||||
@@ -28,6 +29,8 @@ enum ServerMsg {
|
|||||||
ClientConnected { client: ClientId, out: ClientTx },
|
ClientConnected { client: ClientId, out: ClientTx },
|
||||||
/// Drop a client and all its subscriptions.
|
/// Drop a client and all its subscriptions.
|
||||||
ClientDisconnected { client: ClientId },
|
ClientDisconnected { client: ClientId },
|
||||||
|
/// A status change detected internally (OSC 133 / fallback) by a surface actor.
|
||||||
|
StateDetected { surface_id: SurfaceId, state: SurfaceState },
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientId = u64;
|
type ClientId = u64;
|
||||||
@@ -45,9 +48,17 @@ pub async fn serve(socket: &Path, store: Arc<dyn StateStore>) -> Result<()> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let (state_tx, mut state_rx) = mpsc::unbounded_channel::<(SurfaceId, SurfaceState)>();
|
||||||
|
let router_for_state = router_tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some((sid, st)) = state_rx.recv().await {
|
||||||
|
let _ = router_for_state.send(ServerMsg::StateDetected { surface_id: sid, state: st }).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let persister = persist::spawn(store.clone(), Duration::from_millis(500));
|
let persister = persist::spawn(store.clone(), Duration::from_millis(500));
|
||||||
let initial = store.load().unwrap_or_default();
|
let initial = store.load().unwrap_or_default();
|
||||||
let shutdown = tokio::spawn(router(router_rx, router_tx.clone(), exit_tx, persister, initial));
|
let shutdown = tokio::spawn(router(router_rx, router_tx.clone(), exit_tx, state_tx, persister, initial));
|
||||||
|
|
||||||
let mut next_client: ClientId = 0;
|
let mut next_client: ClientId = 0;
|
||||||
loop {
|
loop {
|
||||||
@@ -102,6 +113,7 @@ async fn router(
|
|||||||
mut rx: mpsc::Receiver<ServerMsg>,
|
mut rx: mpsc::Receiver<ServerMsg>,
|
||||||
router_tx: mpsc::Sender<ServerMsg>,
|
router_tx: mpsc::Sender<ServerMsg>,
|
||||||
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
||||||
|
state_tx: mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
||||||
persister: Persister,
|
persister: Persister,
|
||||||
initial: crate::state_store::PersistState,
|
initial: crate::state_store::PersistState,
|
||||||
) {
|
) {
|
||||||
@@ -138,8 +150,14 @@ async fn router(
|
|||||||
let evt = Envelope::Evt(Evt::Exit { surface_id: surface_id.clone(), code });
|
let evt = Envelope::Evt(Evt::Exit { surface_id: surface_id.clone(), code });
|
||||||
broadcast_evt(&clients, &evt);
|
broadcast_evt(&clients, &evt);
|
||||||
}
|
}
|
||||||
|
ServerMsg::StateDetected { surface_id, state } => {
|
||||||
|
if reg.is_running(&surface_id) {
|
||||||
|
reg.set_state(&surface_id, state);
|
||||||
|
broadcast_evt(&clients, &Envelope::Evt(Evt::State { surface_id, state }));
|
||||||
|
}
|
||||||
|
}
|
||||||
ServerMsg::Request { id, cmd, client, out } => {
|
ServerMsg::Request { id, cmd, client, out } => {
|
||||||
handle_request(id, cmd, client, out, &mut reg, &mut subs, &clients, &router_tx, &exit_tx, &persister).await;
|
handle_request(id, cmd, client, out, &mut reg, &mut subs, &clients, &router_tx, &exit_tx, &state_tx, &persister).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,6 +177,20 @@ fn err(id: u64, code: &str, msg: &str) -> Envelope {
|
|||||||
error: Some(ErrorBody { code: code.into(), msg: msg.into() }) }
|
error: Some(ErrorBody { code: code.into(), msg: msg.into() }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute spawn env (hooks for claude agents, zsh integration for zsh shells)
|
||||||
|
/// and whether a deterministic hook source is active.
|
||||||
|
fn spawn_env(sid: &SurfaceId, spec: &spacesh_proto::workspace::SurfaceSpec) -> (Vec<(String, String)>, bool) {
|
||||||
|
if crate::hooks::is_agent(&spec.command, spec.agent_label.as_deref()) {
|
||||||
|
let env = crate::hooks::prepare(sid, &crate::hooks::spacesh_bin());
|
||||||
|
let active = !env.is_empty();
|
||||||
|
(env, active)
|
||||||
|
} else if crate::hooks::is_zsh(&spec.command) {
|
||||||
|
(crate::hooks::shell_env(sid), false)
|
||||||
|
} else {
|
||||||
|
(vec![], false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Emit a `layout_changed` event for a workspace's current tree.
|
/// Emit a `layout_changed` event for a workspace's current tree.
|
||||||
fn emit_layout(reg: &Registry, ws_id: &WorkspaceId, clients: &HashMap<ClientId, ClientTx>) {
|
fn emit_layout(reg: &Registry, ws_id: &WorkspaceId, clients: &HashMap<ClientId, ClientTx>) {
|
||||||
if let Some(w) = reg.workspace(ws_id) {
|
if let Some(w) = reg.workspace(ws_id) {
|
||||||
@@ -179,6 +211,7 @@ async fn handle_request(
|
|||||||
clients: &HashMap<ClientId, ClientTx>,
|
clients: &HashMap<ClientId, ClientTx>,
|
||||||
router_tx: &mpsc::Sender<ServerMsg>,
|
router_tx: &mpsc::Sender<ServerMsg>,
|
||||||
exit_tx: &mpsc::UnboundedSender<(SurfaceId, i32)>,
|
exit_tx: &mpsc::UnboundedSender<(SurfaceId, i32)>,
|
||||||
|
state_tx: &mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
||||||
persister: &Persister,
|
persister: &Persister,
|
||||||
) {
|
) {
|
||||||
use spacesh_proto::message::SplitDir;
|
use spacesh_proto::message::SplitDir;
|
||||||
@@ -207,7 +240,8 @@ async fn handle_request(
|
|||||||
command: shell, args: args.clone(), cwd: ws.path.clone(),
|
command: shell, args: args.clone(), cwd: ws.path.clone(),
|
||||||
agent_label: command, cols, rows, autostart: false,
|
agent_label: command, cols, rows, autostart: false,
|
||||||
};
|
};
|
||||||
match crate::surface::spawn_from_spec(sid.clone(), workspace_id.clone(), &spec, exit_tx.clone()) {
|
let (env, hooks_active) = spawn_env(&sid, &spec);
|
||||||
|
match crate::surface::spawn_from_spec(sid.clone(), workspace_id.clone(), &spec, env, hooks_active, state_tx.clone(), exit_tx.clone()) {
|
||||||
Ok(handle) => {
|
Ok(handle) => {
|
||||||
spawn_output_bridge(sid.clone(), &handle, router_tx.clone());
|
spawn_output_bridge(sid.clone(), &handle, router_tx.clone());
|
||||||
reg.set_live(handle);
|
reg.set_live(handle);
|
||||||
@@ -238,7 +272,8 @@ async fn handle_request(
|
|||||||
let new_sid = reg.new_surface_id();
|
let new_sid = reg.new_surface_id();
|
||||||
let shell = command.clone().unwrap_or_else(|| std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()));
|
let shell = command.clone().unwrap_or_else(|| std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()));
|
||||||
let spec = SurfaceSpec { command: shell, args, cwd: ws.path.clone(), agent_label: command, cols: 80, rows: 24, autostart: false };
|
let spec = SurfaceSpec { command: shell, args, cwd: ws.path.clone(), agent_label: command, cols: 80, rows: 24, autostart: false };
|
||||||
match crate::surface::spawn_from_spec(new_sid.clone(), ws_id.clone(), &spec, exit_tx.clone()) {
|
let (env, hooks_active) = spawn_env(&new_sid, &spec);
|
||||||
|
match crate::surface::spawn_from_spec(new_sid.clone(), ws_id.clone(), &spec, env, hooks_active, state_tx.clone(), exit_tx.clone()) {
|
||||||
Ok(handle) => {
|
Ok(handle) => {
|
||||||
spawn_output_bridge(new_sid.clone(), &handle, router_tx.clone());
|
spawn_output_bridge(new_sid.clone(), &handle, router_tx.clone());
|
||||||
reg.set_live(handle);
|
reg.set_live(handle);
|
||||||
@@ -311,7 +346,8 @@ async fn handle_request(
|
|||||||
let shell = command.clone().unwrap_or_else(|| std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()));
|
let shell = command.clone().unwrap_or_else(|| std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()));
|
||||||
let args = slot.map(|s| s.args.clone()).unwrap_or_default();
|
let args = slot.map(|s| s.args.clone()).unwrap_or_default();
|
||||||
let spec = SurfaceSpec { command: shell, args, cwd: ws.path.clone(), agent_label: command, cols: 80, rows: 24, autostart: false };
|
let spec = SurfaceSpec { command: shell, args, cwd: ws.path.clone(), agent_label: command, cols: 80, rows: 24, autostart: false };
|
||||||
match crate::surface::spawn_from_spec(new_sid.clone(), workspace_id.clone(), &spec, exit_tx.clone()) {
|
let (env, hooks_active) = spawn_env(&new_sid, &spec);
|
||||||
|
match crate::surface::spawn_from_spec(new_sid.clone(), workspace_id.clone(), &spec, env, hooks_active, state_tx.clone(), exit_tx.clone()) {
|
||||||
Ok(handle) => {
|
Ok(handle) => {
|
||||||
spawn_output_bridge(new_sid.clone(), &handle, router_tx.clone());
|
spawn_output_bridge(new_sid.clone(), &handle, router_tx.clone());
|
||||||
reg.set_live(handle);
|
reg.set_live(handle);
|
||||||
@@ -341,7 +377,8 @@ async fn handle_request(
|
|||||||
let _ = out.send(err(id, "NOT_FOUND", "surface")).await; return;
|
let _ = out.send(err(id, "NOT_FOUND", "surface")).await; return;
|
||||||
};
|
};
|
||||||
let ws_id = reg.workspace_of(&surface_id).unwrap();
|
let ws_id = reg.workspace_of(&surface_id).unwrap();
|
||||||
match crate::surface::spawn_from_spec(surface_id.clone(), ws_id.clone(), &spec, exit_tx.clone()) {
|
let (env, hooks_active) = spawn_env(&surface_id, &spec);
|
||||||
|
match crate::surface::spawn_from_spec(surface_id.clone(), ws_id.clone(), &spec, env, hooks_active, state_tx.clone(), exit_tx.clone()) {
|
||||||
Ok(handle) => {
|
Ok(handle) => {
|
||||||
spawn_output_bridge(surface_id.clone(), &handle, router_tx.clone());
|
spawn_output_bridge(surface_id.clone(), &handle, router_tx.clone());
|
||||||
reg.set_live(handle);
|
reg.set_live(handle);
|
||||||
@@ -355,7 +392,7 @@ async fn handle_request(
|
|||||||
|
|
||||||
Cmd::CloseWorkspace { workspace_id } => {
|
Cmd::CloseWorkspace { workspace_id } => {
|
||||||
let ids = reg.close_workspace(&workspace_id);
|
let ids = reg.close_workspace(&workspace_id);
|
||||||
for sid in &ids { subs.remove(sid); }
|
for sid in &ids { crate::hooks::cleanup(sid); crate::hooks::cleanup_shell(sid); subs.remove(sid); }
|
||||||
broadcast_evt(clients, &Envelope::Evt(Evt::WorkspaceClosed { workspace_id: workspace_id.clone() }));
|
broadcast_evt(clients, &Envelope::Evt(Evt::WorkspaceClosed { workspace_id: workspace_id.clone() }));
|
||||||
persister.mark_dirty(reg.persist_state());
|
persister.mark_dirty(reg.persist_state());
|
||||||
let _ = out.send(ok(id, serde_json::Value::Null)).await;
|
let _ = out.send(ok(id, serde_json::Value::Null)).await;
|
||||||
@@ -462,6 +499,8 @@ async fn handle_request(
|
|||||||
let ws_id = reg.workspace_of(&surface_id);
|
let ws_id = reg.workspace_of(&surface_id);
|
||||||
reg.remove_surface(&surface_id);
|
reg.remove_surface(&surface_id);
|
||||||
subs.remove(&surface_id);
|
subs.remove(&surface_id);
|
||||||
|
crate::hooks::cleanup(&surface_id);
|
||||||
|
crate::hooks::cleanup_shell(&surface_id);
|
||||||
broadcast_evt(clients, &Envelope::Evt(Evt::SurfaceClosed { surface_id: surface_id.clone() }));
|
broadcast_evt(clients, &Envelope::Evt(Evt::SurfaceClosed { surface_id: surface_id.clone() }));
|
||||||
if let Some(ws_id) = ws_id { emit_layout(reg, &ws_id, clients); }
|
if let Some(ws_id) = ws_id { emit_layout(reg, &ws_id, clients); }
|
||||||
persister.mark_dirty(reg.persist_state());
|
persister.mark_dirty(reg.persist_state());
|
||||||
@@ -769,4 +808,40 @@ mod tests {
|
|||||||
assert_eq!(sv["running"], false, "restored panels are stopped");
|
assert_eq!(sv["running"], false, "restored panels are stopped");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn osc133_in_pty_sets_status_over_socket() {
|
||||||
|
let _serial = crate::test_support::serial();
|
||||||
|
let dir = tempdir_path();
|
||||||
|
let sock = dir.join("sock");
|
||||||
|
let store: std::sync::Arc<dyn crate::state_store::StateStore> =
|
||||||
|
std::sync::Arc::new(crate::state_store::JsonStateStore::new(dir.join("state.json")));
|
||||||
|
let sock2 = sock.clone();
|
||||||
|
tokio::spawn(async move { let _ = serve(&sock2, store).await; });
|
||||||
|
wait_for_socket(&sock).await;
|
||||||
|
let mut s = UnixStream::connect(&sock).await.unwrap();
|
||||||
|
|
||||||
|
let r = req(&mut s, 1, Cmd::Open { path: std::env::temp_dir().to_string_lossy().into() }).await;
|
||||||
|
let ws = res_data(&r)["workspace_id"].as_str().unwrap().to_string();
|
||||||
|
let r = req(&mut s, 2, Cmd::NewSurface {
|
||||||
|
workspace_id: spacesh_proto::WorkspaceId(ws.clone()),
|
||||||
|
command: Some("/bin/sh".into()),
|
||||||
|
args: vec!["-c".into(), "printf '\\033]133;C\\007'; printf hi; printf '\\033]133;D;0\\007'; sleep 1".into()],
|
||||||
|
cols: 80, rows: 24,
|
||||||
|
}).await;
|
||||||
|
let sid = res_data(&r)["surface_id"].as_str().unwrap().to_string();
|
||||||
|
let surface_id = spacesh_proto::SurfaceId(sid.clone());
|
||||||
|
let _ = req(&mut s, 3, Cmd::Attach { surface_id }).await;
|
||||||
|
|
||||||
|
// Wait for a State event to flow (Work then Done).
|
||||||
|
let mut saw_done = false;
|
||||||
|
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(3);
|
||||||
|
while tokio::time::Instant::now() < deadline {
|
||||||
|
if let Ok(Ok(Some(Envelope::Evt(Evt::State { state, .. })))) =
|
||||||
|
tokio::time::timeout(tokio::time::Duration::from_millis(200), read_frame(&mut s)).await {
|
||||||
|
if state == spacesh_proto::status::SurfaceState::Done { saw_done = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(saw_done, "expected a Done state event from OSC 133");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# spacesh zsh OSC 133 integration. Sourced from a per-surface ZDOTDIR .zshrc
|
||||||
|
# after the user's ~/.zshrc. Emits semantic-prompt markers so the daemon can
|
||||||
|
# detect command start/end and exit status.
|
||||||
|
autoload -Uz add-zsh-hook 2>/dev/null
|
||||||
|
|
||||||
|
_spacesh_precmd() {
|
||||||
|
local code=$?
|
||||||
|
# End previous command (D) with its exit code, then mark prompt start (A).
|
||||||
|
print -n "\e]133;D;${code}\a\e]133;A\a"
|
||||||
|
}
|
||||||
|
_spacesh_preexec() {
|
||||||
|
# Command output begins (C).
|
||||||
|
print -n "\e]133;C\a"
|
||||||
|
}
|
||||||
|
add-zsh-hook precmd _spacesh_precmd 2>/dev/null
|
||||||
|
add-zsh-hook preexec _spacesh_preexec 2>/dev/null
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
use spacesh_core::{snapshot::snapshot_ansi, GridSurface};
|
use spacesh_core::{snapshot::snapshot_ansi, GridSurface};
|
||||||
use spacesh_core::snapshot::Snapshot;
|
use spacesh_core::snapshot::Snapshot;
|
||||||
|
use spacesh_core::detect::{FallbackScanner, Osc133Scanner};
|
||||||
use spacesh_proto::{SurfaceId, WorkspaceId};
|
use spacesh_proto::{SurfaceId, WorkspaceId};
|
||||||
|
use spacesh_proto::status::SurfaceState;
|
||||||
use spacesh_proto::workspace::SurfaceSpec;
|
use spacesh_proto::workspace::SurfaceSpec;
|
||||||
use spacesh_pty::{PtyHandle, SpawnSpec};
|
use spacesh_pty::{PtyHandle, SpawnSpec};
|
||||||
use tokio::sync::{broadcast, mpsc, oneshot};
|
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||||
@@ -12,18 +14,23 @@ pub fn spawn_from_spec(
|
|||||||
id: SurfaceId,
|
id: SurfaceId,
|
||||||
workspace_id: WorkspaceId,
|
workspace_id: WorkspaceId,
|
||||||
spec: &SurfaceSpec,
|
spec: &SurfaceSpec,
|
||||||
|
extra_env: Vec<(String, String)>,
|
||||||
|
hooks_active: bool,
|
||||||
|
state_tx: mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
||||||
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
||||||
) -> std::io::Result<SurfaceHandle> {
|
) -> std::io::Result<SurfaceHandle> {
|
||||||
|
let mut env = vec![("SPACESH_SURFACE_ID".to_string(), id.0.clone())];
|
||||||
|
env.extend(extra_env);
|
||||||
let pty = PtyHandle::spawn(SpawnSpec {
|
let pty = PtyHandle::spawn(SpawnSpec {
|
||||||
command: spec.command.clone(),
|
command: spec.command.clone(),
|
||||||
args: spec.args.clone(),
|
args: spec.args.clone(),
|
||||||
cwd: std::path::PathBuf::from(&spec.cwd),
|
cwd: std::path::PathBuf::from(&spec.cwd),
|
||||||
cols: spec.cols,
|
cols: spec.cols,
|
||||||
rows: spec.rows,
|
rows: spec.rows,
|
||||||
env: vec![("SPACESH_SURFACE_ID".into(), id.0.clone())],
|
env,
|
||||||
})
|
})
|
||||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||||
Ok(spawn_surface(id, workspace_id, pty, spec.cols, spec.rows, exit_tx))
|
Ok(spawn_surface(id, workspace_id, pty, spec.cols, spec.rows, hooks_active, state_tx, exit_tx))
|
||||||
}
|
}
|
||||||
|
|
||||||
const BROADCAST_CAP: usize = 1024;
|
const BROADCAST_CAP: usize = 1024;
|
||||||
@@ -52,18 +59,25 @@ pub fn spawn_surface(
|
|||||||
mut pty: PtyHandle,
|
mut pty: PtyHandle,
|
||||||
cols: u16,
|
cols: u16,
|
||||||
rows: u16,
|
rows: u16,
|
||||||
|
hooks_active: bool,
|
||||||
|
state_tx: mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
||||||
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
||||||
) -> SurfaceHandle {
|
) -> SurfaceHandle {
|
||||||
let (tx, mut rx) = mpsc::channel::<SurfaceMsg>(64);
|
let (tx, mut rx) = mpsc::channel::<SurfaceMsg>(64);
|
||||||
let (bcast, _) = broadcast::channel::<Vec<u8>>(BROADCAST_CAP);
|
let (bcast, _) = broadcast::channel::<Vec<u8>>(BROADCAST_CAP);
|
||||||
let actor_id = id.clone();
|
let actor_id = id.clone();
|
||||||
|
let detect_id = id.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut grid = GridSurface::new(cols, rows);
|
let mut grid = GridSurface::new(cols, rows);
|
||||||
let mut pending: Vec<u8> = Vec::with_capacity(FLUSH_BYTES);
|
let mut pending: Vec<u8> = Vec::with_capacity(FLUSH_BYTES);
|
||||||
let mut flush_deadline: Option<Instant> = None;
|
let mut flush_deadline: Option<Instant> = None;
|
||||||
|
let mut osc = Osc133Scanner::new();
|
||||||
|
// `deterministic` suppresses fallback once a reliable source is seen
|
||||||
|
// (hooks active, or any OSC 133 marker observed).
|
||||||
|
let mut deterministic = hooks_active;
|
||||||
|
let mut last_state = SurfaceState::Idle;
|
||||||
|
|
||||||
// Helper closure can't borrow across awaits cleanly; inline the flush logic.
|
|
||||||
loop {
|
loop {
|
||||||
// Copy the deadline into an owned local so the timer future doesn't
|
// Copy the deadline into an owned local so the timer future doesn't
|
||||||
// hold a borrow of `flush_deadline` across the select! (other arms mutate it).
|
// hold a borrow of `flush_deadline` across the select! (other arms mutate it).
|
||||||
@@ -105,26 +119,18 @@ pub fn spawn_surface(
|
|||||||
flush_deadline = Some(Instant::now() + FLUSH_INTERVAL);
|
flush_deadline = Some(Instant::now() + FLUSH_INTERVAL);
|
||||||
}
|
}
|
||||||
if pending.len() >= FLUSH_BYTES {
|
if pending.len() >= FLUSH_BYTES {
|
||||||
grid.feed(&pending);
|
flush(&mut pending, &mut grid, &mut osc, &mut deterministic, &mut last_state, &detect_id, &bcast, &state_tx);
|
||||||
let _ = bcast.send(std::mem::take(&mut pending));
|
|
||||||
flush_deadline = None;
|
flush_deadline = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Final flush on EOF.
|
flush(&mut pending, &mut grid, &mut osc, &mut deterministic, &mut last_state, &detect_id, &bcast, &state_tx);
|
||||||
if !pending.is_empty() {
|
|
||||||
grid.feed(&pending);
|
|
||||||
let _ = bcast.send(std::mem::take(&mut pending));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = timer => {
|
_ = timer => {
|
||||||
if !pending.is_empty() {
|
flush(&mut pending, &mut grid, &mut osc, &mut deterministic, &mut last_state, &detect_id, &bcast, &state_tx);
|
||||||
grid.feed(&pending);
|
|
||||||
let _ = bcast.send(std::mem::take(&mut pending));
|
|
||||||
}
|
|
||||||
flush_deadline = None;
|
flush_deadline = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,6 +142,47 @@ pub fn spawn_surface(
|
|||||||
SurfaceHandle { id, workspace_id, tx }
|
SurfaceHandle { id, workspace_id, tx }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Feed pending bytes into the grid, run detectors, broadcast output, and emit a
|
||||||
|
/// state change (if any). No-op when pending is empty.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn flush(
|
||||||
|
pending: &mut Vec<u8>,
|
||||||
|
grid: &mut GridSurface,
|
||||||
|
osc: &mut Osc133Scanner,
|
||||||
|
deterministic: &mut bool,
|
||||||
|
last_state: &mut SurfaceState,
|
||||||
|
id: &SurfaceId,
|
||||||
|
bcast: &broadcast::Sender<Vec<u8>>,
|
||||||
|
state_tx: &mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
||||||
|
) {
|
||||||
|
if pending.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Deterministic source: OSC 133 markers in this chunk.
|
||||||
|
// Emit each distinct state transition immediately so no marker is dropped
|
||||||
|
// when multiple arrive in a single flush (e.g. C + D in the same buffer).
|
||||||
|
let osc_states = osc.feed(&pending[..]);
|
||||||
|
let had_osc = !osc_states.is_empty();
|
||||||
|
for st in osc_states {
|
||||||
|
*deterministic = true;
|
||||||
|
if st != *last_state {
|
||||||
|
*last_state = st;
|
||||||
|
let _ = state_tx.send((id.clone(), st));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grid.feed(&pending[..]);
|
||||||
|
// Best-effort fallback only when no deterministic source is active.
|
||||||
|
if !had_osc && !*deterministic {
|
||||||
|
if let Some(st) = FallbackScanner::scan(&grid.tail_text(6)) {
|
||||||
|
if st != *last_state {
|
||||||
|
*last_state = st;
|
||||||
|
let _ = state_tx.send((id.clone(), st));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = bcast.send(std::mem::take(pending));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -156,8 +203,9 @@ mod tests {
|
|||||||
async fn attach_receives_output() {
|
async fn attach_receives_output() {
|
||||||
let _serial = crate::test_support::serial();
|
let _serial = crate::test_support::serial();
|
||||||
let pty = PtyHandle::spawn(spec("printf HELLO; sleep 0.3")).unwrap();
|
let pty = PtyHandle::spawn(spec("printf HELLO; sleep 0.3")).unwrap();
|
||||||
|
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
||||||
let (exit_tx, _exit_rx) = mpsc::unbounded_channel();
|
let (exit_tx, _exit_rx) = mpsc::unbounded_channel();
|
||||||
let handle = spawn_surface(SurfaceId("s_1".into()), WorkspaceId("w_1".into()), pty, 80, 24, exit_tx);
|
let handle = spawn_surface(SurfaceId("s_1".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx);
|
||||||
|
|
||||||
let (reply_tx, reply_rx) = oneshot::channel();
|
let (reply_tx, reply_rx) = oneshot::channel();
|
||||||
handle.tx.send(SurfaceMsg::Attach { reply: reply_tx }).await.unwrap();
|
handle.tx.send(SurfaceMsg::Attach { reply: reply_tx }).await.unwrap();
|
||||||
@@ -180,8 +228,9 @@ mod tests {
|
|||||||
async fn exit_is_reported() {
|
async fn exit_is_reported() {
|
||||||
let _serial = crate::test_support::serial();
|
let _serial = crate::test_support::serial();
|
||||||
let pty = PtyHandle::spawn(spec("exit 7")).unwrap();
|
let pty = PtyHandle::spawn(spec("exit 7")).unwrap();
|
||||||
|
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
||||||
let (exit_tx, mut exit_rx) = mpsc::unbounded_channel();
|
let (exit_tx, mut exit_rx) = mpsc::unbounded_channel();
|
||||||
let _handle = spawn_surface(SurfaceId("s_2".into()), WorkspaceId("w_1".into()), pty, 80, 24, exit_tx);
|
let _handle = spawn_surface(SurfaceId("s_2".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx);
|
||||||
let (sid, code) = tokio::time::timeout(tokio::time::Duration::from_secs(3), exit_rx.recv())
|
let (sid, code) = tokio::time::timeout(tokio::time::Duration::from_secs(3), exit_rx.recv())
|
||||||
.await.unwrap().unwrap();
|
.await.unwrap().unwrap();
|
||||||
assert_eq!(sid, SurfaceId("s_2".into()));
|
assert_eq!(sid, SurfaceId("s_2".into()));
|
||||||
@@ -192,8 +241,9 @@ mod tests {
|
|||||||
async fn attach_snapshot_reflects_prior_output() {
|
async fn attach_snapshot_reflects_prior_output() {
|
||||||
let _serial = crate::test_support::serial();
|
let _serial = crate::test_support::serial();
|
||||||
let pty = PtyHandle::spawn(spec("printf SNAPME; sleep 0.5")).unwrap();
|
let pty = PtyHandle::spawn(spec("printf SNAPME; sleep 0.5")).unwrap();
|
||||||
|
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
||||||
let (exit_tx, _exit_rx) = mpsc::unbounded_channel();
|
let (exit_tx, _exit_rx) = mpsc::unbounded_channel();
|
||||||
let handle = spawn_surface(SurfaceId("s_s".into()), WorkspaceId("w_1".into()), pty, 80, 24, exit_tx);
|
let handle = spawn_surface(SurfaceId("s_s".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx);
|
||||||
|
|
||||||
// Give the child time to write and the actor time to flush into the grid.
|
// Give the child time to write and the actor time to flush into the grid.
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||||
@@ -213,8 +263,9 @@ mod tests {
|
|||||||
cwd: std::env::temp_dir().to_string_lossy().into(),
|
cwd: std::env::temp_dir().to_string_lossy().into(),
|
||||||
agent_label: None, cols: 80, rows: 24, autostart: false,
|
agent_label: None, cols: 80, rows: 24, autostart: false,
|
||||||
};
|
};
|
||||||
|
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
||||||
let (exit_tx, _rx) = mpsc::unbounded_channel();
|
let (exit_tx, _rx) = mpsc::unbounded_channel();
|
||||||
let handle = spawn_from_spec(SurfaceId("s_r".into()), WorkspaceId("w_1".into()), &spec, exit_tx).unwrap();
|
let handle = spawn_from_spec(SurfaceId("s_r".into()), WorkspaceId("w_1".into()), &spec, vec![], false, state_tx, exit_tx).unwrap();
|
||||||
let (reply_tx, reply_rx) = oneshot::channel();
|
let (reply_tx, reply_rx) = oneshot::channel();
|
||||||
handle.tx.send(SurfaceMsg::Attach { reply: reply_tx }).await.unwrap();
|
handle.tx.send(SurfaceMsg::Attach { reply: reply_tx }).await.unwrap();
|
||||||
let mut sub = reply_rx.await.unwrap();
|
let mut sub = reply_rx.await.unwrap();
|
||||||
@@ -228,4 +279,23 @@ mod tests {
|
|||||||
}
|
}
|
||||||
assert!(got.contains("RESPAWN"), "got: {got:?}");
|
assert!(got.contains("RESPAWN"), "got: {got:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn osc133_output_drives_state_detection() {
|
||||||
|
let _serial = crate::test_support::serial();
|
||||||
|
let pty = PtyHandle::spawn(spec("printf '\\033]133;C\\007'; printf working; printf '\\033]133;D;0\\007'; sleep 0.3")).unwrap();
|
||||||
|
let (state_tx, mut state_rx) = mpsc::unbounded_channel();
|
||||||
|
let (exit_tx, _exit_rx) = mpsc::unbounded_channel();
|
||||||
|
let _h = spawn_surface(SurfaceId("s_o".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx);
|
||||||
|
let mut seen = Vec::new();
|
||||||
|
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(2);
|
||||||
|
while tokio::time::Instant::now() < deadline {
|
||||||
|
if let Ok(Some((_, st))) = tokio::time::timeout(tokio::time::Duration::from_millis(100), state_rx.recv()).await {
|
||||||
|
seen.push(st);
|
||||||
|
if seen.contains(&SurfaceState::Done) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(seen.contains(&SurfaceState::Work), "states: {seen:?}");
|
||||||
|
assert!(seen.contains(&SurfaceState::Done), "states: {seen:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user