feat(gui): launcher window (create fleet, node controls, actions)

This commit is contained in:
2026-10-25 00:36:41 -07:00
parent 2be008e4b0
commit 97ea5f8f12
9 changed files with 1208 additions and 49 deletions
+632 -1
View File
@@ -11,6 +11,20 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "accesskit_atspi_common"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954"
dependencies = [
"accesskit",
"accesskit_consumer 0.36.0",
"atspi-common",
"phf",
"serde",
"zvariant",
]
[[package]] [[package]]
name = "accesskit_consumer" name = "accesskit_consumer"
version = "0.35.0" version = "0.35.0"
@@ -21,6 +35,86 @@ dependencies = [
"hashbrown 0.16.1", "hashbrown 0.16.1",
] ]
[[package]]
name = "accesskit_consumer"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60"
dependencies = [
"accesskit",
"hashbrown 0.16.1",
]
[[package]]
name = "accesskit_consumer"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245"
dependencies = [
"accesskit",
"hashbrown 0.16.1",
]
[[package]]
name = "accesskit_macos"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076"
dependencies = [
"accesskit",
"accesskit_consumer 0.38.0",
"hashbrown 0.16.1",
"objc2 0.5.3",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
]
[[package]]
name = "accesskit_unix"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c"
dependencies = [
"accesskit",
"accesskit_atspi_common",
"async-channel",
"async-executor",
"async-task",
"atspi",
"futures-lite",
"futures-util",
"serde",
"zbus",
]
[[package]]
name = "accesskit_windows"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21"
dependencies = [
"accesskit",
"accesskit_consumer 0.35.0",
"hashbrown 0.16.1",
"static_assertions",
"windows",
"windows-core",
]
[[package]]
name = "accesskit_winit"
version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313"
dependencies = [
"accesskit",
"accesskit_macos",
"accesskit_unix",
"accesskit_windows",
"raw-window-handle",
"winit",
]
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.1" version = "2.0.1"
@@ -106,6 +200,12 @@ version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
[[package]]
name = "as-raw-xcb-connection"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
[[package]] [[package]]
name = "ash" name = "ash"
version = "0.38.0+1.3.281" version = "0.38.0+1.3.281"
@@ -115,6 +215,137 @@ dependencies = [
"libloading", "libloading",
] ]
[[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 1.1.5",
"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 1.1.5",
]
[[package]]
name = "async-recursion"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[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 1.1.5",
"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.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]] [[package]]
name = "atomic-polyfill" name = "atomic-polyfill"
version = "1.0.3" version = "1.0.3"
@@ -130,6 +361,43 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "atspi"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d"
dependencies = [
"atspi-common",
"atspi-proxies",
]
[[package]]
name = "atspi-common"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d"
dependencies = [
"enumflags2",
"serde",
"static_assertions",
"zbus",
"zbus-lockstep",
"zbus-lockstep-macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "atspi-proxies"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc"
dependencies = [
"atspi-common",
"serde",
"zbus",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.1" version = "1.5.1"
@@ -199,6 +467,19 @@ dependencies = [
"objc2 0.6.5", "objc2 0.6.5",
] ]
[[package]]
name = "blocking"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]] [[package]]
name = "bluer" name = "bluer"
version = "0.17.4" version = "0.17.4"
@@ -745,6 +1026,7 @@ version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98466000559d66ba4db786a30448b01483946add4188e7f10f522a27b4fb89cc" checksum = "98466000559d66ba4db786a30448b01483946add4188e7f10f522a27b4fb89cc"
dependencies = [ dependencies = [
"accesskit_winit",
"arboard", "arboard",
"bytemuck", "bytemuck",
"egui", "egui",
@@ -818,6 +1100,33 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[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.119",
]
[[package]] [[package]]
name = "epaint" name = "epaint"
version = "0.36.2" version = "0.36.2"
@@ -880,6 +1189,26 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "event-listener"
version = "5.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
dependencies = [
"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.5.0" version = "2.5.0"
@@ -1020,6 +1349,19 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
[[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.34" version = "0.3.34"
@@ -1492,7 +1834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ceaa75eb0036a32b6b9833962eb18137449e9817e2e586006471925b727fd5" checksum = "90ceaa75eb0036a32b6b9833962eb18137449e9817e2e586006471925b727fd5"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_consumer", "accesskit_consumer 0.35.0",
] ]
[[package]] [[package]]
@@ -1636,6 +1978,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -2180,6 +2531,22 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[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]]
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"
@@ -2222,6 +2589,49 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
"phf_macros",
"phf_shared",
"serde",
]
[[package]]
name = "phf_generator"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
dependencies = [
"fastrand",
"phf_shared",
]
[[package]]
name = "phf_macros"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "phf_shared"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
dependencies = [
"siphasher",
]
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.13" version = "1.1.13"
@@ -2248,6 +2658,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.34" version = "0.3.34"
@@ -2599,6 +3020,17 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "serde_repr"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "1.1.2" version = "1.1.2"
@@ -2646,6 +3078,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
[[package]] [[package]]
name = "skrifa" name = "skrifa"
version = "0.44.0" version = "0.44.0"
@@ -3084,14 +3522,29 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [ dependencies = [
"log", "log",
"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.119",
]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.36" version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
[[package]] [[package]]
name = "type-map" name = "type-map"
@@ -3108,6 +3561,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 = "unicode-general-category" name = "unicode-general-category"
version = "1.1.0" version = "1.1.0"
@@ -3140,6 +3604,7 @@ checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514"
dependencies = [ dependencies = [
"getrandom 0.4.3", "getrandom 0.4.3",
"js-sys", "js-sys",
"serde_core",
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -3822,6 +4287,7 @@ dependencies = [
"atomic-waker", "atomic-waker",
"bitflags 2.13.2", "bitflags 2.13.2",
"block2 0.5.1", "block2 0.5.1",
"bytemuck",
"calloop 0.13.0", "calloop 0.13.0",
"cfg_aliases", "cfg_aliases",
"concurrent-queue", "concurrent-queue",
@@ -3838,6 +4304,7 @@ dependencies = [
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-ui-kit 0.2.2", "objc2-ui-kit 0.2.2",
"orbclient", "orbclient",
"percent-encoding",
"pin-project", "pin-project",
"raw-window-handle", "raw-window-handle",
"redox_syscall 0.4.1", "redox_syscall 0.4.1",
@@ -3855,6 +4322,8 @@ dependencies = [
"web-sys", "web-sys",
"web-time", "web-time",
"windows-sys 0.52.0", "windows-sys 0.52.0",
"x11-dl",
"x11rb",
"xkbcommon-dl", "xkbcommon-dl",
] ]
@@ -3873,13 +4342,28 @@ version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "x11-dl"
version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
dependencies = [
"libc",
"once_cell",
"pkg-config",
]
[[package]] [[package]]
name = "x11rb" name = "x11rb"
version = "0.13.2" version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [ dependencies = [
"as-raw-xcb-connection",
"gethostname", "gethostname",
"libc",
"libloading",
"once_cell",
"rustix 1.1.5", "rustix 1.1.5",
"x11rb-protocol", "x11rb-protocol",
] ]
@@ -3921,6 +4405,112 @@ version = "0.8.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "zbus"
version = "5.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
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 1.1.5",
"serde",
"serde_repr",
"tracing",
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus-lockstep"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863"
dependencies = [
"zbus_xml",
"zvariant",
]
[[package]]
name = "zbus-lockstep-macros"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"zbus-lockstep",
"zbus_xml",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "5.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.9",
"zbus_names",
"zvariant",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "4.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
dependencies = [
"serde",
"winnow",
"zvariant",
]
[[package]]
name = "zbus_xml"
version = "5.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a"
dependencies = [
"serde",
"winnow",
"zbus_names",
"zvariant",
]
[[package]]
name = "zcheapstr"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.63" version = "0.8.63"
@@ -3967,3 +4557,44 @@ checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [ dependencies = [
"zune-core", "zune-core",
] ]
[[package]]
name = "zvariant"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow",
"zcheapstr",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.9",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 3.0.9",
"winnow",
]
+1 -1
View File
@@ -15,7 +15,7 @@ serde_json = "1"
notify = "8" notify = "8"
[dev-dependencies] [dev-dependencies]
egui_kittest = { version = "0.36", features = ["wgpu"] } egui_kittest = { version = "0.36", features = ["wgpu", "eframe"] }
tempfile = "3" tempfile = "3"
# Like fuzz/, this crate sits outside the workspace (the GUI is a separate # Like fuzz/, this crate sits outside the workspace (the GUI is a separate
+330
View File
@@ -0,0 +1,330 @@
//! The launcher window: fleet creation, node/container controls, and
//! per-node window spawns.
//!
//! [`Launcher::update`] renders and queues intents — it never spawns a
//! process (except the one-time bus daemon inside `Fleet::create`, which is
//! a non-blocking `spawn`). Everything else drains through
//! [`Launcher::drain_actions`], called by `main.rs` after each frame; the
//! headless tests drive `update` and assert on the queued actions.
use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;
use std::process::Child;
use egui::Context;
use crate::config::fleet_dir;
use crate::docker::{ContainerId, Docker};
use crate::fleet::{Fleet, Mode};
/// One node container the launcher knows about.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerEntry {
/// The node name (also the dir and container-name stem).
pub name: String,
/// The container id `docker run -d` returned.
pub id: ContainerId,
/// How the node connects to its peers.
pub mode: Mode,
}
/// An intent the UI queued; `main.rs` executes these by draining the queue.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LauncherAction {
/// Add the node to the fleet and start its container.
StartContainer {
/// The node name.
name: String,
/// How the node connects to its peers.
mode: Mode,
},
/// Stop a running container.
StopContainer(ContainerId),
/// Remove a stopped container.
RemoveContainer(ContainerId),
/// Spawn a dedicated window process for this node.
OpenWindow {
/// The node name.
name: String,
/// The node's config dir (the `--node` arg).
dir: PathBuf,
/// The container the window controls.
id: ContainerId,
/// How the node connects to its peers.
mode: Mode,
},
/// Build the runtime image.
BuildImage,
}
/// The launcher window's full state.
pub struct Launcher {
/// The created fleet; `None` until Create succeeds.
pub fleet: Option<Fleet>,
/// The docker frontend.
pub docker: Docker,
/// Known node containers (only successful starts land here).
pub containers: Vec<ContainerEntry>,
/// Spawned node-window processes.
pub children: Vec<Child>,
/// Intents queued by [`Launcher::update`], drained by `main.rs`.
pub actions: VecDeque<LauncherAction>,
/// The fleet-name field.
pub fleet_name: String,
/// The node-name field for Add.
pub node_name: String,
/// The bt peer field (bt mode only; the pinned `StartContainer` action
/// carries no peer, so it is read from here at drain time).
pub node_peer: String,
/// The mode picker.
pub mode: Mode,
/// The selected row's name (drives the log pane).
pub selected: Option<String>,
/// The selected container's last-fetched log tail.
pub log_tail: Option<String>,
/// The latest error to surface.
pub error: Option<String>,
/// `docker inspect` results, refreshed at most once per second.
running: HashMap<ContainerId, bool>,
/// When the running map was last refreshed.
last_running_check: f64,
}
impl Default for Launcher {
fn default() -> Self {
Self::new()
}
}
impl Launcher {
/// An empty launcher: no fleet, no containers, sim mode.
pub fn new() -> Launcher {
Launcher {
fleet: None,
docker: Docker,
containers: Vec::new(),
children: Vec::new(),
actions: VecDeque::new(),
fleet_name: String::new(),
node_name: String::new(),
node_peer: String::new(),
mode: Mode::Sim,
selected: None,
log_tail: None,
error: None,
running: HashMap::new(),
last_running_check: f64::NEG_INFINITY,
}
}
/// Render the launcher and queue intents. Side effects are limited to
/// the fleet creation itself (a non-blocking daemon spawn) and one
/// `docker logs` fetch when a row is selected.
pub fn update(&mut self, ctx: &Context) {
let now = ctx.input(|i| i.time);
if now - self.last_running_check > 1.0 {
self.last_running_check = now;
self.running = self
.containers
.iter()
.map(|c| (c.id.clone(), self.docker.is_running(&c.id)))
.collect();
}
// egui 0.36 is Ui-based: rebuild the root Ui the eframe App would
// have passed, so the panels have a parent.
let mut ui = egui::Ui::new(
ctx.clone(),
egui::Id::new("tines-gui-root"),
egui::UiBuilder::new().max_rect(ctx.content_rect()),
);
egui::Panel::top("controls").show(&mut ui, |ui| {
ui.horizontal(|ui| {
ui.label("Mode:");
ui.selectable_value(&mut self.mode, Mode::Bt, "Bt");
ui.selectable_value(&mut self.mode, Mode::Sim, "Sim");
});
ui.horizontal(|ui| {
ui.label("Fleet name:");
ui.text_edit_singleline(&mut self.fleet_name);
if ui.button("Create").clicked() {
match Fleet::create(self.fleet_name.trim()) {
Ok(fleet) => {
self.fleet = Some(fleet);
self.error = None;
}
Err(e) => self.error = Some(format!("cannot create fleet: {e}")),
}
}
});
if self.mode == Mode::Bt {
ui.horizontal(|ui| {
ui.label("Peer (bt):");
ui.text_edit_singleline(&mut self.node_peer);
});
}
ui.horizontal(|ui| {
ui.label("Node name:");
ui.text_edit_singleline(&mut self.node_name);
if ui.button("Add").clicked() {
self.actions.push_back(LauncherAction::StartContainer {
name: self.node_name.trim().to_string(),
mode: self.mode,
});
}
});
if ui.button("Build image").clicked() {
self.actions.push_back(LauncherAction::BuildImage);
}
if let Some(error) = &self.error {
ui.colored_label(egui::Color32::from_rgb(0xff, 0x55, 0x55), error);
}
});
egui::CentralPanel::default().show(&mut ui, |ui| {
egui::Grid::new("nodes")
.striped(true)
.min_col_width(80.0)
.show(ui, |ui| {
ui.label("node");
ui.label("mode");
ui.label("state");
ui.label("");
ui.end_row();
for entry in &self.containers {
let running = self.running.get(&entry.id).copied().unwrap_or(false);
let name = entry.name.clone();
if ui
.selectable_label(self.selected.as_deref() == Some(&name), &entry.name)
.clicked()
{
self.selected = Some(name.clone());
self.log_tail = self.docker.logs(&entry.id, 50).ok();
}
ui.label(match entry.mode {
Mode::Bt => "bt",
Mode::Sim => "sim",
});
ui.label(if running { "running" } else { "stopped" });
ui.horizontal(|ui| {
if ui.button("Open").clicked() {
self.actions.push_back(LauncherAction::OpenWindow {
name: name.clone(),
dir: fleet_dir(self.fleet_name.trim()).join(&name),
id: entry.id.clone(),
mode: entry.mode,
});
}
if ui.button("Stop").clicked() {
self.actions
.push_back(LauncherAction::StopContainer(entry.id.clone()));
}
if ui.button("Remove").clicked() {
self.actions
.push_back(LauncherAction::RemoveContainer(entry.id.clone()));
}
});
ui.end_row();
}
});
});
egui::Panel::bottom("logs").show(&mut ui, |ui| {
ui.heading(match &self.selected {
Some(name) => format!("logs — {name}"),
None => "logs — select a container".to_string(),
});
if let Some(tail) = &self.log_tail {
egui::ScrollArea::vertical()
.max_height(120.0)
.auto_shrink([false, false])
.show(ui, |ui| {
ui.monospace(tail);
});
}
});
}
/// Execute every queued intent. This is the only place containers are
/// started/stopped/removed and node windows spawned; `main.rs` calls it
/// after each frame.
pub fn drain_actions(&mut self) {
while let Some(action) = self.actions.pop_front() {
match action {
LauncherAction::StartContainer { name, mode } => {
let Some(fleet) = self.fleet.as_ref() else {
self.error = Some("create a fleet first".to_string());
continue;
};
let peer = (mode == Mode::Bt && !self.node_peer.trim().is_empty())
.then(|| self.node_peer.trim().to_string());
if let Err(e) = fleet.add_node(&name, None, mode, peer.as_deref()) {
self.error = Some(format!("cannot add node {name}: {e}"));
continue;
}
let started = match mode {
Mode::Bt => self.docker.start_bt(&name, self.fleet_name.trim()),
Mode::Sim => self.docker.start_sim(&name, self.fleet_name.trim()),
};
match started {
Ok(id) => self.containers.push(ContainerEntry { name, id, mode }),
Err(e) => self.error = Some(format!("cannot start {name}: {e}")),
}
}
LauncherAction::StopContainer(id) => {
if let Err(e) = self.docker.stop(&id) {
self.error = Some(format!("cannot stop {}: {e}", id.0));
}
}
LauncherAction::RemoveContainer(id) => {
if let Err(e) = self.docker.remove(&id) {
self.error = Some(format!("cannot remove {}: {e}", id.0));
}
self.containers.retain(|c| c.id != id);
if self.selected.as_ref() == Some(&id.0) {
self.selected = None;
self.log_tail = None;
}
}
LauncherAction::OpenWindow {
name,
dir,
id,
mode,
} => {
let mode_arg = match mode {
Mode::Bt => "bt",
Mode::Sim => "sim",
};
match std::env::current_exe().and_then(|exe| {
std::process::Command::new(exe)
.arg("--node")
.arg(&dir)
.arg(&id.0)
.arg(mode_arg)
.spawn()
}) {
Ok(child) => self.children.push(child),
Err(e) => {
self.error = Some(format!("cannot open window for {name}: {e}"));
}
}
}
LauncherAction::BuildImage => {
if let Err(e) = self.docker.build_image() {
self.error = Some(format!("image build failed: {e}"));
}
}
}
}
}
}
impl eframe::App for Launcher {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
Launcher::update(self, ui.ctx());
}
}
+1
View File
@@ -11,4 +11,5 @@ pub mod config;
pub mod docker; pub mod docker;
pub mod fleet; pub mod fleet;
pub mod ipc_client; pub mod ipc_client;
pub mod launcher;
pub mod theme; pub mod theme;
+68 -5
View File
@@ -1,9 +1,72 @@
//! The console binary. Task 8 wires the real UI; for now the launcher //! tines-gui entry point: no args opens the launcher window;
//! sentence is the whole program. //! `--node <dir> <container-id> <mode>` opens a node window (Task 8b).
fn main() { use eframe::egui;
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
if args.len() < 2 { match args.as_slice() {
println!("tines-gui: launcher"); [_, flag, dir, id, mode] if flag == "--node" => {
let _ = (dir, id, mode);
eprintln!("tines-gui: node window is Task 8b — not wired yet");
std::process::exit(1);
}
[_] => run_launcher()?,
_ => {
eprintln!("usage: tines-gui [--node <dir> <container-id> <mode>]");
std::process::exit(2);
}
}
Ok(())
}
/// The launcher window: renders via `Launcher::update`, then drains the
/// action queue — the only place subprocess spawns happen.
struct LauncherApp {
launcher: tines_gui::launcher::Launcher,
theme: tines_gui::theme::OmarchyTheme,
watching: bool,
}
impl eframe::App for LauncherApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
self.theme = tines_gui::theme::OmarchyTheme::load();
self.theme.apply(&ctx);
if !self.watching {
let ctx = ctx.clone();
if tines_gui::theme::OmarchyTheme::watch(move || ctx.request_repaint()).is_ok() {
self.watching = true;
}
}
self.launcher.update(&ctx);
self.launcher.drain_actions();
} }
} }
fn run_launcher() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let viewport = egui::ViewportBuilder::default()
.with_app_id("tines-gui")
.with_title("tines — fleet console")
.with_inner_size([760.0, 520.0]);
let options = eframe::NativeOptions {
viewport,
glow_options: eframe::egui_glow::GlowConfiguration {
vsync: false,
..Default::default()
},
..Default::default()
};
eframe::run_native(
"tines-gui",
options,
Box::new(|_cc| {
Ok(Box::new(LauncherApp {
launcher: tines_gui::launcher::Launcher::new(),
theme: tines_gui::theme::OmarchyTheme::load(),
watching: false,
}))
}),
)
.map_err(|e| format!("eframe: {e}").into())
}
+20
View File
@@ -39,6 +39,26 @@ impl OmarchyTheme {
} }
} }
/// Apply the palette: egui visuals plus an opaque background fill so
/// the window is never transparent on Wayland. Call every frame.
pub fn apply(&self, ctx: &egui::Context) {
let mut visuals = egui::Visuals::dark();
visuals.panel_fill = self.background;
visuals.window_fill = self.background;
visuals.window_stroke = egui::Stroke::NONE;
visuals.override_text_color = Some(self.foreground);
visuals.selection.bg_fill = self.accent;
visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.0, self.accent);
visuals.widgets.active.bg_stroke = egui::Stroke::new(1.0, self.accent);
visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, self.accent);
ctx.set_visuals(visuals);
let painter = ctx.layer_painter(egui::LayerId::new(
egui::Order::Background,
egui::Id::new("tines-gui-bg"),
));
painter.rect_filled(ctx.content_rect(), 0.0, self.background);
}
/// Watch the omarchy `current` theme dir (recursively) and call /// Watch the omarchy `current` theme dir (recursively) and call
/// `on_change` whenever it changes — the caller repaints from there. /// `on_change` whenever it changes — the caller repaints from there.
/// Returns an error only if the watcher cannot be installed. /// Returns an error only if the watcher cannot be installed.
+38
View File
@@ -0,0 +1,38 @@
//! Shared test setup for the integration suites. Compiled per test binary;
//! only declare `mod common;` in tests that use these helpers.
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::sync::OnceLock;
/// One shared scratch setup per test binary: a temp dir that becomes both
/// XDG_STATE_HOME (fleet dirs) and a PATH dir holding a fake `tines-busd`.
/// Env is process-global, so it is set exactly once before any test touches
/// `fleet_dir` — a per-test `set_var` from parallel test threads would race.
pub fn env_with_fake_busd() -> &'static PathBuf {
static DIR: OnceLock<PathBuf> = OnceLock::new();
DIR.get_or_init(|| {
let dir = std::env::temp_dir().join(format!("tines-gui-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
// Fake tines-busd: create the socket named by argv[1], then stay
// alive so the spawning Fleet keeps a live child.
let fake = dir.join("tines-busd");
std::fs::write(&fake, "#!/bin/sh\ntouch \"$1\"\nsleep 60\n").unwrap();
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
let path = format!(
"{}:{}",
dir.display(),
std::env::var("PATH").unwrap_or_default()
);
// SAFETY: set once, before any fleet_dir call; this process is a
// test binary that exits afterwards, so nothing else observes it.
unsafe {
std::env::set_var("XDG_STATE_HOME", &dir);
std::env::set_var("PATH", path);
}
dir
})
}
+10 -42
View File
@@ -1,48 +1,16 @@
//! Fleet layout and node-config tests. The bus daemon is a fake `tines-busd` //! Fleet layout and node-config tests. The bus daemon is a fake `tines-busd`
//! seeded onto PATH (the real binary isn't on PATH for unit tests): it //! seeded onto PATH (see `common`; the real binary isn't on PATH for unit
//! records its argv and creates the socket file, so the fleet's spawn //! tests): it creates the socket file named by argv[1], so the fleet's
//! protocol is what's under test — the real daemon is covered by tines-bus's //! spawn protocol is what's under test — the real daemon is covered by
//! own suite and Task 4's e2e. //! tines-bus's own suite and Task 4's e2e.
mod common;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tines_gui::fleet::{Fleet, Mode}; use tines_gui::fleet::{Fleet, Mode};
/// One shared scratch setup for every test in this binary: a temp dir that
/// becomes both XDG_STATE_HOME (fleet dirs) and a PATH dir holding the fake
/// `tines-busd`. Env is process-global, so it is set exactly once before any
/// test touches fleet_dir.
fn setup() -> &'static PathBuf {
static DIR: OnceLock<PathBuf> = OnceLock::new();
DIR.get_or_init(|| {
let dir = std::env::temp_dir().join(format!("tines-gui-fleet-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
// Fake tines-busd: touch the socket named by argv[1], then stay alive.
let fake = dir.join("tines-busd");
std::fs::write(&fake, "#!/bin/sh\ntouch \"$1\"\nsleep 60\n").unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
let path = format!(
"{}:{}",
dir.display(),
std::env::var("PATH").unwrap_or_default()
);
// SAFETY: set once, before any fleet_dir call; this process is a
// test binary that exits afterwards, so nothing else observes it.
unsafe {
std::env::set_var("XDG_STATE_HOME", &dir);
std::env::set_var("PATH", path);
}
dir
})
}
fn fleet_name(tag: &str) -> String { fn fleet_name(tag: &str) -> String {
format!( format!(
"test-{tag}-{}", "test-{tag}-{}",
@@ -52,7 +20,7 @@ fn fleet_name(tag: &str) -> String {
#[test] #[test]
fn fleet_dir_layout() { fn fleet_dir_layout() {
setup(); common::env_with_fake_busd();
let fleet = Fleet::create(&fleet_name("layout")).expect("fleet creates"); let fleet = Fleet::create(&fleet_name("layout")).expect("fleet creates");
let dir = tines_gui::config::fleet_dir(&fleet_name("layout")); let dir = tines_gui::config::fleet_dir(&fleet_name("layout"));
let meta = std::fs::metadata(&dir).expect("fleet dir exists"); let meta = std::fs::metadata(&dir).expect("fleet dir exists");
@@ -75,7 +43,7 @@ fn fleet_dir_layout() {
#[test] #[test]
fn add_node_bus_config_mounts_data() { fn add_node_bus_config_mounts_data() {
setup(); common::env_with_fake_busd();
let fleet_name = fleet_name("bus"); let fleet_name = fleet_name("bus");
let fleet = Fleet::create(&fleet_name).expect("fleet creates"); let fleet = Fleet::create(&fleet_name).expect("fleet creates");
let spec = fleet let spec = fleet
@@ -95,7 +63,7 @@ fn add_node_bus_config_mounts_data() {
#[test] #[test]
fn add_node_bt_config_has_peer() { fn add_node_bt_config_has_peer() {
setup(); common::env_with_fake_busd();
let fleet_name = fleet_name("bt"); let fleet_name = fleet_name("bt");
let fleet = Fleet::create(&fleet_name).expect("fleet creates"); let fleet = Fleet::create(&fleet_name).expect("fleet creates");
let peer = "22".repeat(16); let peer = "22".repeat(16);
@@ -114,7 +82,7 @@ fn add_node_bt_config_has_peer() {
#[test] #[test]
fn generated_id_roundtrips() { fn generated_id_roundtrips() {
setup(); common::env_with_fake_busd();
let fleet_name = fleet_name("gen"); let fleet_name = fleet_name("gen");
let fleet = Fleet::create(&fleet_name).expect("fleet creates"); let fleet = Fleet::create(&fleet_name).expect("fleet creates");
let spec = fleet let spec = fleet
+108
View File
@@ -0,0 +1,108 @@
//! Launcher UI tests (egui_kittest, headless). No real docker or daemon:
//! the fake `tines-busd` is seeded on PATH (see `common`), and the
//! containers are injected directly into the launcher's state.
mod common;
use egui::accesskit;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use tines_gui::config::fleet_dir;
use tines_gui::docker::ContainerId;
use tines_gui::fleet::Mode;
use tines_gui::launcher::{ContainerEntry, Launcher, LauncherAction};
/// The test-side action drain: peek at what the UI queued (main.rs owns the
/// real drain with the subprocess spawns).
fn drain(launcher: &mut Launcher) -> Vec<LauncherAction> {
launcher.actions.drain(..).collect()
}
#[test]
fn create_fleet_action_queues_spawn() {
common::env_with_fake_busd();
let mut harness = Harness::new_eframe(|_cc| Launcher::new());
harness.run();
// Type the fleet name into the first text field, then click Create.
harness
.get_all_by_role(accesskit::Role::TextInput)
.next()
.expect("fleet name field")
.focus();
harness.run();
harness
.get_all_by_role(accesskit::Role::TextInput)
.next()
.expect("fleet name field")
.type_text("yard");
harness.run();
harness.get_by_label("Create").click();
harness.run();
let launcher = harness.state();
assert!(
launcher.fleet.is_some(),
"Create must set the fleet (spawning the bus daemon)"
);
// The action queue must be drainable; fleet creation itself is not an
// action (it is the fleet field).
let launcher = harness.state_mut();
assert!(drain(launcher).is_empty());
}
#[test]
fn open_window_queues_spawn() {
common::env_with_fake_busd();
let mut harness = Harness::new_eframe(|_cc| {
let mut launcher = Launcher::new();
launcher.fleet_name = "yard".to_string();
launcher.containers.push(ContainerEntry {
name: "alpha".to_string(),
id: ContainerId("deadbeef".to_string()),
mode: Mode::Sim,
});
launcher
});
harness.run();
harness.get_by_label("Open").click();
harness.run();
let actions = drain(harness.state_mut());
assert_eq!(
actions,
vec![LauncherAction::OpenWindow {
name: "alpha".to_string(),
dir: fleet_dir("yard").join("alpha"),
id: ContainerId("deadbeef".to_string()),
mode: Mode::Sim,
}]
);
}
#[test]
fn stop_queues_stop_action() {
common::env_with_fake_busd();
let mut harness = Harness::new_eframe(|_cc| {
let mut launcher = Launcher::new();
launcher.containers.push(ContainerEntry {
name: "bravo".to_string(),
id: ContainerId("cafebabe".to_string()),
mode: Mode::Bt,
});
launcher
});
harness.run();
harness.get_by_label("Stop").click();
harness.run();
let actions = drain(harness.state_mut());
assert_eq!(
actions,
vec![LauncherAction::StopContainer(ContainerId(
"cafebabe".to_string()
))]
);
}