chore: initial commit
32 files changed, +20163 -0
+4-0.gitignore
| @@ -0,0 +1,4 @@ | ||
| 1 | +/target | |
| 2 | +.superpowers/ | |
| 3 | +/private_samples/ | |
| 4 | +/tests/private/ |
+5517-0Cargo.lock
| @@ -0,0 +1,5517 @@ | ||
| 1 | +# This file is automatically @generated by Cargo. | |
| 2 | +# It is not intended for manual editing. | |
| 3 | +version = 4 | |
| 4 | + | |
| 5 | +[[package]] | |
| 6 | +name = "ab_glyph" | |
| 7 | +version = "0.2.32" | |
| 8 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 9 | +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" | |
| 10 | +dependencies = [ | |
| 11 | + "ab_glyph_rasterizer", | |
| 12 | + "owned_ttf_parser", | |
| 13 | +] | |
| 14 | + | |
| 15 | +[[package]] | |
| 16 | +name = "ab_glyph_rasterizer" | |
| 17 | +version = "0.1.10" | |
| 18 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 19 | +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" | |
| 20 | + | |
| 21 | +[[package]] | |
| 22 | +name = "accesskit" | |
| 23 | +version = "0.17.1" | |
| 24 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 25 | +checksum = "d3d3b8f9bae46a948369bc4a03e815d4ed6d616bd00de4051133a5019dc31c5a" | |
| 26 | +dependencies = [ | |
| 27 | + "enumn", | |
| 28 | + "serde", | |
| 29 | +] | |
| 30 | + | |
| 31 | +[[package]] | |
| 32 | +name = "accesskit_atspi_common" | |
| 33 | +version = "0.10.1" | |
| 34 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 35 | +checksum = "7c5dd55e6e94949498698daf4d48fb5659e824d7abec0d394089656ceaf99d4f" | |
| 36 | +dependencies = [ | |
| 37 | + "accesskit", | |
| 38 | + "accesskit_consumer", | |
| 39 | + "atspi-common", | |
| 40 | + "serde", | |
| 41 | + "thiserror 1.0.69", | |
| 42 | + "zvariant 4.2.0", | |
| 43 | +] | |
| 44 | + | |
| 45 | +[[package]] | |
| 46 | +name = "accesskit_consumer" | |
| 47 | +version = "0.26.0" | |
| 48 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 49 | +checksum = "f47983a1084940ba9a39c077a8c63e55c619388be5476ac04c804cfbd1e63459" | |
| 50 | +dependencies = [ | |
| 51 | + "accesskit", | |
| 52 | + "hashbrown 0.15.5", | |
| 53 | + "immutable-chunkmap", | |
| 54 | +] | |
| 55 | + | |
| 56 | +[[package]] | |
| 57 | +name = "accesskit_macos" | |
| 58 | +version = "0.18.1" | |
| 59 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 60 | +checksum = "7329821f3bd1101e03a7d2e03bd339e3ac0dc64c70b4c9f9ae1949e3ba8dece1" | |
| 61 | +dependencies = [ | |
| 62 | + "accesskit", | |
| 63 | + "accesskit_consumer", | |
| 64 | + "hashbrown 0.15.5", | |
| 65 | + "objc2 0.5.2", | |
| 66 | + "objc2-app-kit 0.2.2", | |
| 67 | + "objc2-foundation 0.2.2", | |
| 68 | +] | |
| 69 | + | |
| 70 | +[[package]] | |
| 71 | +name = "accesskit_unix" | |
| 72 | +version = "0.13.1" | |
| 73 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 74 | +checksum = "fcee751cc20d88678c33edaf9c07e8b693cd02819fe89053776f5313492273f5" | |
| 75 | +dependencies = [ | |
| 76 | + "accesskit", | |
| 77 | + "accesskit_atspi_common", | |
| 78 | + "async-channel", | |
| 79 | + "async-executor", | |
| 80 | + "async-task", | |
| 81 | + "atspi", | |
| 82 | + "futures-lite", | |
| 83 | + "futures-util", | |
| 84 | + "serde", | |
| 85 | + "zbus 4.4.0", | |
| 86 | +] | |
| 87 | + | |
| 88 | +[[package]] | |
| 89 | +name = "accesskit_windows" | |
| 90 | +version = "0.24.1" | |
| 91 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 92 | +checksum = "24fcd5d23d70670992b823e735e859374d694a3d12bfd8dd32bd3bd8bedb5d81" | |
| 93 | +dependencies = [ | |
| 94 | + "accesskit", | |
| 95 | + "accesskit_consumer", | |
| 96 | + "hashbrown 0.15.5", | |
| 97 | + "paste", | |
| 98 | + "static_assertions", | |
| 99 | + "windows", | |
| 100 | + "windows-core 0.58.0", | |
| 101 | +] | |
| 102 | + | |
| 103 | +[[package]] | |
| 104 | +name = "accesskit_winit" | |
| 105 | +version = "0.23.1" | |
| 106 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 107 | +checksum = "6a6a48dad5530b6deb9fc7a52cc6c3bf72cdd9eb8157ac9d32d69f2427a5e879" | |
| 108 | +dependencies = [ | |
| 109 | + "accesskit", | |
| 110 | + "accesskit_macos", | |
| 111 | + "accesskit_unix", | |
| 112 | + "accesskit_windows", | |
| 113 | + "raw-window-handle", | |
| 114 | + "winit", | |
| 115 | +] | |
| 116 | + | |
| 117 | +[[package]] | |
| 118 | +name = "adler2" | |
| 119 | +version = "2.0.1" | |
| 120 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 121 | +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" | |
| 122 | + | |
| 123 | +[[package]] | |
| 124 | +name = "ahash" | |
| 125 | +version = "0.8.12" | |
| 126 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 127 | +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" | |
| 128 | +dependencies = [ | |
| 129 | + "cfg-if", | |
| 130 | + "getrandom 0.3.4", | |
| 131 | + "once_cell", | |
| 132 | + "serde", | |
| 133 | + "version_check", | |
| 134 | + "zerocopy", | |
| 135 | +] | |
| 136 | + | |
| 137 | +[[package]] | |
| 138 | +name = "aho-corasick" | |
| 139 | +version = "1.1.4" | |
| 140 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 141 | +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" | |
| 142 | +dependencies = [ | |
| 143 | + "memchr", | |
| 144 | +] | |
| 145 | + | |
| 146 | +[[package]] | |
| 147 | +name = "allocator-api2" | |
| 148 | +version = "0.2.21" | |
| 149 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 150 | +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" | |
| 151 | + | |
| 152 | +[[package]] | |
| 153 | +name = "android-activity" | |
| 154 | +version = "0.6.0" | |
| 155 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 156 | +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" | |
| 157 | +dependencies = [ | |
| 158 | + "android-properties", | |
| 159 | + "bitflags 2.11.0", | |
| 160 | + "cc", | |
| 161 | + "cesu8", | |
| 162 | + "jni 0.21.1", | |
| 163 | + "jni-sys 0.3.0", | |
| 164 | + "libc", | |
| 165 | + "log", | |
| 166 | + "ndk", | |
| 167 | + "ndk-context", | |
| 168 | + "ndk-sys 0.6.0+11769913", | |
| 169 | + "num_enum", | |
| 170 | + "thiserror 1.0.69", | |
| 171 | +] | |
| 172 | + | |
| 173 | +[[package]] | |
| 174 | +name = "android-properties" | |
| 175 | +version = "0.2.2" | |
| 176 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 177 | +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" | |
| 178 | + | |
| 179 | +[[package]] | |
| 180 | +name = "android_system_properties" | |
| 181 | +version = "0.1.5" | |
| 182 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 183 | +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" | |
| 184 | +dependencies = [ | |
| 185 | + "libc", | |
| 186 | +] | |
| 187 | + | |
| 188 | +[[package]] | |
| 189 | +name = "anyhow" | |
| 190 | +version = "1.0.102" | |
| 191 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 192 | +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" | |
| 193 | + | |
| 194 | +[[package]] | |
| 195 | +name = "arboard" | |
| 196 | +version = "3.6.1" | |
| 197 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 198 | +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" | |
| 199 | +dependencies = [ | |
| 200 | + "clipboard-win", | |
| 201 | + "image", | |
| 202 | + "log", | |
| 203 | + "objc2 0.6.4", | |
| 204 | + "objc2-app-kit 0.3.2", | |
| 205 | + "objc2-core-foundation", | |
| 206 | + "objc2-core-graphics", | |
| 207 | + "objc2-foundation 0.3.2", | |
| 208 | + "parking_lot", | |
| 209 | + "percent-encoding", | |
| 210 | + "windows-sys 0.60.2", | |
| 211 | + "x11rb", | |
| 212 | +] | |
| 213 | + | |
| 214 | +[[package]] | |
| 215 | +name = "arrayref" | |
| 216 | +version = "0.3.9" | |
| 217 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 218 | +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" | |
| 219 | + | |
| 220 | +[[package]] | |
| 221 | +name = "arrayvec" | |
| 222 | +version = "0.7.6" | |
| 223 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 224 | +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" | |
| 225 | + | |
| 226 | +[[package]] | |
| 227 | +name = "as-raw-xcb-connection" | |
| 228 | +version = "1.0.1" | |
| 229 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 230 | +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" | |
| 231 | + | |
| 232 | +[[package]] | |
| 233 | +name = "ash" | |
| 234 | +version = "0.38.0+1.3.281" | |
| 235 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 236 | +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" | |
| 237 | +dependencies = [ | |
| 238 | + "libloading", | |
| 239 | +] | |
| 240 | + | |
| 241 | +[[package]] | |
| 242 | +name = "ashpd" | |
| 243 | +version = "0.11.1" | |
| 244 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 245 | +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" | |
| 246 | +dependencies = [ | |
| 247 | + "async-fs", | |
| 248 | + "async-net", | |
| 249 | + "enumflags2", | |
| 250 | + "futures-channel", | |
| 251 | + "futures-util", | |
| 252 | + "rand 0.9.2", | |
| 253 | + "raw-window-handle", | |
| 254 | + "serde", | |
| 255 | + "serde_repr", | |
| 256 | + "url", | |
| 257 | + "wayland-backend", | |
| 258 | + "wayland-client", | |
| 259 | + "wayland-protocols", | |
| 260 | + "zbus 5.14.0", | |
| 261 | +] | |
| 262 | + | |
| 263 | +[[package]] | |
| 264 | +name = "async-broadcast" | |
| 265 | +version = "0.7.2" | |
| 266 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 267 | +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" | |
| 268 | +dependencies = [ | |
| 269 | + "event-listener", | |
| 270 | + "event-listener-strategy", | |
| 271 | + "futures-core", | |
| 272 | + "pin-project-lite", | |
| 273 | +] | |
| 274 | + | |
| 275 | +[[package]] | |
| 276 | +name = "async-channel" | |
| 277 | +version = "2.5.0" | |
| 278 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 279 | +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" | |
| 280 | +dependencies = [ | |
| 281 | + "concurrent-queue", | |
| 282 | + "event-listener-strategy", | |
| 283 | + "futures-core", | |
| 284 | + "pin-project-lite", | |
| 285 | +] | |
| 286 | + | |
| 287 | +[[package]] | |
| 288 | +name = "async-executor" | |
| 289 | +version = "1.14.0" | |
| 290 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 291 | +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" | |
| 292 | +dependencies = [ | |
| 293 | + "async-task", | |
| 294 | + "concurrent-queue", | |
| 295 | + "fastrand", | |
| 296 | + "futures-lite", | |
| 297 | + "pin-project-lite", | |
| 298 | + "slab", | |
| 299 | +] | |
| 300 | + | |
| 301 | +[[package]] | |
| 302 | +name = "async-fs" | |
| 303 | +version = "2.2.0" | |
| 304 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 305 | +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" | |
| 306 | +dependencies = [ | |
| 307 | + "async-lock", | |
| 308 | + "blocking", | |
| 309 | + "futures-lite", | |
| 310 | +] | |
| 311 | + | |
| 312 | +[[package]] | |
| 313 | +name = "async-io" | |
| 314 | +version = "2.6.0" | |
| 315 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 316 | +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" | |
| 317 | +dependencies = [ | |
| 318 | + "autocfg", | |
| 319 | + "cfg-if", | |
| 320 | + "concurrent-queue", | |
| 321 | + "futures-io", | |
| 322 | + "futures-lite", | |
| 323 | + "parking", | |
| 324 | + "polling", | |
| 325 | + "rustix 1.1.4", | |
| 326 | + "slab", | |
| 327 | + "windows-sys 0.61.2", | |
| 328 | +] | |
| 329 | + | |
| 330 | +[[package]] | |
| 331 | +name = "async-lock" | |
| 332 | +version = "3.4.2" | |
| 333 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 334 | +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" | |
| 335 | +dependencies = [ | |
| 336 | + "event-listener", | |
| 337 | + "event-listener-strategy", | |
| 338 | + "pin-project-lite", | |
| 339 | +] | |
| 340 | + | |
| 341 | +[[package]] | |
| 342 | +name = "async-net" | |
| 343 | +version = "2.0.0" | |
| 344 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 345 | +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" | |
| 346 | +dependencies = [ | |
| 347 | + "async-io", | |
| 348 | + "blocking", | |
| 349 | + "futures-lite", | |
| 350 | +] | |
| 351 | + | |
| 352 | +[[package]] | |
| 353 | +name = "async-process" | |
| 354 | +version = "2.5.0" | |
| 355 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 356 | +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" | |
| 357 | +dependencies = [ | |
| 358 | + "async-channel", | |
| 359 | + "async-io", | |
| 360 | + "async-lock", | |
| 361 | + "async-signal", | |
| 362 | + "async-task", | |
| 363 | + "blocking", | |
| 364 | + "cfg-if", | |
| 365 | + "event-listener", | |
| 366 | + "futures-lite", | |
| 367 | + "rustix 1.1.4", | |
| 368 | +] | |
| 369 | + | |
| 370 | +[[package]] | |
| 371 | +name = "async-recursion" | |
| 372 | +version = "1.1.1" | |
| 373 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 374 | +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" | |
| 375 | +dependencies = [ | |
| 376 | + "proc-macro2", | |
| 377 | + "quote", | |
| 378 | + "syn", | |
| 379 | +] | |
| 380 | + | |
| 381 | +[[package]] | |
| 382 | +name = "async-signal" | |
| 383 | +version = "0.2.13" | |
| 384 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 385 | +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" | |
| 386 | +dependencies = [ | |
| 387 | + "async-io", | |
| 388 | + "async-lock", | |
| 389 | + "atomic-waker", | |
| 390 | + "cfg-if", | |
| 391 | + "futures-core", | |
| 392 | + "futures-io", | |
| 393 | + "rustix 1.1.4", | |
| 394 | + "signal-hook-registry", | |
| 395 | + "slab", | |
| 396 | + "windows-sys 0.61.2", | |
| 397 | +] | |
| 398 | + | |
| 399 | +[[package]] | |
| 400 | +name = "async-task" | |
| 401 | +version = "4.7.1" | |
| 402 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 403 | +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" | |
| 404 | + | |
| 405 | +[[package]] | |
| 406 | +name = "async-trait" | |
| 407 | +version = "0.1.89" | |
| 408 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 409 | +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" | |
| 410 | +dependencies = [ | |
| 411 | + "proc-macro2", | |
| 412 | + "quote", | |
| 413 | + "syn", | |
| 414 | +] | |
| 415 | + | |
| 416 | +[[package]] | |
| 417 | +name = "atomic-waker" | |
| 418 | +version = "1.1.2" | |
| 419 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 420 | +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" | |
| 421 | + | |
| 422 | +[[package]] | |
| 423 | +name = "atspi" | |
| 424 | +version = "0.22.0" | |
| 425 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 426 | +checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" | |
| 427 | +dependencies = [ | |
| 428 | + "atspi-common", | |
| 429 | + "atspi-connection", | |
| 430 | + "atspi-proxies", | |
| 431 | +] | |
| 432 | + | |
| 433 | +[[package]] | |
| 434 | +name = "atspi-common" | |
| 435 | +version = "0.6.0" | |
| 436 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 437 | +checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" | |
| 438 | +dependencies = [ | |
| 439 | + "enumflags2", | |
| 440 | + "serde", | |
| 441 | + "static_assertions", | |
| 442 | + "zbus 4.4.0", | |
| 443 | + "zbus-lockstep", | |
| 444 | + "zbus-lockstep-macros", | |
| 445 | + "zbus_names 3.0.0", | |
| 446 | + "zvariant 4.2.0", | |
| 447 | +] | |
| 448 | + | |
| 449 | +[[package]] | |
| 450 | +name = "atspi-connection" | |
| 451 | +version = "0.6.0" | |
| 452 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 453 | +checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" | |
| 454 | +dependencies = [ | |
| 455 | + "atspi-common", | |
| 456 | + "atspi-proxies", | |
| 457 | + "futures-lite", | |
| 458 | + "zbus 4.4.0", | |
| 459 | +] | |
| 460 | + | |
| 461 | +[[package]] | |
| 462 | +name = "atspi-proxies" | |
| 463 | +version = "0.6.0" | |
| 464 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 465 | +checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" | |
| 466 | +dependencies = [ | |
| 467 | + "atspi-common", | |
| 468 | + "serde", | |
| 469 | + "zbus 4.4.0", | |
| 470 | + "zvariant 4.2.0", | |
| 471 | +] | |
| 472 | + | |
| 473 | +[[package]] | |
| 474 | +name = "autocfg" | |
| 475 | +version = "1.5.0" | |
| 476 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 477 | +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" | |
| 478 | + | |
| 479 | +[[package]] | |
| 480 | +name = "base64" | |
| 481 | +version = "0.21.7" | |
| 482 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 483 | +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" | |
| 484 | + | |
| 485 | +[[package]] | |
| 486 | +name = "base64" | |
| 487 | +version = "0.22.1" | |
| 488 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 489 | +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" | |
| 490 | + | |
| 491 | +[[package]] | |
| 492 | +name = "bincode" | |
| 493 | +version = "1.3.3" | |
| 494 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 495 | +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" | |
| 496 | +dependencies = [ | |
| 497 | + "serde", | |
| 498 | +] | |
| 499 | + | |
| 500 | +[[package]] | |
| 501 | +name = "bit-set" | |
| 502 | +version = "0.8.0" | |
| 503 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 504 | +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" | |
| 505 | +dependencies = [ | |
| 506 | + "bit-vec", | |
| 507 | +] | |
| 508 | + | |
| 509 | +[[package]] | |
| 510 | +name = "bit-vec" | |
| 511 | +version = "0.8.0" | |
| 512 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 513 | +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" | |
| 514 | + | |
| 515 | +[[package]] | |
| 516 | +name = "bitflags" | |
| 517 | +version = "1.3.2" | |
| 518 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 519 | +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" | |
| 520 | + | |
| 521 | +[[package]] | |
| 522 | +name = "bitflags" | |
| 523 | +version = "2.11.0" | |
| 524 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 525 | +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" | |
| 526 | +dependencies = [ | |
| 527 | + "serde_core", | |
| 528 | +] | |
| 529 | + | |
| 530 | +[[package]] | |
| 531 | +name = "block" | |
| 532 | +version = "0.1.6" | |
| 533 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 534 | +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" | |
| 535 | + | |
| 536 | +[[package]] | |
| 537 | +name = "block-buffer" | |
| 538 | +version = "0.10.4" | |
| 539 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 540 | +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | |
| 541 | +dependencies = [ | |
| 542 | + "generic-array", | |
| 543 | +] | |
| 544 | + | |
| 545 | +[[package]] | |
| 546 | +name = "block2" | |
| 547 | +version = "0.5.1" | |
| 548 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 549 | +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" | |
| 550 | +dependencies = [ | |
| 551 | + "objc2 0.5.2", | |
| 552 | +] | |
| 553 | + | |
| 554 | +[[package]] | |
| 555 | +name = "block2" | |
| 556 | +version = "0.6.2" | |
| 557 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 558 | +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" | |
| 559 | +dependencies = [ | |
| 560 | + "objc2 0.6.4", | |
| 561 | +] | |
| 562 | + | |
| 563 | +[[package]] | |
| 564 | +name = "blocking" | |
| 565 | +version = "1.6.2" | |
| 566 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 567 | +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" | |
| 568 | +dependencies = [ | |
| 569 | + "async-channel", | |
| 570 | + "async-task", | |
| 571 | + "futures-io", | |
| 572 | + "futures-lite", | |
| 573 | + "piper", | |
| 574 | +] | |
| 575 | + | |
| 576 | +[[package]] | |
| 577 | +name = "bumpalo" | |
| 578 | +version = "3.20.2" | |
| 579 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 580 | +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" | |
| 581 | + | |
| 582 | +[[package]] | |
| 583 | +name = "bytemuck" | |
| 584 | +version = "1.25.0" | |
| 585 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 586 | +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" | |
| 587 | +dependencies = [ | |
| 588 | + "bytemuck_derive", | |
| 589 | +] | |
| 590 | + | |
| 591 | +[[package]] | |
| 592 | +name = "bytemuck_derive" | |
| 593 | +version = "1.10.2" | |
| 594 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 595 | +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" | |
| 596 | +dependencies = [ | |
| 597 | + "proc-macro2", | |
| 598 | + "quote", | |
| 599 | + "syn", | |
| 600 | +] | |
| 601 | + | |
| 602 | +[[package]] | |
| 603 | +name = "byteorder-lite" | |
| 604 | +version = "0.1.0" | |
| 605 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 606 | +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" | |
| 607 | + | |
| 608 | +[[package]] | |
| 609 | +name = "bytes" | |
| 610 | +version = "1.11.1" | |
| 611 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 612 | +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" | |
| 613 | + | |
| 614 | +[[package]] | |
| 615 | +name = "calloop" | |
| 616 | +version = "0.13.0" | |
| 617 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 618 | +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" | |
| 619 | +dependencies = [ | |
| 620 | + "bitflags 2.11.0", | |
| 621 | + "log", | |
| 622 | + "polling", | |
| 623 | + "rustix 0.38.44", | |
| 624 | + "slab", | |
| 625 | + "thiserror 1.0.69", | |
| 626 | +] | |
| 627 | + | |
| 628 | +[[package]] | |
| 629 | +name = "calloop" | |
| 630 | +version = "0.14.4" | |
| 631 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 632 | +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" | |
| 633 | +dependencies = [ | |
| 634 | + "bitflags 2.11.0", | |
| 635 | + "polling", | |
| 636 | + "rustix 1.1.4", | |
| 637 | + "slab", | |
| 638 | + "tracing", | |
| 639 | +] | |
| 640 | + | |
| 641 | +[[package]] | |
| 642 | +name = "calloop-wayland-source" | |
| 643 | +version = "0.3.0" | |
| 644 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 645 | +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" | |
| 646 | +dependencies = [ | |
| 647 | + "calloop 0.13.0", | |
| 648 | + "rustix 0.38.44", | |
| 649 | + "wayland-backend", | |
| 650 | + "wayland-client", | |
| 651 | +] | |
| 652 | + | |
| 653 | +[[package]] | |
| 654 | +name = "calloop-wayland-source" | |
| 655 | +version = "0.4.1" | |
| 656 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 657 | +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" | |
| 658 | +dependencies = [ | |
| 659 | + "calloop 0.14.4", | |
| 660 | + "rustix 1.1.4", | |
| 661 | + "wayland-backend", | |
| 662 | + "wayland-client", | |
| 663 | +] | |
| 664 | + | |
| 665 | +[[package]] | |
| 666 | +name = "cc" | |
| 667 | +version = "1.2.56" | |
| 668 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 669 | +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" | |
| 670 | +dependencies = [ | |
| 671 | + "find-msvc-tools", | |
| 672 | + "jobserver", | |
| 673 | + "libc", | |
| 674 | + "shlex", | |
| 675 | +] | |
| 676 | + | |
| 677 | +[[package]] | |
| 678 | +name = "cesu8" | |
| 679 | +version = "1.1.0" | |
| 680 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 681 | +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" | |
| 682 | + | |
| 683 | +[[package]] | |
| 684 | +name = "cfg-if" | |
| 685 | +version = "1.0.4" | |
| 686 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 687 | +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" | |
| 688 | + | |
| 689 | +[[package]] | |
| 690 | +name = "cfg_aliases" | |
| 691 | +version = "0.2.1" | |
| 692 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 693 | +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" | |
| 694 | + | |
| 695 | +[[package]] | |
| 696 | +name = "cgl" | |
| 697 | +version = "0.3.2" | |
| 698 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 699 | +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" | |
| 700 | +dependencies = [ | |
| 701 | + "libc", | |
| 702 | +] | |
| 703 | + | |
| 704 | +[[package]] | |
| 705 | +name = "chrono" | |
| 706 | +version = "0.4.44" | |
| 707 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 708 | +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" | |
| 709 | +dependencies = [ | |
| 710 | + "iana-time-zone", | |
| 711 | + "js-sys", | |
| 712 | + "num-traits", | |
| 713 | + "serde", | |
| 714 | + "wasm-bindgen", | |
| 715 | + "windows-link", | |
| 716 | +] | |
| 717 | + | |
| 718 | +[[package]] | |
| 719 | +name = "chrono-tz" | |
| 720 | +version = "0.10.4" | |
| 721 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 722 | +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" | |
| 723 | +dependencies = [ | |
| 724 | + "chrono", | |
| 725 | + "phf 0.12.1", | |
| 726 | +] | |
| 727 | + | |
| 728 | +[[package]] | |
| 729 | +name = "chumsky" | |
| 730 | +version = "0.9.3" | |
| 731 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 732 | +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" | |
| 733 | +dependencies = [ | |
| 734 | + "hashbrown 0.14.5", | |
| 735 | +] | |
| 736 | + | |
| 737 | +[[package]] | |
| 738 | +name = "clipboard-win" | |
| 739 | +version = "5.4.1" | |
| 740 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 741 | +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" | |
| 742 | +dependencies = [ | |
| 743 | + "error-code", | |
| 744 | +] | |
| 745 | + | |
| 746 | +[[package]] | |
| 747 | +name = "codespan-reporting" | |
| 748 | +version = "0.11.1" | |
| 749 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 750 | +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" | |
| 751 | +dependencies = [ | |
| 752 | + "termcolor", | |
| 753 | + "unicode-width", | |
| 754 | +] | |
| 755 | + | |
| 756 | +[[package]] | |
| 757 | +name = "combine" | |
| 758 | +version = "4.6.7" | |
| 759 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 760 | +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" | |
| 761 | +dependencies = [ | |
| 762 | + "bytes", | |
| 763 | + "memchr", | |
| 764 | +] | |
| 765 | + | |
| 766 | +[[package]] | |
| 767 | +name = "concurrent-queue" | |
| 768 | +version = "2.5.0" | |
| 769 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 770 | +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" | |
| 771 | +dependencies = [ | |
| 772 | + "crossbeam-utils", | |
| 773 | +] | |
| 774 | + | |
| 775 | +[[package]] | |
| 776 | +name = "core-foundation" | |
| 777 | +version = "0.9.4" | |
| 778 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 779 | +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" | |
| 780 | +dependencies = [ | |
| 781 | + "core-foundation-sys", | |
| 782 | + "libc", | |
| 783 | +] | |
| 784 | + | |
| 785 | +[[package]] | |
| 786 | +name = "core-foundation" | |
| 787 | +version = "0.10.1" | |
| 788 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 789 | +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" | |
| 790 | +dependencies = [ | |
| 791 | + "core-foundation-sys", | |
| 792 | + "libc", | |
| 793 | +] | |
| 794 | + | |
| 795 | +[[package]] | |
| 796 | +name = "core-foundation-sys" | |
| 797 | +version = "0.8.7" | |
| 798 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 799 | +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" | |
| 800 | + | |
| 801 | +[[package]] | |
| 802 | +name = "core-graphics" | |
| 803 | +version = "0.23.2" | |
| 804 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 805 | +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" | |
| 806 | +dependencies = [ | |
| 807 | + "bitflags 1.3.2", | |
| 808 | + "core-foundation 0.9.4", | |
| 809 | + "core-graphics-types", | |
| 810 | + "foreign-types", | |
| 811 | + "libc", | |
| 812 | +] | |
| 813 | + | |
| 814 | +[[package]] | |
| 815 | +name = "core-graphics-types" | |
| 816 | +version = "0.1.3" | |
| 817 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 818 | +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" | |
| 819 | +dependencies = [ | |
| 820 | + "bitflags 1.3.2", | |
| 821 | + "core-foundation 0.9.4", | |
| 822 | + "libc", | |
| 823 | +] | |
| 824 | + | |
| 825 | +[[package]] | |
| 826 | +name = "cpufeatures" | |
| 827 | +version = "0.2.17" | |
| 828 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 829 | +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" | |
| 830 | +dependencies = [ | |
| 831 | + "libc", | |
| 832 | +] | |
| 833 | + | |
| 834 | +[[package]] | |
| 835 | +name = "crc32fast" | |
| 836 | +version = "1.5.0" | |
| 837 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 838 | +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" | |
| 839 | +dependencies = [ | |
| 840 | + "cfg-if", | |
| 841 | +] | |
| 842 | + | |
| 843 | +[[package]] | |
| 844 | +name = "crossbeam-utils" | |
| 845 | +version = "0.8.21" | |
| 846 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 847 | +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" | |
| 848 | + | |
| 849 | +[[package]] | |
| 850 | +name = "crunchy" | |
| 851 | +version = "0.2.4" | |
| 852 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 853 | +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" | |
| 854 | + | |
| 855 | +[[package]] | |
| 856 | +name = "crypto-common" | |
| 857 | +version = "0.1.7" | |
| 858 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 859 | +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" | |
| 860 | +dependencies = [ | |
| 861 | + "generic-array", | |
| 862 | + "typenum", | |
| 863 | +] | |
| 864 | + | |
| 865 | +[[package]] | |
| 866 | +name = "cursor-icon" | |
| 867 | +version = "1.2.0" | |
| 868 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 869 | +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" | |
| 870 | + | |
| 871 | +[[package]] | |
| 872 | +name = "data-url" | |
| 873 | +version = "0.3.2" | |
| 874 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 875 | +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" | |
| 876 | + | |
| 877 | +[[package]] | |
| 878 | +name = "deranged" | |
| 879 | +version = "0.5.8" | |
| 880 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 881 | +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" | |
| 882 | +dependencies = [ | |
| 883 | + "powerfmt", | |
| 884 | +] | |
| 885 | + | |
| 886 | +[[package]] | |
| 887 | +name = "digest" | |
| 888 | +version = "0.10.7" | |
| 889 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 890 | +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" | |
| 891 | +dependencies = [ | |
| 892 | + "block-buffer", | |
| 893 | + "crypto-common", | |
| 894 | +] | |
| 895 | + | |
| 896 | +[[package]] | |
| 897 | +name = "directories" | |
| 898 | +version = "6.0.0" | |
| 899 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 900 | +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" | |
| 901 | +dependencies = [ | |
| 902 | + "dirs-sys", | |
| 903 | +] | |
| 904 | + | |
| 905 | +[[package]] | |
| 906 | +name = "dirs-sys" | |
| 907 | +version = "0.5.0" | |
| 908 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 909 | +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" | |
| 910 | +dependencies = [ | |
| 911 | + "libc", | |
| 912 | + "option-ext", | |
| 913 | + "redox_users", | |
| 914 | + "windows-sys 0.61.2", | |
| 915 | +] | |
| 916 | + | |
| 917 | +[[package]] | |
| 918 | +name = "dispatch" | |
| 919 | +version = "0.2.0" | |
| 920 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 921 | +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" | |
| 922 | + | |
| 923 | +[[package]] | |
| 924 | +name = "dispatch2" | |
| 925 | +version = "0.3.1" | |
| 926 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 927 | +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" | |
| 928 | +dependencies = [ | |
| 929 | + "bitflags 2.11.0", | |
| 930 | + "block2 0.6.2", | |
| 931 | + "libc", | |
| 932 | + "objc2 0.6.4", | |
| 933 | +] | |
| 934 | + | |
| 935 | +[[package]] | |
| 936 | +name = "displaydoc" | |
| 937 | +version = "0.2.5" | |
| 938 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 939 | +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" | |
| 940 | +dependencies = [ | |
| 941 | + "proc-macro2", | |
| 942 | + "quote", | |
| 943 | + "syn", | |
| 944 | +] | |
| 945 | + | |
| 946 | +[[package]] | |
| 947 | +name = "dlib" | |
| 948 | +version = "0.5.3" | |
| 949 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 950 | +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" | |
| 951 | +dependencies = [ | |
| 952 | + "libloading", | |
| 953 | +] | |
| 954 | + | |
| 955 | +[[package]] | |
| 956 | +name = "document-features" | |
| 957 | +version = "0.2.12" | |
| 958 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 959 | +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" | |
| 960 | +dependencies = [ | |
| 961 | + "litrs", | |
| 962 | +] | |
| 963 | + | |
| 964 | +[[package]] | |
| 965 | +name = "downcast-rs" | |
| 966 | +version = "1.2.1" | |
| 967 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 968 | +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" | |
| 969 | + | |
| 970 | +[[package]] | |
| 971 | +name = "dpi" | |
| 972 | +version = "0.1.2" | |
| 973 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 974 | +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" | |
| 975 | + | |
| 976 | +[[package]] | |
| 977 | +name = "dyn-clone" | |
| 978 | +version = "1.0.20" | |
| 979 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 980 | +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" | |
| 981 | + | |
| 982 | +[[package]] | |
| 983 | +name = "ecolor" | |
| 984 | +version = "0.31.1" | |
| 985 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 986 | +checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" | |
| 987 | +dependencies = [ | |
| 988 | + "bytemuck", | |
| 989 | + "emath", | |
| 990 | + "serde", | |
| 991 | +] | |
| 992 | + | |
| 993 | +[[package]] | |
| 994 | +name = "eframe" | |
| 995 | +version = "0.31.1" | |
| 996 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 997 | +checksum = "d0dfe0859f3fb1bc6424c57d41e10e9093fe938f426b691e42272c2f336d915c" | |
| 998 | +dependencies = [ | |
| 999 | + "ahash", | |
| 1000 | + "bytemuck", | |
| 1001 | + "document-features", | |
| 1002 | + "egui", | |
| 1003 | + "egui-wgpu", | |
| 1004 | + "egui-winit", | |
| 1005 | + "egui_glow", | |
| 1006 | + "glow", | |
| 1007 | + "glutin", | |
| 1008 | + "glutin-winit", | |
| 1009 | + "home", | |
| 1010 | + "image", | |
| 1011 | + "js-sys", | |
| 1012 | + "log", | |
| 1013 | + "objc2 0.5.2", | |
| 1014 | + "objc2-app-kit 0.2.2", | |
| 1015 | + "objc2-foundation 0.2.2", | |
| 1016 | + "parking_lot", | |
| 1017 | + "percent-encoding", | |
| 1018 | + "profiling", | |
| 1019 | + "raw-window-handle", | |
| 1020 | + "ron", | |
| 1021 | + "serde", | |
| 1022 | + "static_assertions", | |
| 1023 | + "wasm-bindgen", | |
| 1024 | + "wasm-bindgen-futures", | |
| 1025 | + "web-sys", | |
| 1026 | + "web-time", | |
| 1027 | + "winapi", | |
| 1028 | + "windows-sys 0.59.0", | |
| 1029 | + "winit", | |
| 1030 | +] | |
| 1031 | + | |
| 1032 | +[[package]] | |
| 1033 | +name = "egui" | |
| 1034 | +version = "0.31.1" | |
| 1035 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1036 | +checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" | |
| 1037 | +dependencies = [ | |
| 1038 | + "accesskit", | |
| 1039 | + "ahash", | |
| 1040 | + "bitflags 2.11.0", | |
| 1041 | + "emath", | |
| 1042 | + "epaint", | |
| 1043 | + "log", | |
| 1044 | + "nohash-hasher", | |
| 1045 | + "profiling", | |
| 1046 | + "ron", | |
| 1047 | + "serde", | |
| 1048 | +] | |
| 1049 | + | |
| 1050 | +[[package]] | |
| 1051 | +name = "egui-phosphor" | |
| 1052 | +version = "0.9.0" | |
| 1053 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1054 | +checksum = "171d57331491e92f0a7c73fad1088716e3200984309f4f8684731b750cee1378" | |
| 1055 | +dependencies = [ | |
| 1056 | + "egui", | |
| 1057 | +] | |
| 1058 | + | |
| 1059 | +[[package]] | |
| 1060 | +name = "egui-wgpu" | |
| 1061 | +version = "0.31.1" | |
| 1062 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1063 | +checksum = "d319dfef570f699b6e9114e235e862a2ddcf75f0d1a061de9e1328d92146d820" | |
| 1064 | +dependencies = [ | |
| 1065 | + "ahash", | |
| 1066 | + "bytemuck", | |
| 1067 | + "document-features", | |
| 1068 | + "egui", | |
| 1069 | + "epaint", | |
| 1070 | + "log", | |
| 1071 | + "profiling", | |
| 1072 | + "thiserror 1.0.69", | |
| 1073 | + "type-map", | |
| 1074 | + "web-time", | |
| 1075 | + "wgpu", | |
| 1076 | + "winit", | |
| 1077 | +] | |
| 1078 | + | |
| 1079 | +[[package]] | |
| 1080 | +name = "egui-winit" | |
| 1081 | +version = "0.31.1" | |
| 1082 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1083 | +checksum = "7d9dfbb78fe4eb9c3a39ad528b90ee5915c252e77bbab9d4ebc576541ab67e13" | |
| 1084 | +dependencies = [ | |
| 1085 | + "accesskit_winit", | |
| 1086 | + "ahash", | |
| 1087 | + "arboard", | |
| 1088 | + "bytemuck", | |
| 1089 | + "egui", | |
| 1090 | + "log", | |
| 1091 | + "profiling", | |
| 1092 | + "raw-window-handle", | |
| 1093 | + "serde", | |
| 1094 | + "smithay-clipboard", | |
| 1095 | + "web-time", | |
| 1096 | + "webbrowser", | |
| 1097 | + "winit", | |
| 1098 | +] | |
| 1099 | + | |
| 1100 | +[[package]] | |
| 1101 | +name = "egui_extras" | |
| 1102 | +version = "0.31.1" | |
| 1103 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1104 | +checksum = "624659a2e972a46f4d5f646557906c55f1cd5a0836eddbe610fdf1afba1b4226" | |
| 1105 | +dependencies = [ | |
| 1106 | + "ahash", | |
| 1107 | + "egui", | |
| 1108 | + "enum-map", | |
| 1109 | + "image", | |
| 1110 | + "log", | |
| 1111 | + "mime_guess2", | |
| 1112 | + "profiling", | |
| 1113 | + "resvg", | |
| 1114 | + "syntect", | |
| 1115 | +] | |
| 1116 | + | |
| 1117 | +[[package]] | |
| 1118 | +name = "egui_glow" | |
| 1119 | +version = "0.31.1" | |
| 1120 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1121 | +checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" | |
| 1122 | +dependencies = [ | |
| 1123 | + "ahash", | |
| 1124 | + "bytemuck", | |
| 1125 | + "egui", | |
| 1126 | + "glow", | |
| 1127 | + "log", | |
| 1128 | + "memoffset", | |
| 1129 | + "profiling", | |
| 1130 | + "wasm-bindgen", | |
| 1131 | + "web-sys", | |
| 1132 | + "winit", | |
| 1133 | +] | |
| 1134 | + | |
| 1135 | +[[package]] | |
| 1136 | +name = "emath" | |
| 1137 | +version = "0.31.1" | |
| 1138 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1139 | +checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" | |
| 1140 | +dependencies = [ | |
| 1141 | + "bytemuck", | |
| 1142 | + "serde", | |
| 1143 | +] | |
| 1144 | + | |
| 1145 | +[[package]] | |
| 1146 | +name = "endi" | |
| 1147 | +version = "1.1.1" | |
| 1148 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1149 | +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" | |
| 1150 | + | |
| 1151 | +[[package]] | |
| 1152 | +name = "enum-map" | |
| 1153 | +version = "2.7.3" | |
| 1154 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1155 | +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" | |
| 1156 | +dependencies = [ | |
| 1157 | + "enum-map-derive", | |
| 1158 | + "serde", | |
| 1159 | +] | |
| 1160 | + | |
| 1161 | +[[package]] | |
| 1162 | +name = "enum-map-derive" | |
| 1163 | +version = "0.17.0" | |
| 1164 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1165 | +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" | |
| 1166 | +dependencies = [ | |
| 1167 | + "proc-macro2", | |
| 1168 | + "quote", | |
| 1169 | + "syn", | |
| 1170 | +] | |
| 1171 | + | |
| 1172 | +[[package]] | |
| 1173 | +name = "enumflags2" | |
| 1174 | +version = "0.7.12" | |
| 1175 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1176 | +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" | |
| 1177 | +dependencies = [ | |
| 1178 | + "enumflags2_derive", | |
| 1179 | + "serde", | |
| 1180 | +] | |
| 1181 | + | |
| 1182 | +[[package]] | |
| 1183 | +name = "enumflags2_derive" | |
| 1184 | +version = "0.7.12" | |
| 1185 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1186 | +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" | |
| 1187 | +dependencies = [ | |
| 1188 | + "proc-macro2", | |
| 1189 | + "quote", | |
| 1190 | + "syn", | |
| 1191 | +] | |
| 1192 | + | |
| 1193 | +[[package]] | |
| 1194 | +name = "enumn" | |
| 1195 | +version = "0.1.14" | |
| 1196 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1197 | +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" | |
| 1198 | +dependencies = [ | |
| 1199 | + "proc-macro2", | |
| 1200 | + "quote", | |
| 1201 | + "syn", | |
| 1202 | +] | |
| 1203 | + | |
| 1204 | +[[package]] | |
| 1205 | +name = "epaint" | |
| 1206 | +version = "0.31.1" | |
| 1207 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1208 | +checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" | |
| 1209 | +dependencies = [ | |
| 1210 | + "ab_glyph", | |
| 1211 | + "ahash", | |
| 1212 | + "bytemuck", | |
| 1213 | + "ecolor", | |
| 1214 | + "emath", | |
| 1215 | + "epaint_default_fonts", | |
| 1216 | + "log", | |
| 1217 | + "nohash-hasher", | |
| 1218 | + "parking_lot", | |
| 1219 | + "profiling", | |
| 1220 | + "serde", | |
| 1221 | +] | |
| 1222 | + | |
| 1223 | +[[package]] | |
| 1224 | +name = "epaint_default_fonts" | |
| 1225 | +version = "0.31.1" | |
| 1226 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1227 | +checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" | |
| 1228 | + | |
| 1229 | +[[package]] | |
| 1230 | +name = "equivalent" | |
| 1231 | +version = "1.0.2" | |
| 1232 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1233 | +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" | |
| 1234 | + | |
| 1235 | +[[package]] | |
| 1236 | +name = "errno" | |
| 1237 | +version = "0.3.14" | |
| 1238 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1239 | +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | |
| 1240 | +dependencies = [ | |
| 1241 | + "libc", | |
| 1242 | + "windows-sys 0.61.2", | |
| 1243 | +] | |
| 1244 | + | |
| 1245 | +[[package]] | |
| 1246 | +name = "error-code" | |
| 1247 | +version = "3.3.2" | |
| 1248 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1249 | +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" | |
| 1250 | + | |
| 1251 | +[[package]] | |
| 1252 | +name = "event-listener" | |
| 1253 | +version = "5.4.1" | |
| 1254 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1255 | +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" | |
| 1256 | +dependencies = [ | |
| 1257 | + "concurrent-queue", | |
| 1258 | + "parking", | |
| 1259 | + "pin-project-lite", | |
| 1260 | +] | |
| 1261 | + | |
| 1262 | +[[package]] | |
| 1263 | +name = "event-listener-strategy" | |
| 1264 | +version = "0.5.4" | |
| 1265 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1266 | +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" | |
| 1267 | +dependencies = [ | |
| 1268 | + "event-listener", | |
| 1269 | + "pin-project-lite", | |
| 1270 | +] | |
| 1271 | + | |
| 1272 | +[[package]] | |
| 1273 | +name = "fancy-regex" | |
| 1274 | +version = "0.16.2" | |
| 1275 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1276 | +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" | |
| 1277 | +dependencies = [ | |
| 1278 | + "bit-set", | |
| 1279 | + "regex-automata", | |
| 1280 | + "regex-syntax", | |
| 1281 | +] | |
| 1282 | + | |
| 1283 | +[[package]] | |
| 1284 | +name = "fastrand" | |
| 1285 | +version = "2.3.0" | |
| 1286 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1287 | +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" | |
| 1288 | + | |
| 1289 | +[[package]] | |
| 1290 | +name = "fax" | |
| 1291 | +version = "0.2.6" | |
| 1292 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1293 | +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" | |
| 1294 | +dependencies = [ | |
| 1295 | + "fax_derive", | |
| 1296 | +] | |
| 1297 | + | |
| 1298 | +[[package]] | |
| 1299 | +name = "fax_derive" | |
| 1300 | +version = "0.2.0" | |
| 1301 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1302 | +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" | |
| 1303 | +dependencies = [ | |
| 1304 | + "proc-macro2", | |
| 1305 | + "quote", | |
| 1306 | + "syn", | |
| 1307 | +] | |
| 1308 | + | |
| 1309 | +[[package]] | |
| 1310 | +name = "fdeflate" | |
| 1311 | +version = "0.3.7" | |
| 1312 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1313 | +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" | |
| 1314 | +dependencies = [ | |
| 1315 | + "simd-adler32", | |
| 1316 | +] | |
| 1317 | + | |
| 1318 | +[[package]] | |
| 1319 | +name = "find-msvc-tools" | |
| 1320 | +version = "0.1.9" | |
| 1321 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1322 | +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" | |
| 1323 | + | |
| 1324 | +[[package]] | |
| 1325 | +name = "flate2" | |
| 1326 | +version = "1.1.9" | |
| 1327 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1328 | +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" | |
| 1329 | +dependencies = [ | |
| 1330 | + "crc32fast", | |
| 1331 | + "miniz_oxide", | |
| 1332 | +] | |
| 1333 | + | |
| 1334 | +[[package]] | |
| 1335 | +name = "float-cmp" | |
| 1336 | +version = "0.9.0" | |
| 1337 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1338 | +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" | |
| 1339 | + | |
| 1340 | +[[package]] | |
| 1341 | +name = "fnv" | |
| 1342 | +version = "1.0.7" | |
| 1343 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1344 | +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | |
| 1345 | + | |
| 1346 | +[[package]] | |
| 1347 | +name = "foldhash" | |
| 1348 | +version = "0.1.5" | |
| 1349 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1350 | +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" | |
| 1351 | + | |
| 1352 | +[[package]] | |
| 1353 | +name = "foreign-types" | |
| 1354 | +version = "0.5.0" | |
| 1355 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1356 | +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" | |
| 1357 | +dependencies = [ | |
| 1358 | + "foreign-types-macros", | |
| 1359 | + "foreign-types-shared", | |
| 1360 | +] | |
| 1361 | + | |
| 1362 | +[[package]] | |
| 1363 | +name = "foreign-types-macros" | |
| 1364 | +version = "0.2.3" | |
| 1365 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1366 | +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" | |
| 1367 | +dependencies = [ | |
| 1368 | + "proc-macro2", | |
| 1369 | + "quote", | |
| 1370 | + "syn", | |
| 1371 | +] | |
| 1372 | + | |
| 1373 | +[[package]] | |
| 1374 | +name = "foreign-types-shared" | |
| 1375 | +version = "0.3.1" | |
| 1376 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1377 | +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" | |
| 1378 | + | |
| 1379 | +[[package]] | |
| 1380 | +name = "form_urlencoded" | |
| 1381 | +version = "1.2.2" | |
| 1382 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1383 | +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" | |
| 1384 | +dependencies = [ | |
| 1385 | + "percent-encoding", | |
| 1386 | +] | |
| 1387 | + | |
| 1388 | +[[package]] | |
| 1389 | +name = "futures-channel" | |
| 1390 | +version = "0.3.32" | |
| 1391 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1392 | +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" | |
| 1393 | +dependencies = [ | |
| 1394 | + "futures-core", | |
| 1395 | +] | |
| 1396 | + | |
| 1397 | +[[package]] | |
| 1398 | +name = "futures-core" | |
| 1399 | +version = "0.3.32" | |
| 1400 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1401 | +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" | |
| 1402 | + | |
| 1403 | +[[package]] | |
| 1404 | +name = "futures-io" | |
| 1405 | +version = "0.3.32" | |
| 1406 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1407 | +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" | |
| 1408 | + | |
| 1409 | +[[package]] | |
| 1410 | +name = "futures-lite" | |
| 1411 | +version = "2.6.1" | |
| 1412 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1413 | +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" | |
| 1414 | +dependencies = [ | |
| 1415 | + "fastrand", | |
| 1416 | + "futures-core", | |
| 1417 | + "futures-io", | |
| 1418 | + "parking", | |
| 1419 | + "pin-project-lite", | |
| 1420 | +] | |
| 1421 | + | |
| 1422 | +[[package]] | |
| 1423 | +name = "futures-macro" | |
| 1424 | +version = "0.3.32" | |
| 1425 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1426 | +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" | |
| 1427 | +dependencies = [ | |
| 1428 | + "proc-macro2", | |
| 1429 | + "quote", | |
| 1430 | + "syn", | |
| 1431 | +] | |
| 1432 | + | |
| 1433 | +[[package]] | |
| 1434 | +name = "futures-sink" | |
| 1435 | +version = "0.3.32" | |
| 1436 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1437 | +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" | |
| 1438 | + | |
| 1439 | +[[package]] | |
| 1440 | +name = "futures-task" | |
| 1441 | +version = "0.3.32" | |
| 1442 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1443 | +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" | |
| 1444 | + | |
| 1445 | +[[package]] | |
| 1446 | +name = "futures-util" | |
| 1447 | +version = "0.3.32" | |
| 1448 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1449 | +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" | |
| 1450 | +dependencies = [ | |
| 1451 | + "futures-core", | |
| 1452 | + "futures-io", | |
| 1453 | + "futures-macro", | |
| 1454 | + "futures-sink", | |
| 1455 | + "futures-task", | |
| 1456 | + "memchr", | |
| 1457 | + "pin-project-lite", | |
| 1458 | + "slab", | |
| 1459 | +] | |
| 1460 | + | |
| 1461 | +[[package]] | |
| 1462 | +name = "generic-array" | |
| 1463 | +version = "0.14.7" | |
| 1464 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1465 | +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" | |
| 1466 | +dependencies = [ | |
| 1467 | + "typenum", | |
| 1468 | + "version_check", | |
| 1469 | +] | |
| 1470 | + | |
| 1471 | +[[package]] | |
| 1472 | +name = "gethostname" | |
| 1473 | +version = "1.1.0" | |
| 1474 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1475 | +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" | |
| 1476 | +dependencies = [ | |
| 1477 | + "rustix 1.1.4", | |
| 1478 | + "windows-link", | |
| 1479 | +] | |
| 1480 | + | |
| 1481 | +[[package]] | |
| 1482 | +name = "getrandom" | |
| 1483 | +version = "0.2.17" | |
| 1484 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1485 | +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" | |
| 1486 | +dependencies = [ | |
| 1487 | + "cfg-if", | |
| 1488 | + "libc", | |
| 1489 | + "wasi", | |
| 1490 | +] | |
| 1491 | + | |
| 1492 | +[[package]] | |
| 1493 | +name = "getrandom" | |
| 1494 | +version = "0.3.4" | |
| 1495 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1496 | +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" | |
| 1497 | +dependencies = [ | |
| 1498 | + "cfg-if", | |
| 1499 | + "libc", | |
| 1500 | + "r-efi 5.3.0", | |
| 1501 | + "wasip2", | |
| 1502 | +] | |
| 1503 | + | |
| 1504 | +[[package]] | |
| 1505 | +name = "getrandom" | |
| 1506 | +version = "0.4.2" | |
| 1507 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1508 | +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" | |
| 1509 | +dependencies = [ | |
| 1510 | + "cfg-if", | |
| 1511 | + "libc", | |
| 1512 | + "r-efi 6.0.0", | |
| 1513 | + "wasip2", | |
| 1514 | + "wasip3", | |
| 1515 | +] | |
| 1516 | + | |
| 1517 | +[[package]] | |
| 1518 | +name = "gl_generator" | |
| 1519 | +version = "0.14.0" | |
| 1520 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1521 | +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" | |
| 1522 | +dependencies = [ | |
| 1523 | + "khronos_api", | |
| 1524 | + "log", | |
| 1525 | + "xml-rs", | |
| 1526 | +] | |
| 1527 | + | |
| 1528 | +[[package]] | |
| 1529 | +name = "glow" | |
| 1530 | +version = "0.16.0" | |
| 1531 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1532 | +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" | |
| 1533 | +dependencies = [ | |
| 1534 | + "js-sys", | |
| 1535 | + "slotmap", | |
| 1536 | + "wasm-bindgen", | |
| 1537 | + "web-sys", | |
| 1538 | +] | |
| 1539 | + | |
| 1540 | +[[package]] | |
| 1541 | +name = "glutin" | |
| 1542 | +version = "0.32.3" | |
| 1543 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1544 | +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" | |
| 1545 | +dependencies = [ | |
| 1546 | + "bitflags 2.11.0", | |
| 1547 | + "cfg_aliases", | |
| 1548 | + "cgl", | |
| 1549 | + "dispatch2", | |
| 1550 | + "glutin_egl_sys", | |
| 1551 | + "glutin_glx_sys", | |
| 1552 | + "glutin_wgl_sys", | |
| 1553 | + "libloading", | |
| 1554 | + "objc2 0.6.4", | |
| 1555 | + "objc2-app-kit 0.3.2", | |
| 1556 | + "objc2-core-foundation", | |
| 1557 | + "objc2-foundation 0.3.2", | |
| 1558 | + "once_cell", | |
| 1559 | + "raw-window-handle", | |
| 1560 | + "wayland-sys", | |
| 1561 | + "windows-sys 0.52.0", | |
| 1562 | + "x11-dl", | |
| 1563 | +] | |
| 1564 | + | |
| 1565 | +[[package]] | |
| 1566 | +name = "glutin-winit" | |
| 1567 | +version = "0.5.0" | |
| 1568 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1569 | +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" | |
| 1570 | +dependencies = [ | |
| 1571 | + "cfg_aliases", | |
| 1572 | + "glutin", | |
| 1573 | + "raw-window-handle", | |
| 1574 | + "winit", | |
| 1575 | +] | |
| 1576 | + | |
| 1577 | +[[package]] | |
| 1578 | +name = "glutin_egl_sys" | |
| 1579 | +version = "0.7.1" | |
| 1580 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1581 | +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" | |
| 1582 | +dependencies = [ | |
| 1583 | + "gl_generator", | |
| 1584 | + "windows-sys 0.52.0", | |
| 1585 | +] | |
| 1586 | + | |
| 1587 | +[[package]] | |
| 1588 | +name = "glutin_glx_sys" | |
| 1589 | +version = "0.6.1" | |
| 1590 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1591 | +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" | |
| 1592 | +dependencies = [ | |
| 1593 | + "gl_generator", | |
| 1594 | + "x11-dl", | |
| 1595 | +] | |
| 1596 | + | |
| 1597 | +[[package]] | |
| 1598 | +name = "glutin_wgl_sys" | |
| 1599 | +version = "0.6.1" | |
| 1600 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1601 | +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" | |
| 1602 | +dependencies = [ | |
| 1603 | + "gl_generator", | |
| 1604 | +] | |
| 1605 | + | |
| 1606 | +[[package]] | |
| 1607 | +name = "gpu-alloc" | |
| 1608 | +version = "0.6.0" | |
| 1609 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1610 | +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" | |
| 1611 | +dependencies = [ | |
| 1612 | + "bitflags 2.11.0", | |
| 1613 | + "gpu-alloc-types", | |
| 1614 | +] | |
| 1615 | + | |
| 1616 | +[[package]] | |
| 1617 | +name = "gpu-alloc-types" | |
| 1618 | +version = "0.3.0" | |
| 1619 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1620 | +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" | |
| 1621 | +dependencies = [ | |
| 1622 | + "bitflags 2.11.0", | |
| 1623 | +] | |
| 1624 | + | |
| 1625 | +[[package]] | |
| 1626 | +name = "gpu-descriptor" | |
| 1627 | +version = "0.3.2" | |
| 1628 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1629 | +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" | |
| 1630 | +dependencies = [ | |
| 1631 | + "bitflags 2.11.0", | |
| 1632 | + "gpu-descriptor-types", | |
| 1633 | + "hashbrown 0.15.5", | |
| 1634 | +] | |
| 1635 | + | |
| 1636 | +[[package]] | |
| 1637 | +name = "gpu-descriptor-types" | |
| 1638 | +version = "0.2.0" | |
| 1639 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1640 | +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" | |
| 1641 | +dependencies = [ | |
| 1642 | + "bitflags 2.11.0", | |
| 1643 | +] | |
| 1644 | + | |
| 1645 | +[[package]] | |
| 1646 | +name = "half" | |
| 1647 | +version = "2.7.1" | |
| 1648 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1649 | +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" | |
| 1650 | +dependencies = [ | |
| 1651 | + "cfg-if", | |
| 1652 | + "crunchy", | |
| 1653 | + "zerocopy", | |
| 1654 | +] | |
| 1655 | + | |
| 1656 | +[[package]] | |
| 1657 | +name = "hashbrown" | |
| 1658 | +version = "0.14.5" | |
| 1659 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1660 | +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" | |
| 1661 | +dependencies = [ | |
| 1662 | + "ahash", | |
| 1663 | + "allocator-api2", | |
| 1664 | +] | |
| 1665 | + | |
| 1666 | +[[package]] | |
| 1667 | +name = "hashbrown" | |
| 1668 | +version = "0.15.5" | |
| 1669 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1670 | +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" | |
| 1671 | +dependencies = [ | |
| 1672 | + "foldhash", | |
| 1673 | +] | |
| 1674 | + | |
| 1675 | +[[package]] | |
| 1676 | +name = "hashbrown" | |
| 1677 | +version = "0.16.1" | |
| 1678 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1679 | +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" | |
| 1680 | + | |
| 1681 | +[[package]] | |
| 1682 | +name = "heck" | |
| 1683 | +version = "0.5.0" | |
| 1684 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1685 | +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | |
| 1686 | + | |
| 1687 | +[[package]] | |
| 1688 | +name = "hermit-abi" | |
| 1689 | +version = "0.5.2" | |
| 1690 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1691 | +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" | |
| 1692 | + | |
| 1693 | +[[package]] | |
| 1694 | +name = "hex" | |
| 1695 | +version = "0.4.3" | |
| 1696 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1697 | +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" | |
| 1698 | + | |
| 1699 | +[[package]] | |
| 1700 | +name = "hexf-parse" | |
| 1701 | +version = "0.2.1" | |
| 1702 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1703 | +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" | |
| 1704 | + | |
| 1705 | +[[package]] | |
| 1706 | +name = "hifijson" | |
| 1707 | +version = "0.2.3" | |
| 1708 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1709 | +checksum = "0a7763b98ba8a24f59e698bf9ab197e7676c640d6455d1580b4ce7dc560f0f0d" | |
| 1710 | + | |
| 1711 | +[[package]] | |
| 1712 | +name = "home" | |
| 1713 | +version = "0.5.12" | |
| 1714 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1715 | +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" | |
| 1716 | +dependencies = [ | |
| 1717 | + "windows-sys 0.61.2", | |
| 1718 | +] | |
| 1719 | + | |
| 1720 | +[[package]] | |
| 1721 | +name = "iana-time-zone" | |
| 1722 | +version = "0.1.65" | |
| 1723 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1724 | +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" | |
| 1725 | +dependencies = [ | |
| 1726 | + "android_system_properties", | |
| 1727 | + "core-foundation-sys", | |
| 1728 | + "iana-time-zone-haiku", | |
| 1729 | + "js-sys", | |
| 1730 | + "log", | |
| 1731 | + "wasm-bindgen", | |
| 1732 | + "windows-core 0.62.2", | |
| 1733 | +] | |
| 1734 | + | |
| 1735 | +[[package]] | |
| 1736 | +name = "iana-time-zone-haiku" | |
| 1737 | +version = "0.1.2" | |
| 1738 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1739 | +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" | |
| 1740 | +dependencies = [ | |
| 1741 | + "cc", | |
| 1742 | +] | |
| 1743 | + | |
| 1744 | +[[package]] | |
| 1745 | +name = "icu_collections" | |
| 1746 | +version = "2.1.1" | |
| 1747 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1748 | +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" | |
| 1749 | +dependencies = [ | |
| 1750 | + "displaydoc", | |
| 1751 | + "potential_utf", | |
| 1752 | + "yoke", | |
| 1753 | + "zerofrom", | |
| 1754 | + "zerovec", | |
| 1755 | +] | |
| 1756 | + | |
| 1757 | +[[package]] | |
| 1758 | +name = "icu_locale_core" | |
| 1759 | +version = "2.1.1" | |
| 1760 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1761 | +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" | |
| 1762 | +dependencies = [ | |
| 1763 | + "displaydoc", | |
| 1764 | + "litemap", | |
| 1765 | + "tinystr", | |
| 1766 | + "writeable", | |
| 1767 | + "zerovec", | |
| 1768 | +] | |
| 1769 | + | |
| 1770 | +[[package]] | |
| 1771 | +name = "icu_normalizer" | |
| 1772 | +version = "2.1.1" | |
| 1773 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1774 | +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" | |
| 1775 | +dependencies = [ | |
| 1776 | + "icu_collections", | |
| 1777 | + "icu_normalizer_data", | |
| 1778 | + "icu_properties", | |
| 1779 | + "icu_provider", | |
| 1780 | + "smallvec", | |
| 1781 | + "zerovec", | |
| 1782 | +] | |
| 1783 | + | |
| 1784 | +[[package]] | |
| 1785 | +name = "icu_normalizer_data" | |
| 1786 | +version = "2.1.1" | |
| 1787 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1788 | +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" | |
| 1789 | + | |
| 1790 | +[[package]] | |
| 1791 | +name = "icu_properties" | |
| 1792 | +version = "2.1.2" | |
| 1793 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1794 | +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" | |
| 1795 | +dependencies = [ | |
| 1796 | + "icu_collections", | |
| 1797 | + "icu_locale_core", | |
| 1798 | + "icu_properties_data", | |
| 1799 | + "icu_provider", | |
| 1800 | + "zerotrie", | |
| 1801 | + "zerovec", | |
| 1802 | +] | |
| 1803 | + | |
| 1804 | +[[package]] | |
| 1805 | +name = "icu_properties_data" | |
| 1806 | +version = "2.1.2" | |
| 1807 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1808 | +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" | |
| 1809 | + | |
| 1810 | +[[package]] | |
| 1811 | +name = "icu_provider" | |
| 1812 | +version = "2.1.1" | |
| 1813 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1814 | +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" | |
| 1815 | +dependencies = [ | |
| 1816 | + "displaydoc", | |
| 1817 | + "icu_locale_core", | |
| 1818 | + "writeable", | |
| 1819 | + "yoke", | |
| 1820 | + "zerofrom", | |
| 1821 | + "zerotrie", | |
| 1822 | + "zerovec", | |
| 1823 | +] | |
| 1824 | + | |
| 1825 | +[[package]] | |
| 1826 | +name = "id-arena" | |
| 1827 | +version = "2.3.0" | |
| 1828 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1829 | +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" | |
| 1830 | + | |
| 1831 | +[[package]] | |
| 1832 | +name = "idna" | |
| 1833 | +version = "1.1.0" | |
| 1834 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1835 | +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" | |
| 1836 | +dependencies = [ | |
| 1837 | + "idna_adapter", | |
| 1838 | + "smallvec", | |
| 1839 | + "utf8_iter", | |
| 1840 | +] | |
| 1841 | + | |
| 1842 | +[[package]] | |
| 1843 | +name = "idna_adapter" | |
| 1844 | +version = "1.2.1" | |
| 1845 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1846 | +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" | |
| 1847 | +dependencies = [ | |
| 1848 | + "icu_normalizer", | |
| 1849 | + "icu_properties", | |
| 1850 | +] | |
| 1851 | + | |
| 1852 | +[[package]] | |
| 1853 | +name = "image" | |
| 1854 | +version = "0.25.9" | |
| 1855 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1856 | +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" | |
| 1857 | +dependencies = [ | |
| 1858 | + "bytemuck", | |
| 1859 | + "byteorder-lite", | |
| 1860 | + "image-webp", | |
| 1861 | + "moxcms", | |
| 1862 | + "num-traits", | |
| 1863 | + "png 0.18.1", | |
| 1864 | + "tiff", | |
| 1865 | + "zune-core 0.5.1", | |
| 1866 | + "zune-jpeg 0.5.12", | |
| 1867 | +] | |
| 1868 | + | |
| 1869 | +[[package]] | |
| 1870 | +name = "image-webp" | |
| 1871 | +version = "0.2.1" | |
| 1872 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1873 | +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" | |
| 1874 | +dependencies = [ | |
| 1875 | + "byteorder-lite", | |
| 1876 | + "quick-error", | |
| 1877 | +] | |
| 1878 | + | |
| 1879 | +[[package]] | |
| 1880 | +name = "imagesize" | |
| 1881 | +version = "0.12.0" | |
| 1882 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1883 | +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" | |
| 1884 | + | |
| 1885 | +[[package]] | |
| 1886 | +name = "immutable-chunkmap" | |
| 1887 | +version = "2.1.2" | |
| 1888 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1889 | +checksum = "9a3e98b1520e49e252237edc238a39869da9f3241f2ec19dc788c1d24694d1e4" | |
| 1890 | +dependencies = [ | |
| 1891 | + "arrayvec", | |
| 1892 | +] | |
| 1893 | + | |
| 1894 | +[[package]] | |
| 1895 | +name = "indexmap" | |
| 1896 | +version = "2.13.0" | |
| 1897 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1898 | +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" | |
| 1899 | +dependencies = [ | |
| 1900 | + "equivalent", | |
| 1901 | + "hashbrown 0.16.1", | |
| 1902 | + "serde", | |
| 1903 | + "serde_core", | |
| 1904 | +] | |
| 1905 | + | |
| 1906 | +[[package]] | |
| 1907 | +name = "itoa" | |
| 1908 | +version = "1.0.17" | |
| 1909 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1910 | +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" | |
| 1911 | + | |
| 1912 | +[[package]] | |
| 1913 | +name = "jaq-core" | |
| 1914 | +version = "1.5.1" | |
| 1915 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1916 | +checksum = "d6fda09ee08c84c81293fdf811d9ebaa87b327557b5391f290c926d728c2ddd4" | |
| 1917 | +dependencies = [ | |
| 1918 | + "aho-corasick", | |
| 1919 | + "base64 0.22.1", | |
| 1920 | + "chrono", | |
| 1921 | + "hifijson", | |
| 1922 | + "jaq-interpret", | |
| 1923 | + "libm", | |
| 1924 | + "log", | |
| 1925 | + "regex", | |
| 1926 | + "urlencoding", | |
| 1927 | +] | |
| 1928 | + | |
| 1929 | +[[package]] | |
| 1930 | +name = "jaq-interpret" | |
| 1931 | +version = "1.5.0" | |
| 1932 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1933 | +checksum = "2fe95ec3c24af3fd9f3dd1091593f5e49b003a66c496a8aa39d764d0a06ae17b" | |
| 1934 | +dependencies = [ | |
| 1935 | + "ahash", | |
| 1936 | + "dyn-clone", | |
| 1937 | + "hifijson", | |
| 1938 | + "indexmap", | |
| 1939 | + "jaq-syn", | |
| 1940 | + "once_cell", | |
| 1941 | + "serde_json", | |
| 1942 | +] | |
| 1943 | + | |
| 1944 | +[[package]] | |
| 1945 | +name = "jaq-parse" | |
| 1946 | +version = "1.0.3" | |
| 1947 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1948 | +checksum = "0346d7d3146cdda8acd929581f3d6626a332356c74d5c95aeaffaac2eb6dee82" | |
| 1949 | +dependencies = [ | |
| 1950 | + "chumsky", | |
| 1951 | + "jaq-syn", | |
| 1952 | +] | |
| 1953 | + | |
| 1954 | +[[package]] | |
| 1955 | +name = "jaq-std" | |
| 1956 | +version = "1.6.0" | |
| 1957 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1958 | +checksum = "bfbaa55578fd3b70433b594a370741e0c364e4afff92cc0099623fce87311bc1" | |
| 1959 | +dependencies = [ | |
| 1960 | + "jaq-syn", | |
| 1961 | +] | |
| 1962 | + | |
| 1963 | +[[package]] | |
| 1964 | +name = "jaq-syn" | |
| 1965 | +version = "1.6.0" | |
| 1966 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1967 | +checksum = "1ba44fe4428c71304604261ecbae047ee9cfb60c4f1a6bd222ebbb31726d3948" | |
| 1968 | +dependencies = [ | |
| 1969 | + "serde", | |
| 1970 | +] | |
| 1971 | + | |
| 1972 | +[[package]] | |
| 1973 | +name = "jni" | |
| 1974 | +version = "0.21.1" | |
| 1975 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1976 | +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" | |
| 1977 | +dependencies = [ | |
| 1978 | + "cesu8", | |
| 1979 | + "cfg-if", | |
| 1980 | + "combine", | |
| 1981 | + "jni-sys 0.3.0", | |
| 1982 | + "log", | |
| 1983 | + "thiserror 1.0.69", | |
| 1984 | + "walkdir", | |
| 1985 | + "windows-sys 0.45.0", | |
| 1986 | +] | |
| 1987 | + | |
| 1988 | +[[package]] | |
| 1989 | +name = "jni" | |
| 1990 | +version = "0.22.3" | |
| 1991 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1992 | +checksum = "295dc9997acda1562fdf8d299f56063c936443b60f078e63a5d8d3c34ef2642b" | |
| 1993 | +dependencies = [ | |
| 1994 | + "cfg-if", | |
| 1995 | + "combine", | |
| 1996 | + "jni-macros", | |
| 1997 | + "jni-sys 0.4.1", | |
| 1998 | + "log", | |
| 1999 | + "simd_cesu8", | |
| 2000 | + "thiserror 2.0.18", | |
| 2001 | + "walkdir", | |
| 2002 | + "windows-link", | |
| 2003 | +] | |
| 2004 | + | |
| 2005 | +[[package]] | |
| 2006 | +name = "jni-macros" | |
| 2007 | +version = "0.22.2" | |
| 2008 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2009 | +checksum = "1c3d1da60c95c98847b26b9d45f4360fee718b31de746df016d9cd6de916a7ef" | |
| 2010 | +dependencies = [ | |
| 2011 | + "proc-macro2", | |
| 2012 | + "quote", | |
| 2013 | + "rustc_version", | |
| 2014 | + "simd_cesu8", | |
| 2015 | + "syn", | |
| 2016 | +] | |
| 2017 | + | |
| 2018 | +[[package]] | |
| 2019 | +name = "jni-sys" | |
| 2020 | +version = "0.3.0" | |
| 2021 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2022 | +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" | |
| 2023 | + | |
| 2024 | +[[package]] | |
| 2025 | +name = "jni-sys" | |
| 2026 | +version = "0.4.1" | |
| 2027 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2028 | +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" | |
| 2029 | +dependencies = [ | |
| 2030 | + "jni-sys-macros", | |
| 2031 | +] | |
| 2032 | + | |
| 2033 | +[[package]] | |
| 2034 | +name = "jni-sys-macros" | |
| 2035 | +version = "0.4.1" | |
| 2036 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2037 | +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" | |
| 2038 | +dependencies = [ | |
| 2039 | + "quote", | |
| 2040 | + "syn", | |
| 2041 | +] | |
| 2042 | + | |
| 2043 | +[[package]] | |
| 2044 | +name = "jobserver" | |
| 2045 | +version = "0.1.34" | |
| 2046 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2047 | +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" | |
| 2048 | +dependencies = [ | |
| 2049 | + "getrandom 0.3.4", | |
| 2050 | + "libc", | |
| 2051 | +] | |
| 2052 | + | |
| 2053 | +[[package]] | |
| 2054 | +name = "js-sys" | |
| 2055 | +version = "0.3.91" | |
| 2056 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2057 | +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" | |
| 2058 | +dependencies = [ | |
| 2059 | + "once_cell", | |
| 2060 | + "wasm-bindgen", | |
| 2061 | +] | |
| 2062 | + | |
| 2063 | +[[package]] | |
| 2064 | +name = "jv" | |
| 2065 | +version = "0.1.0" | |
| 2066 | +dependencies = [ | |
| 2067 | + "base64 0.22.1", | |
| 2068 | + "chrono", | |
| 2069 | + "chrono-tz", | |
| 2070 | + "directories", | |
| 2071 | + "eframe", | |
| 2072 | + "egui", | |
| 2073 | + "egui-phosphor", | |
| 2074 | + "egui_extras", | |
| 2075 | + "image", | |
| 2076 | + "jaq-core", | |
| 2077 | + "jaq-interpret", | |
| 2078 | + "jaq-parse", | |
| 2079 | + "jaq-std", | |
| 2080 | + "rfd", | |
| 2081 | + "serde", | |
| 2082 | + "serde_json", | |
| 2083 | + "tracing", | |
| 2084 | + "tracing-subscriber", | |
| 2085 | + "uuid", | |
| 2086 | + "walkdir", | |
| 2087 | +] | |
| 2088 | + | |
| 2089 | +[[package]] | |
| 2090 | +name = "khronos-egl" | |
| 2091 | +version = "6.0.0" | |
| 2092 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2093 | +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" | |
| 2094 | +dependencies = [ | |
| 2095 | + "libc", | |
| 2096 | + "libloading", | |
| 2097 | + "pkg-config", | |
| 2098 | +] | |
| 2099 | + | |
| 2100 | +[[package]] | |
| 2101 | +name = "khronos_api" | |
| 2102 | +version = "3.1.0" | |
| 2103 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2104 | +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" | |
| 2105 | + | |
| 2106 | +[[package]] | |
| 2107 | +name = "kurbo" | |
| 2108 | +version = "0.9.5" | |
| 2109 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2110 | +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" | |
| 2111 | +dependencies = [ | |
| 2112 | + "arrayvec", | |
| 2113 | +] | |
| 2114 | + | |
| 2115 | +[[package]] | |
| 2116 | +name = "lazy_static" | |
| 2117 | +version = "1.5.0" | |
| 2118 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2119 | +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" | |
| 2120 | + | |
| 2121 | +[[package]] | |
| 2122 | +name = "leb128fmt" | |
| 2123 | +version = "0.1.0" | |
| 2124 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2125 | +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" | |
| 2126 | + | |
| 2127 | +[[package]] | |
| 2128 | +name = "libc" | |
| 2129 | +version = "0.2.183" | |
| 2130 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2131 | +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" | |
| 2132 | + | |
| 2133 | +[[package]] | |
| 2134 | +name = "libloading" | |
| 2135 | +version = "0.8.9" | |
| 2136 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2137 | +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" | |
| 2138 | +dependencies = [ | |
| 2139 | + "cfg-if", | |
| 2140 | + "windows-link", | |
| 2141 | +] | |
| 2142 | + | |
| 2143 | +[[package]] | |
| 2144 | +name = "libm" | |
| 2145 | +version = "0.2.16" | |
| 2146 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2147 | +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" | |
| 2148 | + | |
| 2149 | +[[package]] | |
| 2150 | +name = "libredox" | |
| 2151 | +version = "0.1.14" | |
| 2152 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2153 | +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" | |
| 2154 | +dependencies = [ | |
| 2155 | + "bitflags 2.11.0", | |
| 2156 | + "libc", | |
| 2157 | + "plain", | |
| 2158 | + "redox_syscall 0.7.3", | |
| 2159 | +] | |
| 2160 | + | |
| 2161 | +[[package]] | |
| 2162 | +name = "linked-hash-map" | |
| 2163 | +version = "0.5.6" | |
| 2164 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2165 | +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" | |
| 2166 | + | |
| 2167 | +[[package]] | |
| 2168 | +name = "linux-raw-sys" | |
| 2169 | +version = "0.4.15" | |
| 2170 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2171 | +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" | |
| 2172 | + | |
| 2173 | +[[package]] | |
| 2174 | +name = "linux-raw-sys" | |
| 2175 | +version = "0.12.1" | |
| 2176 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2177 | +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" | |
| 2178 | + | |
| 2179 | +[[package]] | |
| 2180 | +name = "litemap" | |
| 2181 | +version = "0.8.1" | |
| 2182 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2183 | +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" | |
| 2184 | + | |
| 2185 | +[[package]] | |
| 2186 | +name = "litrs" | |
| 2187 | +version = "1.0.0" | |
| 2188 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2189 | +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" | |
| 2190 | + | |
| 2191 | +[[package]] | |
| 2192 | +name = "lock_api" | |
| 2193 | +version = "0.4.14" | |
| 2194 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2195 | +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" | |
| 2196 | +dependencies = [ | |
| 2197 | + "scopeguard", | |
| 2198 | +] | |
| 2199 | + | |
| 2200 | +[[package]] | |
| 2201 | +name = "log" | |
| 2202 | +version = "0.4.29" | |
| 2203 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2204 | +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" | |
| 2205 | + | |
| 2206 | +[[package]] | |
| 2207 | +name = "malloc_buf" | |
| 2208 | +version = "0.0.6" | |
| 2209 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2210 | +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" | |
| 2211 | +dependencies = [ | |
| 2212 | + "libc", | |
| 2213 | +] | |
| 2214 | + | |
| 2215 | +[[package]] | |
| 2216 | +name = "memchr" | |
| 2217 | +version = "2.8.0" | |
| 2218 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2219 | +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" | |
| 2220 | + | |
| 2221 | +[[package]] | |
| 2222 | +name = "memmap2" | |
| 2223 | +version = "0.9.10" | |
| 2224 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2225 | +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" | |
| 2226 | +dependencies = [ | |
| 2227 | + "libc", | |
| 2228 | +] | |
| 2229 | + | |
| 2230 | +[[package]] | |
| 2231 | +name = "memoffset" | |
| 2232 | +version = "0.9.1" | |
| 2233 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2234 | +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" | |
| 2235 | +dependencies = [ | |
| 2236 | + "autocfg", | |
| 2237 | +] | |
| 2238 | + | |
| 2239 | +[[package]] | |
| 2240 | +name = "metal" | |
| 2241 | +version = "0.31.0" | |
| 2242 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2243 | +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" | |
| 2244 | +dependencies = [ | |
| 2245 | + "bitflags 2.11.0", | |
| 2246 | + "block", | |
| 2247 | + "core-graphics-types", | |
| 2248 | + "foreign-types", | |
| 2249 | + "log", | |
| 2250 | + "objc", | |
| 2251 | + "paste", | |
| 2252 | +] | |
| 2253 | + | |
| 2254 | +[[package]] | |
| 2255 | +name = "mime" | |
| 2256 | +version = "0.3.17" | |
| 2257 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2258 | +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | |
| 2259 | + | |
| 2260 | +[[package]] | |
| 2261 | +name = "mime_guess2" | |
| 2262 | +version = "2.3.1" | |
| 2263 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2264 | +checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" | |
| 2265 | +dependencies = [ | |
| 2266 | + "mime", | |
| 2267 | + "phf 0.11.3", | |
| 2268 | + "phf_shared 0.11.3", | |
| 2269 | + "unicase", | |
| 2270 | +] | |
| 2271 | + | |
| 2272 | +[[package]] | |
| 2273 | +name = "miniz_oxide" | |
| 2274 | +version = "0.8.9" | |
| 2275 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2276 | +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" | |
| 2277 | +dependencies = [ | |
| 2278 | + "adler2", | |
| 2279 | + "simd-adler32", | |
| 2280 | +] | |
| 2281 | + | |
| 2282 | +[[package]] | |
| 2283 | +name = "moxcms" | |
| 2284 | +version = "0.7.11" | |
| 2285 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2286 | +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" | |
| 2287 | +dependencies = [ | |
| 2288 | + "num-traits", | |
| 2289 | + "pxfm", | |
| 2290 | +] | |
| 2291 | + | |
| 2292 | +[[package]] | |
| 2293 | +name = "naga" | |
| 2294 | +version = "24.0.0" | |
| 2295 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2296 | +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" | |
| 2297 | +dependencies = [ | |
| 2298 | + "arrayvec", | |
| 2299 | + "bit-set", | |
| 2300 | + "bitflags 2.11.0", | |
| 2301 | + "cfg_aliases", | |
| 2302 | + "codespan-reporting", | |
| 2303 | + "hexf-parse", | |
| 2304 | + "indexmap", | |
| 2305 | + "log", | |
| 2306 | + "rustc-hash 1.1.0", | |
| 2307 | + "spirv", | |
| 2308 | + "strum", | |
| 2309 | + "termcolor", | |
| 2310 | + "thiserror 2.0.18", | |
| 2311 | + "unicode-xid", | |
| 2312 | +] | |
| 2313 | + | |
| 2314 | +[[package]] | |
| 2315 | +name = "ndk" | |
| 2316 | +version = "0.9.0" | |
| 2317 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2318 | +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" | |
| 2319 | +dependencies = [ | |
| 2320 | + "bitflags 2.11.0", | |
| 2321 | + "jni-sys 0.3.0", | |
| 2322 | + "log", | |
| 2323 | + "ndk-sys 0.6.0+11769913", | |
| 2324 | + "num_enum", | |
| 2325 | + "raw-window-handle", | |
| 2326 | + "thiserror 1.0.69", | |
| 2327 | +] | |
| 2328 | + | |
| 2329 | +[[package]] | |
| 2330 | +name = "ndk-context" | |
| 2331 | +version = "0.1.1" | |
| 2332 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2333 | +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" | |
| 2334 | + | |
| 2335 | +[[package]] | |
| 2336 | +name = "ndk-sys" | |
| 2337 | +version = "0.5.0+25.2.9519653" | |
| 2338 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2339 | +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" | |
| 2340 | +dependencies = [ | |
| 2341 | + "jni-sys 0.3.0", | |
| 2342 | +] | |
| 2343 | + | |
| 2344 | +[[package]] | |
| 2345 | +name = "ndk-sys" | |
| 2346 | +version = "0.6.0+11769913" | |
| 2347 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2348 | +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" | |
| 2349 | +dependencies = [ | |
| 2350 | + "jni-sys 0.3.0", | |
| 2351 | +] | |
| 2352 | + | |
| 2353 | +[[package]] | |
| 2354 | +name = "nix" | |
| 2355 | +version = "0.29.0" | |
| 2356 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2357 | +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" | |
| 2358 | +dependencies = [ | |
| 2359 | + "bitflags 2.11.0", | |
| 2360 | + "cfg-if", | |
| 2361 | + "cfg_aliases", | |
| 2362 | + "libc", | |
| 2363 | + "memoffset", | |
| 2364 | +] | |
| 2365 | + | |
| 2366 | +[[package]] | |
| 2367 | +name = "nohash-hasher" | |
| 2368 | +version = "0.2.0" | |
| 2369 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2370 | +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" | |
| 2371 | + | |
| 2372 | +[[package]] | |
| 2373 | +name = "nu-ansi-term" | |
| 2374 | +version = "0.50.3" | |
| 2375 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2376 | +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" | |
| 2377 | +dependencies = [ | |
| 2378 | + "windows-sys 0.61.2", | |
| 2379 | +] | |
| 2380 | + | |
| 2381 | +[[package]] | |
| 2382 | +name = "num-conv" | |
| 2383 | +version = "0.2.0" | |
| 2384 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2385 | +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" | |
| 2386 | + | |
| 2387 | +[[package]] | |
| 2388 | +name = "num-traits" | |
| 2389 | +version = "0.2.19" | |
| 2390 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2391 | +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" | |
| 2392 | +dependencies = [ | |
| 2393 | + "autocfg", | |
| 2394 | +] | |
| 2395 | + | |
| 2396 | +[[package]] | |
| 2397 | +name = "num_enum" | |
| 2398 | +version = "0.7.5" | |
| 2399 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2400 | +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" | |
| 2401 | +dependencies = [ | |
| 2402 | + "num_enum_derive", | |
| 2403 | + "rustversion", | |
| 2404 | +] | |
| 2405 | + | |
| 2406 | +[[package]] | |
| 2407 | +name = "num_enum_derive" | |
| 2408 | +version = "0.7.5" | |
| 2409 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2410 | +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" | |
| 2411 | +dependencies = [ | |
| 2412 | + "proc-macro-crate", | |
| 2413 | + "proc-macro2", | |
| 2414 | + "quote", | |
| 2415 | + "syn", | |
| 2416 | +] | |
| 2417 | + | |
| 2418 | +[[package]] | |
| 2419 | +name = "objc" | |
| 2420 | +version = "0.2.7" | |
| 2421 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2422 | +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" | |
| 2423 | +dependencies = [ | |
| 2424 | + "malloc_buf", | |
| 2425 | +] | |
| 2426 | + | |
| 2427 | +[[package]] | |
| 2428 | +name = "objc-sys" | |
| 2429 | +version = "0.3.5" | |
| 2430 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2431 | +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" | |
| 2432 | + | |
| 2433 | +[[package]] | |
| 2434 | +name = "objc2" | |
| 2435 | +version = "0.5.2" | |
| 2436 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2437 | +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" | |
| 2438 | +dependencies = [ | |
| 2439 | + "objc-sys", | |
| 2440 | + "objc2-encode", | |
| 2441 | +] | |
| 2442 | + | |
| 2443 | +[[package]] | |
| 2444 | +name = "objc2" | |
| 2445 | +version = "0.6.4" | |
| 2446 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2447 | +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" | |
| 2448 | +dependencies = [ | |
| 2449 | + "objc2-encode", | |
| 2450 | +] | |
| 2451 | + | |
| 2452 | +[[package]] | |
| 2453 | +name = "objc2-app-kit" | |
| 2454 | +version = "0.2.2" | |
| 2455 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2456 | +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" | |
| 2457 | +dependencies = [ | |
| 2458 | + "bitflags 2.11.0", | |
| 2459 | + "block2 0.5.1", | |
| 2460 | + "libc", | |
| 2461 | + "objc2 0.5.2", | |
| 2462 | + "objc2-core-data", | |
| 2463 | + "objc2-core-image", | |
| 2464 | + "objc2-foundation 0.2.2", | |
| 2465 | + "objc2-quartz-core", | |
| 2466 | +] | |
| 2467 | + | |
| 2468 | +[[package]] | |
| 2469 | +name = "objc2-app-kit" | |
| 2470 | +version = "0.3.2" | |
| 2471 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2472 | +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" | |
| 2473 | +dependencies = [ | |
| 2474 | + "bitflags 2.11.0", | |
| 2475 | + "block2 0.6.2", | |
| 2476 | + "objc2 0.6.4", | |
| 2477 | + "objc2-core-foundation", | |
| 2478 | + "objc2-core-graphics", | |
| 2479 | + "objc2-foundation 0.3.2", | |
| 2480 | +] | |
| 2481 | + | |
| 2482 | +[[package]] | |
| 2483 | +name = "objc2-cloud-kit" | |
| 2484 | +version = "0.2.2" | |
| 2485 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2486 | +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" | |
| 2487 | +dependencies = [ | |
| 2488 | + "bitflags 2.11.0", | |
| 2489 | + "block2 0.5.1", | |
| 2490 | + "objc2 0.5.2", | |
| 2491 | + "objc2-core-location", | |
| 2492 | + "objc2-foundation 0.2.2", | |
| 2493 | +] | |
| 2494 | + | |
| 2495 | +[[package]] | |
| 2496 | +name = "objc2-contacts" | |
| 2497 | +version = "0.2.2" | |
| 2498 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2499 | +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" | |
| 2500 | +dependencies = [ | |
| 2501 | + "block2 0.5.1", | |
| 2502 | + "objc2 0.5.2", | |
| 2503 | + "objc2-foundation 0.2.2", | |
| 2504 | +] | |
| 2505 | + | |
| 2506 | +[[package]] | |
| 2507 | +name = "objc2-core-data" | |
| 2508 | +version = "0.2.2" | |
| 2509 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2510 | +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" | |
| 2511 | +dependencies = [ | |
| 2512 | + "bitflags 2.11.0", | |
| 2513 | + "block2 0.5.1", | |
| 2514 | + "objc2 0.5.2", | |
| 2515 | + "objc2-foundation 0.2.2", | |
| 2516 | +] | |
| 2517 | + | |
| 2518 | +[[package]] | |
| 2519 | +name = "objc2-core-foundation" | |
| 2520 | +version = "0.3.2" | |
| 2521 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2522 | +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" | |
| 2523 | +dependencies = [ | |
| 2524 | + "bitflags 2.11.0", | |
| 2525 | + "dispatch2", | |
| 2526 | + "objc2 0.6.4", | |
| 2527 | +] | |
| 2528 | + | |
| 2529 | +[[package]] | |
| 2530 | +name = "objc2-core-graphics" | |
| 2531 | +version = "0.3.2" | |
| 2532 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2533 | +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" | |
| 2534 | +dependencies = [ | |
| 2535 | + "bitflags 2.11.0", | |
| 2536 | + "dispatch2", | |
| 2537 | + "objc2 0.6.4", | |
| 2538 | + "objc2-core-foundation", | |
| 2539 | + "objc2-io-surface", | |
| 2540 | +] | |
| 2541 | + | |
| 2542 | +[[package]] | |
| 2543 | +name = "objc2-core-image" | |
| 2544 | +version = "0.2.2" | |
| 2545 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2546 | +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" | |
| 2547 | +dependencies = [ | |
| 2548 | + "block2 0.5.1", | |
| 2549 | + "objc2 0.5.2", | |
| 2550 | + "objc2-foundation 0.2.2", | |
| 2551 | + "objc2-metal", | |
| 2552 | +] | |
| 2553 | + | |
| 2554 | +[[package]] | |
| 2555 | +name = "objc2-core-location" | |
| 2556 | +version = "0.2.2" | |
| 2557 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2558 | +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" | |
| 2559 | +dependencies = [ | |
| 2560 | + "block2 0.5.1", | |
| 2561 | + "objc2 0.5.2", | |
| 2562 | + "objc2-contacts", | |
| 2563 | + "objc2-foundation 0.2.2", | |
| 2564 | +] | |
| 2565 | + | |
| 2566 | +[[package]] | |
| 2567 | +name = "objc2-encode" | |
| 2568 | +version = "4.1.0" | |
| 2569 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2570 | +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" | |
| 2571 | + | |
| 2572 | +[[package]] | |
| 2573 | +name = "objc2-foundation" | |
| 2574 | +version = "0.2.2" | |
| 2575 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2576 | +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" | |
| 2577 | +dependencies = [ | |
| 2578 | + "bitflags 2.11.0", | |
| 2579 | + "block2 0.5.1", | |
| 2580 | + "dispatch", | |
| 2581 | + "libc", | |
| 2582 | + "objc2 0.5.2", | |
| 2583 | +] | |
| 2584 | + | |
| 2585 | +[[package]] | |
| 2586 | +name = "objc2-foundation" | |
| 2587 | +version = "0.3.2" | |
| 2588 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2589 | +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" | |
| 2590 | +dependencies = [ | |
| 2591 | + "bitflags 2.11.0", | |
| 2592 | + "objc2 0.6.4", | |
| 2593 | + "objc2-core-foundation", | |
| 2594 | +] | |
| 2595 | + | |
| 2596 | +[[package]] | |
| 2597 | +name = "objc2-io-surface" | |
| 2598 | +version = "0.3.2" | |
| 2599 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2600 | +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" | |
| 2601 | +dependencies = [ | |
| 2602 | + "bitflags 2.11.0", | |
| 2603 | + "objc2 0.6.4", | |
| 2604 | + "objc2-core-foundation", | |
| 2605 | +] | |
| 2606 | + | |
| 2607 | +[[package]] | |
| 2608 | +name = "objc2-link-presentation" | |
| 2609 | +version = "0.2.2" | |
| 2610 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2611 | +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" | |
| 2612 | +dependencies = [ | |
| 2613 | + "block2 0.5.1", | |
| 2614 | + "objc2 0.5.2", | |
| 2615 | + "objc2-app-kit 0.2.2", | |
| 2616 | + "objc2-foundation 0.2.2", | |
| 2617 | +] | |
| 2618 | + | |
| 2619 | +[[package]] | |
| 2620 | +name = "objc2-metal" | |
| 2621 | +version = "0.2.2" | |
| 2622 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2623 | +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" | |
| 2624 | +dependencies = [ | |
| 2625 | + "bitflags 2.11.0", | |
| 2626 | + "block2 0.5.1", | |
| 2627 | + "objc2 0.5.2", | |
| 2628 | + "objc2-foundation 0.2.2", | |
| 2629 | +] | |
| 2630 | + | |
| 2631 | +[[package]] | |
| 2632 | +name = "objc2-quartz-core" | |
| 2633 | +version = "0.2.2" | |
| 2634 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2635 | +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" | |
| 2636 | +dependencies = [ | |
| 2637 | + "bitflags 2.11.0", | |
| 2638 | + "block2 0.5.1", | |
| 2639 | + "objc2 0.5.2", | |
| 2640 | + "objc2-foundation 0.2.2", | |
| 2641 | + "objc2-metal", | |
| 2642 | +] | |
| 2643 | + | |
| 2644 | +[[package]] | |
| 2645 | +name = "objc2-symbols" | |
| 2646 | +version = "0.2.2" | |
| 2647 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2648 | +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" | |
| 2649 | +dependencies = [ | |
| 2650 | + "objc2 0.5.2", | |
| 2651 | + "objc2-foundation 0.2.2", | |
| 2652 | +] | |
| 2653 | + | |
| 2654 | +[[package]] | |
| 2655 | +name = "objc2-ui-kit" | |
| 2656 | +version = "0.2.2" | |
| 2657 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2658 | +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" | |
| 2659 | +dependencies = [ | |
| 2660 | + "bitflags 2.11.0", | |
| 2661 | + "block2 0.5.1", | |
| 2662 | + "objc2 0.5.2", | |
| 2663 | + "objc2-cloud-kit", | |
| 2664 | + "objc2-core-data", | |
| 2665 | + "objc2-core-image", | |
| 2666 | + "objc2-core-location", | |
| 2667 | + "objc2-foundation 0.2.2", | |
| 2668 | + "objc2-link-presentation", | |
| 2669 | + "objc2-quartz-core", | |
| 2670 | + "objc2-symbols", | |
| 2671 | + "objc2-uniform-type-identifiers", | |
| 2672 | + "objc2-user-notifications", | |
| 2673 | +] | |
| 2674 | + | |
| 2675 | +[[package]] | |
| 2676 | +name = "objc2-uniform-type-identifiers" | |
| 2677 | +version = "0.2.2" | |
| 2678 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2679 | +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" | |
| 2680 | +dependencies = [ | |
| 2681 | + "block2 0.5.1", | |
| 2682 | + "objc2 0.5.2", | |
| 2683 | + "objc2-foundation 0.2.2", | |
| 2684 | +] | |
| 2685 | + | |
| 2686 | +[[package]] | |
| 2687 | +name = "objc2-user-notifications" | |
| 2688 | +version = "0.2.2" | |
| 2689 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2690 | +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" | |
| 2691 | +dependencies = [ | |
| 2692 | + "bitflags 2.11.0", | |
| 2693 | + "block2 0.5.1", | |
| 2694 | + "objc2 0.5.2", | |
| 2695 | + "objc2-core-location", | |
| 2696 | + "objc2-foundation 0.2.2", | |
| 2697 | +] | |
| 2698 | + | |
| 2699 | +[[package]] | |
| 2700 | +name = "once_cell" | |
| 2701 | +version = "1.21.3" | |
| 2702 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2703 | +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" | |
| 2704 | + | |
| 2705 | +[[package]] | |
| 2706 | +name = "option-ext" | |
| 2707 | +version = "0.2.0" | |
| 2708 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2709 | +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" | |
| 2710 | + | |
| 2711 | +[[package]] | |
| 2712 | +name = "orbclient" | |
| 2713 | +version = "0.3.50" | |
| 2714 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2715 | +checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" | |
| 2716 | +dependencies = [ | |
| 2717 | + "libc", | |
| 2718 | + "libredox", | |
| 2719 | +] | |
| 2720 | + | |
| 2721 | +[[package]] | |
| 2722 | +name = "ordered-float" | |
| 2723 | +version = "4.6.0" | |
| 2724 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2725 | +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" | |
| 2726 | +dependencies = [ | |
| 2727 | + "num-traits", | |
| 2728 | +] | |
| 2729 | + | |
| 2730 | +[[package]] | |
| 2731 | +name = "ordered-stream" | |
| 2732 | +version = "0.2.0" | |
| 2733 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2734 | +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" | |
| 2735 | +dependencies = [ | |
| 2736 | + "futures-core", | |
| 2737 | + "pin-project-lite", | |
| 2738 | +] | |
| 2739 | + | |
| 2740 | +[[package]] | |
| 2741 | +name = "owned_ttf_parser" | |
| 2742 | +version = "0.25.1" | |
| 2743 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2744 | +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" | |
| 2745 | +dependencies = [ | |
| 2746 | + "ttf-parser", | |
| 2747 | +] | |
| 2748 | + | |
| 2749 | +[[package]] | |
| 2750 | +name = "parking" | |
| 2751 | +version = "2.2.1" | |
| 2752 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2753 | +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" | |
| 2754 | + | |
| 2755 | +[[package]] | |
| 2756 | +name = "parking_lot" | |
| 2757 | +version = "0.12.5" | |
| 2758 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2759 | +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" | |
| 2760 | +dependencies = [ | |
| 2761 | + "lock_api", | |
| 2762 | + "parking_lot_core", | |
| 2763 | +] | |
| 2764 | + | |
| 2765 | +[[package]] | |
| 2766 | +name = "parking_lot_core" | |
| 2767 | +version = "0.9.12" | |
| 2768 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2769 | +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" | |
| 2770 | +dependencies = [ | |
| 2771 | + "cfg-if", | |
| 2772 | + "libc", | |
| 2773 | + "redox_syscall 0.5.18", | |
| 2774 | + "smallvec", | |
| 2775 | + "windows-link", | |
| 2776 | +] | |
| 2777 | + | |
| 2778 | +[[package]] | |
| 2779 | +name = "paste" | |
| 2780 | +version = "1.0.15" | |
| 2781 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2782 | +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" | |
| 2783 | + | |
| 2784 | +[[package]] | |
| 2785 | +name = "percent-encoding" | |
| 2786 | +version = "2.3.2" | |
| 2787 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2788 | +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" | |
| 2789 | + | |
| 2790 | +[[package]] | |
| 2791 | +name = "phf" | |
| 2792 | +version = "0.11.3" | |
| 2793 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2794 | +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" | |
| 2795 | +dependencies = [ | |
| 2796 | + "phf_macros", | |
| 2797 | + "phf_shared 0.11.3", | |
| 2798 | +] | |
| 2799 | + | |
| 2800 | +[[package]] | |
| 2801 | +name = "phf" | |
| 2802 | +version = "0.12.1" | |
| 2803 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2804 | +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" | |
| 2805 | +dependencies = [ | |
| 2806 | + "phf_shared 0.12.1", | |
| 2807 | +] | |
| 2808 | + | |
| 2809 | +[[package]] | |
| 2810 | +name = "phf_generator" | |
| 2811 | +version = "0.11.3" | |
| 2812 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2813 | +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" | |
| 2814 | +dependencies = [ | |
| 2815 | + "phf_shared 0.11.3", | |
| 2816 | + "rand 0.8.5", | |
| 2817 | +] | |
| 2818 | + | |
| 2819 | +[[package]] | |
| 2820 | +name = "phf_macros" | |
| 2821 | +version = "0.11.3" | |
| 2822 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2823 | +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" | |
| 2824 | +dependencies = [ | |
| 2825 | + "phf_generator", | |
| 2826 | + "phf_shared 0.11.3", | |
| 2827 | + "proc-macro2", | |
| 2828 | + "quote", | |
| 2829 | + "syn", | |
| 2830 | + "unicase", | |
| 2831 | +] | |
| 2832 | + | |
| 2833 | +[[package]] | |
| 2834 | +name = "phf_shared" | |
| 2835 | +version = "0.11.3" | |
| 2836 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2837 | +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" | |
| 2838 | +dependencies = [ | |
| 2839 | + "siphasher 1.0.2", | |
| 2840 | + "unicase", | |
| 2841 | +] | |
| 2842 | + | |
| 2843 | +[[package]] | |
| 2844 | +name = "phf_shared" | |
| 2845 | +version = "0.12.1" | |
| 2846 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2847 | +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" | |
| 2848 | +dependencies = [ | |
| 2849 | + "siphasher 1.0.2", | |
| 2850 | +] | |
| 2851 | + | |
| 2852 | +[[package]] | |
| 2853 | +name = "pico-args" | |
| 2854 | +version = "0.5.0" | |
| 2855 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2856 | +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" | |
| 2857 | + | |
| 2858 | +[[package]] | |
| 2859 | +name = "pin-project" | |
| 2860 | +version = "1.1.11" | |
| 2861 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2862 | +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" | |
| 2863 | +dependencies = [ | |
| 2864 | + "pin-project-internal", | |
| 2865 | +] | |
| 2866 | + | |
| 2867 | +[[package]] | |
| 2868 | +name = "pin-project-internal" | |
| 2869 | +version = "1.1.11" | |
| 2870 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2871 | +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" | |
| 2872 | +dependencies = [ | |
| 2873 | + "proc-macro2", | |
| 2874 | + "quote", | |
| 2875 | + "syn", | |
| 2876 | +] | |
| 2877 | + | |
| 2878 | +[[package]] | |
| 2879 | +name = "pin-project-lite" | |
| 2880 | +version = "0.2.17" | |
| 2881 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2882 | +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" | |
| 2883 | + | |
| 2884 | +[[package]] | |
| 2885 | +name = "piper" | |
| 2886 | +version = "0.2.5" | |
| 2887 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2888 | +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" | |
| 2889 | +dependencies = [ | |
| 2890 | + "atomic-waker", | |
| 2891 | + "fastrand", | |
| 2892 | + "futures-io", | |
| 2893 | +] | |
| 2894 | + | |
| 2895 | +[[package]] | |
| 2896 | +name = "pkg-config" | |
| 2897 | +version = "0.3.32" | |
| 2898 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2899 | +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" | |
| 2900 | + | |
| 2901 | +[[package]] | |
| 2902 | +name = "plain" | |
| 2903 | +version = "0.2.3" | |
| 2904 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2905 | +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" | |
| 2906 | + | |
| 2907 | +[[package]] | |
| 2908 | +name = "plist" | |
| 2909 | +version = "1.8.0" | |
| 2910 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2911 | +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" | |
| 2912 | +dependencies = [ | |
| 2913 | + "base64 0.22.1", | |
| 2914 | + "indexmap", | |
| 2915 | + "quick-xml 0.38.4", | |
| 2916 | + "serde", | |
| 2917 | + "time", | |
| 2918 | +] | |
| 2919 | + | |
| 2920 | +[[package]] | |
| 2921 | +name = "png" | |
| 2922 | +version = "0.17.16" | |
| 2923 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2924 | +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" | |
| 2925 | +dependencies = [ | |
| 2926 | + "bitflags 1.3.2", | |
| 2927 | + "crc32fast", | |
| 2928 | + "fdeflate", | |
| 2929 | + "flate2", | |
| 2930 | + "miniz_oxide", | |
| 2931 | +] | |
| 2932 | + | |
| 2933 | +[[package]] | |
| 2934 | +name = "png" | |
| 2935 | +version = "0.18.1" | |
| 2936 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2937 | +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" | |
| 2938 | +dependencies = [ | |
| 2939 | + "bitflags 2.11.0", | |
| 2940 | + "crc32fast", | |
| 2941 | + "fdeflate", | |
| 2942 | + "flate2", | |
| 2943 | + "miniz_oxide", | |
| 2944 | +] | |
| 2945 | + | |
| 2946 | +[[package]] | |
| 2947 | +name = "polling" | |
| 2948 | +version = "3.11.0" | |
| 2949 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2950 | +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" | |
| 2951 | +dependencies = [ | |
| 2952 | + "cfg-if", | |
| 2953 | + "concurrent-queue", | |
| 2954 | + "hermit-abi", | |
| 2955 | + "pin-project-lite", | |
| 2956 | + "rustix 1.1.4", | |
| 2957 | + "windows-sys 0.61.2", | |
| 2958 | +] | |
| 2959 | + | |
| 2960 | +[[package]] | |
| 2961 | +name = "pollster" | |
| 2962 | +version = "0.4.0" | |
| 2963 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2964 | +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" | |
| 2965 | + | |
| 2966 | +[[package]] | |
| 2967 | +name = "potential_utf" | |
| 2968 | +version = "0.1.4" | |
| 2969 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2970 | +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" | |
| 2971 | +dependencies = [ | |
| 2972 | + "zerovec", | |
| 2973 | +] | |
| 2974 | + | |
| 2975 | +[[package]] | |
| 2976 | +name = "powerfmt" | |
| 2977 | +version = "0.2.0" | |
| 2978 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2979 | +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" | |
| 2980 | + | |
| 2981 | +[[package]] | |
| 2982 | +name = "ppv-lite86" | |
| 2983 | +version = "0.2.21" | |
| 2984 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2985 | +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" | |
| 2986 | +dependencies = [ | |
| 2987 | + "zerocopy", | |
| 2988 | +] | |
| 2989 | + | |
| 2990 | +[[package]] | |
| 2991 | +name = "prettyplease" | |
| 2992 | +version = "0.2.37" | |
| 2993 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2994 | +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" | |
| 2995 | +dependencies = [ | |
| 2996 | + "proc-macro2", | |
| 2997 | + "syn", | |
| 2998 | +] | |
| 2999 | + | |
| 3000 | +[[package]] | |
| 3001 | +name = "proc-macro-crate" | |
| 3002 | +version = "3.5.0" | |
| 3003 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3004 | +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" | |
| 3005 | +dependencies = [ | |
| 3006 | + "toml_edit", | |
| 3007 | +] | |
| 3008 | + | |
| 3009 | +[[package]] | |
| 3010 | +name = "proc-macro2" | |
| 3011 | +version = "1.0.106" | |
| 3012 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3013 | +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" | |
| 3014 | +dependencies = [ | |
| 3015 | + "unicode-ident", | |
| 3016 | +] | |
| 3017 | + | |
| 3018 | +[[package]] | |
| 3019 | +name = "profiling" | |
| 3020 | +version = "1.0.17" | |
| 3021 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3022 | +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" | |
| 3023 | + | |
| 3024 | +[[package]] | |
| 3025 | +name = "pxfm" | |
| 3026 | +version = "0.1.28" | |
| 3027 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3028 | +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" | |
| 3029 | + | |
| 3030 | +[[package]] | |
| 3031 | +name = "quick-error" | |
| 3032 | +version = "2.0.1" | |
| 3033 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3034 | +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" | |
| 3035 | + | |
| 3036 | +[[package]] | |
| 3037 | +name = "quick-xml" | |
| 3038 | +version = "0.30.0" | |
| 3039 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3040 | +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" | |
| 3041 | +dependencies = [ | |
| 3042 | + "memchr", | |
| 3043 | + "serde", | |
| 3044 | +] | |
| 3045 | + | |
| 3046 | +[[package]] | |
| 3047 | +name = "quick-xml" | |
| 3048 | +version = "0.38.4" | |
| 3049 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3050 | +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" | |
| 3051 | +dependencies = [ | |
| 3052 | + "memchr", | |
| 3053 | +] | |
| 3054 | + | |
| 3055 | +[[package]] | |
| 3056 | +name = "quick-xml" | |
| 3057 | +version = "0.39.2" | |
| 3058 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3059 | +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" | |
| 3060 | +dependencies = [ | |
| 3061 | + "memchr", | |
| 3062 | +] | |
| 3063 | + | |
| 3064 | +[[package]] | |
| 3065 | +name = "quote" | |
| 3066 | +version = "1.0.45" | |
| 3067 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3068 | +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" | |
| 3069 | +dependencies = [ | |
| 3070 | + "proc-macro2", | |
| 3071 | +] | |
| 3072 | + | |
| 3073 | +[[package]] | |
| 3074 | +name = "r-efi" | |
| 3075 | +version = "5.3.0" | |
| 3076 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3077 | +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" | |
| 3078 | + | |
| 3079 | +[[package]] | |
| 3080 | +name = "r-efi" | |
| 3081 | +version = "6.0.0" | |
| 3082 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3083 | +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" | |
| 3084 | + | |
| 3085 | +[[package]] | |
| 3086 | +name = "rand" | |
| 3087 | +version = "0.8.5" | |
| 3088 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3089 | +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" | |
| 3090 | +dependencies = [ | |
| 3091 | + "libc", | |
| 3092 | + "rand_chacha 0.3.1", | |
| 3093 | + "rand_core 0.6.4", | |
| 3094 | +] | |
| 3095 | + | |
| 3096 | +[[package]] | |
| 3097 | +name = "rand" | |
| 3098 | +version = "0.9.2" | |
| 3099 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3100 | +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" | |
| 3101 | +dependencies = [ | |
| 3102 | + "rand_chacha 0.9.0", | |
| 3103 | + "rand_core 0.9.5", | |
| 3104 | +] | |
| 3105 | + | |
| 3106 | +[[package]] | |
| 3107 | +name = "rand_chacha" | |
| 3108 | +version = "0.3.1" | |
| 3109 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3110 | +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" | |
| 3111 | +dependencies = [ | |
| 3112 | + "ppv-lite86", | |
| 3113 | + "rand_core 0.6.4", | |
| 3114 | +] | |
| 3115 | + | |
| 3116 | +[[package]] | |
| 3117 | +name = "rand_chacha" | |
| 3118 | +version = "0.9.0" | |
| 3119 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3120 | +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" | |
| 3121 | +dependencies = [ | |
| 3122 | + "ppv-lite86", | |
| 3123 | + "rand_core 0.9.5", | |
| 3124 | +] | |
| 3125 | + | |
| 3126 | +[[package]] | |
| 3127 | +name = "rand_core" | |
| 3128 | +version = "0.6.4" | |
| 3129 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3130 | +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" | |
| 3131 | +dependencies = [ | |
| 3132 | + "getrandom 0.2.17", | |
| 3133 | +] | |
| 3134 | + | |
| 3135 | +[[package]] | |
| 3136 | +name = "rand_core" | |
| 3137 | +version = "0.9.5" | |
| 3138 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3139 | +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" | |
| 3140 | +dependencies = [ | |
| 3141 | + "getrandom 0.3.4", | |
| 3142 | +] | |
| 3143 | + | |
| 3144 | +[[package]] | |
| 3145 | +name = "raw-window-handle" | |
| 3146 | +version = "0.6.2" | |
| 3147 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3148 | +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" | |
| 3149 | + | |
| 3150 | +[[package]] | |
| 3151 | +name = "rctree" | |
| 3152 | +version = "0.5.0" | |
| 3153 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3154 | +checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" | |
| 3155 | + | |
| 3156 | +[[package]] | |
| 3157 | +name = "redox_syscall" | |
| 3158 | +version = "0.4.1" | |
| 3159 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3160 | +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" | |
| 3161 | +dependencies = [ | |
| 3162 | + "bitflags 1.3.2", | |
| 3163 | +] | |
| 3164 | + | |
| 3165 | +[[package]] | |
| 3166 | +name = "redox_syscall" | |
| 3167 | +version = "0.5.18" | |
| 3168 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3169 | +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" | |
| 3170 | +dependencies = [ | |
| 3171 | + "bitflags 2.11.0", | |
| 3172 | +] | |
| 3173 | + | |
| 3174 | +[[package]] | |
| 3175 | +name = "redox_syscall" | |
| 3176 | +version = "0.7.3" | |
| 3177 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3178 | +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" | |
| 3179 | +dependencies = [ | |
| 3180 | + "bitflags 2.11.0", | |
| 3181 | +] | |
| 3182 | + | |
| 3183 | +[[package]] | |
| 3184 | +name = "redox_users" | |
| 3185 | +version = "0.5.2" | |
| 3186 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3187 | +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" | |
| 3188 | +dependencies = [ | |
| 3189 | + "getrandom 0.2.17", | |
| 3190 | + "libredox", | |
| 3191 | + "thiserror 2.0.18", | |
| 3192 | +] | |
| 3193 | + | |
| 3194 | +[[package]] | |
| 3195 | +name = "regex" | |
| 3196 | +version = "1.12.3" | |
| 3197 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3198 | +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" | |
| 3199 | +dependencies = [ | |
| 3200 | + "aho-corasick", | |
| 3201 | + "memchr", | |
| 3202 | + "regex-automata", | |
| 3203 | + "regex-syntax", | |
| 3204 | +] | |
| 3205 | + | |
| 3206 | +[[package]] | |
| 3207 | +name = "regex-automata" | |
| 3208 | +version = "0.4.14" | |
| 3209 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3210 | +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" | |
| 3211 | +dependencies = [ | |
| 3212 | + "aho-corasick", | |
| 3213 | + "memchr", | |
| 3214 | + "regex-syntax", | |
| 3215 | +] | |
| 3216 | + | |
| 3217 | +[[package]] | |
| 3218 | +name = "regex-syntax" | |
| 3219 | +version = "0.8.10" | |
| 3220 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3221 | +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" | |
| 3222 | + | |
| 3223 | +[[package]] | |
| 3224 | +name = "renderdoc-sys" | |
| 3225 | +version = "1.1.0" | |
| 3226 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3227 | +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" | |
| 3228 | + | |
| 3229 | +[[package]] | |
| 3230 | +name = "resvg" | |
| 3231 | +version = "0.37.0" | |
| 3232 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3233 | +checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4" | |
| 3234 | +dependencies = [ | |
| 3235 | + "log", | |
| 3236 | + "pico-args", | |
| 3237 | + "rgb", | |
| 3238 | + "svgtypes", | |
| 3239 | + "tiny-skia", | |
| 3240 | + "usvg", | |
| 3241 | +] | |
| 3242 | + | |
| 3243 | +[[package]] | |
| 3244 | +name = "rfd" | |
| 3245 | +version = "0.15.4" | |
| 3246 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3247 | +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" | |
| 3248 | +dependencies = [ | |
| 3249 | + "ashpd", | |
| 3250 | + "block2 0.6.2", | |
| 3251 | + "dispatch2", | |
| 3252 | + "js-sys", | |
| 3253 | + "log", | |
| 3254 | + "objc2 0.6.4", | |
| 3255 | + "objc2-app-kit 0.3.2", | |
| 3256 | + "objc2-core-foundation", | |
| 3257 | + "objc2-foundation 0.3.2", | |
| 3258 | + "pollster", | |
| 3259 | + "raw-window-handle", | |
| 3260 | + "urlencoding", | |
| 3261 | + "wasm-bindgen", | |
| 3262 | + "wasm-bindgen-futures", | |
| 3263 | + "web-sys", | |
| 3264 | + "windows-sys 0.59.0", | |
| 3265 | +] | |
| 3266 | + | |
| 3267 | +[[package]] | |
| 3268 | +name = "rgb" | |
| 3269 | +version = "0.8.52" | |
| 3270 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3271 | +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" | |
| 3272 | +dependencies = [ | |
| 3273 | + "bytemuck", | |
| 3274 | +] | |
| 3275 | + | |
| 3276 | +[[package]] | |
| 3277 | +name = "ron" | |
| 3278 | +version = "0.8.1" | |
| 3279 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3280 | +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" | |
| 3281 | +dependencies = [ | |
| 3282 | + "base64 0.21.7", | |
| 3283 | + "bitflags 2.11.0", | |
| 3284 | + "serde", | |
| 3285 | + "serde_derive", | |
| 3286 | +] | |
| 3287 | + | |
| 3288 | +[[package]] | |
| 3289 | +name = "roxmltree" | |
| 3290 | +version = "0.19.0" | |
| 3291 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3292 | +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" | |
| 3293 | + | |
| 3294 | +[[package]] | |
| 3295 | +name = "rustc-hash" | |
| 3296 | +version = "1.1.0" | |
| 3297 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3298 | +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" | |
| 3299 | + | |
| 3300 | +[[package]] | |
| 3301 | +name = "rustc-hash" | |
| 3302 | +version = "2.1.1" | |
| 3303 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3304 | +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" | |
| 3305 | + | |
| 3306 | +[[package]] | |
| 3307 | +name = "rustc_version" | |
| 3308 | +version = "0.4.1" | |
| 3309 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3310 | +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" | |
| 3311 | +dependencies = [ | |
| 3312 | + "semver", | |
| 3313 | +] | |
| 3314 | + | |
| 3315 | +[[package]] | |
| 3316 | +name = "rustix" | |
| 3317 | +version = "0.38.44" | |
| 3318 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3319 | +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" | |
| 3320 | +dependencies = [ | |
| 3321 | + "bitflags 2.11.0", | |
| 3322 | + "errno", | |
| 3323 | + "libc", | |
| 3324 | + "linux-raw-sys 0.4.15", | |
| 3325 | + "windows-sys 0.59.0", | |
| 3326 | +] | |
| 3327 | + | |
| 3328 | +[[package]] | |
| 3329 | +name = "rustix" | |
| 3330 | +version = "1.1.4" | |
| 3331 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3332 | +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" | |
| 3333 | +dependencies = [ | |
| 3334 | + "bitflags 2.11.0", | |
| 3335 | + "errno", | |
| 3336 | + "libc", | |
| 3337 | + "linux-raw-sys 0.12.1", | |
| 3338 | + "windows-sys 0.61.2", | |
| 3339 | +] | |
| 3340 | + | |
| 3341 | +[[package]] | |
| 3342 | +name = "rustversion" | |
| 3343 | +version = "1.0.22" | |
| 3344 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3345 | +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" | |
| 3346 | + | |
| 3347 | +[[package]] | |
| 3348 | +name = "same-file" | |
| 3349 | +version = "1.0.6" | |
| 3350 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3351 | +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" | |
| 3352 | +dependencies = [ | |
| 3353 | + "winapi-util", | |
| 3354 | +] | |
| 3355 | + | |
| 3356 | +[[package]] | |
| 3357 | +name = "scoped-tls" | |
| 3358 | +version = "1.0.1" | |
| 3359 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3360 | +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" | |
| 3361 | + | |
| 3362 | +[[package]] | |
| 3363 | +name = "scopeguard" | |
| 3364 | +version = "1.2.0" | |
| 3365 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3366 | +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" | |
| 3367 | + | |
| 3368 | +[[package]] | |
| 3369 | +name = "sctk-adwaita" | |
| 3370 | +version = "0.10.1" | |
| 3371 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3372 | +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" | |
| 3373 | +dependencies = [ | |
| 3374 | + "ab_glyph", | |
| 3375 | + "log", | |
| 3376 | + "memmap2", | |
| 3377 | + "smithay-client-toolkit 0.19.2", | |
| 3378 | + "tiny-skia", | |
| 3379 | +] | |
| 3380 | + | |
| 3381 | +[[package]] | |
| 3382 | +name = "semver" | |
| 3383 | +version = "1.0.27" | |
| 3384 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3385 | +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" | |
| 3386 | + | |
| 3387 | +[[package]] | |
| 3388 | +name = "serde" | |
| 3389 | +version = "1.0.228" | |
| 3390 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3391 | +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" | |
| 3392 | +dependencies = [ | |
| 3393 | + "serde_core", | |
| 3394 | + "serde_derive", | |
| 3395 | +] | |
| 3396 | + | |
| 3397 | +[[package]] | |
| 3398 | +name = "serde_core" | |
| 3399 | +version = "1.0.228" | |
| 3400 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3401 | +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" | |
| 3402 | +dependencies = [ | |
| 3403 | + "serde_derive", | |
| 3404 | +] | |
| 3405 | + | |
| 3406 | +[[package]] | |
| 3407 | +name = "serde_derive" | |
| 3408 | +version = "1.0.228" | |
| 3409 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3410 | +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" | |
| 3411 | +dependencies = [ | |
| 3412 | + "proc-macro2", | |
| 3413 | + "quote", | |
| 3414 | + "syn", | |
| 3415 | +] | |
| 3416 | + | |
| 3417 | +[[package]] | |
| 3418 | +name = "serde_json" | |
| 3419 | +version = "1.0.149" | |
| 3420 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3421 | +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" | |
| 3422 | +dependencies = [ | |
| 3423 | + "itoa", | |
| 3424 | + "memchr", | |
| 3425 | + "serde", | |
| 3426 | + "serde_core", | |
| 3427 | + "zmij", | |
| 3428 | +] | |
| 3429 | + | |
| 3430 | +[[package]] | |
| 3431 | +name = "serde_repr" | |
| 3432 | +version = "0.1.20" | |
| 3433 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3434 | +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" | |
| 3435 | +dependencies = [ | |
| 3436 | + "proc-macro2", | |
| 3437 | + "quote", | |
| 3438 | + "syn", | |
| 3439 | +] | |
| 3440 | + | |
| 3441 | +[[package]] | |
| 3442 | +name = "sha1" | |
| 3443 | +version = "0.10.6" | |
| 3444 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3445 | +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" | |
| 3446 | +dependencies = [ | |
| 3447 | + "cfg-if", | |
| 3448 | + "cpufeatures", | |
| 3449 | + "digest", | |
| 3450 | +] | |
| 3451 | + | |
| 3452 | +[[package]] | |
| 3453 | +name = "sharded-slab" | |
| 3454 | +version = "0.1.7" | |
| 3455 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3456 | +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" | |
| 3457 | +dependencies = [ | |
| 3458 | + "lazy_static", | |
| 3459 | +] | |
| 3460 | + | |
| 3461 | +[[package]] | |
| 3462 | +name = "shlex" | |
| 3463 | +version = "1.3.0" | |
| 3464 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3465 | +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" | |
| 3466 | + | |
| 3467 | +[[package]] | |
| 3468 | +name = "signal-hook-registry" | |
| 3469 | +version = "1.4.8" | |
| 3470 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3471 | +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" | |
| 3472 | +dependencies = [ | |
| 3473 | + "errno", | |
| 3474 | + "libc", | |
| 3475 | +] | |
| 3476 | + | |
| 3477 | +[[package]] | |
| 3478 | +name = "simd-adler32" | |
| 3479 | +version = "0.3.8" | |
| 3480 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3481 | +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" | |
| 3482 | + | |
| 3483 | +[[package]] | |
| 3484 | +name = "simd_cesu8" | |
| 3485 | +version = "1.1.1" | |
| 3486 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3487 | +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" | |
| 3488 | +dependencies = [ | |
| 3489 | + "rustc_version", | |
| 3490 | + "simdutf8", | |
| 3491 | +] | |
| 3492 | + | |
| 3493 | +[[package]] | |
| 3494 | +name = "simdutf8" | |
| 3495 | +version = "0.1.5" | |
| 3496 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3497 | +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" | |
| 3498 | + | |
| 3499 | +[[package]] | |
| 3500 | +name = "simplecss" | |
| 3501 | +version = "0.2.2" | |
| 3502 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3503 | +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" | |
| 3504 | +dependencies = [ | |
| 3505 | + "log", | |
| 3506 | +] | |
| 3507 | + | |
| 3508 | +[[package]] | |
| 3509 | +name = "siphasher" | |
| 3510 | +version = "0.3.11" | |
| 3511 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3512 | +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" | |
| 3513 | + | |
| 3514 | +[[package]] | |
| 3515 | +name = "siphasher" | |
| 3516 | +version = "1.0.2" | |
| 3517 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3518 | +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" | |
| 3519 | + | |
| 3520 | +[[package]] | |
| 3521 | +name = "slab" | |
| 3522 | +version = "0.4.12" | |
| 3523 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3524 | +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" | |
| 3525 | + | |
| 3526 | +[[package]] | |
| 3527 | +name = "slotmap" | |
| 3528 | +version = "1.1.1" | |
| 3529 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3530 | +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" | |
| 3531 | +dependencies = [ | |
| 3532 | + "version_check", | |
| 3533 | +] | |
| 3534 | + | |
| 3535 | +[[package]] | |
| 3536 | +name = "smallvec" | |
| 3537 | +version = "1.15.1" | |
| 3538 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3539 | +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" | |
| 3540 | + | |
| 3541 | +[[package]] | |
| 3542 | +name = "smithay-client-toolkit" | |
| 3543 | +version = "0.19.2" | |
| 3544 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3545 | +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" | |
| 3546 | +dependencies = [ | |
| 3547 | + "bitflags 2.11.0", | |
| 3548 | + "calloop 0.13.0", | |
| 3549 | + "calloop-wayland-source 0.3.0", | |
| 3550 | + "cursor-icon", | |
| 3551 | + "libc", | |
| 3552 | + "log", | |
| 3553 | + "memmap2", | |
| 3554 | + "rustix 0.38.44", | |
| 3555 | + "thiserror 1.0.69", | |
| 3556 | + "wayland-backend", | |
| 3557 | + "wayland-client", | |
| 3558 | + "wayland-csd-frame", | |
| 3559 | + "wayland-cursor", | |
| 3560 | + "wayland-protocols", | |
| 3561 | + "wayland-protocols-wlr", | |
| 3562 | + "wayland-scanner", | |
| 3563 | + "xkeysym", | |
| 3564 | +] | |
| 3565 | + | |
| 3566 | +[[package]] | |
| 3567 | +name = "smithay-client-toolkit" | |
| 3568 | +version = "0.20.0" | |
| 3569 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3570 | +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" | |
| 3571 | +dependencies = [ | |
| 3572 | + "bitflags 2.11.0", | |
| 3573 | + "calloop 0.14.4", | |
| 3574 | + "calloop-wayland-source 0.4.1", | |
| 3575 | + "cursor-icon", | |
| 3576 | + "libc", | |
| 3577 | + "log", | |
| 3578 | + "memmap2", | |
| 3579 | + "rustix 1.1.4", | |
| 3580 | + "thiserror 2.0.18", | |
| 3581 | + "wayland-backend", | |
| 3582 | + "wayland-client", | |
| 3583 | + "wayland-csd-frame", | |
| 3584 | + "wayland-cursor", | |
| 3585 | + "wayland-protocols", | |
| 3586 | + "wayland-protocols-experimental", | |
| 3587 | + "wayland-protocols-misc", | |
| 3588 | + "wayland-protocols-wlr", | |
| 3589 | + "wayland-scanner", | |
| 3590 | + "xkeysym", | |
| 3591 | +] | |
| 3592 | + | |
| 3593 | +[[package]] | |
| 3594 | +name = "smithay-clipboard" | |
| 3595 | +version = "0.7.3" | |
| 3596 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3597 | +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" | |
| 3598 | +dependencies = [ | |
| 3599 | + "libc", | |
| 3600 | + "smithay-client-toolkit 0.20.0", | |
| 3601 | + "wayland-backend", | |
| 3602 | +] | |
| 3603 | + | |
| 3604 | +[[package]] | |
| 3605 | +name = "smol_str" | |
| 3606 | +version = "0.2.2" | |
| 3607 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3608 | +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" | |
| 3609 | +dependencies = [ | |
| 3610 | + "serde", | |
| 3611 | +] | |
| 3612 | + | |
| 3613 | +[[package]] | |
| 3614 | +name = "spirv" | |
| 3615 | +version = "0.3.0+sdk-1.3.268.0" | |
| 3616 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3617 | +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" | |
| 3618 | +dependencies = [ | |
| 3619 | + "bitflags 2.11.0", | |
| 3620 | +] | |
| 3621 | + | |
| 3622 | +[[package]] | |
| 3623 | +name = "stable_deref_trait" | |
| 3624 | +version = "1.2.1" | |
| 3625 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3626 | +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" | |
| 3627 | + | |
| 3628 | +[[package]] | |
| 3629 | +name = "static_assertions" | |
| 3630 | +version = "1.1.0" | |
| 3631 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3632 | +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" | |
| 3633 | + | |
| 3634 | +[[package]] | |
| 3635 | +name = "strict-num" | |
| 3636 | +version = "0.1.1" | |
| 3637 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3638 | +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" | |
| 3639 | +dependencies = [ | |
| 3640 | + "float-cmp", | |
| 3641 | +] | |
| 3642 | + | |
| 3643 | +[[package]] | |
| 3644 | +name = "strum" | |
| 3645 | +version = "0.26.3" | |
| 3646 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3647 | +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" | |
| 3648 | +dependencies = [ | |
| 3649 | + "strum_macros", | |
| 3650 | +] | |
| 3651 | + | |
| 3652 | +[[package]] | |
| 3653 | +name = "strum_macros" | |
| 3654 | +version = "0.26.4" | |
| 3655 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3656 | +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" | |
| 3657 | +dependencies = [ | |
| 3658 | + "heck", | |
| 3659 | + "proc-macro2", | |
| 3660 | + "quote", | |
| 3661 | + "rustversion", | |
| 3662 | + "syn", | |
| 3663 | +] | |
| 3664 | + | |
| 3665 | +[[package]] | |
| 3666 | +name = "svgtypes" | |
| 3667 | +version = "0.13.0" | |
| 3668 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3669 | +checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" | |
| 3670 | +dependencies = [ | |
| 3671 | + "kurbo", | |
| 3672 | + "siphasher 0.3.11", | |
| 3673 | +] | |
| 3674 | + | |
| 3675 | +[[package]] | |
| 3676 | +name = "syn" | |
| 3677 | +version = "2.0.117" | |
| 3678 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3679 | +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" | |
| 3680 | +dependencies = [ | |
| 3681 | + "proc-macro2", | |
| 3682 | + "quote", | |
| 3683 | + "unicode-ident", | |
| 3684 | +] | |
| 3685 | + | |
| 3686 | +[[package]] | |
| 3687 | +name = "synstructure" | |
| 3688 | +version = "0.13.2" | |
| 3689 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3690 | +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" | |
| 3691 | +dependencies = [ | |
| 3692 | + "proc-macro2", | |
| 3693 | + "quote", | |
| 3694 | + "syn", | |
| 3695 | +] | |
| 3696 | + | |
| 3697 | +[[package]] | |
| 3698 | +name = "syntect" | |
| 3699 | +version = "5.3.0" | |
| 3700 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3701 | +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" | |
| 3702 | +dependencies = [ | |
| 3703 | + "bincode", | |
| 3704 | + "fancy-regex", | |
| 3705 | + "flate2", | |
| 3706 | + "fnv", | |
| 3707 | + "once_cell", | |
| 3708 | + "plist", | |
| 3709 | + "regex-syntax", | |
| 3710 | + "serde", | |
| 3711 | + "serde_derive", | |
| 3712 | + "serde_json", | |
| 3713 | + "thiserror 2.0.18", | |
| 3714 | + "walkdir", | |
| 3715 | + "yaml-rust", | |
| 3716 | +] | |
| 3717 | + | |
| 3718 | +[[package]] | |
| 3719 | +name = "tempfile" | |
| 3720 | +version = "3.26.0" | |
| 3721 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3722 | +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" | |
| 3723 | +dependencies = [ | |
| 3724 | + "fastrand", | |
| 3725 | + "getrandom 0.4.2", | |
| 3726 | + "once_cell", | |
| 3727 | + "rustix 1.1.4", | |
| 3728 | + "windows-sys 0.61.2", | |
| 3729 | +] | |
| 3730 | + | |
| 3731 | +[[package]] | |
| 3732 | +name = "termcolor" | |
| 3733 | +version = "1.4.1" | |
| 3734 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3735 | +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" | |
| 3736 | +dependencies = [ | |
| 3737 | + "winapi-util", | |
| 3738 | +] | |
| 3739 | + | |
| 3740 | +[[package]] | |
| 3741 | +name = "thiserror" | |
| 3742 | +version = "1.0.69" | |
| 3743 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3744 | +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" | |
| 3745 | +dependencies = [ | |
| 3746 | + "thiserror-impl 1.0.69", | |
| 3747 | +] | |
| 3748 | + | |
| 3749 | +[[package]] | |
| 3750 | +name = "thiserror" | |
| 3751 | +version = "2.0.18" | |
| 3752 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3753 | +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" | |
| 3754 | +dependencies = [ | |
| 3755 | + "thiserror-impl 2.0.18", | |
| 3756 | +] | |
| 3757 | + | |
| 3758 | +[[package]] | |
| 3759 | +name = "thiserror-impl" | |
| 3760 | +version = "1.0.69" | |
| 3761 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3762 | +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" | |
| 3763 | +dependencies = [ | |
| 3764 | + "proc-macro2", | |
| 3765 | + "quote", | |
| 3766 | + "syn", | |
| 3767 | +] | |
| 3768 | + | |
| 3769 | +[[package]] | |
| 3770 | +name = "thiserror-impl" | |
| 3771 | +version = "2.0.18" | |
| 3772 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3773 | +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" | |
| 3774 | +dependencies = [ | |
| 3775 | + "proc-macro2", | |
| 3776 | + "quote", | |
| 3777 | + "syn", | |
| 3778 | +] | |
| 3779 | + | |
| 3780 | +[[package]] | |
| 3781 | +name = "thread_local" | |
| 3782 | +version = "1.1.9" | |
| 3783 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3784 | +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" | |
| 3785 | +dependencies = [ | |
| 3786 | + "cfg-if", | |
| 3787 | +] | |
| 3788 | + | |
| 3789 | +[[package]] | |
| 3790 | +name = "tiff" | |
| 3791 | +version = "0.10.3" | |
| 3792 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3793 | +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" | |
| 3794 | +dependencies = [ | |
| 3795 | + "fax", | |
| 3796 | + "flate2", | |
| 3797 | + "half", | |
| 3798 | + "quick-error", | |
| 3799 | + "weezl", | |
| 3800 | + "zune-jpeg 0.4.21", | |
| 3801 | +] | |
| 3802 | + | |
| 3803 | +[[package]] | |
| 3804 | +name = "time" | |
| 3805 | +version = "0.3.47" | |
| 3806 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3807 | +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" | |
| 3808 | +dependencies = [ | |
| 3809 | + "deranged", | |
| 3810 | + "itoa", | |
| 3811 | + "num-conv", | |
| 3812 | + "powerfmt", | |
| 3813 | + "serde_core", | |
| 3814 | + "time-core", | |
| 3815 | + "time-macros", | |
| 3816 | +] | |
| 3817 | + | |
| 3818 | +[[package]] | |
| 3819 | +name = "time-core" | |
| 3820 | +version = "0.1.8" | |
| 3821 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3822 | +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" | |
| 3823 | + | |
| 3824 | +[[package]] | |
| 3825 | +name = "time-macros" | |
| 3826 | +version = "0.2.27" | |
| 3827 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3828 | +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" | |
| 3829 | +dependencies = [ | |
| 3830 | + "num-conv", | |
| 3831 | + "time-core", | |
| 3832 | +] | |
| 3833 | + | |
| 3834 | +[[package]] | |
| 3835 | +name = "tiny-skia" | |
| 3836 | +version = "0.11.4" | |
| 3837 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3838 | +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" | |
| 3839 | +dependencies = [ | |
| 3840 | + "arrayref", | |
| 3841 | + "arrayvec", | |
| 3842 | + "bytemuck", | |
| 3843 | + "cfg-if", | |
| 3844 | + "log", | |
| 3845 | + "png 0.17.16", | |
| 3846 | + "tiny-skia-path", | |
| 3847 | +] | |
| 3848 | + | |
| 3849 | +[[package]] | |
| 3850 | +name = "tiny-skia-path" | |
| 3851 | +version = "0.11.4" | |
| 3852 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3853 | +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" | |
| 3854 | +dependencies = [ | |
| 3855 | + "arrayref", | |
| 3856 | + "bytemuck", | |
| 3857 | + "strict-num", | |
| 3858 | +] | |
| 3859 | + | |
| 3860 | +[[package]] | |
| 3861 | +name = "tinystr" | |
| 3862 | +version = "0.8.2" | |
| 3863 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3864 | +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" | |
| 3865 | +dependencies = [ | |
| 3866 | + "displaydoc", | |
| 3867 | + "zerovec", | |
| 3868 | +] | |
| 3869 | + | |
| 3870 | +[[package]] | |
| 3871 | +name = "toml_datetime" | |
| 3872 | +version = "1.0.0+spec-1.1.0" | |
| 3873 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3874 | +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" | |
| 3875 | +dependencies = [ | |
| 3876 | + "serde_core", | |
| 3877 | +] | |
| 3878 | + | |
| 3879 | +[[package]] | |
| 3880 | +name = "toml_edit" | |
| 3881 | +version = "0.25.4+spec-1.1.0" | |
| 3882 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3883 | +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" | |
| 3884 | +dependencies = [ | |
| 3885 | + "indexmap", | |
| 3886 | + "toml_datetime", | |
| 3887 | + "toml_parser", | |
| 3888 | + "winnow", | |
| 3889 | +] | |
| 3890 | + | |
| 3891 | +[[package]] | |
| 3892 | +name = "toml_parser" | |
| 3893 | +version = "1.0.9+spec-1.1.0" | |
| 3894 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3895 | +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" | |
| 3896 | +dependencies = [ | |
| 3897 | + "winnow", | |
| 3898 | +] | |
| 3899 | + | |
| 3900 | +[[package]] | |
| 3901 | +name = "tracing" | |
| 3902 | +version = "0.1.44" | |
| 3903 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3904 | +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" | |
| 3905 | +dependencies = [ | |
| 3906 | + "log", | |
| 3907 | + "pin-project-lite", | |
| 3908 | + "tracing-attributes", | |
| 3909 | + "tracing-core", | |
| 3910 | +] | |
| 3911 | + | |
| 3912 | +[[package]] | |
| 3913 | +name = "tracing-attributes" | |
| 3914 | +version = "0.1.31" | |
| 3915 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3916 | +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" | |
| 3917 | +dependencies = [ | |
| 3918 | + "proc-macro2", | |
| 3919 | + "quote", | |
| 3920 | + "syn", | |
| 3921 | +] | |
| 3922 | + | |
| 3923 | +[[package]] | |
| 3924 | +name = "tracing-core" | |
| 3925 | +version = "0.1.36" | |
| 3926 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3927 | +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" | |
| 3928 | +dependencies = [ | |
| 3929 | + "once_cell", | |
| 3930 | + "valuable", | |
| 3931 | +] | |
| 3932 | + | |
| 3933 | +[[package]] | |
| 3934 | +name = "tracing-log" | |
| 3935 | +version = "0.2.0" | |
| 3936 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3937 | +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" | |
| 3938 | +dependencies = [ | |
| 3939 | + "log", | |
| 3940 | + "once_cell", | |
| 3941 | + "tracing-core", | |
| 3942 | +] | |
| 3943 | + | |
| 3944 | +[[package]] | |
| 3945 | +name = "tracing-subscriber" | |
| 3946 | +version = "0.3.22" | |
| 3947 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3948 | +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" | |
| 3949 | +dependencies = [ | |
| 3950 | + "nu-ansi-term", | |
| 3951 | + "sharded-slab", | |
| 3952 | + "smallvec", | |
| 3953 | + "thread_local", | |
| 3954 | + "tracing-core", | |
| 3955 | + "tracing-log", | |
| 3956 | +] | |
| 3957 | + | |
| 3958 | +[[package]] | |
| 3959 | +name = "ttf-parser" | |
| 3960 | +version = "0.25.1" | |
| 3961 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3962 | +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" | |
| 3963 | + | |
| 3964 | +[[package]] | |
| 3965 | +name = "type-map" | |
| 3966 | +version = "0.5.1" | |
| 3967 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3968 | +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" | |
| 3969 | +dependencies = [ | |
| 3970 | + "rustc-hash 2.1.1", | |
| 3971 | +] | |
| 3972 | + | |
| 3973 | +[[package]] | |
| 3974 | +name = "typenum" | |
| 3975 | +version = "1.19.0" | |
| 3976 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3977 | +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" | |
| 3978 | + | |
| 3979 | +[[package]] | |
| 3980 | +name = "uds_windows" | |
| 3981 | +version = "1.2.0" | |
| 3982 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3983 | +checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" | |
| 3984 | +dependencies = [ | |
| 3985 | + "memoffset", | |
| 3986 | + "tempfile", | |
| 3987 | + "windows-sys 0.61.2", | |
| 3988 | +] | |
| 3989 | + | |
| 3990 | +[[package]] | |
| 3991 | +name = "unicase" | |
| 3992 | +version = "2.9.0" | |
| 3993 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3994 | +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" | |
| 3995 | + | |
| 3996 | +[[package]] | |
| 3997 | +name = "unicode-ident" | |
| 3998 | +version = "1.0.24" | |
| 3999 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4000 | +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" | |
| 4001 | + | |
| 4002 | +[[package]] | |
| 4003 | +name = "unicode-segmentation" | |
| 4004 | +version = "1.12.0" | |
| 4005 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4006 | +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" | |
| 4007 | + | |
| 4008 | +[[package]] | |
| 4009 | +name = "unicode-width" | |
| 4010 | +version = "0.1.14" | |
| 4011 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4012 | +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" | |
| 4013 | + | |
| 4014 | +[[package]] | |
| 4015 | +name = "unicode-xid" | |
| 4016 | +version = "0.2.6" | |
| 4017 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4018 | +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" | |
| 4019 | + | |
| 4020 | +[[package]] | |
| 4021 | +name = "url" | |
| 4022 | +version = "2.5.8" | |
| 4023 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4024 | +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" | |
| 4025 | +dependencies = [ | |
| 4026 | + "form_urlencoded", | |
| 4027 | + "idna", | |
| 4028 | + "percent-encoding", | |
| 4029 | + "serde", | |
| 4030 | + "serde_derive", | |
| 4031 | +] | |
| 4032 | + | |
| 4033 | +[[package]] | |
| 4034 | +name = "urlencoding" | |
| 4035 | +version = "2.1.3" | |
| 4036 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4037 | +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" | |
| 4038 | + | |
| 4039 | +[[package]] | |
| 4040 | +name = "usvg" | |
| 4041 | +version = "0.37.0" | |
| 4042 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4043 | +checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" | |
| 4044 | +dependencies = [ | |
| 4045 | + "base64 0.21.7", | |
| 4046 | + "log", | |
| 4047 | + "pico-args", | |
| 4048 | + "usvg-parser", | |
| 4049 | + "usvg-tree", | |
| 4050 | + "xmlwriter", | |
| 4051 | +] | |
| 4052 | + | |
| 4053 | +[[package]] | |
| 4054 | +name = "usvg-parser" | |
| 4055 | +version = "0.37.0" | |
| 4056 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4057 | +checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" | |
| 4058 | +dependencies = [ | |
| 4059 | + "data-url", | |
| 4060 | + "flate2", | |
| 4061 | + "imagesize", | |
| 4062 | + "kurbo", | |
| 4063 | + "log", | |
| 4064 | + "roxmltree", | |
| 4065 | + "simplecss", | |
| 4066 | + "siphasher 0.3.11", | |
| 4067 | + "svgtypes", | |
| 4068 | + "usvg-tree", | |
| 4069 | +] | |
| 4070 | + | |
| 4071 | +[[package]] | |
| 4072 | +name = "usvg-tree" | |
| 4073 | +version = "0.37.0" | |
| 4074 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4075 | +checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" | |
| 4076 | +dependencies = [ | |
| 4077 | + "rctree", | |
| 4078 | + "strict-num", | |
| 4079 | + "svgtypes", | |
| 4080 | + "tiny-skia-path", | |
| 4081 | +] | |
| 4082 | + | |
| 4083 | +[[package]] | |
| 4084 | +name = "utf8_iter" | |
| 4085 | +version = "1.0.4" | |
| 4086 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4087 | +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" | |
| 4088 | + | |
| 4089 | +[[package]] | |
| 4090 | +name = "uuid" | |
| 4091 | +version = "1.22.0" | |
| 4092 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4093 | +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" | |
| 4094 | +dependencies = [ | |
| 4095 | + "getrandom 0.4.2", | |
| 4096 | + "js-sys", | |
| 4097 | + "serde_core", | |
| 4098 | + "wasm-bindgen", | |
| 4099 | +] | |
| 4100 | + | |
| 4101 | +[[package]] | |
| 4102 | +name = "valuable" | |
| 4103 | +version = "0.1.1" | |
| 4104 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4105 | +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" | |
| 4106 | + | |
| 4107 | +[[package]] | |
| 4108 | +name = "version_check" | |
| 4109 | +version = "0.9.5" | |
| 4110 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4111 | +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | |
| 4112 | + | |
| 4113 | +[[package]] | |
| 4114 | +name = "walkdir" | |
| 4115 | +version = "2.5.0" | |
| 4116 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4117 | +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" | |
| 4118 | +dependencies = [ | |
| 4119 | + "same-file", | |
| 4120 | + "winapi-util", | |
| 4121 | +] | |
| 4122 | + | |
| 4123 | +[[package]] | |
| 4124 | +name = "wasi" | |
| 4125 | +version = "0.11.1+wasi-snapshot-preview1" | |
| 4126 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4127 | +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" | |
| 4128 | + | |
| 4129 | +[[package]] | |
| 4130 | +name = "wasip2" | |
| 4131 | +version = "1.0.2+wasi-0.2.9" | |
| 4132 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4133 | +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" | |
| 4134 | +dependencies = [ | |
| 4135 | + "wit-bindgen", | |
| 4136 | +] | |
| 4137 | + | |
| 4138 | +[[package]] | |
| 4139 | +name = "wasip3" | |
| 4140 | +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" | |
| 4141 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4142 | +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" | |
| 4143 | +dependencies = [ | |
| 4144 | + "wit-bindgen", | |
| 4145 | +] | |
| 4146 | + | |
| 4147 | +[[package]] | |
| 4148 | +name = "wasm-bindgen" | |
| 4149 | +version = "0.2.114" | |
| 4150 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4151 | +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" | |
| 4152 | +dependencies = [ | |
| 4153 | + "cfg-if", | |
| 4154 | + "once_cell", | |
| 4155 | + "rustversion", | |
| 4156 | + "wasm-bindgen-macro", | |
| 4157 | + "wasm-bindgen-shared", | |
| 4158 | +] | |
| 4159 | + | |
| 4160 | +[[package]] | |
| 4161 | +name = "wasm-bindgen-futures" | |
| 4162 | +version = "0.4.64" | |
| 4163 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4164 | +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" | |
| 4165 | +dependencies = [ | |
| 4166 | + "cfg-if", | |
| 4167 | + "futures-util", | |
| 4168 | + "js-sys", | |
| 4169 | + "once_cell", | |
| 4170 | + "wasm-bindgen", | |
| 4171 | + "web-sys", | |
| 4172 | +] | |
| 4173 | + | |
| 4174 | +[[package]] | |
| 4175 | +name = "wasm-bindgen-macro" | |
| 4176 | +version = "0.2.114" | |
| 4177 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4178 | +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" | |
| 4179 | +dependencies = [ | |
| 4180 | + "quote", | |
| 4181 | + "wasm-bindgen-macro-support", | |
| 4182 | +] | |
| 4183 | + | |
| 4184 | +[[package]] | |
| 4185 | +name = "wasm-bindgen-macro-support" | |
| 4186 | +version = "0.2.114" | |
| 4187 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4188 | +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" | |
| 4189 | +dependencies = [ | |
| 4190 | + "bumpalo", | |
| 4191 | + "proc-macro2", | |
| 4192 | + "quote", | |
| 4193 | + "syn", | |
| 4194 | + "wasm-bindgen-shared", | |
| 4195 | +] | |
| 4196 | + | |
| 4197 | +[[package]] | |
| 4198 | +name = "wasm-bindgen-shared" | |
| 4199 | +version = "0.2.114" | |
| 4200 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4201 | +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" | |
| 4202 | +dependencies = [ | |
| 4203 | + "unicode-ident", | |
| 4204 | +] | |
| 4205 | + | |
| 4206 | +[[package]] | |
| 4207 | +name = "wasm-encoder" | |
| 4208 | +version = "0.244.0" | |
| 4209 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4210 | +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" | |
| 4211 | +dependencies = [ | |
| 4212 | + "leb128fmt", | |
| 4213 | + "wasmparser", | |
| 4214 | +] | |
| 4215 | + | |
| 4216 | +[[package]] | |
| 4217 | +name = "wasm-metadata" | |
| 4218 | +version = "0.244.0" | |
| 4219 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4220 | +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" | |
| 4221 | +dependencies = [ | |
| 4222 | + "anyhow", | |
| 4223 | + "indexmap", | |
| 4224 | + "wasm-encoder", | |
| 4225 | + "wasmparser", | |
| 4226 | +] | |
| 4227 | + | |
| 4228 | +[[package]] | |
| 4229 | +name = "wasmparser" | |
| 4230 | +version = "0.244.0" | |
| 4231 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4232 | +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" | |
| 4233 | +dependencies = [ | |
| 4234 | + "bitflags 2.11.0", | |
| 4235 | + "hashbrown 0.15.5", | |
| 4236 | + "indexmap", | |
| 4237 | + "semver", | |
| 4238 | +] | |
| 4239 | + | |
| 4240 | +[[package]] | |
| 4241 | +name = "wayland-backend" | |
| 4242 | +version = "0.3.14" | |
| 4243 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4244 | +checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" | |
| 4245 | +dependencies = [ | |
| 4246 | + "cc", | |
| 4247 | + "downcast-rs", | |
| 4248 | + "rustix 1.1.4", | |
| 4249 | + "scoped-tls", | |
| 4250 | + "smallvec", | |
| 4251 | + "wayland-sys", | |
| 4252 | +] | |
| 4253 | + | |
| 4254 | +[[package]] | |
| 4255 | +name = "wayland-client" | |
| 4256 | +version = "0.31.13" | |
| 4257 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4258 | +checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" | |
| 4259 | +dependencies = [ | |
| 4260 | + "bitflags 2.11.0", | |
| 4261 | + "rustix 1.1.4", | |
| 4262 | + "wayland-backend", | |
| 4263 | + "wayland-scanner", | |
| 4264 | +] | |
| 4265 | + | |
| 4266 | +[[package]] | |
| 4267 | +name = "wayland-csd-frame" | |
| 4268 | +version = "0.3.0" | |
| 4269 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4270 | +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" | |
| 4271 | +dependencies = [ | |
| 4272 | + "bitflags 2.11.0", | |
| 4273 | + "cursor-icon", | |
| 4274 | + "wayland-backend", | |
| 4275 | +] | |
| 4276 | + | |
| 4277 | +[[package]] | |
| 4278 | +name = "wayland-cursor" | |
| 4279 | +version = "0.31.13" | |
| 4280 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4281 | +checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091" | |
| 4282 | +dependencies = [ | |
| 4283 | + "rustix 1.1.4", | |
| 4284 | + "wayland-client", | |
| 4285 | + "xcursor", | |
| 4286 | +] | |
| 4287 | + | |
| 4288 | +[[package]] | |
| 4289 | +name = "wayland-protocols" | |
| 4290 | +version = "0.32.11" | |
| 4291 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4292 | +checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" | |
| 4293 | +dependencies = [ | |
| 4294 | + "bitflags 2.11.0", | |
| 4295 | + "wayland-backend", | |
| 4296 | + "wayland-client", | |
| 4297 | + "wayland-scanner", | |
| 4298 | +] | |
| 4299 | + | |
| 4300 | +[[package]] | |
| 4301 | +name = "wayland-protocols-experimental" | |
| 4302 | +version = "20250721.0.1" | |
| 4303 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4304 | +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" | |
| 4305 | +dependencies = [ | |
| 4306 | + "bitflags 2.11.0", | |
| 4307 | + "wayland-backend", | |
| 4308 | + "wayland-client", | |
| 4309 | + "wayland-protocols", | |
| 4310 | + "wayland-scanner", | |
| 4311 | +] | |
| 4312 | + | |
| 4313 | +[[package]] | |
| 4314 | +name = "wayland-protocols-misc" | |
| 4315 | +version = "0.3.11" | |
| 4316 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4317 | +checksum = "429b99200febaf95d4f4e46deff6fe4382bcff3280ee16a41cf887b3c3364984" | |
| 4318 | +dependencies = [ | |
| 4319 | + "bitflags 2.11.0", | |
| 4320 | + "wayland-backend", | |
| 4321 | + "wayland-client", | |
| 4322 | + "wayland-protocols", | |
| 4323 | + "wayland-scanner", | |
| 4324 | +] | |
| 4325 | + | |
| 4326 | +[[package]] | |
| 4327 | +name = "wayland-protocols-plasma" | |
| 4328 | +version = "0.3.11" | |
| 4329 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4330 | +checksum = "d392fc283a87774afc9beefcd6f931582bb97fe0e6ced0b306a62cb1d026527c" | |
| 4331 | +dependencies = [ | |
| 4332 | + "bitflags 2.11.0", | |
| 4333 | + "wayland-backend", | |
| 4334 | + "wayland-client", | |
| 4335 | + "wayland-protocols", | |
| 4336 | + "wayland-scanner", | |
| 4337 | +] | |
| 4338 | + | |
| 4339 | +[[package]] | |
| 4340 | +name = "wayland-protocols-wlr" | |
| 4341 | +version = "0.3.11" | |
| 4342 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4343 | +checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235" | |
| 4344 | +dependencies = [ | |
| 4345 | + "bitflags 2.11.0", | |
| 4346 | + "wayland-backend", | |
| 4347 | + "wayland-client", | |
| 4348 | + "wayland-protocols", | |
| 4349 | + "wayland-scanner", | |
| 4350 | +] | |
| 4351 | + | |
| 4352 | +[[package]] | |
| 4353 | +name = "wayland-scanner" | |
| 4354 | +version = "0.31.9" | |
| 4355 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4356 | +checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" | |
| 4357 | +dependencies = [ | |
| 4358 | + "proc-macro2", | |
| 4359 | + "quick-xml 0.39.2", | |
| 4360 | + "quote", | |
| 4361 | +] | |
| 4362 | + | |
| 4363 | +[[package]] | |
| 4364 | +name = "wayland-sys" | |
| 4365 | +version = "0.31.10" | |
| 4366 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4367 | +checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" | |
| 4368 | +dependencies = [ | |
| 4369 | + "dlib", | |
| 4370 | + "log", | |
| 4371 | + "once_cell", | |
| 4372 | + "pkg-config", | |
| 4373 | +] | |
| 4374 | + | |
| 4375 | +[[package]] | |
| 4376 | +name = "web-sys" | |
| 4377 | +version = "0.3.91" | |
| 4378 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4379 | +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" | |
| 4380 | +dependencies = [ | |
| 4381 | + "js-sys", | |
| 4382 | + "wasm-bindgen", | |
| 4383 | +] | |
| 4384 | + | |
| 4385 | +[[package]] | |
| 4386 | +name = "web-time" | |
| 4387 | +version = "1.1.0" | |
| 4388 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4389 | +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" | |
| 4390 | +dependencies = [ | |
| 4391 | + "js-sys", | |
| 4392 | + "wasm-bindgen", | |
| 4393 | +] | |
| 4394 | + | |
| 4395 | +[[package]] | |
| 4396 | +name = "webbrowser" | |
| 4397 | +version = "1.2.0" | |
| 4398 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4399 | +checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" | |
| 4400 | +dependencies = [ | |
| 4401 | + "core-foundation 0.10.1", | |
| 4402 | + "jni 0.22.3", | |
| 4403 | + "log", | |
| 4404 | + "ndk-context", | |
| 4405 | + "objc2 0.6.4", | |
| 4406 | + "objc2-foundation 0.3.2", | |
| 4407 | + "url", | |
| 4408 | + "web-sys", | |
| 4409 | +] | |
| 4410 | + | |
| 4411 | +[[package]] | |
| 4412 | +name = "weezl" | |
| 4413 | +version = "0.1.12" | |
| 4414 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4415 | +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" | |
| 4416 | + | |
| 4417 | +[[package]] | |
| 4418 | +name = "wgpu" | |
| 4419 | +version = "24.0.5" | |
| 4420 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4421 | +checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" | |
| 4422 | +dependencies = [ | |
| 4423 | + "arrayvec", | |
| 4424 | + "bitflags 2.11.0", | |
| 4425 | + "cfg_aliases", | |
| 4426 | + "document-features", | |
| 4427 | + "js-sys", | |
| 4428 | + "log", | |
| 4429 | + "parking_lot", | |
| 4430 | + "profiling", | |
| 4431 | + "raw-window-handle", | |
| 4432 | + "smallvec", | |
| 4433 | + "static_assertions", | |
| 4434 | + "wasm-bindgen", | |
| 4435 | + "wasm-bindgen-futures", | |
| 4436 | + "web-sys", | |
| 4437 | + "wgpu-core", | |
| 4438 | + "wgpu-hal", | |
| 4439 | + "wgpu-types", | |
| 4440 | +] | |
| 4441 | + | |
| 4442 | +[[package]] | |
| 4443 | +name = "wgpu-core" | |
| 4444 | +version = "24.0.5" | |
| 4445 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4446 | +checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" | |
| 4447 | +dependencies = [ | |
| 4448 | + "arrayvec", | |
| 4449 | + "bit-vec", | |
| 4450 | + "bitflags 2.11.0", | |
| 4451 | + "cfg_aliases", | |
| 4452 | + "document-features", | |
| 4453 | + "indexmap", | |
| 4454 | + "log", | |
| 4455 | + "naga", | |
| 4456 | + "once_cell", | |
| 4457 | + "parking_lot", | |
| 4458 | + "profiling", | |
| 4459 | + "raw-window-handle", | |
| 4460 | + "rustc-hash 1.1.0", | |
| 4461 | + "smallvec", | |
| 4462 | + "thiserror 2.0.18", | |
| 4463 | + "wgpu-hal", | |
| 4464 | + "wgpu-types", | |
| 4465 | +] | |
| 4466 | + | |
| 4467 | +[[package]] | |
| 4468 | +name = "wgpu-hal" | |
| 4469 | +version = "24.0.4" | |
| 4470 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4471 | +checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" | |
| 4472 | +dependencies = [ | |
| 4473 | + "android_system_properties", | |
| 4474 | + "arrayvec", | |
| 4475 | + "ash", | |
| 4476 | + "bitflags 2.11.0", | |
| 4477 | + "bytemuck", | |
| 4478 | + "cfg_aliases", | |
| 4479 | + "core-graphics-types", | |
| 4480 | + "glow", | |
| 4481 | + "glutin_wgl_sys", | |
| 4482 | + "gpu-alloc", | |
| 4483 | + "gpu-descriptor", | |
| 4484 | + "js-sys", | |
| 4485 | + "khronos-egl", | |
| 4486 | + "libc", | |
| 4487 | + "libloading", | |
| 4488 | + "log", | |
| 4489 | + "metal", | |
| 4490 | + "naga", | |
| 4491 | + "ndk-sys 0.5.0+25.2.9519653", | |
| 4492 | + "objc", | |
| 4493 | + "once_cell", | |
| 4494 | + "ordered-float", | |
| 4495 | + "parking_lot", | |
| 4496 | + "profiling", | |
| 4497 | + "raw-window-handle", | |
| 4498 | + "renderdoc-sys", | |
| 4499 | + "rustc-hash 1.1.0", | |
| 4500 | + "smallvec", | |
| 4501 | + "thiserror 2.0.18", | |
| 4502 | + "wasm-bindgen", | |
| 4503 | + "web-sys", | |
| 4504 | + "wgpu-types", | |
| 4505 | + "windows", | |
| 4506 | +] | |
| 4507 | + | |
| 4508 | +[[package]] | |
| 4509 | +name = "wgpu-types" | |
| 4510 | +version = "24.0.0" | |
| 4511 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4512 | +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" | |
| 4513 | +dependencies = [ | |
| 4514 | + "bitflags 2.11.0", | |
| 4515 | + "js-sys", | |
| 4516 | + "log", | |
| 4517 | + "web-sys", | |
| 4518 | +] | |
| 4519 | + | |
| 4520 | +[[package]] | |
| 4521 | +name = "winapi" | |
| 4522 | +version = "0.3.9" | |
| 4523 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4524 | +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" | |
| 4525 | +dependencies = [ | |
| 4526 | + "winapi-i686-pc-windows-gnu", | |
| 4527 | + "winapi-x86_64-pc-windows-gnu", | |
| 4528 | +] | |
| 4529 | + | |
| 4530 | +[[package]] | |
| 4531 | +name = "winapi-i686-pc-windows-gnu" | |
| 4532 | +version = "0.4.0" | |
| 4533 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4534 | +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" | |
| 4535 | + | |
| 4536 | +[[package]] | |
| 4537 | +name = "winapi-util" | |
| 4538 | +version = "0.1.11" | |
| 4539 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4540 | +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" | |
| 4541 | +dependencies = [ | |
| 4542 | + "windows-sys 0.61.2", | |
| 4543 | +] | |
| 4544 | + | |
| 4545 | +[[package]] | |
| 4546 | +name = "winapi-x86_64-pc-windows-gnu" | |
| 4547 | +version = "0.4.0" | |
| 4548 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4549 | +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" | |
| 4550 | + | |
| 4551 | +[[package]] | |
| 4552 | +name = "windows" | |
| 4553 | +version = "0.58.0" | |
| 4554 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4555 | +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" | |
| 4556 | +dependencies = [ | |
| 4557 | + "windows-core 0.58.0", | |
| 4558 | + "windows-targets 0.52.6", | |
| 4559 | +] | |
| 4560 | + | |
| 4561 | +[[package]] | |
| 4562 | +name = "windows-core" | |
| 4563 | +version = "0.58.0" | |
| 4564 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4565 | +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" | |
| 4566 | +dependencies = [ | |
| 4567 | + "windows-implement 0.58.0", | |
| 4568 | + "windows-interface 0.58.0", | |
| 4569 | + "windows-result 0.2.0", | |
| 4570 | + "windows-strings 0.1.0", | |
| 4571 | + "windows-targets 0.52.6", | |
| 4572 | +] | |
| 4573 | + | |
| 4574 | +[[package]] | |
| 4575 | +name = "windows-core" | |
| 4576 | +version = "0.62.2" | |
| 4577 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4578 | +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" | |
| 4579 | +dependencies = [ | |
| 4580 | + "windows-implement 0.60.2", | |
| 4581 | + "windows-interface 0.59.3", | |
| 4582 | + "windows-link", | |
| 4583 | + "windows-result 0.4.1", | |
| 4584 | + "windows-strings 0.5.1", | |
| 4585 | +] | |
| 4586 | + | |
| 4587 | +[[package]] | |
| 4588 | +name = "windows-implement" | |
| 4589 | +version = "0.58.0" | |
| 4590 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4591 | +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" | |
| 4592 | +dependencies = [ | |
| 4593 | + "proc-macro2", | |
| 4594 | + "quote", | |
| 4595 | + "syn", | |
| 4596 | +] | |
| 4597 | + | |
| 4598 | +[[package]] | |
| 4599 | +name = "windows-implement" | |
| 4600 | +version = "0.60.2" | |
| 4601 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4602 | +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" | |
| 4603 | +dependencies = [ | |
| 4604 | + "proc-macro2", | |
| 4605 | + "quote", | |
| 4606 | + "syn", | |
| 4607 | +] | |
| 4608 | + | |
| 4609 | +[[package]] | |
| 4610 | +name = "windows-interface" | |
| 4611 | +version = "0.58.0" | |
| 4612 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4613 | +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" | |
| 4614 | +dependencies = [ | |
| 4615 | + "proc-macro2", | |
| 4616 | + "quote", | |
| 4617 | + "syn", | |
| 4618 | +] | |
| 4619 | + | |
| 4620 | +[[package]] | |
| 4621 | +name = "windows-interface" | |
| 4622 | +version = "0.59.3" | |
| 4623 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4624 | +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" | |
| 4625 | +dependencies = [ | |
| 4626 | + "proc-macro2", | |
| 4627 | + "quote", | |
| 4628 | + "syn", | |
| 4629 | +] | |
| 4630 | + | |
| 4631 | +[[package]] | |
| 4632 | +name = "windows-link" | |
| 4633 | +version = "0.2.1" | |
| 4634 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4635 | +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | |
| 4636 | + | |
| 4637 | +[[package]] | |
| 4638 | +name = "windows-result" | |
| 4639 | +version = "0.2.0" | |
| 4640 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4641 | +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" | |
| 4642 | +dependencies = [ | |
| 4643 | + "windows-targets 0.52.6", | |
| 4644 | +] | |
| 4645 | + | |
| 4646 | +[[package]] | |
| 4647 | +name = "windows-result" | |
| 4648 | +version = "0.4.1" | |
| 4649 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4650 | +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" | |
| 4651 | +dependencies = [ | |
| 4652 | + "windows-link", | |
| 4653 | +] | |
| 4654 | + | |
| 4655 | +[[package]] | |
| 4656 | +name = "windows-strings" | |
| 4657 | +version = "0.1.0" | |
| 4658 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4659 | +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" | |
| 4660 | +dependencies = [ | |
| 4661 | + "windows-result 0.2.0", | |
| 4662 | + "windows-targets 0.52.6", | |
| 4663 | +] | |
| 4664 | + | |
| 4665 | +[[package]] | |
| 4666 | +name = "windows-strings" | |
| 4667 | +version = "0.5.1" | |
| 4668 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4669 | +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" | |
| 4670 | +dependencies = [ | |
| 4671 | + "windows-link", | |
| 4672 | +] | |
| 4673 | + | |
| 4674 | +[[package]] | |
| 4675 | +name = "windows-sys" | |
| 4676 | +version = "0.45.0" | |
| 4677 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4678 | +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" | |
| 4679 | +dependencies = [ | |
| 4680 | + "windows-targets 0.42.2", | |
| 4681 | +] | |
| 4682 | + | |
| 4683 | +[[package]] | |
| 4684 | +name = "windows-sys" | |
| 4685 | +version = "0.52.0" | |
| 4686 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4687 | +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" | |
| 4688 | +dependencies = [ | |
| 4689 | + "windows-targets 0.52.6", | |
| 4690 | +] | |
| 4691 | + | |
| 4692 | +[[package]] | |
| 4693 | +name = "windows-sys" | |
| 4694 | +version = "0.59.0" | |
| 4695 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4696 | +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" | |
| 4697 | +dependencies = [ | |
| 4698 | + "windows-targets 0.52.6", | |
| 4699 | +] | |
| 4700 | + | |
| 4701 | +[[package]] | |
| 4702 | +name = "windows-sys" | |
| 4703 | +version = "0.60.2" | |
| 4704 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4705 | +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" | |
| 4706 | +dependencies = [ | |
| 4707 | + "windows-targets 0.53.5", | |
| 4708 | +] | |
| 4709 | + | |
| 4710 | +[[package]] | |
| 4711 | +name = "windows-sys" | |
| 4712 | +version = "0.61.2" | |
| 4713 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4714 | +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" | |
| 4715 | +dependencies = [ | |
| 4716 | + "windows-link", | |
| 4717 | +] | |
| 4718 | + | |
| 4719 | +[[package]] | |
| 4720 | +name = "windows-targets" | |
| 4721 | +version = "0.42.2" | |
| 4722 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4723 | +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" | |
| 4724 | +dependencies = [ | |
| 4725 | + "windows_aarch64_gnullvm 0.42.2", | |
| 4726 | + "windows_aarch64_msvc 0.42.2", | |
| 4727 | + "windows_i686_gnu 0.42.2", | |
| 4728 | + "windows_i686_msvc 0.42.2", | |
| 4729 | + "windows_x86_64_gnu 0.42.2", | |
| 4730 | + "windows_x86_64_gnullvm 0.42.2", | |
| 4731 | + "windows_x86_64_msvc 0.42.2", | |
| 4732 | +] | |
| 4733 | + | |
| 4734 | +[[package]] | |
| 4735 | +name = "windows-targets" | |
| 4736 | +version = "0.52.6" | |
| 4737 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4738 | +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" | |
| 4739 | +dependencies = [ | |
| 4740 | + "windows_aarch64_gnullvm 0.52.6", | |
| 4741 | + "windows_aarch64_msvc 0.52.6", | |
| 4742 | + "windows_i686_gnu 0.52.6", | |
| 4743 | + "windows_i686_gnullvm 0.52.6", | |
| 4744 | + "windows_i686_msvc 0.52.6", | |
| 4745 | + "windows_x86_64_gnu 0.52.6", | |
| 4746 | + "windows_x86_64_gnullvm 0.52.6", | |
| 4747 | + "windows_x86_64_msvc 0.52.6", | |
| 4748 | +] | |
| 4749 | + | |
| 4750 | +[[package]] | |
| 4751 | +name = "windows-targets" | |
| 4752 | +version = "0.53.5" | |
| 4753 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4754 | +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" | |
| 4755 | +dependencies = [ | |
| 4756 | + "windows-link", | |
| 4757 | + "windows_aarch64_gnullvm 0.53.1", | |
| 4758 | + "windows_aarch64_msvc 0.53.1", | |
| 4759 | + "windows_i686_gnu 0.53.1", | |
| 4760 | + "windows_i686_gnullvm 0.53.1", | |
| 4761 | + "windows_i686_msvc 0.53.1", | |
| 4762 | + "windows_x86_64_gnu 0.53.1", | |
| 4763 | + "windows_x86_64_gnullvm 0.53.1", | |
| 4764 | + "windows_x86_64_msvc 0.53.1", | |
| 4765 | +] | |
| 4766 | + | |
| 4767 | +[[package]] | |
| 4768 | +name = "windows_aarch64_gnullvm" | |
| 4769 | +version = "0.42.2" | |
| 4770 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4771 | +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" | |
| 4772 | + | |
| 4773 | +[[package]] | |
| 4774 | +name = "windows_aarch64_gnullvm" | |
| 4775 | +version = "0.52.6" | |
| 4776 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4777 | +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" | |
| 4778 | + | |
| 4779 | +[[package]] | |
| 4780 | +name = "windows_aarch64_gnullvm" | |
| 4781 | +version = "0.53.1" | |
| 4782 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4783 | +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" | |
| 4784 | + | |
| 4785 | +[[package]] | |
| 4786 | +name = "windows_aarch64_msvc" | |
| 4787 | +version = "0.42.2" | |
| 4788 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4789 | +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" | |
| 4790 | + | |
| 4791 | +[[package]] | |
| 4792 | +name = "windows_aarch64_msvc" | |
| 4793 | +version = "0.52.6" | |
| 4794 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4795 | +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" | |
| 4796 | + | |
| 4797 | +[[package]] | |
| 4798 | +name = "windows_aarch64_msvc" | |
| 4799 | +version = "0.53.1" | |
| 4800 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4801 | +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" | |
| 4802 | + | |
| 4803 | +[[package]] | |
| 4804 | +name = "windows_i686_gnu" | |
| 4805 | +version = "0.42.2" | |
| 4806 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4807 | +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" | |
| 4808 | + | |
| 4809 | +[[package]] | |
| 4810 | +name = "windows_i686_gnu" | |
| 4811 | +version = "0.52.6" | |
| 4812 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4813 | +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" | |
| 4814 | + | |
| 4815 | +[[package]] | |
| 4816 | +name = "windows_i686_gnu" | |
| 4817 | +version = "0.53.1" | |
| 4818 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4819 | +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" | |
| 4820 | + | |
| 4821 | +[[package]] | |
| 4822 | +name = "windows_i686_gnullvm" | |
| 4823 | +version = "0.52.6" | |
| 4824 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4825 | +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" | |
| 4826 | + | |
| 4827 | +[[package]] | |
| 4828 | +name = "windows_i686_gnullvm" | |
| 4829 | +version = "0.53.1" | |
| 4830 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4831 | +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" | |
| 4832 | + | |
| 4833 | +[[package]] | |
| 4834 | +name = "windows_i686_msvc" | |
| 4835 | +version = "0.42.2" | |
| 4836 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4837 | +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" | |
| 4838 | + | |
| 4839 | +[[package]] | |
| 4840 | +name = "windows_i686_msvc" | |
| 4841 | +version = "0.52.6" | |
| 4842 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4843 | +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" | |
| 4844 | + | |
| 4845 | +[[package]] | |
| 4846 | +name = "windows_i686_msvc" | |
| 4847 | +version = "0.53.1" | |
| 4848 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4849 | +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" | |
| 4850 | + | |
| 4851 | +[[package]] | |
| 4852 | +name = "windows_x86_64_gnu" | |
| 4853 | +version = "0.42.2" | |
| 4854 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4855 | +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" | |
| 4856 | + | |
| 4857 | +[[package]] | |
| 4858 | +name = "windows_x86_64_gnu" | |
| 4859 | +version = "0.52.6" | |
| 4860 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4861 | +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" | |
| 4862 | + | |
| 4863 | +[[package]] | |
| 4864 | +name = "windows_x86_64_gnu" | |
| 4865 | +version = "0.53.1" | |
| 4866 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4867 | +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" | |
| 4868 | + | |
| 4869 | +[[package]] | |
| 4870 | +name = "windows_x86_64_gnullvm" | |
| 4871 | +version = "0.42.2" | |
| 4872 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4873 | +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" | |
| 4874 | + | |
| 4875 | +[[package]] | |
| 4876 | +name = "windows_x86_64_gnullvm" | |
| 4877 | +version = "0.52.6" | |
| 4878 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4879 | +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" | |
| 4880 | + | |
| 4881 | +[[package]] | |
| 4882 | +name = "windows_x86_64_gnullvm" | |
| 4883 | +version = "0.53.1" | |
| 4884 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4885 | +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" | |
| 4886 | + | |
| 4887 | +[[package]] | |
| 4888 | +name = "windows_x86_64_msvc" | |
| 4889 | +version = "0.42.2" | |
| 4890 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4891 | +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" | |
| 4892 | + | |
| 4893 | +[[package]] | |
| 4894 | +name = "windows_x86_64_msvc" | |
| 4895 | +version = "0.52.6" | |
| 4896 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4897 | +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | |
| 4898 | + | |
| 4899 | +[[package]] | |
| 4900 | +name = "windows_x86_64_msvc" | |
| 4901 | +version = "0.53.1" | |
| 4902 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4903 | +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" | |
| 4904 | + | |
| 4905 | +[[package]] | |
| 4906 | +name = "winit" | |
| 4907 | +version = "0.30.13" | |
| 4908 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4909 | +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" | |
| 4910 | +dependencies = [ | |
| 4911 | + "ahash", | |
| 4912 | + "android-activity", | |
| 4913 | + "atomic-waker", | |
| 4914 | + "bitflags 2.11.0", | |
| 4915 | + "block2 0.5.1", | |
| 4916 | + "bytemuck", | |
| 4917 | + "calloop 0.13.0", | |
| 4918 | + "cfg_aliases", | |
| 4919 | + "concurrent-queue", | |
| 4920 | + "core-foundation 0.9.4", | |
| 4921 | + "core-graphics", | |
| 4922 | + "cursor-icon", | |
| 4923 | + "dpi", | |
| 4924 | + "js-sys", | |
| 4925 | + "libc", | |
| 4926 | + "memmap2", | |
| 4927 | + "ndk", | |
| 4928 | + "objc2 0.5.2", | |
| 4929 | + "objc2-app-kit 0.2.2", | |
| 4930 | + "objc2-foundation 0.2.2", | |
| 4931 | + "objc2-ui-kit", | |
| 4932 | + "orbclient", | |
| 4933 | + "percent-encoding", | |
| 4934 | + "pin-project", | |
| 4935 | + "raw-window-handle", | |
| 4936 | + "redox_syscall 0.4.1", | |
| 4937 | + "rustix 0.38.44", | |
| 4938 | + "sctk-adwaita", | |
| 4939 | + "smithay-client-toolkit 0.19.2", | |
| 4940 | + "smol_str", | |
| 4941 | + "tracing", | |
| 4942 | + "unicode-segmentation", | |
| 4943 | + "wasm-bindgen", | |
| 4944 | + "wasm-bindgen-futures", | |
| 4945 | + "wayland-backend", | |
| 4946 | + "wayland-client", | |
| 4947 | + "wayland-protocols", | |
| 4948 | + "wayland-protocols-plasma", | |
| 4949 | + "web-sys", | |
| 4950 | + "web-time", | |
| 4951 | + "windows-sys 0.52.0", | |
| 4952 | + "x11-dl", | |
| 4953 | + "x11rb", | |
| 4954 | + "xkbcommon-dl", | |
| 4955 | +] | |
| 4956 | + | |
| 4957 | +[[package]] | |
| 4958 | +name = "winnow" | |
| 4959 | +version = "0.7.15" | |
| 4960 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4961 | +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" | |
| 4962 | +dependencies = [ | |
| 4963 | + "memchr", | |
| 4964 | +] | |
| 4965 | + | |
| 4966 | +[[package]] | |
| 4967 | +name = "wit-bindgen" | |
| 4968 | +version = "0.51.0" | |
| 4969 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4970 | +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" | |
| 4971 | +dependencies = [ | |
| 4972 | + "wit-bindgen-rust-macro", | |
| 4973 | +] | |
| 4974 | + | |
| 4975 | +[[package]] | |
| 4976 | +name = "wit-bindgen-core" | |
| 4977 | +version = "0.51.0" | |
| 4978 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4979 | +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" | |
| 4980 | +dependencies = [ | |
| 4981 | + "anyhow", | |
| 4982 | + "heck", | |
| 4983 | + "wit-parser", | |
| 4984 | +] | |
| 4985 | + | |
| 4986 | +[[package]] | |
| 4987 | +name = "wit-bindgen-rust" | |
| 4988 | +version = "0.51.0" | |
| 4989 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4990 | +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" | |
| 4991 | +dependencies = [ | |
| 4992 | + "anyhow", | |
| 4993 | + "heck", | |
| 4994 | + "indexmap", | |
| 4995 | + "prettyplease", | |
| 4996 | + "syn", | |
| 4997 | + "wasm-metadata", | |
| 4998 | + "wit-bindgen-core", | |
| 4999 | + "wit-component", | |
| 5000 | +] | |
| 5001 | + | |
| 5002 | +[[package]] | |
| 5003 | +name = "wit-bindgen-rust-macro" | |
| 5004 | +version = "0.51.0" | |
| 5005 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5006 | +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" | |
| 5007 | +dependencies = [ | |
| 5008 | + "anyhow", | |
| 5009 | + "prettyplease", | |
| 5010 | + "proc-macro2", | |
| 5011 | + "quote", | |
| 5012 | + "syn", | |
| 5013 | + "wit-bindgen-core", | |
| 5014 | + "wit-bindgen-rust", | |
| 5015 | +] | |
| 5016 | + | |
| 5017 | +[[package]] | |
| 5018 | +name = "wit-component" | |
| 5019 | +version = "0.244.0" | |
| 5020 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5021 | +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" | |
| 5022 | +dependencies = [ | |
| 5023 | + "anyhow", | |
| 5024 | + "bitflags 2.11.0", | |
| 5025 | + "indexmap", | |
| 5026 | + "log", | |
| 5027 | + "serde", | |
| 5028 | + "serde_derive", | |
| 5029 | + "serde_json", | |
| 5030 | + "wasm-encoder", | |
| 5031 | + "wasm-metadata", | |
| 5032 | + "wasmparser", | |
| 5033 | + "wit-parser", | |
| 5034 | +] | |
| 5035 | + | |
| 5036 | +[[package]] | |
| 5037 | +name = "wit-parser" | |
| 5038 | +version = "0.244.0" | |
| 5039 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5040 | +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" | |
| 5041 | +dependencies = [ | |
| 5042 | + "anyhow", | |
| 5043 | + "id-arena", | |
| 5044 | + "indexmap", | |
| 5045 | + "log", | |
| 5046 | + "semver", | |
| 5047 | + "serde", | |
| 5048 | + "serde_derive", | |
| 5049 | + "serde_json", | |
| 5050 | + "unicode-xid", | |
| 5051 | + "wasmparser", | |
| 5052 | +] | |
| 5053 | + | |
| 5054 | +[[package]] | |
| 5055 | +name = "writeable" | |
| 5056 | +version = "0.6.2" | |
| 5057 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5058 | +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" | |
| 5059 | + | |
| 5060 | +[[package]] | |
| 5061 | +name = "x11-dl" | |
| 5062 | +version = "2.21.0" | |
| 5063 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5064 | +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" | |
| 5065 | +dependencies = [ | |
| 5066 | + "libc", | |
| 5067 | + "once_cell", | |
| 5068 | + "pkg-config", | |
| 5069 | +] | |
| 5070 | + | |
| 5071 | +[[package]] | |
| 5072 | +name = "x11rb" | |
| 5073 | +version = "0.13.2" | |
| 5074 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5075 | +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" | |
| 5076 | +dependencies = [ | |
| 5077 | + "as-raw-xcb-connection", | |
| 5078 | + "gethostname", | |
| 5079 | + "libc", | |
| 5080 | + "libloading", | |
| 5081 | + "once_cell", | |
| 5082 | + "rustix 1.1.4", | |
| 5083 | + "x11rb-protocol", | |
| 5084 | +] | |
| 5085 | + | |
| 5086 | +[[package]] | |
| 5087 | +name = "x11rb-protocol" | |
| 5088 | +version = "0.13.2" | |
| 5089 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5090 | +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" | |
| 5091 | + | |
| 5092 | +[[package]] | |
| 5093 | +name = "xcursor" | |
| 5094 | +version = "0.3.10" | |
| 5095 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5096 | +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" | |
| 5097 | + | |
| 5098 | +[[package]] | |
| 5099 | +name = "xdg-home" | |
| 5100 | +version = "1.3.0" | |
| 5101 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5102 | +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" | |
| 5103 | +dependencies = [ | |
| 5104 | + "libc", | |
| 5105 | + "windows-sys 0.59.0", | |
| 5106 | +] | |
| 5107 | + | |
| 5108 | +[[package]] | |
| 5109 | +name = "xkbcommon-dl" | |
| 5110 | +version = "0.4.2" | |
| 5111 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5112 | +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" | |
| 5113 | +dependencies = [ | |
| 5114 | + "bitflags 2.11.0", | |
| 5115 | + "dlib", | |
| 5116 | + "log", | |
| 5117 | + "once_cell", | |
| 5118 | + "xkeysym", | |
| 5119 | +] | |
| 5120 | + | |
| 5121 | +[[package]] | |
| 5122 | +name = "xkeysym" | |
| 5123 | +version = "0.2.1" | |
| 5124 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5125 | +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" | |
| 5126 | + | |
| 5127 | +[[package]] | |
| 5128 | +name = "xml-rs" | |
| 5129 | +version = "0.8.28" | |
| 5130 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5131 | +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" | |
| 5132 | + | |
| 5133 | +[[package]] | |
| 5134 | +name = "xmlwriter" | |
| 5135 | +version = "0.1.0" | |
| 5136 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5137 | +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" | |
| 5138 | + | |
| 5139 | +[[package]] | |
| 5140 | +name = "yaml-rust" | |
| 5141 | +version = "0.4.5" | |
| 5142 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5143 | +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" | |
| 5144 | +dependencies = [ | |
| 5145 | + "linked-hash-map", | |
| 5146 | +] | |
| 5147 | + | |
| 5148 | +[[package]] | |
| 5149 | +name = "yoke" | |
| 5150 | +version = "0.8.1" | |
| 5151 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5152 | +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" | |
| 5153 | +dependencies = [ | |
| 5154 | + "stable_deref_trait", | |
| 5155 | + "yoke-derive", | |
| 5156 | + "zerofrom", | |
| 5157 | +] | |
| 5158 | + | |
| 5159 | +[[package]] | |
| 5160 | +name = "yoke-derive" | |
| 5161 | +version = "0.8.1" | |
| 5162 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5163 | +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" | |
| 5164 | +dependencies = [ | |
| 5165 | + "proc-macro2", | |
| 5166 | + "quote", | |
| 5167 | + "syn", | |
| 5168 | + "synstructure", | |
| 5169 | +] | |
| 5170 | + | |
| 5171 | +[[package]] | |
| 5172 | +name = "zbus" | |
| 5173 | +version = "4.4.0" | |
| 5174 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5175 | +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" | |
| 5176 | +dependencies = [ | |
| 5177 | + "async-broadcast", | |
| 5178 | + "async-executor", | |
| 5179 | + "async-fs", | |
| 5180 | + "async-io", | |
| 5181 | + "async-lock", | |
| 5182 | + "async-process", | |
| 5183 | + "async-recursion", | |
| 5184 | + "async-task", | |
| 5185 | + "async-trait", | |
| 5186 | + "blocking", | |
| 5187 | + "enumflags2", | |
| 5188 | + "event-listener", | |
| 5189 | + "futures-core", | |
| 5190 | + "futures-sink", | |
| 5191 | + "futures-util", | |
| 5192 | + "hex", | |
| 5193 | + "nix", | |
| 5194 | + "ordered-stream", | |
| 5195 | + "rand 0.8.5", | |
| 5196 | + "serde", | |
| 5197 | + "serde_repr", | |
| 5198 | + "sha1", | |
| 5199 | + "static_assertions", | |
| 5200 | + "tracing", | |
| 5201 | + "uds_windows", | |
| 5202 | + "windows-sys 0.52.0", | |
| 5203 | + "xdg-home", | |
| 5204 | + "zbus_macros 4.4.0", | |
| 5205 | + "zbus_names 3.0.0", | |
| 5206 | + "zvariant 4.2.0", | |
| 5207 | +] | |
| 5208 | + | |
| 5209 | +[[package]] | |
| 5210 | +name = "zbus" | |
| 5211 | +version = "5.14.0" | |
| 5212 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5213 | +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" | |
| 5214 | +dependencies = [ | |
| 5215 | + "async-broadcast", | |
| 5216 | + "async-executor", | |
| 5217 | + "async-io", | |
| 5218 | + "async-lock", | |
| 5219 | + "async-process", | |
| 5220 | + "async-recursion", | |
| 5221 | + "async-task", | |
| 5222 | + "async-trait", | |
| 5223 | + "blocking", | |
| 5224 | + "enumflags2", | |
| 5225 | + "event-listener", | |
| 5226 | + "futures-core", | |
| 5227 | + "futures-lite", | |
| 5228 | + "hex", | |
| 5229 | + "libc", | |
| 5230 | + "ordered-stream", | |
| 5231 | + "rustix 1.1.4", | |
| 5232 | + "serde", | |
| 5233 | + "serde_repr", | |
| 5234 | + "tracing", | |
| 5235 | + "uds_windows", | |
| 5236 | + "uuid", | |
| 5237 | + "windows-sys 0.61.2", | |
| 5238 | + "winnow", | |
| 5239 | + "zbus_macros 5.14.0", | |
| 5240 | + "zbus_names 4.3.1", | |
| 5241 | + "zvariant 5.10.0", | |
| 5242 | +] | |
| 5243 | + | |
| 5244 | +[[package]] | |
| 5245 | +name = "zbus-lockstep" | |
| 5246 | +version = "0.4.4" | |
| 5247 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5248 | +checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" | |
| 5249 | +dependencies = [ | |
| 5250 | + "zbus_xml", | |
| 5251 | + "zvariant 4.2.0", | |
| 5252 | +] | |
| 5253 | + | |
| 5254 | +[[package]] | |
| 5255 | +name = "zbus-lockstep-macros" | |
| 5256 | +version = "0.4.4" | |
| 5257 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5258 | +checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" | |
| 5259 | +dependencies = [ | |
| 5260 | + "proc-macro2", | |
| 5261 | + "quote", | |
| 5262 | + "syn", | |
| 5263 | + "zbus-lockstep", | |
| 5264 | + "zbus_xml", | |
| 5265 | + "zvariant 4.2.0", | |
| 5266 | +] | |
| 5267 | + | |
| 5268 | +[[package]] | |
| 5269 | +name = "zbus_macros" | |
| 5270 | +version = "4.4.0" | |
| 5271 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5272 | +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" | |
| 5273 | +dependencies = [ | |
| 5274 | + "proc-macro-crate", | |
| 5275 | + "proc-macro2", | |
| 5276 | + "quote", | |
| 5277 | + "syn", | |
| 5278 | + "zvariant_utils 2.1.0", | |
| 5279 | +] | |
| 5280 | + | |
| 5281 | +[[package]] | |
| 5282 | +name = "zbus_macros" | |
| 5283 | +version = "5.14.0" | |
| 5284 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5285 | +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" | |
| 5286 | +dependencies = [ | |
| 5287 | + "proc-macro-crate", | |
| 5288 | + "proc-macro2", | |
| 5289 | + "quote", | |
| 5290 | + "syn", | |
| 5291 | + "zbus_names 4.3.1", | |
| 5292 | + "zvariant 5.10.0", | |
| 5293 | + "zvariant_utils 3.3.0", | |
| 5294 | +] | |
| 5295 | + | |
| 5296 | +[[package]] | |
| 5297 | +name = "zbus_names" | |
| 5298 | +version = "3.0.0" | |
| 5299 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5300 | +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" | |
| 5301 | +dependencies = [ | |
| 5302 | + "serde", | |
| 5303 | + "static_assertions", | |
| 5304 | + "zvariant 4.2.0", | |
| 5305 | +] | |
| 5306 | + | |
| 5307 | +[[package]] | |
| 5308 | +name = "zbus_names" | |
| 5309 | +version = "4.3.1" | |
| 5310 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5311 | +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" | |
| 5312 | +dependencies = [ | |
| 5313 | + "serde", | |
| 5314 | + "winnow", | |
| 5315 | + "zvariant 5.10.0", | |
| 5316 | +] | |
| 5317 | + | |
| 5318 | +[[package]] | |
| 5319 | +name = "zbus_xml" | |
| 5320 | +version = "4.0.0" | |
| 5321 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5322 | +checksum = "ab3f374552b954f6abb4bd6ce979e6c9b38fb9d0cd7cc68a7d796e70c9f3a233" | |
| 5323 | +dependencies = [ | |
| 5324 | + "quick-xml 0.30.0", | |
| 5325 | + "serde", | |
| 5326 | + "static_assertions", | |
| 5327 | + "zbus_names 3.0.0", | |
| 5328 | + "zvariant 4.2.0", | |
| 5329 | +] | |
| 5330 | + | |
| 5331 | +[[package]] | |
| 5332 | +name = "zerocopy" | |
| 5333 | +version = "0.8.42" | |
| 5334 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5335 | +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" | |
| 5336 | +dependencies = [ | |
| 5337 | + "zerocopy-derive", | |
| 5338 | +] | |
| 5339 | + | |
| 5340 | +[[package]] | |
| 5341 | +name = "zerocopy-derive" | |
| 5342 | +version = "0.8.42" | |
| 5343 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5344 | +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" | |
| 5345 | +dependencies = [ | |
| 5346 | + "proc-macro2", | |
| 5347 | + "quote", | |
| 5348 | + "syn", | |
| 5349 | +] | |
| 5350 | + | |
| 5351 | +[[package]] | |
| 5352 | +name = "zerofrom" | |
| 5353 | +version = "0.1.6" | |
| 5354 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5355 | +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" | |
| 5356 | +dependencies = [ | |
| 5357 | + "zerofrom-derive", | |
| 5358 | +] | |
| 5359 | + | |
| 5360 | +[[package]] | |
| 5361 | +name = "zerofrom-derive" | |
| 5362 | +version = "0.1.6" | |
| 5363 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5364 | +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" | |
| 5365 | +dependencies = [ | |
| 5366 | + "proc-macro2", | |
| 5367 | + "quote", | |
| 5368 | + "syn", | |
| 5369 | + "synstructure", | |
| 5370 | +] | |
| 5371 | + | |
| 5372 | +[[package]] | |
| 5373 | +name = "zerotrie" | |
| 5374 | +version = "0.2.3" | |
| 5375 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5376 | +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" | |
| 5377 | +dependencies = [ | |
| 5378 | + "displaydoc", | |
| 5379 | + "yoke", | |
| 5380 | + "zerofrom", | |
| 5381 | +] | |
| 5382 | + | |
| 5383 | +[[package]] | |
| 5384 | +name = "zerovec" | |
| 5385 | +version = "0.11.5" | |
| 5386 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5387 | +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" | |
| 5388 | +dependencies = [ | |
| 5389 | + "yoke", | |
| 5390 | + "zerofrom", | |
| 5391 | + "zerovec-derive", | |
| 5392 | +] | |
| 5393 | + | |
| 5394 | +[[package]] | |
| 5395 | +name = "zerovec-derive" | |
| 5396 | +version = "0.11.2" | |
| 5397 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5398 | +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" | |
| 5399 | +dependencies = [ | |
| 5400 | + "proc-macro2", | |
| 5401 | + "quote", | |
| 5402 | + "syn", | |
| 5403 | +] | |
| 5404 | + | |
| 5405 | +[[package]] | |
| 5406 | +name = "zmij" | |
| 5407 | +version = "1.0.21" | |
| 5408 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5409 | +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" | |
| 5410 | + | |
| 5411 | +[[package]] | |
| 5412 | +name = "zune-core" | |
| 5413 | +version = "0.4.12" | |
| 5414 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5415 | +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" | |
| 5416 | + | |
| 5417 | +[[package]] | |
| 5418 | +name = "zune-core" | |
| 5419 | +version = "0.5.1" | |
| 5420 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5421 | +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" | |
| 5422 | + | |
| 5423 | +[[package]] | |
| 5424 | +name = "zune-jpeg" | |
| 5425 | +version = "0.4.21" | |
| 5426 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5427 | +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" | |
| 5428 | +dependencies = [ | |
| 5429 | + "zune-core 0.4.12", | |
| 5430 | +] | |
| 5431 | + | |
| 5432 | +[[package]] | |
| 5433 | +name = "zune-jpeg" | |
| 5434 | +version = "0.5.12" | |
| 5435 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5436 | +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" | |
| 5437 | +dependencies = [ | |
| 5438 | + "zune-core 0.5.1", | |
| 5439 | +] | |
| 5440 | + | |
| 5441 | +[[package]] | |
| 5442 | +name = "zvariant" | |
| 5443 | +version = "4.2.0" | |
| 5444 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5445 | +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" | |
| 5446 | +dependencies = [ | |
| 5447 | + "endi", | |
| 5448 | + "enumflags2", | |
| 5449 | + "serde", | |
| 5450 | + "static_assertions", | |
| 5451 | + "zvariant_derive 4.2.0", | |
| 5452 | +] | |
| 5453 | + | |
| 5454 | +[[package]] | |
| 5455 | +name = "zvariant" | |
| 5456 | +version = "5.10.0" | |
| 5457 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5458 | +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" | |
| 5459 | +dependencies = [ | |
| 5460 | + "endi", | |
| 5461 | + "enumflags2", | |
| 5462 | + "serde", | |
| 5463 | + "url", | |
| 5464 | + "winnow", | |
| 5465 | + "zvariant_derive 5.10.0", | |
| 5466 | + "zvariant_utils 3.3.0", | |
| 5467 | +] | |
| 5468 | + | |
| 5469 | +[[package]] | |
| 5470 | +name = "zvariant_derive" | |
| 5471 | +version = "4.2.0" | |
| 5472 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5473 | +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" | |
| 5474 | +dependencies = [ | |
| 5475 | + "proc-macro-crate", | |
| 5476 | + "proc-macro2", | |
| 5477 | + "quote", | |
| 5478 | + "syn", | |
| 5479 | + "zvariant_utils 2.1.0", | |
| 5480 | +] | |
| 5481 | + | |
| 5482 | +[[package]] | |
| 5483 | +name = "zvariant_derive" | |
| 5484 | +version = "5.10.0" | |
| 5485 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5486 | +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" | |
| 5487 | +dependencies = [ | |
| 5488 | + "proc-macro-crate", | |
| 5489 | + "proc-macro2", | |
| 5490 | + "quote", | |
| 5491 | + "syn", | |
| 5492 | + "zvariant_utils 3.3.0", | |
| 5493 | +] | |
| 5494 | + | |
| 5495 | +[[package]] | |
| 5496 | +name = "zvariant_utils" | |
| 5497 | +version = "2.1.0" | |
| 5498 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5499 | +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" | |
| 5500 | +dependencies = [ | |
| 5501 | + "proc-macro2", | |
| 5502 | + "quote", | |
| 5503 | + "syn", | |
| 5504 | +] | |
| 5505 | + | |
| 5506 | +[[package]] | |
| 5507 | +name = "zvariant_utils" | |
| 5508 | +version = "3.3.0" | |
| 5509 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5510 | +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" | |
| 5511 | +dependencies = [ | |
| 5512 | + "proc-macro2", | |
| 5513 | + "quote", | |
| 5514 | + "serde", | |
| 5515 | + "syn", | |
| 5516 | + "winnow", | |
| 5517 | +] |
+27-0Cargo.toml
| @@ -0,0 +1,27 @@ | ||
| 1 | +[package] | |
| 2 | +name = "jv" | |
| 3 | +version = "0.1.0" | |
| 4 | +edition = "2021" | |
| 5 | +description = "Hyper-focused JSON viewer for Rust developers" | |
| 6 | + | |
| 7 | +[dependencies] | |
| 8 | +eframe = { version = "0.31", features = ["persistence"] } | |
| 9 | +egui = "0.31" | |
| 10 | +egui_extras = { version = "0.31", features = ["syntect", "image", "svg"] } | |
| 11 | +image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] } | |
| 12 | +base64 = "0.22" | |
| 13 | +serde = { version = "1", features = ["derive"] } | |
| 14 | +serde_json = "1" | |
| 15 | +jaq-interpret = "1" | |
| 16 | +jaq-parse = "1" | |
| 17 | +jaq-core = "1" | |
| 18 | +jaq-std = "1" | |
| 19 | +chrono = { version = "0.4", features = ["serde"] } | |
| 20 | +chrono-tz = "0.10" | |
| 21 | +rfd = "0.15" | |
| 22 | +directories = "6" | |
| 23 | +uuid = { version = "1", features = ["v4"] } | |
| 24 | +walkdir = "2" | |
| 25 | +egui-phosphor = "0.9" | |
| 26 | +tracing = "0.1" | |
| 27 | +tracing-subscriber = "0.3" |
+521-0docs/superpowers/plans/2026-03-10-mode-restructure.md
| @@ -0,0 +1,521 @@ | ||
| 1 | +# Mode Restructure Implementation Plan | |
| 2 | + | |
| 3 | +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. | |
| 4 | + | |
| 5 | +**Goal:** Restructure jv from flat 4-tab layout into three modes (File/Shared/All) with per-mode sub-tabs and adaptive sidebar. | |
| 6 | + | |
| 7 | +**Architecture:** Add `AppMode` enum alongside existing `ViewTab`. Mode switcher renders as pills above the tab bar. Each mode filters which tabs are available and what the sidebar shows. Shared mode gets a new schema matrix table view. All mode gets a combined Rust codegen view. | |
| 8 | + | |
| 9 | +**Tech Stack:** Rust, egui 0.31, egui-phosphor 0.9, existing schema/codegen infrastructure. | |
| 10 | + | |
| 11 | +--- | |
| 12 | + | |
| 13 | +## Chunk 1: Core Mode Switching | |
| 14 | + | |
| 15 | +### Task 1: Add AppMode enum and state | |
| 16 | + | |
| 17 | +**Files:** | |
| 18 | +- Modify: `src/app.rs:8-14` (enums) and `src/app.rs:16-47` (struct fields) | |
| 19 | + | |
| 20 | +- [ ] **Step 1: Add AppMode enum and field** | |
| 21 | + | |
| 22 | +Add above the existing `ViewTab` enum in `src/app.rs`: | |
| 23 | + | |
| 24 | +```rust | |
| 25 | +#[derive(Debug, Clone, Copy, PartialEq)] | |
| 26 | +pub enum AppMode { | |
| 27 | + File, | |
| 28 | + Shared, | |
| 29 | + All, | |
| 30 | +} | |
| 31 | +``` | |
| 32 | + | |
| 33 | +Add to `JvApp` struct: | |
| 34 | + | |
| 35 | +```rust | |
| 36 | +active_mode: AppMode, | |
| 37 | +``` | |
| 38 | + | |
| 39 | +Initialize in `new()`: | |
| 40 | + | |
| 41 | +```rust | |
| 42 | +active_mode: AppMode::File, | |
| 43 | +``` | |
| 44 | + | |
| 45 | +- [ ] **Step 2: Build and verify it compiles** | |
| 46 | + | |
| 47 | +Run: `cargo build 2>&1 | grep "^error"` | |
| 48 | +Expected: no errors | |
| 49 | + | |
| 50 | +- [ ] **Step 3: Commit** | |
| 51 | + | |
| 52 | +```bash | |
| 53 | +git add src/app.rs | |
| 54 | +git commit -m "feat: add AppMode enum (File/Shared/All)" | |
| 55 | +``` | |
| 56 | + | |
| 57 | +### Task 2: Render mode switcher pills above tab bar | |
| 58 | + | |
| 59 | +**Files:** | |
| 60 | +- Modify: `src/app.rs` — `show_main_content()` method (around line 420) | |
| 61 | + | |
| 62 | +- [ ] **Step 1: Add mode switcher UI** | |
| 63 | + | |
| 64 | +In `show_main_content()`, before the existing tab bar `ui.horizontal`, add: | |
| 65 | + | |
| 66 | +```rust | |
| 67 | +// Mode switcher | |
| 68 | +ui.horizontal(|ui| { | |
| 69 | + ui.spacing_mut().item_spacing.x = 4.0; | |
| 70 | + for (mode, label, icon) in [ | |
| 71 | + (AppMode::File, "File", regular::FILE), | |
| 72 | + (AppMode::Shared, "Shared", regular::TREE_STRUCTURE), | |
| 73 | + (AppMode::All, "All", regular::CODE), | |
| 74 | + ] { | |
| 75 | + let selected = self.active_mode == mode; | |
| 76 | + let text_color = if selected { | |
| 77 | + CatppuccinMocha::BLUE | |
| 78 | + } else { | |
| 79 | + CatppuccinMocha::OVERLAY0 | |
| 80 | + }; | |
| 81 | + | |
| 82 | + let btn = ui.add( | |
| 83 | + egui::Button::new( | |
| 84 | + RichText::new(format!(" {} {} ", icon, label)) | |
| 85 | + .color(text_color) | |
| 86 | + .size(12.0), | |
| 87 | + ) | |
| 88 | + .fill(if selected { | |
| 89 | + CatppuccinMocha::SURFACE0 | |
| 90 | + } else { | |
| 91 | + egui::Color32::TRANSPARENT | |
| 92 | + }) | |
| 93 | + .corner_radius(6.0), | |
| 94 | + ); | |
| 95 | + | |
| 96 | + if btn.clicked() && self.active_mode != mode { | |
| 97 | + self.active_mode = mode; | |
| 98 | + // Reset to first available tab for the new mode | |
| 99 | + self.active_tab = match mode { | |
| 100 | + AppMode::File => ViewTab::Table, | |
| 101 | + AppMode::Shared => ViewTab::Table, | |
| 102 | + AppMode::All => ViewTab::Rust, | |
| 103 | + }; | |
| 104 | + } | |
| 105 | + } | |
| 106 | +}); | |
| 107 | +ui.add_space(2.0); | |
| 108 | +``` | |
| 109 | + | |
| 110 | +- [ ] **Step 2: Filter tab bar by active mode** | |
| 111 | + | |
| 112 | +Replace the existing tab loop array with a conditional: | |
| 113 | + | |
| 114 | +```rust | |
| 115 | +let tabs: Vec<(ViewTab, &str, &str)> = match self.active_mode { | |
| 116 | + AppMode::File => vec![ | |
| 117 | + (ViewTab::Table, "Table", regular::TABLE), | |
| 118 | + (ViewTab::Json, "JSON", regular::BRACKETS_CURLY), | |
| 119 | + (ViewTab::Rust, "Rust", regular::GEAR), | |
| 120 | + (ViewTab::Jq, "jq", regular::FUNNEL), | |
| 121 | + ], | |
| 122 | + AppMode::Shared => vec![ | |
| 123 | + (ViewTab::Table, "Table", regular::TABLE), | |
| 124 | + (ViewTab::Rust, "Rust", regular::GEAR), | |
| 125 | + ], | |
| 126 | + AppMode::All => vec![], // No tab bar for All mode | |
| 127 | +}; | |
| 128 | +``` | |
| 129 | + | |
| 130 | +Only render the tab bar horizontal block if `!tabs.is_empty()`. | |
| 131 | + | |
| 132 | +- [ ] **Step 3: Build and verify** | |
| 133 | + | |
| 134 | +Run: `cargo build 2>&1 | grep "^error"` | |
| 135 | +Expected: no errors | |
| 136 | + | |
| 137 | +- [ ] **Step 4: Commit** | |
| 138 | + | |
| 139 | +```bash | |
| 140 | +git add src/app.rs | |
| 141 | +git commit -m "feat: render mode switcher pills and filter tabs per mode" | |
| 142 | +``` | |
| 143 | + | |
| 144 | +### Task 3: Route content rendering by mode | |
| 145 | + | |
| 146 | +**Files:** | |
| 147 | +- Modify: `src/app.rs` — `show_main_content()` content section (around line 509-535) | |
| 148 | + | |
| 149 | +- [ ] **Step 1: Branch content by mode** | |
| 150 | + | |
| 151 | +Replace the single `match self.active_tab` block with mode-aware routing: | |
| 152 | + | |
| 153 | +```rust | |
| 154 | +match self.active_mode { | |
| 155 | + AppMode::File => { | |
| 156 | + // Existing per-file content (current match block) | |
| 157 | + // Requires a selected file - show empty state if none | |
| 158 | + let has_file = self.current_session | |
| 159 | + .as_ref() | |
| 160 | + .is_some_and(|l| !l.parsed_files.is_empty()); | |
| 161 | + if !has_file { | |
| 162 | + // existing empty state UI | |
| 163 | + return; | |
| 164 | + } | |
| 165 | + // ... existing file change detection + match self.active_tab | |
| 166 | + } | |
| 167 | + AppMode::Shared => { | |
| 168 | + let has_schema = self.schema.as_ref().is_some_and(|s| !s.structs.is_empty()); | |
| 169 | + if !has_schema { | |
| 170 | + ui.centered_and_justified(|ui| { | |
| 171 | + ui.label( | |
| 172 | + RichText::new("Import multiple files to see shared types") | |
| 173 | + .color(CatppuccinMocha::OVERLAY0) | |
| 174 | + .size(16.0), | |
| 175 | + ); | |
| 176 | + }); | |
| 177 | + return; | |
| 178 | + } | |
| 179 | + let structs = self.schema.as_ref().unwrap().structs.clone(); | |
| 180 | + match self.active_tab { | |
| 181 | + ViewTab::Table => { | |
| 182 | + // TODO: Task 5 — schema matrix view | |
| 183 | + ui.label("Schema matrix — coming soon"); | |
| 184 | + } | |
| 185 | + ViewTab::Rust => { | |
| 186 | + self.rust_view.show_schema(ui, &structs); | |
| 187 | + } | |
| 188 | + _ => {} | |
| 189 | + } | |
| 190 | + } | |
| 191 | + AppMode::All => { | |
| 192 | + let has_schema = self.schema.as_ref().is_some_and(|s| !s.structs.is_empty()); | |
| 193 | + if !has_schema { | |
| 194 | + ui.centered_and_justified(|ui| { | |
| 195 | + ui.label( | |
| 196 | + RichText::new("Import files to generate Rust code") | |
| 197 | + .color(CatppuccinMocha::OVERLAY0) | |
| 198 | + .size(16.0), | |
| 199 | + ); | |
| 200 | + }); | |
| 201 | + return; | |
| 202 | + } | |
| 203 | + // All mode: show_schema with all structs | |
| 204 | + let structs = self.schema.as_ref().unwrap().structs.clone(); | |
| 205 | + self.rust_view.show_schema(ui, &structs); | |
| 206 | + } | |
| 207 | +} | |
| 208 | +``` | |
| 209 | + | |
| 210 | +- [ ] **Step 2: Build, launch, verify mode switching works** | |
| 211 | + | |
| 212 | +Run: `cargo build && cargo run` | |
| 213 | +Expected: clicking File/Shared/All switches modes, tabs update accordingly | |
| 214 | + | |
| 215 | +- [ ] **Step 3: Commit** | |
| 216 | + | |
| 217 | +```bash | |
| 218 | +git add src/app.rs | |
| 219 | +git commit -m "feat: route content rendering by active mode" | |
| 220 | +``` | |
| 221 | + | |
| 222 | +### Task 4: Adaptive sidebar per mode | |
| 223 | + | |
| 224 | +**Files:** | |
| 225 | +- Modify: `src/app.rs` — `show_sidebar()` method | |
| 226 | + | |
| 227 | +- [ ] **Step 1: Branch sidebar by mode** | |
| 228 | + | |
| 229 | +Wrap the existing sidebar content in a mode check. The SESSION section stays in all modes. Then branch: | |
| 230 | + | |
| 231 | +```rust | |
| 232 | +// SESSION section — always shown (existing code for session selector + buttons) | |
| 233 | +// ... | |
| 234 | + | |
| 235 | +ui.add_space(16.0); | |
| 236 | + | |
| 237 | +match self.active_mode { | |
| 238 | + AppMode::File => { | |
| 239 | + // Existing FILES section + file list + SHARED TYPES section | |
| 240 | + // (move current code here) | |
| 241 | + } | |
| 242 | + AppMode::Shared | AppMode::All => { | |
| 243 | + // STRUCTS section — scrollable list of struct names | |
| 244 | + ui.horizontal(|ui| { | |
| 245 | + ui.label( | |
| 246 | + RichText::new(format!("{} ", regular::TREE_STRUCTURE)) | |
| 247 | + .color(CatppuccinMocha::OVERLAY0) | |
| 248 | + .size(12.0), | |
| 249 | + ); | |
| 250 | + ui.label( | |
| 251 | + RichText::new("STRUCTS") | |
| 252 | + .color(CatppuccinMocha::OVERLAY0) | |
| 253 | + .small() | |
| 254 | + .strong(), | |
| 255 | + ); | |
| 256 | + }); | |
| 257 | + ui.add_space(4.0); | |
| 258 | + | |
| 259 | + if let Some(schema) = &self.schema { | |
| 260 | + egui::ScrollArea::vertical() | |
| 261 | + .id_salt("struct_list_scroll") | |
| 262 | + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) | |
| 263 | + .auto_shrink(false) | |
| 264 | + .show(ui, |ui| { | |
| 265 | + for s in &schema.structs { | |
| 266 | + let label = format!( | |
| 267 | + "{} ({} fields, {}/{})", | |
| 268 | + s.name, | |
| 269 | + s.fields.len(), | |
| 270 | + s.source_files.len(), | |
| 271 | + self.current_session | |
| 272 | + .as_ref() | |
| 273 | + .map(|l| l.session.files.len()) | |
| 274 | + .unwrap_or(0), | |
| 275 | + ); | |
| 276 | + ui.add( | |
| 277 | + egui::Label::new( | |
| 278 | + RichText::new(&label) | |
| 279 | + .color(CatppuccinMocha::LAVENDER) | |
| 280 | + .size(12.0), | |
| 281 | + ), | |
| 282 | + ); | |
| 283 | + } | |
| 284 | + }); | |
| 285 | + } else { | |
| 286 | + ui.label( | |
| 287 | + RichText::new("Import files to see structs") | |
| 288 | + .color(CatppuccinMocha::OVERLAY0) | |
| 289 | + .small(), | |
| 290 | + ); | |
| 291 | + } | |
| 292 | + } | |
| 293 | +} | |
| 294 | +``` | |
| 295 | + | |
| 296 | +- [ ] **Step 2: Build, launch, verify sidebar changes per mode** | |
| 297 | + | |
| 298 | +Run: `cargo build && cargo run` | |
| 299 | +Expected: File mode shows file list; Shared/All show struct list | |
| 300 | + | |
| 301 | +- [ ] **Step 3: Commit** | |
| 302 | + | |
| 303 | +```bash | |
| 304 | +git add src/app.rs | |
| 305 | +git commit -m "feat: adaptive sidebar per mode (files vs struct list)" | |
| 306 | +``` | |
| 307 | + | |
| 308 | +## Chunk 2: Shared Mode Schema Matrix Table | |
| 309 | + | |
| 310 | +### Task 5: Add schema matrix view to TableView | |
| 311 | + | |
| 312 | +**Files:** | |
| 313 | +- Modify: `src/views/table.rs` — add `show_schema_matrix()` method | |
| 314 | + | |
| 315 | +- [ ] **Step 1: Add the show_schema_matrix method** | |
| 316 | + | |
| 317 | +Add a new public method to `TableView`: | |
| 318 | + | |
| 319 | +```rust | |
| 320 | +pub fn show_schema_matrix( | |
| 321 | + &mut self, | |
| 322 | + ui: &mut Ui, | |
| 323 | + structs: &[crate::schema::SharedStruct], | |
| 324 | + filenames: &[String], | |
| 325 | +) { | |
| 326 | + egui::ScrollArea::both() | |
| 327 | + .id_salt("schema_matrix_scroll") | |
| 328 | + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) | |
| 329 | + .auto_shrink(false) | |
| 330 | + .show(ui, |ui| { | |
| 331 | + for shared in structs { | |
| 332 | + let id = ui.id().with(&shared.name); | |
| 333 | + let is_expanded = self.expanded.contains(&shared.name); | |
| 334 | + | |
| 335 | + // Struct header row | |
| 336 | + ui.horizontal(|ui| { | |
| 337 | + let arrow = if is_expanded { | |
| 338 | + egui_phosphor::regular::CARET_DOWN | |
| 339 | + } else { | |
| 340 | + egui_phosphor::regular::CARET_RIGHT | |
| 341 | + }; | |
| 342 | + let header = format!( | |
| 343 | + "{} {} — {} fields, in {}/{} files", | |
| 344 | + arrow, | |
| 345 | + shared.name, | |
| 346 | + shared.fields.len(), | |
| 347 | + shared.source_files.len(), | |
| 348 | + filenames.len(), | |
| 349 | + ); | |
| 350 | + let r = ui.add( | |
| 351 | + egui::Label::new( | |
| 352 | + RichText::new(&header) | |
| 353 | + .color(CatppuccinMocha::LAVENDER) | |
| 354 | + .strong() | |
| 355 | + .size(14.0), | |
| 356 | + ) | |
| 357 | + .sense(egui::Sense::click()), | |
| 358 | + ); | |
| 359 | + if r.clicked() { | |
| 360 | + if is_expanded { | |
| 361 | + self.expanded.remove(&shared.name); | |
| 362 | + } else { | |
| 363 | + self.expanded.insert(shared.name.clone()); | |
| 364 | + } | |
| 365 | + } | |
| 366 | + }); | |
| 367 | + | |
| 368 | + if is_expanded { | |
| 369 | + ui.add_space(4.0); | |
| 370 | + | |
| 371 | + // Build columns: Field + one per source file | |
| 372 | + let num_cols = shared.source_files.len() + 1; | |
| 373 | + | |
| 374 | + egui::Grid::new(id) | |
| 375 | + .num_columns(num_cols) | |
| 376 | + .striped(true) | |
| 377 | + .spacing([12.0, 4.0]) | |
| 378 | + .show(ui, |ui| { | |
| 379 | + // Header row | |
| 380 | + ui.label( | |
| 381 | + RichText::new("Field") | |
| 382 | + .color(CatppuccinMocha::BLUE) | |
| 383 | + .strong(), | |
| 384 | + ); | |
| 385 | + for file in &shared.source_files { | |
| 386 | + ui.label( | |
| 387 | + RichText::new(file) | |
| 388 | + .color(CatppuccinMocha::YELLOW) | |
| 389 | + .strong() | |
| 390 | + .size(11.0), | |
| 391 | + ); | |
| 392 | + } | |
| 393 | + ui.end_row(); | |
| 394 | + | |
| 395 | + // Field rows | |
| 396 | + for (field_name, field_type) in &shared.fields { | |
| 397 | + ui.label( | |
| 398 | + RichText::new(field_name) | |
| 399 | + .color(CatppuccinMocha::BLUE), | |
| 400 | + ); | |
| 401 | + // For each source file, show the type or — | |
| 402 | + for _file in &shared.source_files { | |
| 403 | + // All source files have this struct, show the merged type | |
| 404 | + let type_str = field_type.display_name(); | |
| 405 | + let color = crate::theme::type_color(&type_str); | |
| 406 | + ui.label( | |
| 407 | + RichText::new(&type_str).color(color), | |
| 408 | + ); | |
| 409 | + } | |
| 410 | + ui.end_row(); | |
| 411 | + } | |
| 412 | + }); | |
| 413 | + | |
| 414 | + ui.add_space(12.0); | |
| 415 | + } | |
| 416 | + } | |
| 417 | + }); | |
| 418 | +} | |
| 419 | +``` | |
| 420 | + | |
| 421 | +- [ ] **Step 2: Wire it up in app.rs Shared mode** | |
| 422 | + | |
| 423 | +Replace the `"Schema matrix — coming soon"` placeholder in `show_main_content()`: | |
| 424 | + | |
| 425 | +```rust | |
| 426 | +ViewTab::Table => { | |
| 427 | + let filenames: Vec<String> = self.current_session | |
| 428 | + .as_ref() | |
| 429 | + .map(|l| l.session.files.iter().map(|f| f.filename.clone()).collect()) | |
| 430 | + .unwrap_or_default(); | |
| 431 | + self.table_view.show_schema_matrix(ui, &structs, &filenames); | |
| 432 | +} | |
| 433 | +``` | |
| 434 | + | |
| 435 | +- [ ] **Step 3: Check InferredType has display_name** | |
| 436 | + | |
| 437 | +Verify `InferredType` in `src/types.rs` has a `display_name()` method. If not, add one that returns the Rust type string. | |
| 438 | + | |
| 439 | +- [ ] **Step 4: Build, launch, test Shared mode Table** | |
| 440 | + | |
| 441 | +Run: `cargo build && cargo run` | |
| 442 | +Expected: Shared > Table shows expandable struct sections with file columns | |
| 443 | + | |
| 444 | +- [ ] **Step 5: Commit** | |
| 445 | + | |
| 446 | +```bash | |
| 447 | +git add src/views/table.rs src/app.rs | |
| 448 | +git commit -m "feat: schema matrix table view for Shared mode" | |
| 449 | +``` | |
| 450 | + | |
| 451 | +## Chunk 3: Polish and All Mode | |
| 452 | + | |
| 453 | +### Task 6: All mode combined Rust codegen | |
| 454 | + | |
| 455 | +**Files:** | |
| 456 | +- Modify: `src/views/rust.rs` — add `show_all()` method | |
| 457 | +- Modify: `src/app.rs` — All mode content routing | |
| 458 | + | |
| 459 | +- [ ] **Step 1: Add show_all method to RustView** | |
| 460 | + | |
| 461 | +The All mode should show all structs from the schema. The existing `show_schema()` already does this — it generates code from all SharedStructs. So All mode can just reuse `show_schema()` directly. | |
| 462 | + | |
| 463 | +Verify in `app.rs` that All mode already calls `self.rust_view.show_schema(ui, &structs)` — it should from Task 3. | |
| 464 | + | |
| 465 | +If additional per-file structs are needed that aren't in the shared schema, add a `show_all()` that combines both. For now, reusing `show_schema()` is sufficient. | |
| 466 | + | |
| 467 | +- [ ] **Step 2: Build and verify All mode** | |
| 468 | + | |
| 469 | +Run: `cargo build && cargo run` | |
| 470 | +Expected: All mode shows full Rust codegen, no tab bar | |
| 471 | + | |
| 472 | +- [ ] **Step 3: Commit** | |
| 473 | + | |
| 474 | +```bash | |
| 475 | +git add src/app.rs src/views/rust.rs | |
| 476 | +git commit -m "feat: All mode shows combined Rust codegen" | |
| 477 | +``` | |
| 478 | + | |
| 479 | +### Task 7: Clean up and remove old Shared Types sidebar section | |
| 480 | + | |
| 481 | +**Files:** | |
| 482 | +- Modify: `src/app.rs` — remove the SHARED TYPES section from File mode sidebar (it's now its own mode) | |
| 483 | + | |
| 484 | +- [ ] **Step 1: Remove SHARED TYPES from File mode sidebar** | |
| 485 | + | |
| 486 | +In the File mode branch of the sidebar, remove the "SHARED TYPES" header and the scrollable struct list at the bottom. This information now lives in Shared/All mode sidebars. | |
| 487 | + | |
| 488 | +- [ ] **Step 2: Build, launch, verify File mode sidebar is cleaner** | |
| 489 | + | |
| 490 | +Run: `cargo build && cargo run` | |
| 491 | +Expected: File mode sidebar only shows SESSION + FILES sections | |
| 492 | + | |
| 493 | +- [ ] **Step 3: Commit** | |
| 494 | + | |
| 495 | +```bash | |
| 496 | +git add src/app.rs | |
| 497 | +git commit -m "refactor: remove shared types from file mode sidebar (now in Shared mode)" | |
| 498 | +``` | |
| 499 | + | |
| 500 | +### Task 8: Final integration test | |
| 501 | + | |
| 502 | +- [ ] **Step 1: Manual test all modes** | |
| 503 | + | |
| 504 | +1. File mode: verify Table, JSON, Rust, jq all work per-file | |
| 505 | +2. Shared mode: verify Table shows schema matrix, Rust shows shared codegen | |
| 506 | +3. All mode: verify Rust shows full codegen, no tab bar | |
| 507 | +4. Mode switching: verify tabs reset, sidebar adapts | |
| 508 | +5. Session switching: verify schema rebuilds in all modes | |
| 509 | + | |
| 510 | +- [ ] **Step 2: Take screenshots to verify UI** | |
| 511 | + | |
| 512 | +```bash | |
| 513 | +grim -g "X,Y WxH" -s 2 /tmp/jv_final.png | |
| 514 | +``` | |
| 515 | + | |
| 516 | +- [ ] **Step 3: Final commit** | |
| 517 | + | |
| 518 | +```bash | |
| 519 | +git add -A | |
| 520 | +git commit -m "feat: complete mode restructure (File/Shared/All)" | |
| 521 | +``` |
+1461-0docs/superpowers/plans/2026-03-11-har-import-and-swift-codegen.md
| @@ -0,0 +1,1461 @@ | ||
| 1 | +# HAR Import & Multi-Language Codegen Implementation Plan | |
| 2 | + | |
| 3 | +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. | |
| 4 | + | |
| 5 | +**Goal:** Add HAR file import with intelligent naming, and a language abstraction layer with Swift as the second codegen target alongside Rust. | |
| 6 | + | |
| 7 | +**Architecture:** HAR parsing is a standalone module that produces `(filename, Value)` pairs fed into the existing session pipeline. Language codegen uses a `LanguageGenerator` trait implemented by `RustGenerator` and `SwiftGenerator`, with `CodeGenerator` delegating all formatting through the trait. The code view gets a language toggle that triggers full rebuild. | |
| 8 | + | |
| 9 | +**Tech Stack:** Rust, egui 0.31, serde_json, rfd (file dialogs), no new crate dependencies | |
| 10 | + | |
| 11 | +**Spec:** `docs/superpowers/specs/2026-03-11-har-import-and-swift-codegen-design.md` | |
| 12 | + | |
| 13 | +--- | |
| 14 | + | |
| 15 | +## Chunk 1: HAR File Import | |
| 16 | + | |
| 17 | +### Task 1: HAR Parsing Module | |
| 18 | + | |
| 19 | +**Files:** | |
| 20 | +- Create: `src/har.rs` | |
| 21 | + | |
| 22 | +- [ ] **Step 1: Write tests for HAR extraction** | |
| 23 | + | |
| 24 | +Add `#[cfg(test)]` module at the bottom of `src/har.rs`: | |
| 25 | + | |
| 26 | +```rust | |
| 27 | +#[cfg(test)] | |
| 28 | +mod tests { | |
| 29 | + use super::*; | |
| 30 | + use serde_json::json; | |
| 31 | + | |
| 32 | + #[test] | |
| 33 | + fn test_extract_har_files_basic() { | |
| 34 | + let har = json!({ | |
| 35 | + "log": { | |
| 36 | + "entries": [ | |
| 37 | + { | |
| 38 | + "request": { | |
| 39 | + "method": "GET", | |
| 40 | + "url": "https://api.example.com/api/v2/flights?origin=LAX&date=2026-03-01", | |
| 41 | + "queryString": [ | |
| 42 | + {"name": "origin", "value": "LAX"}, | |
| 43 | + {"name": "date", "value": "2026-03-01"} | |
| 44 | + ] | |
| 45 | + }, | |
| 46 | + "response": { | |
| 47 | + "status": 200, | |
| 48 | + "content": { | |
| 49 | + "mimeType": "application/json", | |
| 50 | + "text": "{\"id\": 1, \"origin\": \"LAX\"}" | |
| 51 | + } | |
| 52 | + } | |
| 53 | + } | |
| 54 | + ] | |
| 55 | + } | |
| 56 | + }); | |
| 57 | + let files = extract_har_files(&har); | |
| 58 | + assert_eq!(files.len(), 1); | |
| 59 | + assert_eq!(files[0].0, "flights_LAX_2026-03-01.json"); | |
| 60 | + } | |
| 61 | + | |
| 62 | + #[test] | |
| 63 | + fn test_extract_har_files_post() { | |
| 64 | + let har = json!({ | |
| 65 | + "log": { | |
| 66 | + "entries": [ | |
| 67 | + { | |
| 68 | + "request": { | |
| 69 | + "method": "POST", | |
| 70 | + "url": "https://api.example.com/auth/login", | |
| 71 | + "queryString": [], | |
| 72 | + "postData": { | |
| 73 | + "mimeType": "application/json", | |
| 74 | + "text": "{\"username\": \"alice\"}" | |
| 75 | + } | |
| 76 | + }, | |
| 77 | + "response": { | |
| 78 | + "status": 200, | |
| 79 | + "content": { | |
| 80 | + "mimeType": "application/json", | |
| 81 | + "text": "{\"token\": \"abc\"}" | |
| 82 | + } | |
| 83 | + } | |
| 84 | + } | |
| 85 | + ] | |
| 86 | + } | |
| 87 | + }); | |
| 88 | + let files = extract_har_files(&har); | |
| 89 | + assert_eq!(files.len(), 1); | |
| 90 | + assert_eq!(files[0].0, "login_alice.json"); | |
| 91 | + } | |
| 92 | + | |
| 93 | + #[test] | |
| 94 | + fn test_extract_har_skips_non_json() { | |
| 95 | + let har = json!({ | |
| 96 | + "log": { | |
| 97 | + "entries": [ | |
| 98 | + { | |
| 99 | + "request": { | |
| 100 | + "method": "GET", | |
| 101 | + "url": "https://example.com/image.png", | |
| 102 | + "queryString": [] | |
| 103 | + }, | |
| 104 | + "response": { | |
| 105 | + "status": 200, | |
| 106 | + "content": { | |
| 107 | + "mimeType": "image/png", | |
| 108 | + "text": "" | |
| 109 | + } | |
| 110 | + } | |
| 111 | + } | |
| 112 | + ] | |
| 113 | + } | |
| 114 | + }); | |
| 115 | + let files = extract_har_files(&har); | |
| 116 | + assert_eq!(files.len(), 0); | |
| 117 | + } | |
| 118 | + | |
| 119 | + #[test] | |
| 120 | + fn test_extract_har_skips_non_2xx() { | |
| 121 | + let har = json!({ | |
| 122 | + "log": { | |
| 123 | + "entries": [ | |
| 124 | + { | |
| 125 | + "request": { | |
| 126 | + "method": "GET", | |
| 127 | + "url": "https://example.com/api/data", | |
| 128 | + "queryString": [] | |
| 129 | + }, | |
| 130 | + "response": { | |
| 131 | + "status": 404, | |
| 132 | + "content": { | |
| 133 | + "mimeType": "application/json", | |
| 134 | + "text": "{\"error\": \"not found\"}" | |
| 135 | + } | |
| 136 | + } | |
| 137 | + } | |
| 138 | + ] | |
| 139 | + } | |
| 140 | + }); | |
| 141 | + let files = extract_har_files(&har); | |
| 142 | + assert_eq!(files.len(), 0); | |
| 143 | + } | |
| 144 | + | |
| 145 | + #[test] | |
| 146 | + fn test_extract_har_skips_base64() { | |
| 147 | + let har = json!({ | |
| 148 | + "log": { | |
| 149 | + "entries": [ | |
| 150 | + { | |
| 151 | + "request": { | |
| 152 | + "method": "GET", | |
| 153 | + "url": "https://example.com/api/data", | |
| 154 | + "queryString": [] | |
| 155 | + }, | |
| 156 | + "response": { | |
| 157 | + "status": 200, | |
| 158 | + "content": { | |
| 159 | + "mimeType": "application/json", | |
| 160 | + "encoding": "base64", | |
| 161 | + "text": "eyJpZCI6IDF9" | |
| 162 | + } | |
| 163 | + } | |
| 164 | + } | |
| 165 | + ] | |
| 166 | + } | |
| 167 | + }); | |
| 168 | + let files = extract_har_files(&har); | |
| 169 | + assert_eq!(files.len(), 0); | |
| 170 | + } | |
| 171 | + | |
| 172 | + #[test] | |
| 173 | + fn test_extract_path_segment() { | |
| 174 | + assert_eq!(extract_path_segment("https://api.example.com/api/v2/flights"), "flights"); | |
| 175 | + assert_eq!(extract_path_segment("https://api.example.com/users/42/bookings"), "bookings"); | |
| 176 | + assert_eq!(extract_path_segment("https://api.example.com/health"), "health"); | |
| 177 | + assert_eq!(extract_path_segment("https://api.example.com/rest/v3/data"), "data"); | |
| 178 | + } | |
| 179 | + | |
| 180 | + #[test] | |
| 181 | + fn test_is_hex_like() { | |
| 182 | + assert!(is_hex_like("a1b2c3d4e5f6a7b8c9d0")); // >50% hex, >15 chars | |
| 183 | + assert!(is_hex_like("550e8400-e29b-41d4-a716-446655440000")); // UUID | |
| 184 | + assert!(!is_hex_like("LAX")); | |
| 185 | + assert!(!is_hex_like("alice")); | |
| 186 | + assert!(!is_hex_like("2026-03-01")); | |
| 187 | + } | |
| 188 | + | |
| 189 | + #[test] | |
| 190 | + fn test_deduplication() { | |
| 191 | + let har = json!({ | |
| 192 | + "log": { | |
| 193 | + "entries": [ | |
| 194 | + { | |
| 195 | + "request": { | |
| 196 | + "method": "GET", | |
| 197 | + "url": "https://example.com/api/flights?origin=LAX", | |
| 198 | + "queryString": [{"name": "origin", "value": "LAX"}] | |
| 199 | + }, | |
| 200 | + "response": { | |
| 201 | + "status": 200, | |
| 202 | + "content": { | |
| 203 | + "mimeType": "application/json", | |
| 204 | + "text": "{\"id\": 1}" | |
| 205 | + } | |
| 206 | + } | |
| 207 | + }, | |
| 208 | + { | |
| 209 | + "request": { | |
| 210 | + "method": "GET", | |
| 211 | + "url": "https://example.com/api/flights?origin=LAX", | |
| 212 | + "queryString": [{"name": "origin", "value": "LAX"}] | |
| 213 | + }, | |
| 214 | + "response": { | |
| 215 | + "status": 200, | |
| 216 | + "content": { | |
| 217 | + "mimeType": "application/json", | |
| 218 | + "text": "{\"id\": 2}" | |
| 219 | + } | |
| 220 | + } | |
| 221 | + } | |
| 222 | + ] | |
| 223 | + } | |
| 224 | + }); | |
| 225 | + let files = extract_har_files(&har); | |
| 226 | + assert_eq!(files.len(), 2); | |
| 227 | + assert_eq!(files[0].0, "flights_LAX.json"); | |
| 228 | + assert_eq!(files[1].0, "flights_LAX_2.json"); | |
| 229 | + } | |
| 230 | + | |
| 231 | + #[test] | |
| 232 | + fn test_excludes_long_tokens() { | |
| 233 | + let har = json!({ | |
| 234 | + "log": { | |
| 235 | + "entries": [ | |
| 236 | + { | |
| 237 | + "request": { | |
| 238 | + "method": "GET", | |
| 239 | + "url": "https://example.com/api/data?token=abcdef123456789012345&name=bob", | |
| 240 | + "queryString": [ | |
| 241 | + {"name": "token", "value": "abcdef123456789012345"}, | |
| 242 | + {"name": "name", "value": "bob"} | |
| 243 | + ] | |
| 244 | + }, | |
| 245 | + "response": { | |
| 246 | + "status": 200, | |
| 247 | + "content": { | |
| 248 | + "mimeType": "application/json", | |
| 249 | + "text": "{\"ok\": true}" | |
| 250 | + } | |
| 251 | + } | |
| 252 | + } | |
| 253 | + ] | |
| 254 | + } | |
| 255 | + }); | |
| 256 | + let files = extract_har_files(&har); | |
| 257 | + assert_eq!(files[0].0, "data_bob.json"); | |
| 258 | + } | |
| 259 | +} | |
| 260 | +``` | |
| 261 | + | |
| 262 | +- [ ] **Step 2: Write the HAR module implementation** | |
| 263 | + | |
| 264 | +Create `src/har.rs`: | |
| 265 | + | |
| 266 | +```rust | |
| 267 | +use serde_json::Value; | |
| 268 | +use std::collections::BTreeSet; | |
| 269 | + | |
| 270 | +/// Extract JSON response bodies from a HAR file, returning (filename, parsed_json) pairs. | |
| 271 | +pub fn extract_har_files(har: &Value) -> Vec<(String, Value)> { | |
| 272 | + let entries = match har.get("log").and_then(|l| l.get("entries")).and_then(|e| e.as_array()) { | |
| 273 | + Some(arr) => arr, | |
| 274 | + None => return Vec::new(), | |
| 275 | + }; | |
| 276 | + | |
| 277 | + let mut results: Vec<(String, Value)> = Vec::new(); | |
| 278 | + let mut used_names: BTreeSet<String> = BTreeSet::new(); | |
| 279 | + | |
| 280 | + for entry in entries { | |
| 281 | + // Check response status is 2xx | |
| 282 | + let status = entry | |
| 283 | + .pointer("/response/status") | |
| 284 | + .and_then(|s| s.as_u64()) | |
| 285 | + .unwrap_or(0); | |
| 286 | + if status < 200 || status >= 300 { | |
| 287 | + continue; | |
| 288 | + } | |
| 289 | + | |
| 290 | + // Check mime type contains "json" | |
| 291 | + let mime = entry | |
| 292 | + .pointer("/response/content/mimeType") | |
| 293 | + .and_then(|m| m.as_str()) | |
| 294 | + .unwrap_or(""); | |
| 295 | + if !mime.contains("json") { | |
| 296 | + continue; | |
| 297 | + } | |
| 298 | + | |
| 299 | + // Skip base64-encoded bodies | |
| 300 | + let encoding = entry | |
| 301 | + .pointer("/response/content/encoding") | |
| 302 | + .and_then(|e| e.as_str()) | |
| 303 | + .unwrap_or(""); | |
| 304 | + if encoding == "base64" { | |
| 305 | + continue; | |
| 306 | + } | |
| 307 | + | |
| 308 | + // Parse response body | |
| 309 | + let text = entry | |
| 310 | + .pointer("/response/content/text") | |
| 311 | + .and_then(|t| t.as_str()) | |
| 312 | + .unwrap_or(""); | |
| 313 | + let parsed: Value = match serde_json::from_str(text) { | |
| 314 | + Ok(v) => v, | |
| 315 | + Err(_) => continue, | |
| 316 | + }; | |
| 317 | + | |
| 318 | + // Build filename | |
| 319 | + let url = entry | |
| 320 | + .pointer("/request/url") | |
| 321 | + .and_then(|u| u.as_str()) | |
| 322 | + .unwrap_or(""); | |
| 323 | + let base = extract_path_segment(url); | |
| 324 | + let values = collect_distinguishing_values(entry); | |
| 325 | + | |
| 326 | + let stem = if values.is_empty() { | |
| 327 | + base.clone() | |
| 328 | + } else { | |
| 329 | + format!("{}_{}", base, values.join("_")) | |
| 330 | + }; | |
| 331 | + | |
| 332 | + // Deduplicate | |
| 333 | + let filename = if used_names.contains(&stem) { | |
| 334 | + let mut i = 2; | |
| 335 | + let deduped = loop { | |
| 336 | + let candidate = format!("{}_{}", stem, i); | |
| 337 | + if !used_names.contains(&candidate) { | |
| 338 | + used_names.insert(candidate.clone()); | |
| 339 | + break candidate; | |
| 340 | + } | |
| 341 | + i += 1; | |
| 342 | + }; | |
| 343 | + format!("{}.json", deduped) | |
| 344 | + } else { | |
| 345 | + used_names.insert(stem.clone()); | |
| 346 | + format!("{}.json", stem) | |
| 347 | + }; | |
| 348 | + | |
| 349 | + results.push((filename, parsed)); | |
| 350 | + } | |
| 351 | + | |
| 352 | + results | |
| 353 | +} | |
| 354 | + | |
| 355 | +/// Extract the last meaningful path segment from a URL. | |
| 356 | +/// Strips /api/, /rest/, /vN/ prefixes and numeric-only segments. | |
| 357 | +fn extract_path_segment(url: &str) -> String { | |
| 358 | + // Split off query string and fragment | |
| 359 | + let path_part = url.split('?').next().unwrap_or(url); | |
| 360 | + let path_part = path_part.split('#').next().unwrap_or(path_part); | |
| 361 | + | |
| 362 | + // Strip scheme + host: find the path after ://host | |
| 363 | + let path = if let Some(after_scheme) = path_part.strip_prefix("https://") | |
| 364 | + .or_else(|| path_part.strip_prefix("http://")) | |
| 365 | + { | |
| 366 | + // Find first / after host | |
| 367 | + after_scheme.find('/').map(|i| &after_scheme[i..]).unwrap_or("/") | |
| 368 | + } else { | |
| 369 | + path_part | |
| 370 | + }; | |
| 371 | + | |
| 372 | + let segments: Vec<&str> = path | |
| 373 | + .split('/') | |
| 374 | + .filter(|s| !s.is_empty()) | |
| 375 | + .filter(|s| { | |
| 376 | + // Skip api, rest, version segments | |
| 377 | + let lower = s.to_ascii_lowercase(); | |
| 378 | + lower != "api" && lower != "rest" && !is_version_segment(&lower) | |
| 379 | + }) | |
| 380 | + .filter(|s| { | |
| 381 | + // Skip numeric-only segments (IDs) | |
| 382 | + !s.chars().all(|c| c.is_ascii_digit()) | |
| 383 | + }) | |
| 384 | + .collect(); | |
| 385 | + | |
| 386 | + segments | |
| 387 | + .last() | |
| 388 | + .unwrap_or(&"response") | |
| 389 | + .to_ascii_lowercase() | |
| 390 | +} | |
| 391 | + | |
| 392 | +/// Collect short, distinguishing values from query params (GET) or POST body. | |
| 393 | +fn collect_distinguishing_values(entry: &Value) -> Vec<String> { | |
| 394 | + let mut values = Vec::new(); | |
| 395 | + | |
| 396 | + // Query string params | |
| 397 | + if let Some(qs) = entry.pointer("/request/queryString").and_then(|q| q.as_array()) { | |
| 398 | + for param in qs { | |
| 399 | + if let Some(val) = param.get("value").and_then(|v| v.as_str()) { | |
| 400 | + if is_useful_value(val) { | |
| 401 | + values.push(val.to_string()); | |
| 402 | + } | |
| 403 | + } | |
| 404 | + } | |
| 405 | + } | |
| 406 | + | |
| 407 | + // POST body fields (top-level string values only) | |
| 408 | + let method = entry | |
| 409 | + .pointer("/request/method") | |
| 410 | + .and_then(|m| m.as_str()) | |
| 411 | + .unwrap_or(""); | |
| 412 | + if matches!(method, "POST" | "PUT" | "PATCH") { | |
| 413 | + if let Some(body_text) = entry | |
| 414 | + .pointer("/request/postData/text") | |
| 415 | + .and_then(|t| t.as_str()) | |
| 416 | + { | |
| 417 | + if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(body_text) { | |
| 418 | + for (_, val) in &map { | |
| 419 | + if let Some(s) = val.as_str() { | |
| 420 | + if is_useful_value(s) { | |
| 421 | + values.push(s.to_string()); | |
| 422 | + } | |
| 423 | + } | |
| 424 | + } | |
| 425 | + } | |
| 426 | + } | |
| 427 | + } | |
| 428 | + | |
| 429 | + values | |
| 430 | +} | |
| 431 | + | |
| 432 | +fn is_version_segment(s: &str) -> bool { | |
| 433 | + s.starts_with('v') && s[1..].chars().all(|c| c.is_ascii_digit()) && s.len() >= 2 | |
| 434 | +} | |
| 435 | + | |
| 436 | +fn is_useful_value(s: &str) -> bool { | |
| 437 | + !s.is_empty() | |
| 438 | + && s.len() <= 15 | |
| 439 | + && !s.chars().all(|c| c.is_ascii_digit()) | |
| 440 | + && !is_hex_like(s) | |
| 441 | +} | |
| 442 | + | |
| 443 | +fn is_hex_like(s: &str) -> bool { | |
| 444 | + // UUID pattern | |
| 445 | + if s.len() == 36 && s.chars().filter(|c| *c == '-').count() == 4 { | |
| 446 | + let without_dashes: String = s.chars().filter(|c| *c != '-').collect(); | |
| 447 | + if without_dashes.len() == 32 && without_dashes.chars().all(|c| c.is_ascii_hexdigit()) { | |
| 448 | + return true; | |
| 449 | + } | |
| 450 | + } | |
| 451 | + // >50% hex digits and length > 15 | |
| 452 | + if s.len() > 15 { | |
| 453 | + let hex_count = s.chars().filter(|c| c.is_ascii_hexdigit()).count(); | |
| 454 | + return hex_count as f64 / s.len() as f64 > 0.5; | |
| 455 | + } | |
| 456 | + false | |
| 457 | +} | |
| 458 | +``` | |
| 459 | + | |
| 460 | +- [ ] **Step 3: Run tests to verify they pass** | |
| 461 | + | |
| 462 | +Run: `cargo test --lib har` | |
| 463 | +Expected: All 8 tests pass | |
| 464 | + | |
| 465 | +- [ ] **Step 4: Commit** | |
| 466 | + | |
| 467 | +```bash | |
| 468 | +git add src/har.rs | |
| 469 | +git commit -m "feat: add HAR file parsing module with intelligent naming" | |
| 470 | +``` | |
| 471 | + | |
| 472 | +--- | |
| 473 | + | |
| 474 | +### Task 2: Integrate HAR Import Into App | |
| 475 | + | |
| 476 | +**Files:** | |
| 477 | +- Modify: `src/main.rs:1-9` (add `mod har;`) | |
| 478 | +- Modify: `src/app.rs:115-137` (`import_file`, `import_directory`) | |
| 479 | +- Modify: `src/app.rs:274-284` (file dialog filters) | |
| 480 | + | |
| 481 | +- [ ] **Step 1: Add `mod har` to `src/main.rs`** | |
| 482 | + | |
| 483 | +Add after line 2 (`mod codegen;`): | |
| 484 | + | |
| 485 | +```rust | |
| 486 | +mod har; | |
| 487 | +``` | |
| 488 | + | |
| 489 | +- [ ] **Step 2: Update `import_file()` in `src/app.rs` to handle HAR files** | |
| 490 | + | |
| 491 | +First, check `src/session.rs` — `LoadedSession::add_file()` uses `PathBuf::from(path).file_name()` to derive the display filename. Since HAR synthetic paths like `"harfile.har:flights_LAX.json"` would produce wrong filenames (the colon is not a path separator on Linux), pass just the synthetic filename as the path: | |
| 492 | + | |
| 493 | +Replace `import_file` (lines 115–125) with: | |
| 494 | + | |
| 495 | +```rust | |
| 496 | +fn import_file(&mut self, path: &std::path::Path) { | |
| 497 | + if let Ok(content) = std::fs::read_to_string(path) { | |
| 498 | + let is_har = path.extension().is_some_and(|e| e == "har"); | |
| 499 | + if is_har { | |
| 500 | + // Parse HAR and extract synthetic JSON files | |
| 501 | + if let Ok(har_value) = serde_json::from_str::<serde_json::Value>(&content) { | |
| 502 | + let files = crate::har::extract_har_files(&har_value); | |
| 503 | + if let Some(loaded) = &mut self.current_session { | |
| 504 | + for (filename, value) in &files { | |
| 505 | + let json_str = serde_json::to_string_pretty(value).unwrap_or_default(); | |
| 506 | + // Pass just the filename so add_file derives the correct display name | |
| 507 | + if loaded.add_file(filename, json_str).is_ok() {} | |
| 508 | + } | |
| 509 | + self.session_manager.update_session(&loaded.session); | |
| 510 | + self.rebuild_schema(); | |
| 511 | + } | |
| 512 | + } | |
| 513 | + } else { | |
| 514 | + let path_str = path.to_string_lossy().to_string(); | |
| 515 | + if let Some(loaded) = &mut self.current_session { | |
| 516 | + if loaded.add_file(&path_str, content).is_ok() { | |
| 517 | + self.session_manager.update_session(&loaded.session); | |
| 518 | + self.rebuild_schema(); | |
| 519 | + } | |
| 520 | + } | |
| 521 | + } | |
| 522 | + } | |
| 523 | +} | |
| 524 | +``` | |
| 525 | + | |
| 526 | +- [ ] **Step 3: Update file dialog filter to accept HAR files** | |
| 527 | + | |
| 528 | +Find the import file dialog (around line 281–284 in `app.rs`) and update the filter: | |
| 529 | + | |
| 530 | +Change: | |
| 531 | +```rust | |
| 532 | +.add_filter("JSON", &["json"]) | |
| 533 | +``` | |
| 534 | +To: | |
| 535 | +```rust | |
| 536 | +.add_filter("JSON & HAR", &["json", "har"]) | |
| 537 | +``` | |
| 538 | + | |
| 539 | +- [ ] **Step 4: Update `import_directory()` to also pick up `.har` files** | |
| 540 | + | |
| 541 | +In `import_directory` (lines 127–137), change the extension filter: | |
| 542 | + | |
| 543 | +```rust | |
| 544 | +if path.is_file() && path.extension().is_some_and(|e| e == "json" || e == "har") { | |
| 545 | +``` | |
| 546 | + | |
| 547 | +- [ ] **Step 5: Update drag-and-drop handler to accept `.har` files** | |
| 548 | + | |
| 549 | +In `src/app.rs`, find `handle_dropped_files()` (around line 830) where it checks `path.extension().is_some_and(|e| e == "json")`. Update to also accept `.har`: | |
| 550 | + | |
| 551 | +```rust | |
| 552 | +if path.is_file() && path.extension().is_some_and(|e| e == "json" || e == "har") { | |
| 553 | +``` | |
| 554 | + | |
| 555 | +- [ ] **Step 6: Build and verify** | |
| 556 | + | |
| 557 | +Run: `cargo build` | |
| 558 | +Expected: Clean build (warnings OK) | |
| 559 | + | |
| 560 | +- [ ] **Step 7: Commit** | |
| 561 | + | |
| 562 | +```bash | |
| 563 | +git add src/main.rs src/app.rs src/har.rs | |
| 564 | +git commit -m "feat: integrate HAR file import into session pipeline" | |
| 565 | +``` | |
| 566 | + | |
| 567 | +--- | |
| 568 | + | |
| 569 | +## Chunk 2: Language Abstraction Layer | |
| 570 | + | |
| 571 | +### Task 3: Create Language Trait and Rust Generator | |
| 572 | + | |
| 573 | +**Files:** | |
| 574 | +- Create: `src/lang/mod.rs` | |
| 575 | +- Create: `src/lang/rust.rs` | |
| 576 | +- Modify: `src/main.rs` (add `mod lang;`) | |
| 577 | + | |
| 578 | +- [ ] **Step 1: Create `src/lang/mod.rs` with trait and enum** | |
| 579 | + | |
| 580 | +```rust | |
| 581 | +pub mod rust; | |
| 582 | +pub mod swift; | |
| 583 | + | |
| 584 | +use crate::types::InferredType; | |
| 585 | + | |
| 586 | +#[derive(Debug, Clone, Copy, PartialEq)] | |
| 587 | +pub enum CodeLanguage { | |
| 588 | + Rust, | |
| 589 | + Swift, | |
| 590 | +} | |
| 591 | + | |
| 592 | +impl CodeLanguage { | |
| 593 | + pub fn display_name(&self) -> &str { | |
| 594 | + match self { | |
| 595 | + Self::Rust => "Rust", | |
| 596 | + Self::Swift => "Swift", | |
| 597 | + } | |
| 598 | + } | |
| 599 | + | |
| 600 | + pub fn generator(&self) -> Box<dyn LanguageGenerator> { | |
| 601 | + match self { | |
| 602 | + Self::Rust => Box::new(rust::RustGenerator), | |
| 603 | + Self::Swift => Box::new(swift::SwiftGenerator), | |
| 604 | + } | |
| 605 | + } | |
| 606 | +} | |
| 607 | + | |
| 608 | +/// Language-neutral field info for code generation. | |
| 609 | +pub struct FieldInfo { | |
| 610 | + pub code_name: String, | |
| 611 | + pub json_name: String, | |
| 612 | + pub code_type: String, | |
| 613 | + pub needs_rename: bool, | |
| 614 | +} | |
| 615 | + | |
| 616 | +pub trait LanguageGenerator { | |
| 617 | + fn file_extension(&self) -> &str; | |
| 618 | + fn file_header(&self) -> String; | |
| 619 | + fn imports_header(&self, needs_temporal: bool, has_shared: bool) -> String; | |
| 620 | + fn struct_open(&self, name: &str) -> String; | |
| 621 | + /// Close a struct. `fields` is (code_name, json_name) pairs for CodingKeys etc. | |
| 622 | + fn struct_close(&self, fields: &[(String, String)]) -> String; | |
| 623 | + fn field_line(&self, code_name: &str, type_name: &str) -> String; | |
| 624 | + fn enum_open(&self, name: &str) -> String; | |
| 625 | + fn enum_close(&self) -> String; | |
| 626 | + fn enum_variant(&self, variant_name: &str, json_value: &str) -> String; | |
| 627 | + fn type_name(&self, inferred: &InferredType) -> String; | |
| 628 | + fn mod_file(&self, file_names: &[&str]) -> Option<String>; | |
| 629 | + fn sanitize_keyword(&self, name: &str) -> String; | |
| 630 | + fn field_name(&self, json_name: &str) -> String; | |
| 631 | + fn file_name(&self, base_name: &str) -> String; | |
| 632 | +} | |
| 633 | +``` | |
| 634 | + | |
| 635 | +- [ ] **Step 2: Create `src/lang/rust.rs`** | |
| 636 | + | |
| 637 | +Note: `to_snake_case` already exists in `codegen.rs:186-208`. Make it `pub` there and import it in `rust.rs` via `use crate::codegen::to_snake_case;` instead of duplicating. Same for `to_pascal_case` if needed. | |
| 638 | + | |
| 639 | +Extract existing logic from `codegen.rs`: | |
| 640 | + | |
| 641 | +```rust | |
| 642 | +use crate::codegen::to_snake_case; | |
| 643 | +use crate::lang::LanguageGenerator; | |
| 644 | +use crate::types::InferredType; | |
| 645 | + | |
| 646 | +pub struct RustGenerator; | |
| 647 | + | |
| 648 | +impl LanguageGenerator for RustGenerator { | |
| 649 | + fn file_extension(&self) -> &str { | |
| 650 | + "rs" | |
| 651 | + } | |
| 652 | + | |
| 653 | + fn file_header(&self) -> String { | |
| 654 | + "#![allow(non_snake_case)]\n".to_string() | |
| 655 | + } | |
| 656 | + | |
| 657 | + fn imports_header(&self, needs_temporal: bool, has_shared: bool) -> String { | |
| 658 | + let mut s = "use serde::{Deserialize, Serialize};\n".to_string(); | |
| 659 | + if needs_temporal { | |
| 660 | + s.push_str("use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};\n"); | |
| 661 | + } | |
| 662 | + if has_shared { | |
| 663 | + s.push_str("use crate::shared::*;\n"); | |
| 664 | + } | |
| 665 | + s | |
| 666 | + } | |
| 667 | + | |
| 668 | + fn struct_open(&self, name: &str) -> String { | |
| 669 | + format!( | |
| 670 | + "#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {} {{\n", | |
| 671 | + name | |
| 672 | + ) | |
| 673 | + } | |
| 674 | + | |
| 675 | + fn struct_close(&self, _fields: &[(String, String)]) -> String { | |
| 676 | + "}\n".to_string() | |
| 677 | + } | |
| 678 | + | |
| 679 | + fn field_line(&self, code_name: &str, type_name: &str) -> String { | |
| 680 | + format!(" pub {}: {},\n", code_name, type_name) | |
| 681 | + } | |
| 682 | + | |
| 683 | + fn enum_open(&self, name: &str) -> String { | |
| 684 | + format!( | |
| 685 | + "#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum {} {{\n", | |
| 686 | + name | |
| 687 | + ) | |
| 688 | + } | |
| 689 | + | |
| 690 | + fn enum_close(&self) -> String { | |
| 691 | + "}\n".to_string() | |
| 692 | + } | |
| 693 | + | |
| 694 | + fn enum_variant(&self, variant_name: &str, json_value: &str) -> String { | |
| 695 | + if variant_name == json_value { | |
| 696 | + format!(" {},\n", variant_name) | |
| 697 | + } else { | |
| 698 | + format!( | |
| 699 | + " #[serde(rename = \"{}\")]\n {},\n", | |
| 700 | + json_value, variant_name | |
| 701 | + ) | |
| 702 | + } | |
| 703 | + } | |
| 704 | + | |
| 705 | + fn type_name(&self, inferred: &InferredType) -> String { | |
| 706 | + inferred.rust_type() | |
| 707 | + } | |
| 708 | + | |
| 709 | + fn mod_file(&self, file_names: &[&str]) -> Option<String> { | |
| 710 | + let mut code = String::new(); | |
| 711 | + for name in file_names { | |
| 712 | + let mod_name = name.trim_end_matches(".rs"); | |
| 713 | + code.push_str(&format!("mod {};\npub use {}::*;\n\n", mod_name, mod_name)); | |
| 714 | + } | |
| 715 | + Some(code) | |
| 716 | + } | |
| 717 | + | |
| 718 | + fn sanitize_keyword(&self, name: &str) -> String { | |
| 719 | + match name { | |
| 720 | + "type" | "struct" | "enum" | "fn" | "let" | "mut" | "ref" | "self" | "super" | |
| 721 | + | "mod" | "use" | "pub" | "crate" | "impl" | "trait" | "for" | "loop" | "while" | |
| 722 | + | "if" | "else" | "match" | "return" | "break" | "continue" | "move" | "async" | |
| 723 | + | "await" | "dyn" | "static" | "const" | "where" | "unsafe" | "extern" | "as" | |
| 724 | + | "in" => format!("r#{}", name), | |
| 725 | + _ => name.to_string(), | |
| 726 | + } | |
| 727 | + } | |
| 728 | + | |
| 729 | + fn field_name(&self, json_name: &str) -> String { | |
| 730 | + self.sanitize_keyword(&to_snake_case(json_name)) | |
| 731 | + } | |
| 732 | + | |
| 733 | + fn file_name(&self, base_name: &str) -> String { | |
| 734 | + format!("{}.rs", base_name) | |
| 735 | + } | |
| 736 | +} | |
| 737 | + | |
| 738 | +// to_snake_case imported from crate::codegen (make it `pub` there) | |
| 739 | +``` | |
| 740 | + | |
| 741 | +- [ ] **Step 3: Add `mod lang` to `src/main.rs`** | |
| 742 | + | |
| 743 | +Add after the `mod har;` line: | |
| 744 | + | |
| 745 | +```rust | |
| 746 | +mod lang; | |
| 747 | +``` | |
| 748 | + | |
| 749 | +- [ ] **Step 4: Create stub `src/lang/swift.rs`** | |
| 750 | + | |
| 751 | +Minimal stub so it compiles: | |
| 752 | + | |
| 753 | +```rust | |
| 754 | +use crate::lang::LanguageGenerator; | |
| 755 | +use crate::types::InferredType; | |
| 756 | + | |
| 757 | +pub struct SwiftGenerator; | |
| 758 | + | |
| 759 | +impl LanguageGenerator for SwiftGenerator { | |
| 760 | + fn file_extension(&self) -> &str { "swift" } | |
| 761 | + fn file_header(&self) -> String { String::new() } | |
| 762 | + fn imports_header(&self, _needs_temporal: bool, _has_shared: bool) -> String { | |
| 763 | + "import Foundation\n".to_string() | |
| 764 | + } | |
| 765 | + fn struct_open(&self, name: &str) -> String { | |
| 766 | + format!("struct {}: Codable {{\n", name) | |
| 767 | + } | |
| 768 | + fn struct_close(&self, _fields: &[(String, String)]) -> String { | |
| 769 | + "}\n".to_string() | |
| 770 | + } | |
| 771 | + fn field_line(&self, code_name: &str, type_name: &str) -> String { | |
| 772 | + format!(" let {}: {}\n", code_name, type_name) | |
| 773 | + } | |
| 774 | + fn enum_open(&self, name: &str) -> String { | |
| 775 | + format!("enum {}: String, Codable {{\n", name) | |
| 776 | + } | |
| 777 | + fn enum_close(&self) -> String { "}\n".to_string() } | |
| 778 | + fn enum_variant(&self, variant_name: &str, json_value: &str) -> String { | |
| 779 | + if variant_name == json_value { | |
| 780 | + format!(" case {}\n", variant_name) | |
| 781 | + } else { | |
| 782 | + format!(" case {} = \"{}\"\n", variant_name, json_value) | |
| 783 | + } | |
| 784 | + } | |
| 785 | + fn type_name(&self, inferred: &InferredType) -> String { | |
| 786 | + inferred.rust_type() // placeholder — will be replaced in Task 4 | |
| 787 | + } | |
| 788 | + fn mod_file(&self, _file_names: &[&str]) -> Option<String> { None } | |
| 789 | + fn sanitize_keyword(&self, name: &str) -> String { name.to_string() } | |
| 790 | + fn field_name(&self, json_name: &str) -> String { json_name.to_string() } | |
| 791 | + fn file_name(&self, base_name: &str) -> String { | |
| 792 | + format!("{}.swift", capitalize_first(base_name)) | |
| 793 | + } | |
| 794 | +} | |
| 795 | + | |
| 796 | +fn capitalize_first(s: &str) -> String { | |
| 797 | + let mut c = s.chars(); | |
| 798 | + match c.next() { | |
| 799 | + None => String::new(), | |
| 800 | + Some(f) => f.to_uppercase().collect::<String>() + c.as_str(), | |
| 801 | + } | |
| 802 | +} | |
| 803 | +``` | |
| 804 | + | |
| 805 | +- [ ] **Step 5: Build and verify** | |
| 806 | + | |
| 807 | +Run: `cargo build` | |
| 808 | +Expected: Clean build | |
| 809 | + | |
| 810 | +- [ ] **Step 6: Commit** | |
| 811 | + | |
| 812 | +```bash | |
| 813 | +git add src/lang/mod.rs src/lang/rust.rs src/lang/swift.rs src/main.rs | |
| 814 | +git commit -m "feat: add language abstraction layer with Rust generator and Swift stub" | |
| 815 | +``` | |
| 816 | + | |
| 817 | +--- | |
| 818 | + | |
| 819 | +### Task 4: Full Swift Generator | |
| 820 | + | |
| 821 | +**Files:** | |
| 822 | +- Modify: `src/lang/swift.rs` | |
| 823 | + | |
| 824 | +- [ ] **Step 1: Write tests for Swift generator** | |
| 825 | + | |
| 826 | +Add to bottom of `src/lang/swift.rs`: | |
| 827 | + | |
| 828 | +```rust | |
| 829 | +#[cfg(test)] | |
| 830 | +mod tests { | |
| 831 | + use super::*; | |
| 832 | + use crate::types::InferredType; | |
| 833 | + use std::collections::BTreeMap; | |
| 834 | + | |
| 835 | + #[test] | |
| 836 | + fn test_swift_type_names() { | |
| 837 | + let gen = SwiftGenerator; | |
| 838 | + assert_eq!(gen.type_name(&InferredType::String), "String"); | |
| 839 | + assert_eq!(gen.type_name(&InferredType::I64), "Int"); | |
| 840 | + assert_eq!(gen.type_name(&InferredType::F64), "Double"); | |
| 841 | + assert_eq!(gen.type_name(&InferredType::Bool), "Bool"); | |
| 842 | + assert_eq!(gen.type_name(&InferredType::DateTime), "Date"); | |
| 843 | + assert_eq!(gen.type_name(&InferredType::Date), "Date"); | |
| 844 | + assert_eq!(gen.type_name(&InferredType::Time), "Date"); | |
| 845 | + assert_eq!(gen.type_name(&InferredType::Null), "Any?"); | |
| 846 | + assert_eq!(gen.type_name(&InferredType::Unknown), "Any"); | |
| 847 | + } | |
| 848 | + | |
| 849 | + #[test] | |
| 850 | + fn test_swift_array_type() { | |
| 851 | + let gen = SwiftGenerator; | |
| 852 | + assert_eq!( | |
| 853 | + gen.type_name(&InferredType::Array(Box::new(InferredType::String))), | |
| 854 | + "[String]" | |
| 855 | + ); | |
| 856 | + } | |
| 857 | + | |
| 858 | + #[test] | |
| 859 | + fn test_swift_option_type() { | |
| 860 | + let gen = SwiftGenerator; | |
| 861 | + assert_eq!( | |
| 862 | + gen.type_name(&InferredType::Option(Box::new(InferredType::I64))), | |
| 863 | + "Int?" | |
| 864 | + ); | |
| 865 | + } | |
| 866 | + | |
| 867 | + #[test] | |
| 868 | + fn test_swift_object_fallback() { | |
| 869 | + let gen = SwiftGenerator; | |
| 870 | + assert_eq!( | |
| 871 | + gen.type_name(&InferredType::Object(BTreeMap::new())), | |
| 872 | + "[String: Any]" | |
| 873 | + ); | |
| 874 | + } | |
| 875 | + | |
| 876 | + #[test] | |
| 877 | + fn test_to_camel_case() { | |
| 878 | + assert_eq!(to_camel_case("departure_date"), "departureDate"); | |
| 879 | + assert_eq!(to_camel_case("seat_abbreviation"), "seatAbbreviation"); | |
| 880 | + assert_eq!(to_camel_case("id"), "id"); | |
| 881 | + assert_eq!(to_camel_case("already_camel"), "alreadyCamel"); | |
| 882 | + assert_eq!(to_camel_case("origin"), "origin"); | |
| 883 | + } | |
| 884 | + | |
| 885 | + #[test] | |
| 886 | + fn test_swift_field_name() { | |
| 887 | + let gen = SwiftGenerator; | |
| 888 | + assert_eq!(gen.field_name("departure_date"), "departureDate"); | |
| 889 | + assert_eq!(gen.field_name("class"), "`class`"); | |
| 890 | + assert_eq!(gen.field_name("origin"), "origin"); | |
| 891 | + } | |
| 892 | + | |
| 893 | + #[test] | |
| 894 | + fn test_coding_keys() { | |
| 895 | + let gen = SwiftGenerator; | |
| 896 | + let fields = vec![ | |
| 897 | + ("origin".to_string(), "origin".to_string()), | |
| 898 | + ("departureDate".to_string(), "departure_date".to_string()), | |
| 899 | + ("seatAbbreviation".to_string(), "seat_abbreviation".to_string()), | |
| 900 | + ]; | |
| 901 | + let result = gen.struct_close(&fields); | |
| 902 | + assert!(result.contains("enum CodingKeys: String, CodingKey")); | |
| 903 | + assert!(result.contains("case departureDate = \"departure_date\"")); | |
| 904 | + assert!(result.contains("case seatAbbreviation = \"seat_abbreviation\"")); | |
| 905 | + // origin should have no = since names match | |
| 906 | + assert!(result.contains("case origin\n") || result.contains("case origin\r\n")); | |
| 907 | + } | |
| 908 | + | |
| 909 | + #[test] | |
| 910 | + fn test_coding_keys_omitted_when_all_match() { | |
| 911 | + let gen = SwiftGenerator; | |
| 912 | + let fields = vec![ | |
| 913 | + ("origin".to_string(), "origin".to_string()), | |
| 914 | + ("id".to_string(), "id".to_string()), | |
| 915 | + ]; | |
| 916 | + let result = gen.struct_close(&fields); | |
| 917 | + assert!(!result.contains("CodingKeys")); | |
| 918 | + } | |
| 919 | + | |
| 920 | + #[test] | |
| 921 | + fn test_swift_keyword_sanitize() { | |
| 922 | + let gen = SwiftGenerator; | |
| 923 | + assert_eq!(gen.sanitize_keyword("class"), "`class`"); | |
| 924 | + assert_eq!(gen.sanitize_keyword("self"), "`self`"); | |
| 925 | + assert_eq!(gen.sanitize_keyword("origin"), "origin"); | |
| 926 | + } | |
| 927 | + | |
| 928 | + #[test] | |
| 929 | + fn test_swift_file_name() { | |
| 930 | + let gen = SwiftGenerator; | |
| 931 | + assert_eq!(gen.file_name("shared"), "Shared.swift"); | |
| 932 | + assert_eq!(gen.file_name("flights"), "Flights.swift"); | |
| 933 | + } | |
| 934 | +} | |
| 935 | +``` | |
| 936 | + | |
| 937 | +- [ ] **Step 2: Implement full Swift generator** | |
| 938 | + | |
| 939 | +Replace the stub in `src/lang/swift.rs`: | |
| 940 | + | |
| 941 | +```rust | |
| 942 | +use crate::lang::LanguageGenerator; | |
| 943 | +use crate::types::InferredType; | |
| 944 | + | |
| 945 | +pub struct SwiftGenerator; | |
| 946 | + | |
| 947 | +const SWIFT_KEYWORDS: &[&str] = &[ | |
| 948 | + "class", "struct", "enum", "protocol", "func", "var", "let", "import", "return", | |
| 949 | + "self", "super", "default", "case", "switch", "where", "in", "is", "as", "try", | |
| 950 | + "throw", "throws", "nil", "true", "false", "do", "catch", "guard", "defer", | |
| 951 | + "repeat", "break", "continue", "fallthrough", "typealias", "associatedtype", | |
| 952 | + "operator", "init", "deinit", "subscript", "private", "public", "internal", | |
| 953 | + "open", "fileprivate", "static", "override", "mutating", "inout", "Any", | |
| 954 | + "Type", "Self", | |
| 955 | +]; | |
| 956 | + | |
| 957 | +impl LanguageGenerator for SwiftGenerator { | |
| 958 | + fn file_extension(&self) -> &str { | |
| 959 | + "swift" | |
| 960 | + } | |
| 961 | + | |
| 962 | + fn file_header(&self) -> String { | |
| 963 | + String::new() | |
| 964 | + } | |
| 965 | + | |
| 966 | + fn imports_header(&self, _needs_temporal: bool, _has_shared: bool) -> String { | |
| 967 | + "import Foundation\n".to_string() | |
| 968 | + } | |
| 969 | + | |
| 970 | + fn struct_open(&self, name: &str) -> String { | |
| 971 | + format!("struct {}: Codable {{\n", name) | |
| 972 | + } | |
| 973 | + | |
| 974 | + fn struct_close(&self, fields: &[(String, String)]) -> String { | |
| 975 | + let needs_coding_keys = fields.iter().any(|(code, json)| code != json); | |
| 976 | + if !needs_coding_keys { | |
| 977 | + return "}\n".to_string(); | |
| 978 | + } | |
| 979 | + let mut s = String::new(); | |
| 980 | + s.push('\n'); | |
| 981 | + s.push_str(" enum CodingKeys: String, CodingKey {\n"); | |
| 982 | + for (code_name, json_name) in fields { | |
| 983 | + if code_name == json_name { | |
| 984 | + s.push_str(&format!(" case {}\n", code_name)); | |
| 985 | + } else { | |
| 986 | + s.push_str(&format!(" case {} = \"{}\"\n", code_name, json_name)); | |
| 987 | + } | |
| 988 | + } | |
| 989 | + s.push_str(" }\n"); | |
| 990 | + s.push_str("}\n"); | |
| 991 | + s | |
| 992 | + } | |
| 993 | + | |
| 994 | + fn field_line(&self, code_name: &str, type_name: &str) -> String { | |
| 995 | + format!(" let {}: {}\n", code_name, type_name) | |
| 996 | + } | |
| 997 | + | |
| 998 | + fn enum_open(&self, name: &str) -> String { | |
| 999 | + format!("enum {}: String, Codable {{\n", name) | |
| 1000 | + } | |
| 1001 | + | |
| 1002 | + fn enum_close(&self) -> String { | |
| 1003 | + "}\n".to_string() | |
| 1004 | + } | |
| 1005 | + | |
| 1006 | + fn enum_variant(&self, variant_name: &str, json_value: &str) -> String { | |
| 1007 | + if variant_name == json_value { | |
| 1008 | + format!(" case {}\n", variant_name) | |
| 1009 | + } else { | |
| 1010 | + format!(" case {} = \"{}\"\n", variant_name, json_value) | |
| 1011 | + } | |
| 1012 | + } | |
| 1013 | + | |
| 1014 | + fn type_name(&self, inferred: &InferredType) -> String { | |
| 1015 | + match inferred { | |
| 1016 | + InferredType::Null => "Any?".to_string(), | |
| 1017 | + InferredType::Bool => "Bool".to_string(), | |
| 1018 | + InferredType::I64 => "Int".to_string(), | |
| 1019 | + InferredType::F64 => "Double".to_string(), | |
| 1020 | + InferredType::String => "String".to_string(), | |
| 1021 | + InferredType::DateTime | InferredType::Date | InferredType::Time => "Date".to_string(), | |
| 1022 | + InferredType::Array(inner) => format!("[{}]", self.type_name(inner)), | |
| 1023 | + InferredType::Object(_) => "[String: Any]".to_string(), | |
| 1024 | + InferredType::Option(inner) => format!("{}?", self.type_name(inner)), | |
| 1025 | + InferredType::Mixed(_) => "Any".to_string(), | |
| 1026 | + InferredType::Unknown => "Any".to_string(), | |
| 1027 | + } | |
| 1028 | + } | |
| 1029 | + | |
| 1030 | + fn mod_file(&self, _file_names: &[&str]) -> Option<String> { | |
| 1031 | + None | |
| 1032 | + } | |
| 1033 | + | |
| 1034 | + fn sanitize_keyword(&self, name: &str) -> String { | |
| 1035 | + if SWIFT_KEYWORDS.contains(&name) { | |
| 1036 | + format!("`{}`", name) | |
| 1037 | + } else { | |
| 1038 | + name.to_string() | |
| 1039 | + } | |
| 1040 | + } | |
| 1041 | + | |
| 1042 | + fn field_name(&self, json_name: &str) -> String { | |
| 1043 | + let camel = to_camel_case(json_name); | |
| 1044 | + self.sanitize_keyword(&camel) | |
| 1045 | + } | |
| 1046 | + | |
| 1047 | + fn file_name(&self, base_name: &str) -> String { | |
| 1048 | + format!("{}.swift", capitalize_first(base_name)) | |
| 1049 | + } | |
| 1050 | +} | |
| 1051 | + | |
| 1052 | +fn to_camel_case(s: &str) -> String { | |
| 1053 | + let mut result = String::new(); | |
| 1054 | + let mut capitalize_next = false; | |
| 1055 | + for ch in s.chars() { | |
| 1056 | + if ch == '_' || ch == '-' || ch == ' ' { | |
| 1057 | + capitalize_next = true; | |
| 1058 | + } else if capitalize_next { | |
| 1059 | + result.push(ch.to_ascii_uppercase()); | |
| 1060 | + capitalize_next = false; | |
| 1061 | + } else { | |
| 1062 | + result.push(ch); | |
| 1063 | + } | |
| 1064 | + } | |
| 1065 | + result | |
| 1066 | +} | |
| 1067 | + | |
| 1068 | +fn capitalize_first(s: &str) -> String { | |
| 1069 | + let mut c = s.chars(); | |
| 1070 | + match c.next() { | |
| 1071 | + None => String::new(), | |
| 1072 | + Some(f) => f.to_uppercase().collect::<String>() + c.as_str(), | |
| 1073 | + } | |
| 1074 | +} | |
| 1075 | +``` | |
| 1076 | + | |
| 1077 | +- [ ] **Step 3: Run tests** | |
| 1078 | + | |
| 1079 | +Run: `cargo test --lib lang` | |
| 1080 | +Expected: All Swift generator tests pass | |
| 1081 | + | |
| 1082 | +- [ ] **Step 4: Commit** | |
| 1083 | + | |
| 1084 | +```bash | |
| 1085 | +git add src/lang/swift.rs | |
| 1086 | +git commit -m "feat: implement full Swift code generator with CodingKeys and camelCase" | |
| 1087 | +``` | |
| 1088 | + | |
| 1089 | +--- | |
| 1090 | + | |
| 1091 | +## Chunk 3: Wire Language Into Code View | |
| 1092 | + | |
| 1093 | +### Task 5: Refactor `CodeGenerator` to Use `LanguageGenerator` | |
| 1094 | + | |
| 1095 | +**Files:** | |
| 1096 | +- Modify: `src/codegen.rs:15-21,35-62,64-143,145-179,182-184` | |
| 1097 | +- Modify: `src/views/schema_diagram.rs` (references `rust_type` on `GeneratedField`) | |
| 1098 | +- Modify: `src/views/code.rs` (references `rust_type` on `GeneratedField`) | |
| 1099 | + | |
| 1100 | +The key architectural change: `GeneratedField` stores the raw `InferredType` instead of pre-resolved language-specific strings. Type resolution happens at render time via `lang.type_name()`. This ensures Swift gets `[String]` not `Vec<String>`. | |
| 1101 | + | |
| 1102 | +- [ ] **Step 1: Update `GeneratedField` to store `InferredType`** | |
| 1103 | + | |
| 1104 | +In `src/codegen.rs`, change the struct (lines 15–21): | |
| 1105 | + | |
| 1106 | +```rust | |
| 1107 | +#[derive(Debug, Clone)] | |
| 1108 | +pub struct GeneratedField { | |
| 1109 | + pub json_name: String, | |
| 1110 | + pub inferred_type: crate::types::InferredType, | |
| 1111 | + pub needs_rename: bool, | |
| 1112 | +} | |
| 1113 | +``` | |
| 1114 | + | |
| 1115 | +Remove the `rust_name` and `rust_type`/`code_type` fields entirely. Language-specific names and types are computed at render time. | |
| 1116 | + | |
| 1117 | +- [ ] **Step 2: Update `from_schema` to populate new field structure** | |
| 1118 | + | |
| 1119 | +In `from_schema` (lines 35–62), change field construction: | |
| 1120 | + | |
| 1121 | +```rust | |
| 1122 | +let rust_name = to_snake_case(key); | |
| 1123 | +let needs_rename = rust_name != *key; | |
| 1124 | +GeneratedField { | |
| 1125 | + json_name: key.clone(), | |
| 1126 | + inferred_type: typ.clone(), | |
| 1127 | + needs_rename, | |
| 1128 | +} | |
| 1129 | +``` | |
| 1130 | + | |
| 1131 | +- [ ] **Step 3: Update `collect_structs` to populate new field structure** | |
| 1132 | + | |
| 1133 | +In `collect_structs` (lines 64–143), the field construction currently stores `rust_type: String`. Change to store `inferred_type`: | |
| 1134 | + | |
| 1135 | +For object fields (line 78–82): store the child struct name as a string type won't work anymore since we need the struct name. Add a helper: when the value is an Object or Array-of-Object, store the inferred type but also track that the type refers to a child struct. The simplest approach: keep a separate `child_struct_name: Option<String>` field on `GeneratedField`, or handle this in `generate_code` by checking if the `InferredType` is `Object`. | |
| 1136 | + | |
| 1137 | +Actually, the simpler approach: `collect_structs` already handles child struct names by returning them directly as type strings. Since this is codegen-specific naming (PascalCase struct names), keep `collect_structs` storing these as resolved names. Add a `resolved_type: Option<String>` field that `generate_code` uses when present (for struct references), falling back to `lang.type_name(&field.inferred_type)` otherwise: | |
| 1138 | + | |
| 1139 | +```rust | |
| 1140 | +#[derive(Debug, Clone)] | |
| 1141 | +pub struct GeneratedField { | |
| 1142 | + pub json_name: String, | |
| 1143 | + pub inferred_type: crate::types::InferredType, | |
| 1144 | + pub resolved_type: Option<String>, // Set when type is a child struct name like "Vec<Passenger>" | |
| 1145 | + pub needs_rename: bool, | |
| 1146 | +} | |
| 1147 | +``` | |
| 1148 | + | |
| 1149 | +In `collect_structs`, set `resolved_type = Some(child_name)` for Object/Array-of-Object fields, and `resolved_type = None` for primitive types. | |
| 1150 | + | |
| 1151 | +- [ ] **Step 4: Update `generate_code` to use `LanguageGenerator`** | |
| 1152 | + | |
| 1153 | +Replace `generate_code()` (lines 145–179): | |
| 1154 | + | |
| 1155 | +```rust | |
| 1156 | + pub fn generate_code(&self, lang: &dyn crate::lang::LanguageGenerator) -> String { | |
| 1157 | + let mut output = String::new(); | |
| 1158 | + let header = lang.file_header(); | |
| 1159 | + if !header.is_empty() { | |
| 1160 | + output.push_str(&header); | |
| 1161 | + output.push('\n'); | |
| 1162 | + } | |
| 1163 | + | |
| 1164 | + let needs_temporal = self.structs.iter().any(|s| { | |
| 1165 | + s.fields.iter().any(|f| { | |
| 1166 | + matches!( | |
| 1167 | + f.inferred_type, | |
| 1168 | + crate::types::InferredType::DateTime | |
| 1169 | + | crate::types::InferredType::Date | |
| 1170 | + | crate::types::InferredType::Time | |
| 1171 | + ) | |
| 1172 | + }) | |
| 1173 | + }); | |
| 1174 | + output.push_str(&lang.imports_header(needs_temporal, false)); | |
| 1175 | + output.push('\n'); | |
| 1176 | + | |
| 1177 | + for (i, s) in self.structs.iter().rev().enumerate() { | |
| 1178 | + if i > 0 { | |
| 1179 | + output.push('\n'); | |
| 1180 | + } | |
| 1181 | + output.push_str(&lang.struct_open(&s.name)); | |
| 1182 | + let mut field_pairs: Vec<(String, String)> = Vec::new(); | |
| 1183 | + for field in &s.fields { | |
| 1184 | + let code_name = lang.field_name(&field.json_name); | |
| 1185 | + let type_str = field.resolved_type.clone() | |
| 1186 | + .unwrap_or_else(|| lang.type_name(&field.inferred_type)); | |
| 1187 | + output.push_str(&lang.field_line(&code_name, &type_str)); | |
| 1188 | + field_pairs.push((code_name, field.json_name.clone())); | |
| 1189 | + } | |
| 1190 | + output.push_str(&lang.struct_close(&field_pairs)); | |
| 1191 | + } | |
| 1192 | + | |
| 1193 | + output | |
| 1194 | + } | |
| 1195 | +``` | |
| 1196 | + | |
| 1197 | +- [ ] **Step 5: Remove `resolve_type` function** | |
| 1198 | + | |
| 1199 | +Delete the `resolve_type` function (lines 182–184) — no longer needed since type resolution goes through the trait. | |
| 1200 | + | |
| 1201 | +- [ ] **Step 6: Fix all references to removed fields in other files** | |
| 1202 | + | |
| 1203 | +The compiler will report errors in files that reference `rust_type`, `rust_name`, or `code_type` on `GeneratedField`. Known locations: | |
| 1204 | + | |
| 1205 | +- `src/views/schema_diagram.rs` — references `f.rust_type` (around line 182). Change to `f.resolved_type.clone().unwrap_or_else(|| f.inferred_type.rust_type())` or use the Rust generator's `type_name()`. | |
| 1206 | +- `src/views/code.rs` — the `generate_file_code_structs` function and `prefix_type` function reference `field.rust_type`. These need to use `field.resolved_type` or `lang.type_name(&field.inferred_type)`. | |
| 1207 | + | |
| 1208 | +Fix each compiler error by using the appropriate resolution. For code that needs a Rust-specific type string (like schema_diagram which always displays Rust types), call `field.inferred_type.rust_type()` directly. | |
| 1209 | + | |
| 1210 | +- [ ] **Step 7: Build and verify** | |
| 1211 | + | |
| 1212 | +Run: `cargo build` | |
| 1213 | +Expected: Clean build with no errors referencing old field names. | |
| 1214 | + | |
| 1215 | +- [ ] **Step 8: Commit** | |
| 1216 | + | |
| 1217 | +```bash | |
| 1218 | +git add src/codegen.rs src/views/schema_diagram.rs src/views/code.rs | |
| 1219 | +git commit -m "refactor: make CodeGenerator language-neutral with LanguageGenerator trait" | |
| 1220 | +``` | |
| 1221 | + | |
| 1222 | +--- | |
| 1223 | + | |
| 1224 | +### Task 6: Add Language Toggle to Code View | |
| 1225 | + | |
| 1226 | +**Files:** | |
| 1227 | +- Modify: `src/views/code.rs` (CodeView struct, rebuild_file_mode, show_selected top bar) | |
| 1228 | + | |
| 1229 | +- [ ] **Step 1: Add `selected_language` to `CodeView`** | |
| 1230 | + | |
| 1231 | +In the `CodeView` struct (around line 55), add: | |
| 1232 | + | |
| 1233 | +```rust | |
| 1234 | + selected_language: crate::lang::CodeLanguage, | |
| 1235 | +``` | |
| 1236 | + | |
| 1237 | +Initialize in `new()`: | |
| 1238 | + | |
| 1239 | +```rust | |
| 1240 | + selected_language: crate::lang::CodeLanguage::Rust, | |
| 1241 | +``` | |
| 1242 | + | |
| 1243 | +- [ ] **Step 2: Include language in cache key computation** | |
| 1244 | + | |
| 1245 | +In `CodeView::show()`, the cache key formula (around lines 141-145) uses pointer address, file count, etc. Add the selected language to the cache key so switching languages triggers rebuild: | |
| 1246 | + | |
| 1247 | +```rust | |
| 1248 | +let lang_hash = self.selected_language as u64; | |
| 1249 | +let new_key = /* existing computation */ ^ lang_hash; | |
| 1250 | +``` | |
| 1251 | + | |
| 1252 | +- [ ] **Step 3: Add language toggle to the top bar** | |
| 1253 | + | |
| 1254 | +In `show_selected()`, after the "Download" button (around line 484), add: | |
| 1255 | + | |
| 1256 | +```rust | |
| 1257 | + ui.separator(); | |
| 1258 | + let lang = &mut self.selected_language; | |
| 1259 | + let prev_lang = *lang; | |
| 1260 | + ui.horizontal(|ui| { | |
| 1261 | + ui.selectable_value(lang, crate::lang::CodeLanguage::Rust, "Rust"); | |
| 1262 | + ui.selectable_value(lang, crate::lang::CodeLanguage::Swift, "Swift"); | |
| 1263 | + }); | |
| 1264 | + if *lang != prev_lang { | |
| 1265 | + self.cache_key = 0; | |
| 1266 | + } | |
| 1267 | +``` | |
| 1268 | + | |
| 1269 | +Note: `self` is borrowed mutably for `lang`, so the implementer may need to restructure slightly — e.g., read `selected_language` into a local, render the toggle, then write back. The exact pattern depends on how the surrounding closure borrows work. | |
| 1270 | + | |
| 1271 | +- [ ] **Step 3: Update `rebuild_file_mode()` to use `LanguageGenerator`** | |
| 1272 | + | |
| 1273 | +This is the biggest change. In `rebuild_file_mode()`: | |
| 1274 | + | |
| 1275 | +1. Get the generator at the top: | |
| 1276 | +```rust | |
| 1277 | +let lang = self.selected_language.generator(); | |
| 1278 | +``` | |
| 1279 | + | |
| 1280 | +2. Replace all `".rs"` extensions with `lang.file_extension()`: | |
| 1281 | + - `"shared.rs"` → `lang.file_name("shared")` | |
| 1282 | + - `format!("{}.rs", group_key)` → `lang.file_name(&group_key)` | |
| 1283 | + - `"enums.rs"` → `lang.file_name("enums")` | |
| 1284 | + | |
| 1285 | +3. Replace hardcoded Rust code generation with `lang` trait calls: | |
| 1286 | + - The `CodeGenerator::generate_code()` calls should pass `lang.as_ref()` | |
| 1287 | + - Enum generation block should use `lang.enum_open()`, `lang.enum_variant()`, `lang.enum_close()` | |
| 1288 | + - mod.rs generation: call `lang.mod_file()`, skip if `None` | |
| 1289 | + | |
| 1290 | +4. Replace the `generate_mod_rs` call with `lang.mod_file()`: | |
| 1291 | +```rust | |
| 1292 | +if let Some(mod_code) = lang.mod_file(&names) { | |
| 1293 | + // ... create mod file | |
| 1294 | +} | |
| 1295 | +``` | |
| 1296 | + | |
| 1297 | +5. File name matching: update string comparisons like `f.name == "enums.rs"` to use the language-aware name or use a flag on `GeneratedFile` to identify file types. | |
| 1298 | + | |
| 1299 | +The implementer should work through each section of `rebuild_file_mode()` methodically, replacing hardcoded Rust syntax with trait method calls. The structure of the method (shared → groups → enums → mod) stays the same. | |
| 1300 | + | |
| 1301 | +- [ ] **Step 4: Build and verify** | |
| 1302 | + | |
| 1303 | +Run: `cargo build` | |
| 1304 | +Expected: Clean build | |
| 1305 | + | |
| 1306 | +- [ ] **Step 5: Update `build_struct_index` for Swift patterns** | |
| 1307 | + | |
| 1308 | +The existing `build_struct_index` (code.rs) looks for `pub struct`, `pub enum`, `pub type`. For Swift, structs are `struct Name: Codable {` and enums are `enum Name: String, Codable {` (no `pub` prefix). Update to detect both: | |
| 1309 | + | |
| 1310 | +```rust | |
| 1311 | +// Detect struct declarations (Rust: "pub struct X", Swift: "struct X: Codable") | |
| 1312 | +let struct_name = trimmed.strip_prefix("pub struct ") | |
| 1313 | + .or_else(|| trimmed.strip_prefix("struct ")) | |
| 1314 | + .and_then(|rest| { | |
| 1315 | + rest.split(|c: char| c == ' ' || c == '{' || c == '<' || c == ':') | |
| 1316 | + .next() | |
| 1317 | + .filter(|n| !n.is_empty()) | |
| 1318 | + }); | |
| 1319 | +``` | |
| 1320 | + | |
| 1321 | +Apply the same pattern for `pub enum` / `enum`. | |
| 1322 | + | |
| 1323 | +- [ ] **Step 6: Update hidden field filtering for Swift** | |
| 1324 | + | |
| 1325 | +The `filter_hidden_fields_from_block` and `filter_hidden_fields_from_code` functions parse `pub field: Type,` patterns. Add Swift pattern detection: `let field: Type` lines. Use the selected language to determine which pattern to match. | |
| 1326 | + | |
| 1327 | +- [ ] **Step 7: Manually test** | |
| 1328 | + | |
| 1329 | +Run the app, load some JSON files, verify: | |
| 1330 | +1. Rust output looks identical to before | |
| 1331 | +2. Switching to Swift shows Swift output with `import Foundation`, `struct: Codable`, `let` fields | |
| 1332 | +3. CodingKeys appear when field names differ | |
| 1333 | +4. Enum conversion still works in both languages | |
| 1334 | +5. Download creates files with correct extensions | |
| 1335 | +6. Clicking a type name navigates to its definition in Swift mode | |
| 1336 | + | |
| 1337 | +- [ ] **Step 8: Commit** | |
| 1338 | + | |
| 1339 | +```bash | |
| 1340 | +git add src/views/code.rs | |
| 1341 | +git commit -m "feat: add language toggle with Swift code generation in code view" | |
| 1342 | +``` | |
| 1343 | + | |
| 1344 | +--- | |
| 1345 | + | |
| 1346 | +### Task 7: Language-Aware Syntax Highlighting | |
| 1347 | + | |
| 1348 | +**Files:** | |
| 1349 | +- Modify: `src/views/code.rs` (`render_rust_line` → `render_code_line`) | |
| 1350 | + | |
| 1351 | +- [ ] **Step 1: Add `CodeLanguage` parameter to `render_rust_line`** | |
| 1352 | + | |
| 1353 | +Rename `render_rust_line` to `render_code_line` and add a `language: CodeLanguage` parameter. | |
| 1354 | + | |
| 1355 | +Update the call site in `show_selected` to pass `self.selected_language`. | |
| 1356 | + | |
| 1357 | +- [ ] **Step 2: Add Swift keyword detection** | |
| 1358 | + | |
| 1359 | +In `render_code_line`, add Swift-specific line detection alongside the existing Rust patterns: | |
| 1360 | + | |
| 1361 | +For Swift, these patterns apply: | |
| 1362 | +- `import Foundation` → MAUVE (like `use` in Rust) | |
| 1363 | +- `struct Name: Codable {` → keyword MAUVE, name YELLOW (like `pub enum` handling) | |
| 1364 | +- `enum Name: String, Codable {` → keyword MAUVE, name YELLOW | |
| 1365 | +- `let field: Type` → field handling (like `pub field: Type,` in Rust) | |
| 1366 | +- `case variant` / `case variant = "value"` → GREEN (like enum variants) | |
| 1367 | +- `enum CodingKeys: String, CodingKey {` → YELLOW (like attribute) | |
| 1368 | + | |
| 1369 | +The simplest approach: add a branch early in `render_code_line` that checks `language == Swift` and handles Swift-specific patterns, falling through to existing Rust handling otherwise. Many patterns (empty lines, `}`, comments) are shared. | |
| 1370 | + | |
| 1371 | +- [ ] **Step 3: Update field line parsing for Swift** | |
| 1372 | + | |
| 1373 | +Rust fields: `pub field: Type,` (split on `:`, field part starts with `pub `) | |
| 1374 | +Swift fields: `let field: Type` (split on `:`, field part starts with `let `) | |
| 1375 | + | |
| 1376 | +Add detection for Swift field pattern: | |
| 1377 | +```rust | |
| 1378 | +let is_field_line = match language { | |
| 1379 | + CodeLanguage::Rust => trimmed.starts_with("pub ") && trimmed.contains(':'), | |
| 1380 | + CodeLanguage::Swift => trimmed.starts_with("let ") && trimmed.contains(':'), | |
| 1381 | +}; | |
| 1382 | +``` | |
| 1383 | + | |
| 1384 | +The interactive features (clickable types, enum buttons, hide/show) work the same way — they just need the field name extracted from the correct prefix (`pub ` vs `let `). | |
| 1385 | + | |
| 1386 | +- [ ] **Step 4: Build and verify** | |
| 1387 | + | |
| 1388 | +Run: `cargo build` | |
| 1389 | +Expected: Clean build | |
| 1390 | + | |
| 1391 | +- [ ] **Step 5: Commit** | |
| 1392 | + | |
| 1393 | +```bash | |
| 1394 | +git add src/views/code.rs | |
| 1395 | +git commit -m "feat: language-aware syntax highlighting for Rust and Swift" | |
| 1396 | +``` | |
| 1397 | + | |
| 1398 | +--- | |
| 1399 | + | |
| 1400 | +### Task 8: Final Integration and Cleanup | |
| 1401 | + | |
| 1402 | +**Files:** | |
| 1403 | +- Modify: `src/views/code.rs` (enum rewriting for Swift) | |
| 1404 | +- Modify: `src/codegen.rs` (remove now-dead Rust-specific code if any) | |
| 1405 | + | |
| 1406 | +- [ ] **Step 1: Update enum type rewriting for Swift** | |
| 1407 | + | |
| 1408 | +In `rebuild_file_mode()`, the enum type rewriting block (currently around lines 290–320) replaces `String` → enum name in file code. Make patterns language-aware: | |
| 1409 | + | |
| 1410 | +```rust | |
| 1411 | +let patterns = match self.selected_language { | |
| 1412 | + CodeLanguage::Rust => vec![ | |
| 1413 | + (format!("pub {}: String,", field_name), format!("pub {}: {},", field_name, enum_name)), | |
| 1414 | + (format!("pub {}: Option<String>,", field_name), format!("pub {}: Option<{}>,", field_name, enum_name)), | |
| 1415 | + ], | |
| 1416 | + CodeLanguage::Swift => { | |
| 1417 | + let swift_name = /* lang.field_name(field_name) */; | |
| 1418 | + vec![ | |
| 1419 | + (format!("let {}: String", swift_name), format!("let {}: {}", swift_name, enum_name)), | |
| 1420 | + (format!("let {}: String?", swift_name), format!("let {}: {}?", swift_name, enum_name)), | |
| 1421 | + ] | |
| 1422 | + }, | |
| 1423 | +}; | |
| 1424 | +``` | |
| 1425 | + | |
| 1426 | +Also include sanitized field name variants (e.g., `r#type` for Rust, `` `class` `` for Swift). | |
| 1427 | + | |
| 1428 | +- [ ] **Step 2: Update enum import injection for Swift** | |
| 1429 | + | |
| 1430 | +Rust adds `use crate::enums::*;` — Swift doesn't need this (all files in a module are visible). | |
| 1431 | + | |
| 1432 | +Wrap the import injection in a language check: | |
| 1433 | +```rust | |
| 1434 | +if self.selected_language == CodeLanguage::Rust && !new_code.contains("use crate::enums::") { | |
| 1435 | + // inject import | |
| 1436 | +} | |
| 1437 | +``` | |
| 1438 | + | |
| 1439 | +- [ ] **Step 3: Build and run full test suite** | |
| 1440 | + | |
| 1441 | +Run: `cargo test --lib` | |
| 1442 | +Expected: All tests pass (HAR + Swift generator) | |
| 1443 | + | |
| 1444 | +Run: `cargo build` | |
| 1445 | +Expected: Clean build | |
| 1446 | + | |
| 1447 | +- [ ] **Step 4: Manual smoke test** | |
| 1448 | + | |
| 1449 | +Test the full flow: | |
| 1450 | +1. Import a `.har` file — verify files appear with intelligent names | |
| 1451 | +2. Switch to Swift — verify Swift output | |
| 1452 | +3. Convert a field to enum — verify enum appears correctly in both Rust and Swift | |
| 1453 | +4. Download as folder — verify correct extensions and content | |
| 1454 | +5. Switch back to Rust — verify Rust output unchanged | |
| 1455 | + | |
| 1456 | +- [ ] **Step 5: Commit** | |
| 1457 | + | |
| 1458 | +```bash | |
| 1459 | +git add -A | |
| 1460 | +git commit -m "feat: complete HAR import and multi-language codegen with Swift support" | |
| 1461 | +``` |
+77-0docs/superpowers/specs/2026-03-10-mode-restructure-design.md
| @@ -0,0 +1,77 @@ | ||
| 1 | +# jv Mode Restructure — Design Spec | |
| 2 | + | |
| 3 | +## Overview | |
| 4 | + | |
| 5 | +Restructure jv from a flat 4-tab layout into three top-level modes (File, Shared, All), each with its own sub-tabs and sidebar behavior. | |
| 6 | + | |
| 7 | +## Modes | |
| 8 | + | |
| 9 | +### File Mode | |
| 10 | +- **Sub-tabs**: Table, JSON, Rust, jq | |
| 11 | +- **Sidebar**: SESSION + FILES list + SHARED TYPES (current behavior) | |
| 12 | +- **Behavior**: Exactly as today. Per-file views. | |
| 13 | + | |
| 14 | +### Shared Mode | |
| 15 | +- **Sub-tabs**: Table, Rust | |
| 16 | +- **Sidebar**: SESSION + scrollable list of shared struct names (clickable to focus/highlight in main content) | |
| 17 | +- **Table view**: Expandable sections, one per shared struct. Each expands to a field comparison matrix — files as columns, fields as rows, cells show inferred type or `—` if absent. | |
| 18 | +- **Rust view**: Only shared struct codegen (from `CodeGenerator::from_schema`). Same Rust view component, fed only shared structs. | |
| 19 | + | |
| 20 | +### All Mode | |
| 21 | +- **Sub-tabs**: Rust only (no tab bar shown) | |
| 22 | +- **Sidebar**: SESSION + scrollable list of struct names for navigation | |
| 23 | +- **Rust view**: Full codegen combining per-file structs + shared structs, deduplicated. One complete Rust output. | |
| 24 | + | |
| 25 | +## UI Layout | |
| 26 | + | |
| 27 | +Mode switcher renders as pill buttons **above the tab bar** in the main content panel. Two rows in main panel header: | |
| 28 | +1. Mode pills: `[ File ] [ Shared ] [ All ]` | |
| 29 | +2. Sub-tabs for current mode (hidden for All mode since it only has one view) | |
| 30 | + | |
| 31 | +## Data Model Changes | |
| 32 | + | |
| 33 | +### New enums | |
| 34 | +```rust | |
| 35 | +enum AppMode { | |
| 36 | + File, | |
| 37 | + Shared, | |
| 38 | + All, | |
| 39 | +} | |
| 40 | +``` | |
| 41 | + | |
| 42 | +`ViewTab` stays but is contextual per mode: | |
| 43 | +- File mode: Table, Json, Rust, Jq | |
| 44 | +- Shared mode: Table, Rust | |
| 45 | +- All mode: Rust (implicit, no selector) | |
| 46 | + | |
| 47 | +### State | |
| 48 | +- `active_mode: AppMode` added to `JvApp` | |
| 49 | +- `active_tab` resets to first available tab when mode changes | |
| 50 | + | |
| 51 | +## File Changes | |
| 52 | + | |
| 53 | +### `src/app.rs` | |
| 54 | +- Add `AppMode` enum | |
| 55 | +- Add `active_mode` field to `JvApp` | |
| 56 | +- `show_main_content()`: render mode pills above tabs, conditionally show sub-tabs per mode | |
| 57 | +- `show_sidebar()`: branch on `active_mode` for sidebar content | |
| 58 | +- File mode sidebar: current behavior | |
| 59 | +- Shared/All mode sidebar: SESSION section + struct name list | |
| 60 | + | |
| 61 | +### `src/views/table.rs` | |
| 62 | +- Add `show_schema_matrix()` method | |
| 63 | +- Takes `&[SharedStruct]` and list of filenames | |
| 64 | +- Renders expandable sections per struct | |
| 65 | +- Each section: files as columns, fields as rows, cells = inferred type or `—` | |
| 66 | + | |
| 67 | +### `src/views/rust.rs` | |
| 68 | +- Already has `show_schema()` for shared structs — reuse for Shared mode | |
| 69 | +- Add `show_all()` method for All mode: generates combined codegen from all files + schema | |
| 70 | + | |
| 71 | +### No changes needed | |
| 72 | +- `src/views/json.rs` | |
| 73 | +- `src/views/jq.rs` | |
| 74 | +- `src/schema.rs` | |
| 75 | +- `src/codegen.rs` | |
| 76 | +- `src/session.rs` | |
| 77 | +- `src/theme.rs` |
+270-0docs/superpowers/specs/2026-03-11-har-import-and-swift-codegen-design.md
| @@ -0,0 +1,270 @@ | ||
| 1 | +# HAR Import & Multi-Language Codegen | |
| 2 | + | |
| 3 | +## Overview | |
| 4 | + | |
| 5 | +Two features for jv: | |
| 6 | + | |
| 7 | +1. **HAR file import** — extract JSON responses from HAR files as synthetic session files with intelligent names | |
| 8 | +2. **Swift code generation** — language abstraction layer with Rust and Swift as first two implementations, toggle in code view | |
| 9 | + | |
| 10 | +--- | |
| 11 | + | |
| 12 | +## Feature 1: HAR File Import | |
| 13 | + | |
| 14 | +### Parsing | |
| 15 | + | |
| 16 | +HAR files are JSON with structure `{ log: { entries: [...] } }`. Each entry has `request` (method, url, queryString, postData) and `response` (status, content with mimeType and text). | |
| 17 | + | |
| 18 | +Extract entries where: | |
| 19 | +- `response.content.mimeType` contains `"json"` | |
| 20 | +- `response.content.text` parses as valid `serde_json::Value` | |
| 21 | +- Response status is 2xx | |
| 22 | +- Skip entries where `response.content.encoding` is `"base64"` (compressed bodies — not worth decoding) | |
| 23 | + | |
| 24 | +No limit on extracted entry count. Users can remove unwanted files from the session afterward. | |
| 25 | + | |
| 26 | +### File Naming | |
| 27 | + | |
| 28 | +Build filename from URL path + param values. Use simple `split('/')` URL parsing (no `url` crate needed — HAR URLs are well-formed). | |
| 29 | + | |
| 30 | +1. Parse URL path, extract last meaningful path segment: | |
| 31 | + - Strip segments matching: `api`, `rest`, or version patterns (`v1`, `v2`, ... i.e. `v\d+`) | |
| 32 | + - Skip numeric-only segments (IDs like `/users/42/`) | |
| 33 | + - Use the last remaining non-numeric segment as base name | |
| 34 | +2. Collect short distinguishing values: | |
| 35 | + - GET: query string param values | |
| 36 | + - POST/PUT: short string values from JSON body (top-level only) | |
| 37 | + - Include values that are: strings ≤ 15 chars, or date-like | |
| 38 | + - Exclude: values > 15 chars, values where >50% of characters are hex digits, UUIDs (`[0-9a-f]{8}-`...), purely numeric values | |
| 39 | +3. Join: `{endpoint}_{val1}_{val2}.json` | |
| 40 | +4. Deduplicate with `_2`, `_3` counters on collision | |
| 41 | + | |
| 42 | +Examples: | |
| 43 | +- `GET /api/v2/flights?origin=LAX&date=2026-03-01` → `flights_LAX_2026-03-01.json` | |
| 44 | +- `POST /auth/login` body `{"username":"alice"}` → `login_alice.json` | |
| 45 | +- `GET /users/42/bookings?status=confirmed` → `bookings_confirmed.json` | |
| 46 | +- `GET /health` → `health.json` | |
| 47 | +- Two identical names → `flights_LAX.json`, `flights_LAX_2.json` | |
| 48 | + | |
| 49 | +### Integration | |
| 50 | + | |
| 51 | +- Extend the import file dialog to accept `.har` files alongside `.json` | |
| 52 | +- When a `.har` file is detected (by extension or by checking for `log.entries` structure), parse and extract synthetic files | |
| 53 | +- Each extracted JSON response becomes a `SessionFile` in the current session | |
| 54 | +- The `original_path` field stores the HAR filename + entry index for provenance | |
| 55 | +- The rest of the pipeline (schema inference, codegen) works unchanged | |
| 56 | + | |
| 57 | +### New Code | |
| 58 | + | |
| 59 | +- `src/har.rs` — HAR parsing, file naming logic | |
| 60 | + - `pub fn extract_har_files(har_json: &Value) -> Vec<(String, Value)>` — returns `(filename, json_body)` pairs | |
| 61 | + - Internal: `fn name_from_request(entry: &Value) -> String` | |
| 62 | + - Internal: `fn extract_path_segment(url: &str) -> String` | |
| 63 | + - Internal: `fn collect_distinguishing_values(entry: &Value) -> Vec<String>` | |
| 64 | + - Internal: `fn is_hex_like(s: &str) -> bool` — >50% hex digits and len > 15, or UUID pattern | |
| 65 | + | |
| 66 | +### Modified Code | |
| 67 | + | |
| 68 | +- `src/app.rs` — `import_file()` checks extension, delegates to `har::extract_har_files()` when `.har` | |
| 69 | +- `src/main.rs` — add `mod har;` | |
| 70 | + | |
| 71 | +--- | |
| 72 | + | |
| 73 | +## Feature 2: Multi-Language Code Generation | |
| 74 | + | |
| 75 | +### Language Abstraction | |
| 76 | + | |
| 77 | +New enum and trait in `src/lang/mod.rs`: | |
| 78 | + | |
| 79 | +```rust | |
| 80 | +#[derive(Debug, Clone, Copy, PartialEq)] | |
| 81 | +pub enum CodeLanguage { | |
| 82 | + Rust, | |
| 83 | + Swift, | |
| 84 | +} | |
| 85 | + | |
| 86 | +pub trait LanguageGenerator { | |
| 87 | + fn file_extension(&self) -> &str; | |
| 88 | + fn file_header(&self) -> String; // #![allow(...)] for Rust, empty for Swift | |
| 89 | + fn imports_header(&self, needs_temporal: bool, has_shared: bool) -> String; | |
| 90 | + fn struct_open(&self, name: &str) -> String; // "pub struct Foo {" or "struct Foo: Codable {" | |
| 91 | + fn struct_close(&self, fields: &[(String, String)]) -> String; // fields=(code_name, json_name) for CodingKeys | |
| 92 | + fn field_line(&self, code_name: &str, type_name: &str, json_name: &str) -> String; | |
| 93 | + fn enum_open(&self, name: &str) -> String; | |
| 94 | + fn enum_close(&self) -> String; | |
| 95 | + fn enum_variant(&self, variant_name: &str, json_value: &str) -> String; | |
| 96 | + fn type_name(&self, inferred: &InferredType) -> String; | |
| 97 | + fn mod_file(&self, file_names: &[&str]) -> Option<String>; // None for Swift | |
| 98 | + fn sanitize_keyword(&self, name: &str) -> String; | |
| 99 | + fn field_name(&self, json_name: &str) -> String; // snake_case for Rust, camelCase for Swift | |
| 100 | +} | |
| 101 | +``` | |
| 102 | + | |
| 103 | +**Key design decisions:** | |
| 104 | +- `struct_close()` takes field mappings so Swift can emit CodingKeys before the closing brace. Rust ignores this parameter. | |
| 105 | +- `field_name()` handles language-specific naming: Rust keeps snake_case, Swift converts to camelCase. | |
| 106 | +- `type_name()` replaces `InferredType::rust_type()` for all codegen. The existing `rust_type()` method remains for non-codegen display (e.g., schema table tooltip) but codegen always goes through the trait. | |
| 107 | + | |
| 108 | +### Code generation flow | |
| 109 | + | |
| 110 | +`CodeGenerator::generate_code()` is refactored to accept `&dyn LanguageGenerator`: | |
| 111 | + | |
| 112 | +``` | |
| 113 | +generate_code(lang): | |
| 114 | + 1. lang.file_header() → "#![allow(non_snake_case)]" or "" | |
| 115 | + 2. lang.imports_header(...) → "use serde::..." or "import Foundation" | |
| 116 | + 3. For each struct: | |
| 117 | + a. lang.struct_open(name) → "pub struct Foo {" or "struct Foo: Codable {" | |
| 118 | + b. For each field: | |
| 119 | + - code_name = lang.field_name(json_name) | |
| 120 | + - type_str = lang.type_name(inferred_type) | |
| 121 | + - lang.field_line(code_name, type_str, json_name) | |
| 122 | + c. lang.struct_close(fields) → "}" or CodingKeys block + "}" | |
| 123 | + 4. For each enum: | |
| 124 | + a. lang.enum_open(name) | |
| 125 | + b. For each variant: lang.enum_variant(variant, json_value) | |
| 126 | + c. lang.enum_close() | |
| 127 | +``` | |
| 128 | + | |
| 129 | +### Refactoring `GeneratedField` | |
| 130 | + | |
| 131 | +The existing `GeneratedField` struct has Rust-specific field names (`rust_name`, `rust_type`). Refactor to language-neutral: | |
| 132 | + | |
| 133 | +```rust | |
| 134 | +pub struct GeneratedField { | |
| 135 | + pub code_name: String, // was rust_name — language-specific (snake_case or camelCase) | |
| 136 | + pub json_name: String, // unchanged — original JSON key | |
| 137 | + pub code_type: String, // was rust_type — language-specific type string | |
| 138 | + pub needs_rename: bool, // code_name != json_name | |
| 139 | +} | |
| 140 | +``` | |
| 141 | + | |
| 142 | +### Rust Generator | |
| 143 | + | |
| 144 | +`src/lang/rust.rs` — extracts existing logic from `codegen.rs`: | |
| 145 | +- `type_name`: same mapping as current `InferredType::rust_type()` | |
| 146 | +- `field_name`: `to_snake_case()` (moved from codegen.rs) | |
| 147 | +- `sanitize_keyword`: `r#` prefix for Rust reserved words | |
| 148 | +- `struct_close`: just `"}\n"` (ignores field mappings) | |
| 149 | +- `mod_file`: generates `mod X; pub use X::*;` pattern | |
| 150 | + | |
| 151 | +### Swift Generator | |
| 152 | + | |
| 153 | +`src/lang/swift.rs` | |
| 154 | + | |
| 155 | +Type mapping: | |
| 156 | +| InferredType | Swift Type | | |
| 157 | +|---|---| | |
| 158 | +| Null | Any? | | |
| 159 | +| Bool | Bool | | |
| 160 | +| I64 | Int | | |
| 161 | +| F64 | Double | | |
| 162 | +| String | String | | |
| 163 | +| DateTime | Date | | |
| 164 | +| Date | Date | | |
| 165 | +| Time | Date | | |
| 166 | +| Array(T) | [T] | | |
| 167 | +| Object(fields) | Named struct (same as Rust — recursive struct generation) | | |
| 168 | +| Object (fallback) | [String: Any] | | |
| 169 | +| Option(T) | T? | | |
| 170 | +| Mixed | Any | | |
| 171 | +| Unknown | Any | | |
| 172 | + | |
| 173 | +- `field_name`: `to_camel_case()` — new utility function (e.g. `departure_date` → `departureDate`) | |
| 174 | +- `sanitize_keyword`: backtick escaping for Swift reserved words (`class`, `struct`, `enum`, `protocol`, `func`, `var`, `let`, `import`, `return`, `self`, `super`, `default`, `case`, `switch`, `where`, `in`, `is`, `as`, `try`, `throw`, `throws`, `nil`, `true`, `false`, `do`, `catch`, `guard`, `defer`, `repeat`, `break`, `continue`, `fallthrough`, `typealias`, `associatedtype`, `operator`, `init`, `deinit`, `subscript`, `private`, `public`, `internal`, `open`, `fileprivate`, `static`, `override`, `mutating`, `inout`, `Any`, `Type`, `Self`) | |
| 175 | +- `struct_close(fields)`: emits `CodingKeys` enum block before `}` when any field's camelCase name differs from its JSON key | |
| 176 | +- `mod_file`: returns `None` | |
| 177 | +- `enum_open`: `enum FooBar: String, Codable {` | |
| 178 | +- `enum_variant`: `case variantName = "json_value"` (or just `case variantName` when equal) | |
| 179 | + | |
| 180 | +Swift enum output: | |
| 181 | +```swift | |
| 182 | +enum SeatClass: String, Codable { | |
| 183 | + case economy | |
| 184 | + case business | |
| 185 | + case firstClass = "first_class" | |
| 186 | +} | |
| 187 | +``` | |
| 188 | + | |
| 189 | +Swift struct output: | |
| 190 | +```swift | |
| 191 | +import Foundation | |
| 192 | + | |
| 193 | +struct Flight: Codable { | |
| 194 | + let origin: String | |
| 195 | + let departureDate: Date | |
| 196 | + let seatAbbreviation: String | |
| 197 | + let passengers: [Passenger]? | |
| 198 | + | |
| 199 | + enum CodingKeys: String, CodingKey { | |
| 200 | + case origin | |
| 201 | + case departureDate = "departure_date" | |
| 202 | + case seatAbbreviation = "seat_abbreviation" | |
| 203 | + case passengers | |
| 204 | + } | |
| 205 | +} | |
| 206 | +``` | |
| 207 | + | |
| 208 | +### Code View Changes | |
| 209 | + | |
| 210 | +**Language toggle:** Segmented button in top bar (`Rust | Swift`). Stored as `selected_language: CodeLanguage` in `CodeView`. | |
| 211 | + | |
| 212 | +**Cache invalidation:** Changing language sets `cache_key = 0`, triggering full rebuild with the new generator. | |
| 213 | + | |
| 214 | +**File list:** `rebuild_file_mode` uses language for file extension and naming: | |
| 215 | +- Rust: mod.rs, shared.rs, enums.rs, {group}.rs | |
| 216 | +- Swift: Shared.swift, Enums.swift, {Group}.swift (no mod file, PascalCase filenames) | |
| 217 | + | |
| 218 | +**Syntax highlighting:** `render_rust_line` becomes `render_code_line`, language-aware. The rendering logic splits into shared vs language-specific parts: | |
| 219 | + | |
| 220 | +Shared (both languages): | |
| 221 | +- Search match highlighting (background + accent bar) | |
| 222 | +- Line numbers | |
| 223 | +- Struct block copy buttons | |
| 224 | +- Clickable type navigation | |
| 225 | +- Enum convert/revert buttons | |
| 226 | +- Field hide/show toggles | |
| 227 | +- Enum usage tooltips | |
| 228 | + | |
| 229 | +Language-specific (driven by `CodeLanguage`): | |
| 230 | +- Keyword detection: `pub struct` / `pub enum` / `use` (Rust) vs `struct` / `enum` / `let` / `import` / `case` (Swift) | |
| 231 | +- Field line parsing: `pub field: Type,` (Rust) vs `let field: Type` (Swift) | |
| 232 | +- Attribute lines: `#[derive(...)]` (Rust) vs none (Swift — `: Codable` is on the struct line) | |
| 233 | +- CodingKeys block: rendered like attributes in YELLOW | |
| 234 | + | |
| 235 | +**Download:** Uses correct extension per language. | |
| 236 | + | |
| 237 | +### File Structure | |
| 238 | + | |
| 239 | +``` | |
| 240 | +src/ | |
| 241 | +├── lang/ | |
| 242 | +│ ├── mod.rs # CodeLanguage enum, LanguageGenerator trait | |
| 243 | +│ ├── rust.rs # RustGenerator | |
| 244 | +│ └── swift.rs # SwiftGenerator | |
| 245 | +├── har.rs # HAR parsing + naming | |
| 246 | +├── codegen.rs # CodeGenerator uses &dyn LanguageGenerator | |
| 247 | +└── views/code.rs # Language toggle, language-aware rendering | |
| 248 | +``` | |
| 249 | + | |
| 250 | +### What stays in `codegen.rs` | |
| 251 | + | |
| 252 | +`CodeGenerator` struct and its struct/field collection logic (`collect_structs`, `from_value`, `from_schema`) stay in `codegen.rs`. These are language-independent — they walk JSON and produce `GeneratedStruct`/`GeneratedField` lists. Only the formatting/output methods (`generate_code`, type resolution, keyword sanitization) delegate to the `LanguageGenerator` trait. | |
| 253 | + | |
| 254 | +Utility functions `to_pascal_case`, `singularize`, `first_normal_word`, `make_unique_name` stay in `codegen.rs` as they're language-independent. | |
| 255 | + | |
| 256 | +### Modified Files | |
| 257 | + | |
| 258 | +- `src/codegen.rs` — `CodeGenerator` takes `&dyn LanguageGenerator`, `GeneratedField` uses neutral names, `generate_code(lang)` delegates formatting | |
| 259 | +- `src/types.rs` — `InferredType::rust_type()` kept for non-codegen display; codegen uses `lang.type_name()` | |
| 260 | +- `src/views/code.rs` — language toggle UI, `rebuild_file_mode()` uses selected language, `render_code_line()` replaces `render_rust_line()` | |
| 261 | +- `src/main.rs` — add `mod lang; mod har;` | |
| 262 | + | |
| 263 | +--- | |
| 264 | + | |
| 265 | +## Out of Scope | |
| 266 | + | |
| 267 | +- SwiftLint disable comments | |
| 268 | +- Other languages beyond Rust and Swift (architecture supports it, implementation deferred) | |
| 269 | +- HAR request body extraction (only responses) | |
| 270 | +- HAR filtering UI (all valid JSON responses extracted) |
+0-0har
Binary file changed.
+59-0sample.json
| @@ -0,0 +1,59 @@ | ||
| 1 | +{ | |
| 2 | + "users": [ | |
| 3 | + { | |
| 4 | + "id": 1, | |
| 5 | + "name": "Alice Johnson", | |
| 6 | + "email": "alice@example.com", | |
| 7 | + "role": "admin", | |
| 8 | + "created_at": "2024-06-15T10:30:00Z", | |
| 9 | + "last_login": 1709251200, | |
| 10 | + "profile": { | |
| 11 | + "bio": "Software engineer with 10 years of experience", | |
| 12 | + "avatar_url": "https://example.com/avatars/alice.jpg", | |
| 13 | + "location": "San Francisco, CA", | |
| 14 | + "timezone": "America/Los_Angeles" | |
| 15 | + }, | |
| 16 | + "tags": ["rust", "python", "devops"], | |
| 17 | + "is_active": true | |
| 18 | + }, | |
| 19 | + { | |
| 20 | + "id": 2, | |
| 21 | + "name": "Bob Smith", | |
| 22 | + "email": "bob@example.com", | |
| 23 | + "role": "developer", | |
| 24 | + "created_at": "2024-08-22T14:15:30+05:30", | |
| 25 | + "last_login": 1709337600, | |
| 26 | + "profile": { | |
| 27 | + "bio": "Full-stack developer", | |
| 28 | + "avatar_url": "https://example.com/avatars/bob.jpg", | |
| 29 | + "location": "Mumbai, India", | |
| 30 | + "timezone": "Asia/Kolkata" | |
| 31 | + }, | |
| 32 | + "tags": ["javascript", "react"], | |
| 33 | + "is_active": true | |
| 34 | + }, | |
| 35 | + { | |
| 36 | + "id": 3, | |
| 37 | + "name": "Charlie Brown", | |
| 38 | + "email": null, | |
| 39 | + "role": "viewer", | |
| 40 | + "created_at": "2025-01-10T09:00:00-08:00", | |
| 41 | + "last_login": null, | |
| 42 | + "profile": { | |
| 43 | + "bio": null, | |
| 44 | + "avatar_url": null, | |
| 45 | + "location": "London, UK", | |
| 46 | + "timezone": "Europe/London" | |
| 47 | + }, | |
| 48 | + "tags": [], | |
| 49 | + "is_active": false | |
| 50 | + } | |
| 51 | + ], | |
| 52 | + "metadata": { | |
| 53 | + "total_count": 3, | |
| 54 | + "page": 1, | |
| 55 | + "per_page": 50, | |
| 56 | + "api_version": "2.1.0", | |
| 57 | + "generated_at": "2025-03-01T12:00:00Z" | |
| 58 | + } | |
| 59 | +} |
+985-0src/app.rs
| @@ -0,0 +1,985 @@ | ||
| 1 | +use egui::{self, RichText}; | |
| 2 | + | |
| 3 | +use crate::schema::SchemaOverview; | |
| 4 | +use crate::session::{LoadedSession, SessionManager}; | |
| 5 | +use crate::theme::CatppuccinMocha; | |
| 6 | +use crate::views::{browser::BrowserView, code::CodeView, schema_diagram::SchemaDiagramView, shared_browser::SharedBrowserView, table::TableView}; | |
| 7 | + | |
| 8 | +#[derive(Debug, Clone, Copy, PartialEq)] | |
| 9 | +pub enum AppMode { | |
| 10 | + Jv, | |
| 11 | + Schema, | |
| 12 | + Groups, | |
| 13 | + Code, | |
| 14 | + Settings, | |
| 15 | +} | |
| 16 | + | |
| 17 | +#[derive(Debug, Clone, Copy, PartialEq)] | |
| 18 | +pub enum ViewTab { | |
| 19 | + Table, | |
| 20 | + Code, | |
| 21 | +} | |
| 22 | + | |
| 23 | +pub struct JvApp { | |
| 24 | + session_manager: SessionManager, | |
| 25 | + current_session: Option<LoadedSession>, | |
| 26 | + selected_file_index: usize, | |
| 27 | + active_mode: AppMode, | |
| 28 | + active_tab: ViewTab, | |
| 29 | + | |
| 30 | + // Views | |
| 31 | + table_view: TableView, | |
| 32 | + code_view: CodeView, | |
| 33 | + browser_view: BrowserView, | |
| 34 | + schema_diagram_view: SchemaDiagramView, | |
| 35 | + shared_browser_view: SharedBrowserView, | |
| 36 | + | |
| 37 | + applied_threshold: f32, | |
| 38 | + | |
| 39 | + // Track file changes | |
| 40 | + last_file_index: usize, | |
| 41 | + last_file_count: usize, | |
| 42 | + | |
| 43 | + // Disabled (toggled-off) source files by index | |
| 44 | + disabled_files: std::collections::BTreeSet<usize>, | |
| 45 | + | |
| 46 | + // UI state | |
| 47 | + new_session_name: String, | |
| 48 | + show_new_session_dialog: bool, | |
| 49 | + sidebar_width: f32, | |
| 50 | + theme_applied: bool, | |
| 51 | + | |
| 52 | + // File drop handling | |
| 53 | + dropped_files: Vec<egui::DroppedFile>, | |
| 54 | + | |
| 55 | + // FPS tracking | |
| 56 | + frame_times: Vec<f64>, | |
| 57 | + fps_display: f64, | |
| 58 | + | |
| 59 | + // Async file dialog results | |
| 60 | + file_dialog_rx: Option<std::sync::mpsc::Receiver<FileDialogResult>>, | |
| 61 | +} | |
| 62 | + | |
| 63 | +enum FileDialogResult { | |
| 64 | + Files(Vec<std::path::PathBuf>), | |
| 65 | + Directory(std::path::PathBuf), | |
| 66 | +} | |
| 67 | + | |
| 68 | +impl JvApp { | |
| 69 | + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { | |
| 70 | + // Load Phosphor icon font | |
| 71 | + let mut fonts = egui::FontDefinitions::default(); | |
| 72 | + egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular); | |
| 73 | + cc.egui_ctx.set_fonts(fonts); | |
| 74 | + egui_extras::install_image_loaders(&cc.egui_ctx); | |
| 75 | + | |
| 76 | + let mut manager = SessionManager::new(); | |
| 77 | + | |
| 78 | + // Auto-create a default session if none exist | |
| 79 | + let current = if manager.sessions.is_empty() { | |
| 80 | + let session = manager.create_session("Default Session"); | |
| 81 | + Some(LoadedSession::new(session)) | |
| 82 | + } else { | |
| 83 | + let session = manager.sessions[0].clone(); | |
| 84 | + Some(LoadedSession::new(session)) | |
| 85 | + }; | |
| 86 | + | |
| 87 | + let mut app = Self { | |
| 88 | + session_manager: manager, | |
| 89 | + current_session: current, | |
| 90 | + selected_file_index: 0, | |
| 91 | + active_mode: AppMode::Jv, | |
| 92 | + active_tab: ViewTab::Table, | |
| 93 | + table_view: TableView::new(), | |
| 94 | + code_view: CodeView::new(), | |
| 95 | + browser_view: BrowserView::new(), | |
| 96 | + schema_diagram_view: SchemaDiagramView::new(), | |
| 97 | + shared_browser_view: SharedBrowserView::new(), | |
| 98 | + applied_threshold: 0.0, | |
| 99 | + last_file_index: usize::MAX, | |
| 100 | + last_file_count: 0, | |
| 101 | + disabled_files: std::collections::BTreeSet::new(), | |
| 102 | + new_session_name: String::new(), | |
| 103 | + show_new_session_dialog: false, | |
| 104 | + sidebar_width: 240.0, | |
| 105 | + theme_applied: false, | |
| 106 | + dropped_files: Vec::new(), | |
| 107 | + frame_times: Vec::with_capacity(60), | |
| 108 | + fps_display: 0.0, | |
| 109 | + file_dialog_rx: None, | |
| 110 | + }; | |
| 111 | + if let Some(loaded) = &app.current_session { | |
| 112 | + app.applied_threshold = loaded.session.jaccard_threshold; | |
| 113 | + } | |
| 114 | + tracing::info!("UI loaded"); | |
| 115 | + app | |
| 116 | + } | |
| 117 | + | |
| 118 | + /// Get only the enabled (non-disabled) parsed files | |
| 119 | + fn active_parsed_files(&self) -> Vec<(String, serde_json::Value)> { | |
| 120 | + let Some(loaded) = &self.current_session else { | |
| 121 | + return Vec::new(); | |
| 122 | + }; | |
| 123 | + loaded | |
| 124 | + .parsed_files | |
| 125 | + .iter() | |
| 126 | + .enumerate() | |
| 127 | + .filter(|(i, _)| !self.disabled_files.contains(i)) | |
| 128 | + .map(|(_, f)| f.clone()) | |
| 129 | + .collect() | |
| 130 | + } | |
| 131 | + | |
| 132 | + fn rebuild_schema(&mut self) { | |
| 133 | + if let Some(loaded) = &mut self.current_session { | |
| 134 | + // Rebuild schema using only enabled files | |
| 135 | + let active: Vec<(String, serde_json::Value)> = loaded | |
| 136 | + .parsed_files | |
| 137 | + .iter() | |
| 138 | + .enumerate() | |
| 139 | + .filter(|(i, _)| !self.disabled_files.contains(i)) | |
| 140 | + .map(|(_, f)| f.clone()) | |
| 141 | + .collect(); | |
| 142 | + if !active.is_empty() { | |
| 143 | + let threshold = loaded.session.jaccard_threshold; | |
| 144 | + loaded.schema = Some(SchemaOverview::infer(&active, threshold)); | |
| 145 | + } else { | |
| 146 | + loaded.schema = None; | |
| 147 | + } | |
| 148 | + self.applied_threshold = loaded.session.jaccard_threshold; | |
| 149 | + } | |
| 150 | + } | |
| 151 | + | |
| 152 | + fn import_file(&mut self, path: &std::path::Path) { | |
| 153 | + if let Ok(content) = std::fs::read_to_string(path) { | |
| 154 | + let is_har = path.extension().is_some_and(|e| e == "har"); | |
| 155 | + if is_har { | |
| 156 | + if let Ok(har_value) = serde_json::from_str::<serde_json::Value>(&content) { | |
| 157 | + let files = crate::har::extract_har_files(&har_value); | |
| 158 | + if let Some(loaded) = &mut self.current_session { | |
| 159 | + for (filename, value) in &files { | |
| 160 | + let json_str = | |
| 161 | + serde_json::to_string_pretty(value).unwrap_or_default(); | |
| 162 | + if loaded.add_file(filename, json_str).is_ok() {} | |
| 163 | + } | |
| 164 | + self.session_manager.update_session(&loaded.session); | |
| 165 | + self.rebuild_schema(); | |
| 166 | + } | |
| 167 | + } | |
| 168 | + } else { | |
| 169 | + let path_str = path.to_string_lossy().to_string(); | |
| 170 | + if let Some(loaded) = &mut self.current_session { | |
| 171 | + if loaded.add_file(&path_str, content).is_ok() { | |
| 172 | + self.session_manager.update_session(&loaded.session); | |
| 173 | + self.rebuild_schema(); | |
| 174 | + } | |
| 175 | + } | |
| 176 | + } | |
| 177 | + } | |
| 178 | + } | |
| 179 | + | |
| 180 | + fn import_directory(&mut self, dir: &std::path::Path) { | |
| 181 | + for entry in walkdir::WalkDir::new(dir) | |
| 182 | + .into_iter() | |
| 183 | + .filter_map(|e| e.ok()) | |
| 184 | + { | |
| 185 | + let path = entry.path(); | |
| 186 | + if path.is_file() && path.extension().is_some_and(|e| e == "json" || e == "har") { | |
| 187 | + self.import_file(path); | |
| 188 | + } | |
| 189 | + } | |
| 190 | + } | |
| 191 | + | |
| 192 | + fn show_sidebar(&mut self, ui: &mut egui::Ui) { | |
| 193 | + ui.vertical(|ui| { | |
| 194 | + use egui_phosphor::regular; | |
| 195 | + | |
| 196 | + // -- Session section -- | |
| 197 | + ui.horizontal(|ui| { | |
| 198 | + ui.label( | |
| 199 | + RichText::new(format!("{} ", regular::FOLDER_OPEN)) | |
| 200 | + .color(CatppuccinMocha::OVERLAY0) | |
| 201 | + .size(12.0), | |
| 202 | + ); | |
| 203 | + ui.label( | |
| 204 | + RichText::new("SESSION") | |
| 205 | + .color(CatppuccinMocha::OVERLAY0) | |
| 206 | + .small() | |
| 207 | + .strong(), | |
| 208 | + ); | |
| 209 | + }); | |
| 210 | + ui.add_space(4.0); | |
| 211 | + | |
| 212 | + let session_names: Vec<String> = self | |
| 213 | + .session_manager | |
| 214 | + .sessions | |
| 215 | + .iter() | |
| 216 | + .map(|s| s.name.clone()) | |
| 217 | + .collect(); | |
| 218 | + | |
| 219 | + let current_name = self | |
| 220 | + .current_session | |
| 221 | + .as_ref() | |
| 222 | + .map(|s| s.session.name.clone()) | |
| 223 | + .unwrap_or_default(); | |
| 224 | + | |
| 225 | + ui.horizontal(|ui| { | |
| 226 | + egui::ComboBox::from_id_salt("session_selector") | |
| 227 | + .selected_text(¤t_name) | |
| 228 | + .width(ui.available_width() - 52.0) | |
| 229 | + .show_ui(ui, |ui| { | |
| 230 | + for (i, name) in session_names.iter().enumerate() { | |
| 231 | + if ui.selectable_label(name == ¤t_name, name).clicked() { | |
| 232 | + let session = self.session_manager.sessions[i].clone(); | |
| 233 | + self.current_session = Some(LoadedSession::new(session)); | |
| 234 | + self.selected_file_index = 0; | |
| 235 | + self.rebuild_schema(); | |
| 236 | + } | |
| 237 | + } | |
| 238 | + }); | |
| 239 | + | |
| 240 | + if ui.add(egui::Button::new( | |
| 241 | + RichText::new(regular::PLUS).size(12.0), | |
| 242 | + ).frame(false)).on_hover_text("New Session").clicked() { | |
| 243 | + self.show_new_session_dialog = true; | |
| 244 | + } | |
| 245 | + if ui.add(egui::Button::new( | |
| 246 | + RichText::new(regular::TRASH).color(CatppuccinMocha::SURFACE2).size(12.0), | |
| 247 | + ).frame(false)).on_hover_text("Delete Session").clicked() { | |
| 248 | + if let Some(loaded) = &self.current_session { | |
| 249 | + let id = loaded.session.id.clone(); | |
| 250 | + self.session_manager.delete_session(&id); | |
| 251 | + self.current_session = if self.session_manager.sessions.is_empty() { | |
| 252 | + let s = self.session_manager.create_session("Default Session"); | |
| 253 | + Some(LoadedSession::new(s)) | |
| 254 | + } else { | |
| 255 | + let s = self.session_manager.sessions[0].clone(); | |
| 256 | + Some(LoadedSession::new(s)) | |
| 257 | + }; | |
| 258 | + self.selected_file_index = 0; | |
| 259 | + self.rebuild_schema(); | |
| 260 | + } | |
| 261 | + } | |
| 262 | + }); | |
| 263 | + | |
| 264 | + ui.add_space(16.0); | |
| 265 | + | |
| 266 | + // -- Source files section (always visible) -- | |
| 267 | + ui.horizontal(|ui| { | |
| 268 | + ui.label( | |
| 269 | + RichText::new(format!("{} ", regular::FILES)) | |
| 270 | + .color(CatppuccinMocha::OVERLAY0) | |
| 271 | + .size(12.0), | |
| 272 | + ); | |
| 273 | + ui.label( | |
| 274 | + RichText::new("SOURCE FILES") | |
| 275 | + .color(CatppuccinMocha::OVERLAY0) | |
| 276 | + .small() | |
| 277 | + .strong(), | |
| 278 | + ); | |
| 279 | + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { | |
| 280 | + // Import buttons | |
| 281 | + if ui.add(egui::Button::new( | |
| 282 | + RichText::new(regular::FOLDER_PLUS).size(12.0), | |
| 283 | + ).frame(false)).on_hover_text("Import Directory").clicked() && self.file_dialog_rx.is_none() { | |
| 284 | + let (tx, rx) = std::sync::mpsc::channel(); | |
| 285 | + let ctx = ui.ctx().clone(); | |
| 286 | + std::thread::spawn(move || { | |
| 287 | + if let Some(dir) = rfd::FileDialog::new().pick_folder() { | |
| 288 | + let _ = tx.send(FileDialogResult::Directory(dir)); | |
| 289 | + ctx.request_repaint(); | |
| 290 | + } | |
| 291 | + }); | |
| 292 | + self.file_dialog_rx = Some(rx); | |
| 293 | + } | |
| 294 | + if ui.add(egui::Button::new( | |
| 295 | + RichText::new(regular::FILE_PLUS).size(12.0), | |
| 296 | + ).frame(false)).on_hover_text("Import File").clicked() && self.file_dialog_rx.is_none() { | |
| 297 | + let (tx, rx) = std::sync::mpsc::channel(); | |
| 298 | + let ctx = ui.ctx().clone(); | |
| 299 | + std::thread::spawn(move || { | |
| 300 | + if let Some(paths) = rfd::FileDialog::new() | |
| 301 | + .add_filter("JSON & HAR", &["json", "har"]) | |
| 302 | + .pick_files() | |
| 303 | + { | |
| 304 | + let _ = tx.send(FileDialogResult::Files(paths)); | |
| 305 | + ctx.request_repaint(); | |
| 306 | + } | |
| 307 | + }); | |
| 308 | + self.file_dialog_rx = Some(rx); | |
| 309 | + } | |
| 310 | + }); | |
| 311 | + }); | |
| 312 | + | |
| 313 | + // Select all / none row | |
| 314 | + let file_count = self.current_session.as_ref().map(|l| l.session.files.len()).unwrap_or(0); | |
| 315 | + if file_count > 0 { | |
| 316 | + let all_enabled = self.disabled_files.is_empty(); | |
| 317 | + let all_disabled = self.disabled_files.len() >= file_count; | |
| 318 | + ui.horizontal(|ui| { | |
| 319 | + let all_color = if all_enabled { CatppuccinMocha::OVERLAY0 } else { CatppuccinMocha::SUBTEXT0 }; | |
| 320 | + let none_color = if all_disabled { CatppuccinMocha::OVERLAY0 } else { CatppuccinMocha::SUBTEXT0 }; | |
| 321 | + if ui.add(egui::Button::new( | |
| 322 | + RichText::new("All").color(all_color).size(11.0), | |
| 323 | + ).frame(false)).clicked() && !all_enabled { | |
| 324 | + self.disabled_files.clear(); | |
| 325 | + self.rebuild_schema(); | |
| 326 | + self.code_view.invalidate(); | |
| 327 | + self.shared_browser_view.invalidate(); | |
| 328 | + self.schema_diagram_view.invalidate(); | |
| 329 | + self.browser_view.invalidate(); | |
| 330 | + } | |
| 331 | + ui.label(RichText::new("·").color(CatppuccinMocha::SURFACE2).size(11.0)); | |
| 332 | + if ui.add(egui::Button::new( | |
| 333 | + RichText::new("None").color(none_color).size(11.0), | |
| 334 | + ).frame(false)).clicked() && !all_disabled { | |
| 335 | + self.disabled_files = (0..file_count).collect(); | |
| 336 | + self.rebuild_schema(); | |
| 337 | + self.code_view.invalidate(); | |
| 338 | + self.shared_browser_view.invalidate(); | |
| 339 | + self.schema_diagram_view.invalidate(); | |
| 340 | + self.browser_view.invalidate(); | |
| 341 | + } | |
| 342 | + ui.label( | |
| 343 | + RichText::new(format!("{}/{}", file_count - self.disabled_files.len(), file_count)) | |
| 344 | + .color(CatppuccinMocha::OVERLAY0) | |
| 345 | + .family(egui::FontFamily::Monospace) | |
| 346 | + .size(10.0), | |
| 347 | + ); | |
| 348 | + }); | |
| 349 | + } | |
| 350 | + ui.add_space(4.0); | |
| 351 | + | |
| 352 | + if let Some(loaded) = &self.current_session { | |
| 353 | + let mut files: Vec<(usize, String)> = loaded | |
| 354 | + .session | |
| 355 | + .files | |
| 356 | + .iter() | |
| 357 | + .enumerate() | |
| 358 | + .map(|(i, f)| (i, f.filename.clone())) | |
| 359 | + .collect(); | |
| 360 | + files.sort_by(|a, b| a.1.to_lowercase().cmp(&b.1.to_lowercase())); | |
| 361 | + | |
| 362 | + let mut to_remove = None; | |
| 363 | + let mut toggled: Option<usize> = None; | |
| 364 | + let mut clicked_file: Option<usize> = None; | |
| 365 | + | |
| 366 | + egui::ScrollArea::vertical() | |
| 367 | + .id_salt("file_list") | |
| 368 | + .show(ui, |ui| { | |
| 369 | + for (idx, filename) in &files { | |
| 370 | + let is_disabled = self.disabled_files.contains(idx); | |
| 371 | + | |
| 372 | + let row_id = ui.id().with(("file_row", idx)); | |
| 373 | + let prev_rect = ui.ctx().data(|d| d.get_temp::<egui::Rect>(row_id)); | |
| 374 | + let hovered = prev_rect | |
| 375 | + .map(|r| ui.rect_contains_pointer(r)) | |
| 376 | + .unwrap_or(false); | |
| 377 | + | |
| 378 | + let bg = if hovered { | |
| 379 | + egui::Color32::from_rgba_premultiplied(40, 40, 55, 255) | |
| 380 | + } else { | |
| 381 | + egui::Color32::TRANSPARENT | |
| 382 | + }; | |
| 383 | + | |
| 384 | + let r = egui::Frame::new() | |
| 385 | + .fill(bg) | |
| 386 | + .corner_radius(6.0) | |
| 387 | + .inner_margin(egui::Margin::symmetric(8, 3)) | |
| 388 | + .show(ui, |ui| { | |
| 389 | + ui.horizontal(|ui| { | |
| 390 | + // Toggle checkbox | |
| 391 | + let mut enabled = !is_disabled; | |
| 392 | + if ui.checkbox(&mut enabled, "").changed() { | |
| 393 | + toggled = Some(*idx); | |
| 394 | + } | |
| 395 | + | |
| 396 | + let text_color = if is_disabled { | |
| 397 | + CatppuccinMocha::OVERLAY0 | |
| 398 | + } else if hovered { | |
| 399 | + CatppuccinMocha::TEXT | |
| 400 | + } else { | |
| 401 | + CatppuccinMocha::SUBTEXT0 | |
| 402 | + }; | |
| 403 | + let file_r = ui.add( | |
| 404 | + egui::Label::new( | |
| 405 | + { | |
| 406 | + let rt = RichText::new(filename) | |
| 407 | + .color(text_color) | |
| 408 | + .size(12.0); | |
| 409 | + if is_disabled { rt.strikethrough() } else { rt } | |
| 410 | + }, | |
| 411 | + ) | |
| 412 | + .sense(egui::Sense::click()), | |
| 413 | + ); | |
| 414 | + if file_r.clicked() { | |
| 415 | + clicked_file = Some(*idx); | |
| 416 | + } | |
| 417 | + | |
| 418 | + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { | |
| 419 | + if hovered { | |
| 420 | + let rm = ui.add( | |
| 421 | + egui::Button::new( | |
| 422 | + RichText::new(regular::X_CIRCLE) | |
| 423 | + .color(if ui.rect_contains_pointer(ui.min_rect()) { | |
| 424 | + CatppuccinMocha::RED | |
| 425 | + } else { | |
| 426 | + CatppuccinMocha::SURFACE2 | |
| 427 | + }) | |
| 428 | + .size(13.0), | |
| 429 | + ) | |
| 430 | + .frame(false), | |
| 431 | + ); | |
| 432 | + if rm.on_hover_text("Remove file").clicked() { | |
| 433 | + to_remove = Some(*idx); | |
| 434 | + } | |
| 435 | + } | |
| 436 | + }); | |
| 437 | + }); | |
| 438 | + }); | |
| 439 | + | |
| 440 | + let row_rect = r.response.rect; | |
| 441 | + ui.ctx().data_mut(|d| d.insert_temp(row_id, row_rect)); | |
| 442 | + } | |
| 443 | + }); | |
| 444 | + | |
| 445 | + // Handle toggle | |
| 446 | + if let Some(idx) = toggled { | |
| 447 | + if self.disabled_files.contains(&idx) { | |
| 448 | + self.disabled_files.remove(&idx); | |
| 449 | + } else { | |
| 450 | + self.disabled_files.insert(idx); | |
| 451 | + } | |
| 452 | + self.rebuild_schema(); | |
| 453 | + self.code_view.invalidate(); | |
| 454 | + self.shared_browser_view.invalidate(); | |
| 455 | + self.schema_diagram_view.invalidate(); | |
| 456 | + self.browser_view.invalidate(); | |
| 457 | + } | |
| 458 | + | |
| 459 | + // Handle click — in Jv mode, navigate browser to file | |
| 460 | + if let Some(idx) = clicked_file { | |
| 461 | + self.selected_file_index = idx; | |
| 462 | + if self.active_mode == AppMode::Jv { | |
| 463 | + if let Some(loaded) = &self.current_session { | |
| 464 | + if let Some((name, _)) = loaded.parsed_files.get(idx) { | |
| 465 | + let display = name.strip_suffix(".json").unwrap_or(name).to_string(); | |
| 466 | + self.browser_view.navigate_to_file(&display, &loaded.parsed_files); | |
| 467 | + } | |
| 468 | + } | |
| 469 | + } | |
| 470 | + } | |
| 471 | + | |
| 472 | + if let Some(idx) = to_remove { | |
| 473 | + if let Some(loaded) = &mut self.current_session { | |
| 474 | + loaded.remove_file(idx); | |
| 475 | + self.session_manager.update_session(&loaded.session); | |
| 476 | + // Clean up disabled_files indices | |
| 477 | + self.disabled_files.remove(&idx); | |
| 478 | + let new_disabled: std::collections::BTreeSet<usize> = self.disabled_files | |
| 479 | + .iter() | |
| 480 | + .map(|&i| if i > idx { i - 1 } else { i }) | |
| 481 | + .collect(); | |
| 482 | + self.disabled_files = new_disabled; | |
| 483 | + if self.selected_file_index >= loaded.session.files.len() | |
| 484 | + && !loaded.session.files.is_empty() | |
| 485 | + { | |
| 486 | + self.selected_file_index = loaded.session.files.len() - 1; | |
| 487 | + } | |
| 488 | + self.rebuild_schema(); | |
| 489 | + } | |
| 490 | + } | |
| 491 | + } | |
| 492 | + }); | |
| 493 | + } | |
| 494 | + | |
| 495 | + fn show_settings(&mut self, ui: &mut egui::Ui) { | |
| 496 | + use egui_phosphor::regular; | |
| 497 | + | |
| 498 | + egui::ScrollArea::vertical() | |
| 499 | + .auto_shrink(false) | |
| 500 | + .show(ui, |ui| { | |
| 501 | + ui.add_space(8.0); | |
| 502 | + | |
| 503 | + // Schema section | |
| 504 | + ui.horizontal(|ui| { | |
| 505 | + ui.label( | |
| 506 | + RichText::new(format!("{} Schema", regular::GRAPH)) | |
| 507 | + .color(CatppuccinMocha::TEXT) | |
| 508 | + .size(14.0) | |
| 509 | + .strong(), | |
| 510 | + ); | |
| 511 | + }); | |
| 512 | + ui.add_space(8.0); | |
| 513 | + | |
| 514 | + // Jaccard similarity slider | |
| 515 | + ui.horizontal(|ui| { | |
| 516 | + ui.add_space(8.0); | |
| 517 | + ui.label( | |
| 518 | + RichText::new("Similarity threshold") | |
| 519 | + .color(CatppuccinMocha::SUBTEXT0) | |
| 520 | + .size(12.0), | |
| 521 | + ); | |
| 522 | + }); | |
| 523 | + ui.add_space(2.0); | |
| 524 | + { | |
| 525 | + let mut val = self | |
| 526 | + .current_session | |
| 527 | + .as_ref() | |
| 528 | + .map(|l| l.session.jaccard_threshold) | |
| 529 | + .unwrap_or(0.8); | |
| 530 | + let response = ui.horizontal(|ui| { | |
| 531 | + ui.add_space(8.0); | |
| 532 | + ui.add( | |
| 533 | + egui::Slider::new(&mut val, 0.3..=1.0) | |
| 534 | + .step_by(0.05) | |
| 535 | + .custom_formatter(|v, _| format!("{:.0}%", v * 100.0)), | |
| 536 | + ) | |
| 537 | + }).inner; | |
| 538 | + if let Some(loaded) = &mut self.current_session { | |
| 539 | + loaded.session.jaccard_threshold = val; | |
| 540 | + } | |
| 541 | + let pending = (val - self.applied_threshold).abs() > f32::EPSILON; | |
| 542 | + if pending && !response.dragged() { | |
| 543 | + self.applied_threshold = val; | |
| 544 | + self.session_manager.update_session( | |
| 545 | + &self.current_session.as_ref().unwrap().session, | |
| 546 | + ); | |
| 547 | + self.rebuild_schema(); | |
| 548 | + self.code_view.invalidate(); | |
| 549 | + self.shared_browser_view.invalidate(); | |
| 550 | + } | |
| 551 | + } | |
| 552 | + ui.add_space(4.0); | |
| 553 | + ui.horizontal(|ui| { | |
| 554 | + ui.add_space(8.0); | |
| 555 | + ui.label( | |
| 556 | + RichText::new( | |
| 557 | + "Controls how similar two object shapes must be to merge into the same struct. Higher values require more identical fields.", | |
| 558 | + ) | |
| 559 | + .color(CatppuccinMocha::OVERLAY0) | |
| 560 | + .size(11.0), | |
| 561 | + ); | |
| 562 | + }); | |
| 563 | + }); | |
| 564 | + } | |
| 565 | + | |
| 566 | + fn show_main_content(&mut self, ui: &mut egui::Ui) { | |
| 567 | + use egui_phosphor::regular; | |
| 568 | + | |
| 569 | + // Mode switcher | |
| 570 | + ui.horizontal(|ui| { | |
| 571 | + ui.spacing_mut().item_spacing.x = 4.0; | |
| 572 | + for (mode, label, icon) in [ | |
| 573 | + (AppMode::Jv, "jv", regular::BROWSERS), | |
| 574 | + (AppMode::Schema, "Schema", regular::GRAPH), | |
| 575 | + (AppMode::Groups, "Groups", regular::TREE_STRUCTURE), | |
| 576 | + (AppMode::Code, "Code", regular::CODE), | |
| 577 | + (AppMode::Settings, "Settings", regular::GEAR_SIX), | |
| 578 | + ] { | |
| 579 | + let selected = self.active_mode == mode; | |
| 580 | + let text_color = if selected { | |
| 581 | + CatppuccinMocha::BLUE | |
| 582 | + } else { | |
| 583 | + CatppuccinMocha::OVERLAY0 | |
| 584 | + }; | |
| 585 | + | |
| 586 | + let btn = ui.add( | |
| 587 | + egui::Button::new( | |
| 588 | + RichText::new(format!(" {} {} ", icon, label)) | |
| 589 | + .color(text_color) | |
| 590 | + .size(12.0), | |
| 591 | + ) | |
| 592 | + .fill(if selected { | |
| 593 | + CatppuccinMocha::SURFACE0 | |
| 594 | + } else { | |
| 595 | + egui::Color32::TRANSPARENT | |
| 596 | + }) | |
| 597 | + .corner_radius(6.0), | |
| 598 | + ); | |
| 599 | + | |
| 600 | + if btn.clicked() && self.active_mode != mode { | |
| 601 | + self.active_mode = mode; | |
| 602 | + self.active_tab = match mode { | |
| 603 | + AppMode::Jv => ViewTab::Table, | |
| 604 | + AppMode::Schema => ViewTab::Table, | |
| 605 | + AppMode::Groups => ViewTab::Table, | |
| 606 | + AppMode::Code => ViewTab::Code, | |
| 607 | + AppMode::Settings => ViewTab::Table, | |
| 608 | + }; | |
| 609 | + } | |
| 610 | + } | |
| 611 | + }); | |
| 612 | + ui.add_space(2.0); | |
| 613 | + | |
| 614 | + // Tab bar (conditional per mode) | |
| 615 | + let tabs: Vec<(ViewTab, &str, &str)> = match self.active_mode { | |
| 616 | + AppMode::Jv => vec![], | |
| 617 | + AppMode::Schema => vec![], | |
| 618 | + AppMode::Groups => vec![], | |
| 619 | + AppMode::Code => vec![], | |
| 620 | + AppMode::Settings => vec![], | |
| 621 | + }; | |
| 622 | + | |
| 623 | + if !tabs.is_empty() { | |
| 624 | + ui.horizontal(|ui| { | |
| 625 | + ui.spacing_mut().item_spacing.x = 2.0; | |
| 626 | + for (tab, label, icon) in &tabs { | |
| 627 | + let selected = self.active_tab == *tab; | |
| 628 | + let text_color = if selected { | |
| 629 | + CatppuccinMocha::TEXT | |
| 630 | + } else { | |
| 631 | + CatppuccinMocha::OVERLAY0 | |
| 632 | + }; | |
| 633 | + | |
| 634 | + let btn = ui.add( | |
| 635 | + egui::Button::new( | |
| 636 | + RichText::new(format!(" {} {} ", icon, label)) | |
| 637 | + .color(text_color) | |
| 638 | + .size(13.0), | |
| 639 | + ) | |
| 640 | + .fill(egui::Color32::TRANSPARENT) | |
| 641 | + .corner_radius(6.0), | |
| 642 | + ); | |
| 643 | + | |
| 644 | + if selected { | |
| 645 | + let r = btn.rect; | |
| 646 | + ui.painter().rect_filled( | |
| 647 | + egui::Rect::from_min_max( | |
| 648 | + egui::pos2(r.left() + 8.0, r.bottom() - 2.0), | |
| 649 | + egui::pos2(r.right() - 8.0, r.bottom()), | |
| 650 | + ), | |
| 651 | + 1.0, | |
| 652 | + CatppuccinMocha::BLUE, | |
| 653 | + ); | |
| 654 | + } | |
| 655 | + | |
| 656 | + if btn.clicked() { | |
| 657 | + self.active_tab = *tab; | |
| 658 | + } | |
| 659 | + } | |
| 660 | + }); | |
| 661 | + } | |
| 662 | + | |
| 663 | + // Subtle divider under tabs | |
| 664 | + let sep_rect = ui.available_rect_before_wrap(); | |
| 665 | + ui.painter().line_segment( | |
| 666 | + [ | |
| 667 | + egui::pos2(sep_rect.left(), sep_rect.top()), | |
| 668 | + egui::pos2(sep_rect.right(), sep_rect.top()), | |
| 669 | + ], | |
| 670 | + egui::Stroke::new(1.0, CatppuccinMocha::SURFACE0), | |
| 671 | + ); | |
| 672 | + ui.add_space(8.0); | |
| 673 | + | |
| 674 | + // Content — routed by mode | |
| 675 | + match self.active_mode { | |
| 676 | + AppMode::Schema => { | |
| 677 | + let has_schema = self.current_session.as_ref() | |
| 678 | + .and_then(|l| l.schema.as_ref()) | |
| 679 | + .is_some_and(|s| !s.structs.is_empty()); | |
| 680 | + if !has_schema { | |
| 681 | + ui.centered_and_justified(|ui| { | |
| 682 | + ui.label( | |
| 683 | + RichText::new("Import files to see schema diagram") | |
| 684 | + .color(CatppuccinMocha::OVERLAY0) | |
| 685 | + .size(16.0), | |
| 686 | + ); | |
| 687 | + }); | |
| 688 | + return; | |
| 689 | + } | |
| 690 | + let active_files = self.active_parsed_files(); | |
| 691 | + let loaded = self.current_session.as_ref().unwrap(); | |
| 692 | + let schema = loaded.schema.as_ref().unwrap(); | |
| 693 | + let structs = schema.structs.clone(); | |
| 694 | + let unique_structs = schema.unique_structs.clone(); | |
| 695 | + let enum_conversions = loaded.session.enum_conversions.clone(); | |
| 696 | + let hidden_fields = loaded.session.hidden_fields.clone(); | |
| 697 | + self.schema_diagram_view.show( | |
| 698 | + ui, | |
| 699 | + &active_files, | |
| 700 | + &structs, | |
| 701 | + &unique_structs, | |
| 702 | + &enum_conversions, | |
| 703 | + &hidden_fields, | |
| 704 | + ); | |
| 705 | + } | |
| 706 | + AppMode::Jv => { | |
| 707 | + let has_file = self | |
| 708 | + .current_session | |
| 709 | + .as_ref() | |
| 710 | + .is_some_and(|l| !l.parsed_files.is_empty()); | |
| 711 | + | |
| 712 | + if !has_file { | |
| 713 | + ui.centered_and_justified(|ui| { | |
| 714 | + ui.label( | |
| 715 | + RichText::new( | |
| 716 | + "Import a JSON file to get started\n\nDrag & drop files here, or use the Import buttons in the sidebar", | |
| 717 | + ) | |
| 718 | + .color(CatppuccinMocha::OVERLAY0) | |
| 719 | + .size(16.0), | |
| 720 | + ); | |
| 721 | + }); | |
| 722 | + return; | |
| 723 | + } | |
| 724 | + | |
| 725 | + let loaded = self.current_session.as_ref().unwrap(); | |
| 726 | + let file_idx = self.selected_file_index.min(loaded.parsed_files.len().saturating_sub(1)); | |
| 727 | + let file_count = loaded.parsed_files.len(); | |
| 728 | + | |
| 729 | + // Invalidate only when files are added/removed (not on selection change) | |
| 730 | + if file_count != self.last_file_count { | |
| 731 | + self.browser_view.invalidate(); | |
| 732 | + self.code_view.invalidate(); | |
| 733 | + self.schema_diagram_view.invalidate(); | |
| 734 | + self.last_file_count = file_count; | |
| 735 | + } | |
| 736 | + self.last_file_index = file_idx; | |
| 737 | + | |
| 738 | + self.browser_view.show(ui, &loaded.parsed_files); | |
| 739 | + | |
| 740 | + // Sync sidebar selection from browser's current file | |
| 741 | + if let Some(file_key) = self.browser_view.current_file_key(&loaded.parsed_files) { | |
| 742 | + if let Some(idx) = loaded.parsed_files.iter().position(|(n, _)| { | |
| 743 | + n.strip_suffix(".json").unwrap_or(n) == file_key | |
| 744 | + }) { | |
| 745 | + self.selected_file_index = idx; | |
| 746 | + } | |
| 747 | + } | |
| 748 | + } | |
| 749 | + AppMode::Groups => { | |
| 750 | + let loaded = self.current_session.as_ref(); | |
| 751 | + let has_schema = loaded | |
| 752 | + .and_then(|l| l.schema.as_ref()) | |
| 753 | + .is_some_and(|s| !s.structs.is_empty() || !s.unique_structs.is_empty()); | |
| 754 | + if !has_schema { | |
| 755 | + ui.centered_and_justified(|ui| { | |
| 756 | + ui.label( | |
| 757 | + RichText::new("Import multiple files to see shared types") | |
| 758 | + .color(CatppuccinMocha::OVERLAY0) | |
| 759 | + .size(16.0), | |
| 760 | + ); | |
| 761 | + }); | |
| 762 | + return; | |
| 763 | + } | |
| 764 | + | |
| 765 | + let active_files = self.active_parsed_files(); | |
| 766 | + let loaded = self.current_session.as_ref().unwrap(); | |
| 767 | + let schema = loaded.schema.as_ref().unwrap(); | |
| 768 | + self.shared_browser_view.show(ui, schema, &active_files); | |
| 769 | + } | |
| 770 | + AppMode::Settings => { | |
| 771 | + self.show_settings(ui); | |
| 772 | + } | |
| 773 | + AppMode::Code => { | |
| 774 | + let has_schema = self.current_session.as_ref() | |
| 775 | + .and_then(|l| l.schema.as_ref()) | |
| 776 | + .is_some_and(|s| !s.structs.is_empty()); | |
| 777 | + if !has_schema { | |
| 778 | + ui.centered_and_justified(|ui| { | |
| 779 | + ui.label( | |
| 780 | + RichText::new("Import files to generate code") | |
| 781 | + .color(CatppuccinMocha::OVERLAY0) | |
| 782 | + .size(16.0), | |
| 783 | + ); | |
| 784 | + }); | |
| 785 | + return; | |
| 786 | + } | |
| 787 | + let active_files = self.active_parsed_files(); | |
| 788 | + let loaded = self.current_session.as_ref().unwrap(); | |
| 789 | + let schema = loaded.schema.clone(); | |
| 790 | + let prev_enums = loaded.session.enum_conversions.clone(); | |
| 791 | + let prev_hidden_count = loaded.session.hidden_fields.len(); | |
| 792 | + let loaded = self.current_session.as_mut().unwrap(); | |
| 793 | + self.code_view.show( | |
| 794 | + ui, | |
| 795 | + &active_files, | |
| 796 | + schema.as_ref(), | |
| 797 | + &mut loaded.session.enum_conversions, | |
| 798 | + &mut loaded.session.hidden_fields, | |
| 799 | + ); | |
| 800 | + // Persist session if enum conversions or hidden fields changed | |
| 801 | + if loaded.session.enum_conversions != prev_enums | |
| 802 | + || loaded.session.hidden_fields.len() != prev_hidden_count | |
| 803 | + { | |
| 804 | + self.session_manager.update_session(&loaded.session); | |
| 805 | + } | |
| 806 | + } | |
| 807 | + } | |
| 808 | + } | |
| 809 | + | |
| 810 | + fn handle_dropped_files(&mut self, ctx: &egui::Context) { | |
| 811 | + // Collect dropped files | |
| 812 | + ctx.input(|i| { | |
| 813 | + for f in &i.raw.dropped_files { | |
| 814 | + self.dropped_files.push(f.clone()); | |
| 815 | + } | |
| 816 | + }); | |
| 817 | + | |
| 818 | + // Process drops | |
| 819 | + let files: Vec<egui::DroppedFile> = self.dropped_files.drain(..).collect(); | |
| 820 | + for file in files { | |
| 821 | + if let Some(path) = &file.path { | |
| 822 | + if path.is_dir() { | |
| 823 | + self.import_directory(path); | |
| 824 | + } else if path.extension().is_some_and(|e| e == "json" || e == "har") { | |
| 825 | + self.import_file(path); | |
| 826 | + } | |
| 827 | + } else if let Some(bytes) = &file.bytes { | |
| 828 | + let content = String::from_utf8_lossy(bytes).to_string(); | |
| 829 | + let name = file.name.clone(); | |
| 830 | + if let Some(loaded) = &mut self.current_session { | |
| 831 | + if loaded.add_file(&name, content).is_ok() { | |
| 832 | + self.session_manager.update_session(&loaded.session); | |
| 833 | + self.rebuild_schema(); | |
| 834 | + } | |
| 835 | + } | |
| 836 | + } | |
| 837 | + } | |
| 838 | + } | |
| 839 | +} | |
| 840 | + | |
| 841 | +impl eframe::App for JvApp { | |
| 842 | + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { | |
| 843 | + // Poll async file dialog results | |
| 844 | + if let Some(rx) = &self.file_dialog_rx { | |
| 845 | + match rx.try_recv() { | |
| 846 | + Ok(result) => { | |
| 847 | + match result { | |
| 848 | + FileDialogResult::Files(paths) => { | |
| 849 | + for path in paths { | |
| 850 | + self.import_file(&path); | |
| 851 | + } | |
| 852 | + } | |
| 853 | + FileDialogResult::Directory(dir) => { | |
| 854 | + self.import_directory(&dir); | |
| 855 | + } | |
| 856 | + } | |
| 857 | + self.file_dialog_rx = None; | |
| 858 | + } | |
| 859 | + Err(std::sync::mpsc::TryRecvError::Disconnected) => { | |
| 860 | + // Dialog was cancelled | |
| 861 | + self.file_dialog_rx = None; | |
| 862 | + } | |
| 863 | + Err(std::sync::mpsc::TryRecvError::Empty) => {} | |
| 864 | + } | |
| 865 | + } | |
| 866 | + | |
| 867 | + if !self.theme_applied { | |
| 868 | + CatppuccinMocha::apply(ctx); | |
| 869 | + ctx.style_mut(|style| { | |
| 870 | + style.spacing.scroll.floating = false; | |
| 871 | + style.spacing.scroll.bar_width = 8.0; | |
| 872 | + style.interaction.tooltip_delay = 0.0; | |
| 873 | + }); | |
| 874 | + self.theme_applied = true; | |
| 875 | + } | |
| 876 | + | |
| 877 | + // Boost scroll speed by scaling raw scroll events | |
| 878 | + ctx.input_mut(|input| { | |
| 879 | + for event in &mut input.events { | |
| 880 | + if let egui::Event::MouseWheel { delta, .. } = event { | |
| 881 | + delta.y *= 3.0; | |
| 882 | + } | |
| 883 | + } | |
| 884 | + }); | |
| 885 | + | |
| 886 | + self.handle_dropped_files(ctx); | |
| 887 | + | |
| 888 | + // FPS tracking (rolling average of last 60 frames) | |
| 889 | + let dt = ctx.input(|i| i.stable_dt) as f64; | |
| 890 | + self.frame_times.push(dt); | |
| 891 | + if self.frame_times.len() > 60 { | |
| 892 | + self.frame_times.remove(0); | |
| 893 | + } | |
| 894 | + if !self.frame_times.is_empty() { | |
| 895 | + let avg_dt: f64 = self.frame_times.iter().sum::<f64>() / self.frame_times.len() as f64; | |
| 896 | + self.fps_display = if avg_dt > 0.0 { 1.0 / avg_dt } else { 0.0 }; | |
| 897 | + } | |
| 898 | + | |
| 899 | + // FPS overlay — subtle pill in top-right | |
| 900 | + egui::Area::new(egui::Id::new("fps_overlay")) | |
| 901 | + .fixed_pos(egui::pos2(ctx.screen_rect().right() - 72.0, 6.0)) | |
| 902 | + .interactable(false) | |
| 903 | + .show(ctx, |ui| { | |
| 904 | + egui::Frame::new() | |
| 905 | + .fill(CatppuccinMocha::SURFACE0) | |
| 906 | + .corner_radius(10.0) | |
| 907 | + .inner_margin(egui::Margin::symmetric(8, 2)) | |
| 908 | + .show(ui, |ui| { | |
| 909 | + let fps = self.fps_display; | |
| 910 | + let color = if fps >= 55.0 { | |
| 911 | + CatppuccinMocha::GREEN | |
| 912 | + } else if fps >= 30.0 { | |
| 913 | + CatppuccinMocha::YELLOW | |
| 914 | + } else { | |
| 915 | + CatppuccinMocha::RED | |
| 916 | + }; | |
| 917 | + ui.horizontal(|ui| { | |
| 918 | + ui.spacing_mut().item_spacing.x = 3.0; | |
| 919 | + ui.label( | |
| 920 | + RichText::new(format!("{:.0}", fps)) | |
| 921 | + .color(color) | |
| 922 | + .family(egui::FontFamily::Monospace) | |
| 923 | + .size(10.0), | |
| 924 | + ); | |
| 925 | + ui.label( | |
| 926 | + RichText::new("fps") | |
| 927 | + .color(CatppuccinMocha::OVERLAY0) | |
| 928 | + .family(egui::FontFamily::Monospace) | |
| 929 | + .size(10.0), | |
| 930 | + ); | |
| 931 | + }); | |
| 932 | + }); | |
| 933 | + }); | |
| 934 | + | |
| 935 | + // New session dialog | |
| 936 | + if self.show_new_session_dialog { | |
| 937 | + egui::Window::new("New Session") | |
| 938 | + .collapsible(false) | |
| 939 | + .resizable(false) | |
| 940 | + .show(ctx, |ui| { | |
| 941 | + ui.horizontal(|ui| { | |
| 942 | + ui.label("Name:"); | |
| 943 | + ui.text_edit_singleline(&mut self.new_session_name); | |
| 944 | + }); | |
| 945 | + ui.horizontal(|ui| { | |
| 946 | + if ui.button("Create").clicked() && !self.new_session_name.is_empty() { | |
| 947 | + let session = self | |
| 948 | + .session_manager | |
| 949 | + .create_session(&self.new_session_name); | |
| 950 | + self.current_session = Some(LoadedSession::new(session)); | |
| 951 | + self.selected_file_index = 0; | |
| 952 | + self.new_session_name.clear(); | |
| 953 | + self.show_new_session_dialog = false; | |
| 954 | + } | |
| 955 | + if ui.button("Cancel").clicked() { | |
| 956 | + self.new_session_name.clear(); | |
| 957 | + self.show_new_session_dialog = false; | |
| 958 | + } | |
| 959 | + }); | |
| 960 | + }); | |
| 961 | + } | |
| 962 | + | |
| 963 | + // Sidebar | |
| 964 | + egui::SidePanel::left("sidebar") | |
| 965 | + .default_width(self.sidebar_width) | |
| 966 | + .min_width(180.0) | |
| 967 | + .max_width(400.0) | |
| 968 | + .resizable(true) | |
| 969 | + .frame(egui::Frame::new().fill(CatppuccinMocha::MANTLE).inner_margin(12.0)) | |
| 970 | + .show(ctx, |ui| { | |
| 971 | + self.show_sidebar(ui); | |
| 972 | + }); | |
| 973 | + | |
| 974 | + // Main content | |
| 975 | + egui::CentralPanel::default() | |
| 976 | + .frame( | |
| 977 | + egui::Frame::new() | |
| 978 | + .fill(CatppuccinMocha::BASE) | |
| 979 | + .inner_margin(16.0), | |
| 980 | + ) | |
| 981 | + .show(ctx, |ui| { | |
| 982 | + self.show_main_content(ui); | |
| 983 | + }); | |
| 984 | + } | |
| 985 | +} |
+318-0src/codegen.rs
| @@ -0,0 +1,318 @@ | ||
| 1 | +use std::collections::BTreeSet; | |
| 2 | + | |
| 3 | +use crate::types::InferredType; | |
| 4 | + | |
| 5 | +pub struct CodeGenerator { | |
| 6 | + pub structs: Vec<GeneratedStruct>, | |
| 7 | +} | |
| 8 | + | |
| 9 | +#[derive(Debug, Clone)] | |
| 10 | +pub struct GeneratedStruct { | |
| 11 | + pub name: String, | |
| 12 | + pub fields: Vec<GeneratedField>, | |
| 13 | +} | |
| 14 | + | |
| 15 | +#[derive(Debug, Clone)] | |
| 16 | +pub struct GeneratedField { | |
| 17 | + pub json_name: String, | |
| 18 | + pub inferred_type: InferredType, | |
| 19 | + /// Set when type is a child struct name (e.g., "Passenger", "Vec<Passenger>") | |
| 20 | + /// generate_code uses this instead of lang.type_name() when present | |
| 21 | + pub resolved_type: Option<String>, | |
| 22 | + pub needs_rename: bool, | |
| 23 | +} | |
| 24 | + | |
| 25 | +impl CodeGenerator { | |
| 26 | + pub fn from_value(value: &serde_json::Value) -> Self { | |
| 27 | + Self::from_value_named(value, "Root") | |
| 28 | + } | |
| 29 | + | |
| 30 | + pub fn from_value_named(value: &serde_json::Value, root_name: &str) -> Self { | |
| 31 | + let mut structs = Vec::new(); | |
| 32 | + let mut seen_names = BTreeSet::new(); | |
| 33 | + Self::collect_structs(value, root_name, &mut structs, &mut seen_names); | |
| 34 | + Self { structs } | |
| 35 | + } | |
| 36 | + | |
| 37 | + pub fn from_schema(shared: &[crate::schema::SharedStruct]) -> Self { | |
| 38 | + let mut structs = Vec::new(); | |
| 39 | + | |
| 40 | + for s in shared { | |
| 41 | + let fields = s | |
| 42 | + .fields | |
| 43 | + .iter() | |
| 44 | + .map(|(key, typ)| { | |
| 45 | + let snake = to_snake_case(key); | |
| 46 | + let needs_rename = snake != *key; | |
| 47 | + GeneratedField { | |
| 48 | + json_name: key.clone(), | |
| 49 | + inferred_type: typ.clone(), | |
| 50 | + resolved_type: None, | |
| 51 | + needs_rename, | |
| 52 | + } | |
| 53 | + }) | |
| 54 | + .collect(); | |
| 55 | + | |
| 56 | + structs.push(GeneratedStruct { | |
| 57 | + name: s.name.clone(), | |
| 58 | + fields, | |
| 59 | + }); | |
| 60 | + } | |
| 61 | + | |
| 62 | + Self { structs } | |
| 63 | + } | |
| 64 | + | |
| 65 | + fn collect_structs( | |
| 66 | + value: &serde_json::Value, | |
| 67 | + name: &str, | |
| 68 | + structs: &mut Vec<GeneratedStruct>, | |
| 69 | + seen: &mut BTreeSet<String>, | |
| 70 | + ) { | |
| 71 | + match value { | |
| 72 | + serde_json::Value::Object(map) => { | |
| 73 | + let mut fields = Vec::new(); | |
| 74 | + for (key, val) in map { | |
| 75 | + let typ = crate::types::infer_type(val); | |
| 76 | + let snake = to_snake_case(key); | |
| 77 | + let needs_rename = snake != *key; | |
| 78 | + | |
| 79 | + let resolved_type = match val { | |
| 80 | + serde_json::Value::Object(_) => { | |
| 81 | + let child_name = to_pascal_case(key); | |
| 82 | + Self::collect_structs(val, &child_name, structs, seen); | |
| 83 | + Some(child_name) | |
| 84 | + } | |
| 85 | + serde_json::Value::Array(arr) => { | |
| 86 | + if let Some(first) = arr.first() { | |
| 87 | + if first.is_object() { | |
| 88 | + let child_name = to_pascal_case(&singularize(key)); | |
| 89 | + Self::collect_structs(first, &child_name, structs, seen); | |
| 90 | + Some(format!("Vec<{}>", child_name)) | |
| 91 | + } else { | |
| 92 | + None | |
| 93 | + } | |
| 94 | + } else { | |
| 95 | + None | |
| 96 | + } | |
| 97 | + } | |
| 98 | + _ => None, | |
| 99 | + }; | |
| 100 | + | |
| 101 | + fields.push(GeneratedField { | |
| 102 | + json_name: key.clone(), | |
| 103 | + inferred_type: typ, | |
| 104 | + resolved_type, | |
| 105 | + needs_rename, | |
| 106 | + }); | |
| 107 | + } | |
| 108 | + | |
| 109 | + // Check if an existing struct with this name has identical fields | |
| 110 | + if seen.contains(name) { | |
| 111 | + let field_sig: Vec<(&str, &InferredType, Option<&str>)> = fields | |
| 112 | + .iter() | |
| 113 | + .map(|f| (f.json_name.as_str(), &f.inferred_type, f.resolved_type.as_deref())) | |
| 114 | + .collect(); | |
| 115 | + let already_exists = structs.iter().any(|s| { | |
| 116 | + s.name == name && s.fields.len() == fields.len() && { | |
| 117 | + let existing_sig: Vec<(&str, &InferredType, Option<&str>)> = s.fields | |
| 118 | + .iter() | |
| 119 | + .map(|f| (f.json_name.as_str(), &f.inferred_type, f.resolved_type.as_deref())) | |
| 120 | + .collect(); | |
| 121 | + existing_sig == field_sig | |
| 122 | + } | |
| 123 | + }); | |
| 124 | + if already_exists { | |
| 125 | + return; | |
| 126 | + } | |
| 127 | + } | |
| 128 | + | |
| 129 | + let unique_name = make_unique_name(name, seen); | |
| 130 | + seen.insert(unique_name.clone()); | |
| 131 | + | |
| 132 | + structs.push(GeneratedStruct { | |
| 133 | + name: unique_name, | |
| 134 | + fields, | |
| 135 | + }); | |
| 136 | + } | |
| 137 | + serde_json::Value::Array(arr) => { | |
| 138 | + if let Some(first) = arr.first() { | |
| 139 | + Self::collect_structs(first, name, structs, seen); | |
| 140 | + } | |
| 141 | + } | |
| 142 | + _ => {} | |
| 143 | + } | |
| 144 | + } | |
| 145 | + | |
| 146 | + pub fn generate_code(&self, lang: &dyn crate::lang::LanguageGenerator) -> String { | |
| 147 | + let mut output = String::new(); | |
| 148 | + output.push_str(&lang.file_header()); | |
| 149 | + | |
| 150 | + // Check if any field uses temporal types | |
| 151 | + let needs_temporal = self.structs.iter().any(|s| { | |
| 152 | + s.fields.iter().any(|f| { | |
| 153 | + matches!( | |
| 154 | + f.inferred_type, | |
| 155 | + InferredType::DateTime | InferredType::Date | InferredType::Time | |
| 156 | + ) | |
| 157 | + }) | |
| 158 | + }); | |
| 159 | + output.push_str(&lang.imports_header(needs_temporal, false)); | |
| 160 | + if !output.is_empty() && !output.ends_with('\n') { | |
| 161 | + output.push('\n'); | |
| 162 | + } | |
| 163 | + | |
| 164 | + for (i, s) in self.structs.iter().rev().enumerate() { | |
| 165 | + if i > 0 { | |
| 166 | + output.push('\n'); | |
| 167 | + } | |
| 168 | + output.push_str(&lang.struct_open(&s.name)); | |
| 169 | + | |
| 170 | + let mut field_pairs: Vec<(String, String)> = Vec::new(); | |
| 171 | + for field in &s.fields { | |
| 172 | + let code_name = lang.field_name(&field.json_name); | |
| 173 | + let type_str = field | |
| 174 | + .resolved_type | |
| 175 | + .clone() | |
| 176 | + .unwrap_or_else(|| lang.type_name(&field.inferred_type)); | |
| 177 | + output.push_str(&lang.field_line(&code_name, &type_str)); | |
| 178 | + field_pairs.push((code_name, field.json_name.clone())); | |
| 179 | + } | |
| 180 | + | |
| 181 | + output.push_str(&lang.struct_close(&field_pairs)); | |
| 182 | + } | |
| 183 | + | |
| 184 | + output | |
| 185 | + } | |
| 186 | +} | |
| 187 | + | |
| 188 | +pub fn to_snake_case(s: &str) -> String { | |
| 189 | + let mut result = String::new(); | |
| 190 | + for (i, ch) in s.chars().enumerate() { | |
| 191 | + if ch.is_ascii_uppercase() { | |
| 192 | + if i > 0 { | |
| 193 | + let prev = s.chars().nth(i - 1).unwrap_or('_'); | |
| 194 | + if prev != '_' && !prev.is_ascii_uppercase() { | |
| 195 | + result.push('_'); | |
| 196 | + } | |
| 197 | + } | |
| 198 | + result.push(ch.to_ascii_lowercase()); | |
| 199 | + } else if ch == '-' || ch == ' ' { | |
| 200 | + result.push('_'); | |
| 201 | + } else { | |
| 202 | + result.push(ch); | |
| 203 | + } | |
| 204 | + } | |
| 205 | + // Ensure it's a valid Rust identifier | |
| 206 | + if result.starts_with(|c: char| c.is_ascii_digit()) { | |
| 207 | + result.insert(0, '_'); | |
| 208 | + } | |
| 209 | + sanitize_keyword(&result) | |
| 210 | +} | |
| 211 | + | |
| 212 | +pub fn to_pascal_case(s: &str) -> String { | |
| 213 | + let mut result = String::new(); | |
| 214 | + let mut capitalize_next = true; | |
| 215 | + for ch in s.chars() { | |
| 216 | + if ch == '_' || ch == '-' || ch == ' ' { | |
| 217 | + capitalize_next = true; | |
| 218 | + } else if capitalize_next { | |
| 219 | + result.push(ch.to_ascii_uppercase()); | |
| 220 | + capitalize_next = false; | |
| 221 | + } else { | |
| 222 | + result.push(ch); | |
| 223 | + } | |
| 224 | + } | |
| 225 | + result | |
| 226 | +} | |
| 227 | + | |
| 228 | +pub fn singularize(s: &str) -> String { | |
| 229 | + let lower = s.to_ascii_lowercase(); | |
| 230 | + | |
| 231 | + // Words that end in 's' but aren't plural | |
| 232 | + const NOT_PLURAL: &[&str] = &[ | |
| 233 | + "status", "address", "bus", "canvas", "atlas", "alias", "basis", | |
| 234 | + "radius", "focus", "census", "corpus", "consensus", "osis", | |
| 235 | + "sis", "nexus", "plus", "minus", "gas", "class", "pass", | |
| 236 | + "process", "access", "success", "progress", "express", | |
| 237 | + ]; | |
| 238 | + for &word in NOT_PLURAL { | |
| 239 | + if lower == word || lower.ends_with(word) { | |
| 240 | + return s.to_string(); | |
| 241 | + } | |
| 242 | + } | |
| 243 | + | |
| 244 | + if lower.ends_with("ies") && lower.len() > 4 { | |
| 245 | + // categories -> category, companies -> company | |
| 246 | + format!("{}y", &s[..s.len() - 3]) | |
| 247 | + } else if lower.ends_with("ses") || lower.ends_with("xes") || lower.ends_with("zes") { | |
| 248 | + // responses -> response, indexes -> index, buzzes -> buzz | |
| 249 | + // but not "ses" alone | |
| 250 | + if lower.len() > 4 { | |
| 251 | + s[..s.len() - 2].to_string() | |
| 252 | + } else { | |
| 253 | + s.to_string() | |
| 254 | + } | |
| 255 | + } else if lower.ends_with("ves") { | |
| 256 | + // leaves -> leaf (but this is rare in JSON, just strip the s) | |
| 257 | + s[..s.len() - 1].to_string() | |
| 258 | + } else if lower.ends_with('s') | |
| 259 | + && !lower.ends_with("ss") | |
| 260 | + && !lower.ends_with("us") | |
| 261 | + && !lower.ends_with("is") | |
| 262 | + { | |
| 263 | + // trips -> trip, users -> user | |
| 264 | + s[..s.len() - 1].to_string() | |
| 265 | + } else { | |
| 266 | + s.to_string() | |
| 267 | + } | |
| 268 | +} | |
| 269 | + | |
| 270 | +/// Extract the first "normal" word from a filename (skip dates, numbers, version strings) | |
| 271 | +/// e.g. "trip_29_feb2026_rv0.json" -> "trip", "users.json" -> "users" | |
| 272 | +/// Returns lowercase, not PascalCased — caller decides casing. | |
| 273 | +pub fn first_normal_word(filename: &str) -> Option<String> { | |
| 274 | + let stem = std::path::Path::new(filename) | |
| 275 | + .file_stem() | |
| 276 | + .and_then(|s| s.to_str()) | |
| 277 | + .unwrap_or(filename); | |
| 278 | + | |
| 279 | + for part in stem.split(|c: char| c == '_' || c == '-' || c == ' ' || c == '.') { | |
| 280 | + if part.is_empty() { | |
| 281 | + continue; | |
| 282 | + } | |
| 283 | + if part.chars().all(|c| c.is_ascii_digit()) { | |
| 284 | + continue; | |
| 285 | + } | |
| 286 | + if part.len() <= 4 && part.chars().any(|c| c.is_ascii_digit()) { | |
| 287 | + continue; | |
| 288 | + } | |
| 289 | + return Some(part.to_ascii_lowercase()); | |
| 290 | + } | |
| 291 | + None | |
| 292 | +} | |
| 293 | + | |
| 294 | +fn make_unique_name(name: &str, seen: &BTreeSet<String>) -> String { | |
| 295 | + if !seen.contains(name) { | |
| 296 | + return name.to_string(); | |
| 297 | + } | |
| 298 | + let mut i = 2; | |
| 299 | + loop { | |
| 300 | + let candidate = format!("{}{}", name, i); | |
| 301 | + if !seen.contains(&candidate) { | |
| 302 | + return candidate; | |
| 303 | + } | |
| 304 | + i += 1; | |
| 305 | + } | |
| 306 | +} | |
| 307 | + | |
| 308 | +fn sanitize_keyword(s: &str) -> String { | |
| 309 | + match s { | |
| 310 | + "type" | "struct" | "enum" | "fn" | "let" | "mut" | "ref" | "self" | "super" | "mod" | |
| 311 | + | "use" | "pub" | "crate" | "impl" | "trait" | "for" | "loop" | "while" | "if" | |
| 312 | + | "else" | "match" | "return" | "break" | "continue" | "move" | "async" | "await" | |
| 313 | + | "dyn" | "static" | "const" | "where" | "unsafe" | "extern" | "as" | "in" => { | |
| 314 | + format!("r#{}", s) | |
| 315 | + } | |
| 316 | + _ => s.to_string(), | |
| 317 | + } | |
| 318 | +} |
+388-0src/har.rs
| @@ -0,0 +1,388 @@ | ||
| 1 | +use serde_json::Value; | |
| 2 | +use std::collections::HashMap; | |
| 3 | + | |
| 4 | +/// Extracts JSON response bodies from a HAR file, returning (filename, parsed_json) pairs. | |
| 5 | +pub fn extract_har_files(har: &Value) -> Vec<(String, Value)> { | |
| 6 | + let entries = match har.pointer("/log/entries") { | |
| 7 | + Some(Value::Array(arr)) => arr, | |
| 8 | + _ => return vec![], | |
| 9 | + }; | |
| 10 | + | |
| 11 | + let mut name_counts: HashMap<String, usize> = HashMap::new(); | |
| 12 | + let mut results = Vec::new(); | |
| 13 | + | |
| 14 | + for entry in entries { | |
| 15 | + // Filter: response status must be 2xx | |
| 16 | + let status = entry | |
| 17 | + .pointer("/response/status") | |
| 18 | + .and_then(|v| v.as_u64()) | |
| 19 | + .unwrap_or(0); | |
| 20 | + if status < 200 || status >= 300 { | |
| 21 | + continue; | |
| 22 | + } | |
| 23 | + | |
| 24 | + // Filter: mimeType must contain "json" | |
| 25 | + let mime = entry | |
| 26 | + .pointer("/response/content/mimeType") | |
| 27 | + .and_then(|v| v.as_str()) | |
| 28 | + .unwrap_or(""); | |
| 29 | + if !mime.contains("json") { | |
| 30 | + continue; | |
| 31 | + } | |
| 32 | + | |
| 33 | + // Filter: must not be base64 encoded | |
| 34 | + let encoding = entry | |
| 35 | + .pointer("/response/content/encoding") | |
| 36 | + .and_then(|v| v.as_str()) | |
| 37 | + .unwrap_or(""); | |
| 38 | + if encoding == "base64" { | |
| 39 | + continue; | |
| 40 | + } | |
| 41 | + | |
| 42 | + // Parse response body | |
| 43 | + let text = match entry.pointer("/response/content/text").and_then(|v| v.as_str()) { | |
| 44 | + Some(t) => t, | |
| 45 | + None => continue, | |
| 46 | + }; | |
| 47 | + let parsed: Value = match serde_json::from_str(text) { | |
| 48 | + Ok(v) => v, | |
| 49 | + Err(_) => continue, | |
| 50 | + }; | |
| 51 | + | |
| 52 | + // Build filename | |
| 53 | + let url = entry | |
| 54 | + .pointer("/request/url") | |
| 55 | + .and_then(|v| v.as_str()) | |
| 56 | + .unwrap_or(""); | |
| 57 | + let method = entry | |
| 58 | + .pointer("/request/method") | |
| 59 | + .and_then(|v| v.as_str()) | |
| 60 | + .unwrap_or("GET"); | |
| 61 | + | |
| 62 | + let path_seg = extract_path_segment(url); | |
| 63 | + let values = collect_distinguishing_values(entry, method, url); | |
| 64 | + | |
| 65 | + let base_name = if values.is_empty() { | |
| 66 | + path_seg.clone() | |
| 67 | + } else { | |
| 68 | + format!("{}_{}", path_seg, values.join("_")) | |
| 69 | + }; | |
| 70 | + | |
| 71 | + let count = name_counts.entry(base_name.clone()).or_insert(0); | |
| 72 | + *count += 1; | |
| 73 | + let filename = if *count == 1 { | |
| 74 | + format!("{}.json", base_name) | |
| 75 | + } else { | |
| 76 | + format!("{}_{}.json", base_name, count) | |
| 77 | + }; | |
| 78 | + | |
| 79 | + results.push((filename, parsed)); | |
| 80 | + } | |
| 81 | + | |
| 82 | + results | |
| 83 | +} | |
| 84 | + | |
| 85 | +/// Extracts the last meaningful URL path segment, stripping common API prefixes | |
| 86 | +/// and numeric-only segments. | |
| 87 | +fn extract_path_segment(url: &str) -> String { | |
| 88 | + // Strip query string and fragment | |
| 89 | + let path_part = url.split('?').next().unwrap_or(url); | |
| 90 | + let path_part = path_part.split('#').next().unwrap_or(path_part); | |
| 91 | + | |
| 92 | + // Extract path from full URL (strip scheme + host) | |
| 93 | + let path = if let Some(idx) = path_part.find("://") { | |
| 94 | + let after_scheme = &path_part[idx + 3..]; | |
| 95 | + after_scheme.find('/').map_or("", |i| &after_scheme[i..]) | |
| 96 | + } else { | |
| 97 | + path_part | |
| 98 | + }; | |
| 99 | + | |
| 100 | + let segments: Vec<&str> = path | |
| 101 | + .split('/') | |
| 102 | + .filter(|s| !s.is_empty()) | |
| 103 | + .filter(|s| { | |
| 104 | + let lower = s.to_lowercase(); | |
| 105 | + lower != "api" && lower != "rest" && !is_version_segment(s) | |
| 106 | + }) | |
| 107 | + .collect(); | |
| 108 | + | |
| 109 | + // Walk backwards to find a non-numeric segment | |
| 110 | + for seg in segments.iter().rev() { | |
| 111 | + if !seg.chars().all(|c| c.is_ascii_digit()) { | |
| 112 | + return seg.to_string(); | |
| 113 | + } | |
| 114 | + } | |
| 115 | + | |
| 116 | + // Fallback: use last segment even if numeric, or "response" | |
| 117 | + segments | |
| 118 | + .last() | |
| 119 | + .map(|s| s.to_string()) | |
| 120 | + .unwrap_or_else(|| "response".to_string()) | |
| 121 | +} | |
| 122 | + | |
| 123 | +/// Collects short, useful param values from the request for naming purposes. | |
| 124 | +fn collect_distinguishing_values(entry: &Value, method: &str, url: &str) -> Vec<String> { | |
| 125 | + let mut values = Vec::new(); | |
| 126 | + | |
| 127 | + // Extract query string values | |
| 128 | + if let Some(query) = url.split('?').nth(1) { | |
| 129 | + for pair in query.split('&') { | |
| 130 | + let val = pair.split('=').nth(1).unwrap_or(""); | |
| 131 | + if is_useful_value(val) { | |
| 132 | + values.push(val.to_string()); | |
| 133 | + } | |
| 134 | + } | |
| 135 | + } | |
| 136 | + | |
| 137 | + // For POST, also look at body fields | |
| 138 | + if method.eq_ignore_ascii_case("POST") { | |
| 139 | + if let Some(text) = entry | |
| 140 | + .pointer("/request/postData/text") | |
| 141 | + .and_then(|v| v.as_str()) | |
| 142 | + { | |
| 143 | + if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(text) { | |
| 144 | + for (_key, val) in &map { | |
| 145 | + if let Some(s) = val.as_str() { | |
| 146 | + if is_useful_value(s) { | |
| 147 | + values.push(s.to_string()); | |
| 148 | + } | |
| 149 | + } | |
| 150 | + } | |
| 151 | + } | |
| 152 | + } | |
| 153 | + } | |
| 154 | + | |
| 155 | + values | |
| 156 | +} | |
| 157 | + | |
| 158 | +/// Returns true for version path segments like v1, v2, v10, etc. | |
| 159 | +fn is_version_segment(s: &str) -> bool { | |
| 160 | + let lower = s.to_lowercase(); | |
| 161 | + if let Some(rest) = lower.strip_prefix('v') { | |
| 162 | + !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) | |
| 163 | + } else { | |
| 164 | + false | |
| 165 | + } | |
| 166 | +} | |
| 167 | + | |
| 168 | +/// Returns true if the value is short, not all digits, and not hex-like. | |
| 169 | +fn is_useful_value(s: &str) -> bool { | |
| 170 | + if s.is_empty() || s.len() > 15 { | |
| 171 | + return false; | |
| 172 | + } | |
| 173 | + if s.chars().all(|c| c.is_ascii_digit()) { | |
| 174 | + return false; | |
| 175 | + } | |
| 176 | + if is_hex_like(s) { | |
| 177 | + return false; | |
| 178 | + } | |
| 179 | + true | |
| 180 | +} | |
| 181 | + | |
| 182 | +/// Returns true if the string looks like a UUID or is predominantly hex digits. | |
| 183 | +fn is_hex_like(s: &str) -> bool { | |
| 184 | + // UUID pattern: 8-4-4-4-12 hex digits | |
| 185 | + let stripped = s.replace('-', ""); | |
| 186 | + if stripped.len() == 32 && stripped.chars().all(|c| c.is_ascii_hexdigit()) { | |
| 187 | + return true; | |
| 188 | + } | |
| 189 | + // Long strings with >50% hex digits | |
| 190 | + if s.len() > 15 { | |
| 191 | + let hex_count = s.chars().filter(|c| c.is_ascii_hexdigit()).count(); | |
| 192 | + if hex_count as f64 / s.len() as f64 > 0.5 { | |
| 193 | + return true; | |
| 194 | + } | |
| 195 | + } | |
| 196 | + false | |
| 197 | +} | |
| 198 | + | |
| 199 | +#[cfg(test)] | |
| 200 | +mod tests { | |
| 201 | + use super::*; | |
| 202 | + use serde_json::json; | |
| 203 | + | |
| 204 | + fn make_entry(method: &str, url: &str, status: u64, mime: &str, body: &str) -> Value { | |
| 205 | + make_entry_with_opts(method, url, status, mime, body, None, None) | |
| 206 | + } | |
| 207 | + | |
| 208 | + fn make_entry_with_opts( | |
| 209 | + method: &str, | |
| 210 | + url: &str, | |
| 211 | + status: u64, | |
| 212 | + mime: &str, | |
| 213 | + body: &str, | |
| 214 | + encoding: Option<&str>, | |
| 215 | + post_data: Option<Value>, | |
| 216 | + ) -> Value { | |
| 217 | + let mut entry = json!({ | |
| 218 | + "request": { | |
| 219 | + "method": method, | |
| 220 | + "url": url, | |
| 221 | + }, | |
| 222 | + "response": { | |
| 223 | + "status": status, | |
| 224 | + "content": { | |
| 225 | + "mimeType": mime, | |
| 226 | + "text": body, | |
| 227 | + } | |
| 228 | + } | |
| 229 | + }); | |
| 230 | + if let Some(enc) = encoding { | |
| 231 | + entry["response"]["content"]["encoding"] = json!(enc); | |
| 232 | + } | |
| 233 | + if let Some(pd) = post_data { | |
| 234 | + entry["request"]["postData"] = pd; | |
| 235 | + } | |
| 236 | + entry | |
| 237 | + } | |
| 238 | + | |
| 239 | + fn make_har(entries: Vec<Value>) -> Value { | |
| 240 | + json!({ "log": { "entries": entries } }) | |
| 241 | + } | |
| 242 | + | |
| 243 | + #[test] | |
| 244 | + fn test_extract_har_files_basic() { | |
| 245 | + let entry = make_entry( | |
| 246 | + "GET", | |
| 247 | + "https://api.example.com/api/v2/flights?origin=LAX&date=2026-03-01", | |
| 248 | + 200, | |
| 249 | + "application/json", | |
| 250 | + r#"{"flights":[]}"#, | |
| 251 | + ); | |
| 252 | + let har = make_har(vec![entry]); | |
| 253 | + let results = extract_har_files(&har); | |
| 254 | + assert_eq!(results.len(), 1); | |
| 255 | + assert_eq!(results[0].0, "flights_LAX_2026-03-01.json"); | |
| 256 | + } | |
| 257 | + | |
| 258 | + #[test] | |
| 259 | + fn test_extract_har_files_post() { | |
| 260 | + let entry = make_entry_with_opts( | |
| 261 | + "POST", | |
| 262 | + "https://example.com/auth/login", | |
| 263 | + 200, | |
| 264 | + "application/json", | |
| 265 | + r#"{"token":"abc"}"#, | |
| 266 | + None, | |
| 267 | + Some(json!({ "text": r#"{"username":"alice"}"# })), | |
| 268 | + ); | |
| 269 | + let har = make_har(vec![entry]); | |
| 270 | + let results = extract_har_files(&har); | |
| 271 | + assert_eq!(results.len(), 1); | |
| 272 | + assert_eq!(results[0].0, "login_alice.json"); | |
| 273 | + } | |
| 274 | + | |
| 275 | + #[test] | |
| 276 | + fn test_extract_har_skips_non_json() { | |
| 277 | + let entry = make_entry("GET", "https://example.com/image", 200, "image/png", ""); | |
| 278 | + let har = make_har(vec![entry]); | |
| 279 | + let results = extract_har_files(&har); | |
| 280 | + assert!(results.is_empty()); | |
| 281 | + } | |
| 282 | + | |
| 283 | + #[test] | |
| 284 | + fn test_extract_har_skips_non_2xx() { | |
| 285 | + let entry = make_entry( | |
| 286 | + "GET", | |
| 287 | + "https://example.com/missing", | |
| 288 | + 404, | |
| 289 | + "application/json", | |
| 290 | + r#"{"error":"not found"}"#, | |
| 291 | + ); | |
| 292 | + let har = make_har(vec![entry]); | |
| 293 | + let results = extract_har_files(&har); | |
| 294 | + assert!(results.is_empty()); | |
| 295 | + } | |
| 296 | + | |
| 297 | + #[test] | |
| 298 | + fn test_extract_har_skips_base64() { | |
| 299 | + let entry = make_entry_with_opts( | |
| 300 | + "GET", | |
| 301 | + "https://example.com/data", | |
| 302 | + 200, | |
| 303 | + "application/json", | |
| 304 | + "eyJrZXkiOiJ2YWx1ZSJ9", | |
| 305 | + Some("base64"), | |
| 306 | + None, | |
| 307 | + ); | |
| 308 | + let har = make_har(vec![entry]); | |
| 309 | + let results = extract_har_files(&har); | |
| 310 | + assert!(results.is_empty()); | |
| 311 | + } | |
| 312 | + | |
| 313 | + #[test] | |
| 314 | + fn test_extract_path_segment() { | |
| 315 | + assert_eq!( | |
| 316 | + extract_path_segment("https://api.example.com/api/v2/flights?origin=LAX"), | |
| 317 | + "flights" | |
| 318 | + ); | |
| 319 | + assert_eq!( | |
| 320 | + extract_path_segment("https://example.com/users/42/bookings?status=confirmed"), | |
| 321 | + "bookings" | |
| 322 | + ); | |
| 323 | + assert_eq!( | |
| 324 | + extract_path_segment("https://example.com/health"), | |
| 325 | + "health" | |
| 326 | + ); | |
| 327 | + assert_eq!( | |
| 328 | + extract_path_segment("https://example.com/rest/v1/items"), | |
| 329 | + "items" | |
| 330 | + ); | |
| 331 | + assert_eq!( | |
| 332 | + extract_path_segment("https://example.com/auth/login"), | |
| 333 | + "login" | |
| 334 | + ); | |
| 335 | + } | |
| 336 | + | |
| 337 | + #[test] | |
| 338 | + fn test_is_hex_like() { | |
| 339 | + // UUID | |
| 340 | + assert!(is_hex_like("550e8400-e29b-41d4-a716-446655440000")); | |
| 341 | + // Long hex string | |
| 342 | + assert!(is_hex_like("abcdef1234567890abcdef")); | |
| 343 | + // Short string — not hex-like | |
| 344 | + assert!(!is_hex_like("LAX")); | |
| 345 | + assert!(!is_hex_like("hello")); | |
| 346 | + // Short hex but under length threshold | |
| 347 | + assert!(!is_hex_like("abcdef")); | |
| 348 | + } | |
| 349 | + | |
| 350 | + #[test] | |
| 351 | + fn test_deduplication() { | |
| 352 | + let entry1 = make_entry( | |
| 353 | + "GET", | |
| 354 | + "https://example.com/api/v2/flights?origin=LAX", | |
| 355 | + 200, | |
| 356 | + "application/json", | |
| 357 | + r#"{"id":1}"#, | |
| 358 | + ); | |
| 359 | + let entry2 = make_entry( | |
| 360 | + "GET", | |
| 361 | + "https://example.com/api/v2/flights?origin=LAX", | |
| 362 | + 200, | |
| 363 | + "application/json", | |
| 364 | + r#"{"id":2}"#, | |
| 365 | + ); | |
| 366 | + let har = make_har(vec![entry1, entry2]); | |
| 367 | + let results = extract_har_files(&har); | |
| 368 | + assert_eq!(results.len(), 2); | |
| 369 | + assert_eq!(results[0].0, "flights_LAX.json"); | |
| 370 | + assert_eq!(results[1].0, "flights_LAX_2.json"); | |
| 371 | + } | |
| 372 | + | |
| 373 | + #[test] | |
| 374 | + fn test_excludes_long_tokens() { | |
| 375 | + let entry = make_entry( | |
| 376 | + "GET", | |
| 377 | + "https://example.com/api/data?token=abcdefghijklmnopqrstuvwxyz&lang=en", | |
| 378 | + 200, | |
| 379 | + "application/json", | |
| 380 | + r#"{"ok":true}"#, | |
| 381 | + ); | |
| 382 | + let har = make_har(vec![entry]); | |
| 383 | + let results = extract_har_files(&har); | |
| 384 | + assert_eq!(results.len(), 1); | |
| 385 | + // Long token excluded, short "en" kept | |
| 386 | + assert_eq!(results[0].0, "data_en.json"); | |
| 387 | + } | |
| 388 | +} |
+98-0src/jq_engine.rs
| @@ -0,0 +1,98 @@ | ||
| 1 | +use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val}; | |
| 2 | + | |
| 3 | +pub struct JqEngine; | |
| 4 | + | |
| 5 | +#[derive(Debug)] | |
| 6 | +pub struct JqResult { | |
| 7 | + pub output: Vec<String>, | |
| 8 | + pub error: Option<String>, | |
| 9 | +} | |
| 10 | + | |
| 11 | +impl JqEngine { | |
| 12 | + pub fn execute(query: &str, input: &serde_json::Value) -> JqResult { | |
| 13 | + if query.trim().is_empty() { | |
| 14 | + return JqResult { | |
| 15 | + output: vec![serde_json::to_string_pretty(input).unwrap_or_default()], | |
| 16 | + error: None, | |
| 17 | + }; | |
| 18 | + } | |
| 19 | + | |
| 20 | + let mut defs = ParseCtx::new(Vec::new()); | |
| 21 | + defs.insert_natives(jaq_core::core()); | |
| 22 | + defs.insert_defs(jaq_std::std()); | |
| 23 | + | |
| 24 | + let (filter, errs) = jaq_parse::parse(query, jaq_parse::main()); | |
| 25 | + | |
| 26 | + if !errs.is_empty() { | |
| 27 | + return JqResult { | |
| 28 | + output: Vec::new(), | |
| 29 | + error: Some(format!("Parse error in query")), | |
| 30 | + }; | |
| 31 | + } | |
| 32 | + | |
| 33 | + let Some(filter) = filter else { | |
| 34 | + return JqResult { | |
| 35 | + output: Vec::new(), | |
| 36 | + error: Some("Failed to parse query".to_string()), | |
| 37 | + }; | |
| 38 | + }; | |
| 39 | + | |
| 40 | + let filter = defs.compile(filter); | |
| 41 | + | |
| 42 | + let inputs = RcIter::new(core::iter::empty()); | |
| 43 | + let val = Val::from(input.clone()); | |
| 44 | + | |
| 45 | + let mut output = Vec::new(); | |
| 46 | + for result in filter.run((Ctx::new([], &inputs), val)) { | |
| 47 | + match result { | |
| 48 | + Ok(v) => { | |
| 49 | + let json_val: serde_json::Value = v.into(); | |
| 50 | + output.push( | |
| 51 | + serde_json::to_string_pretty(&json_val) | |
| 52 | + .unwrap_or_else(|_| format!("{:?}", json_val)), | |
| 53 | + ); | |
| 54 | + } | |
| 55 | + Err(e) => { | |
| 56 | + return JqResult { | |
| 57 | + output, | |
| 58 | + error: Some(format!("{}", e)), | |
| 59 | + }; | |
| 60 | + } | |
| 61 | + } | |
| 62 | + } | |
| 63 | + | |
| 64 | + JqResult { | |
| 65 | + output, | |
| 66 | + error: None, | |
| 67 | + } | |
| 68 | + } | |
| 69 | +} | |
| 70 | + | |
| 71 | +#[cfg(test)] | |
| 72 | +mod tests { | |
| 73 | + use super::*; | |
| 74 | + | |
| 75 | + #[test] | |
| 76 | + fn test_identity() { | |
| 77 | + let input: serde_json::Value = serde_json::json!({"a": 1, "b": 2}); | |
| 78 | + let result = JqEngine::execute(".", &input); | |
| 79 | + assert!(result.error.is_none(), "Error: {:?}", result.error); | |
| 80 | + assert_eq!(result.output.len(), 1); | |
| 81 | + } | |
| 82 | + | |
| 83 | + #[test] | |
| 84 | + fn test_field_access() { | |
| 85 | + let input: serde_json::Value = serde_json::json!({"name": "Alice", "age": 30}); | |
| 86 | + let result = JqEngine::execute(".name", &input); | |
| 87 | + assert!(result.error.is_none(), "Error: {:?}", result.error); | |
| 88 | + assert_eq!(result.output, vec!["\"Alice\""]); | |
| 89 | + } | |
| 90 | + | |
| 91 | + #[test] | |
| 92 | + fn test_array_iter() { | |
| 93 | + let input: serde_json::Value = serde_json::json!({"items": [1, 2, 3]}); | |
| 94 | + let result = JqEngine::execute(".items[]", &input); | |
| 95 | + assert!(result.error.is_none(), "Error: {:?}", result.error); | |
| 96 | + assert_eq!(result.output, vec!["1", "2", "3"]); | |
| 97 | + } | |
| 98 | +} |
+44-0src/lang/mod.rs
| @@ -0,0 +1,44 @@ | ||
| 1 | +pub mod rust; | |
| 2 | +pub mod swift; | |
| 3 | + | |
| 4 | +use crate::types::InferredType; | |
| 5 | + | |
| 6 | +#[derive(Debug, Clone, Copy, PartialEq)] | |
| 7 | +pub enum CodeLanguage { | |
| 8 | + Rust, | |
| 9 | + Swift, | |
| 10 | +} | |
| 11 | + | |
| 12 | +impl CodeLanguage { | |
| 13 | + pub fn display_name(&self) -> &str { | |
| 14 | + match self { | |
| 15 | + Self::Rust => "Rust", | |
| 16 | + Self::Swift => "Swift", | |
| 17 | + } | |
| 18 | + } | |
| 19 | + | |
| 20 | + pub fn generator(&self) -> Box<dyn LanguageGenerator> { | |
| 21 | + match self { | |
| 22 | + Self::Rust => Box::new(rust::RustGenerator), | |
| 23 | + Self::Swift => Box::new(swift::SwiftGenerator), | |
| 24 | + } | |
| 25 | + } | |
| 26 | +} | |
| 27 | + | |
| 28 | +pub trait LanguageGenerator { | |
| 29 | + fn file_extension(&self) -> &str; | |
| 30 | + fn file_header(&self) -> String; | |
| 31 | + fn imports_header(&self, needs_temporal: bool, has_shared: bool) -> String; | |
| 32 | + fn struct_open(&self, name: &str) -> String; | |
| 33 | + /// Close a struct. `fields` is (code_name, json_name) pairs for CodingKeys etc. | |
| 34 | + fn struct_close(&self, fields: &[(String, String)]) -> String; | |
| 35 | + fn field_line(&self, code_name: &str, type_name: &str) -> String; | |
| 36 | + fn enum_open(&self, name: &str) -> String; | |
| 37 | + fn enum_close(&self) -> String; | |
| 38 | + fn enum_variant(&self, variant_name: &str, json_value: &str) -> String; | |
| 39 | + fn type_name(&self, inferred: &InferredType) -> String; | |
| 40 | + fn mod_file(&self, file_names: &[&str]) -> Option<String>; | |
| 41 | + fn sanitize_keyword(&self, name: &str) -> String; | |
| 42 | + fn field_name(&self, json_name: &str) -> String; | |
| 43 | + fn file_name(&self, base_name: &str) -> String; | |
| 44 | +} |
+98-0src/lang/rust.rs
| @@ -0,0 +1,98 @@ | ||
| 1 | +use crate::codegen::to_snake_case; | |
| 2 | +use crate::lang::LanguageGenerator; | |
| 3 | +use crate::types::InferredType; | |
| 4 | + | |
| 5 | +pub struct RustGenerator; | |
| 6 | + | |
| 7 | +const RUST_KEYWORDS: &[&str] = &[ | |
| 8 | + "type", "struct", "enum", "fn", "let", "mut", "ref", "self", "super", "mod", "use", "pub", | |
| 9 | + "crate", "impl", "trait", "for", "loop", "while", "if", "else", "match", "return", "break", | |
| 10 | + "continue", "move", "async", "await", "dyn", "static", "const", "where", "unsafe", "extern", | |
| 11 | + "as", "in", | |
| 12 | +]; | |
| 13 | + | |
| 14 | +impl LanguageGenerator for RustGenerator { | |
| 15 | + fn file_extension(&self) -> &str { | |
| 16 | + "rs" | |
| 17 | + } | |
| 18 | + | |
| 19 | + fn file_header(&self) -> String { | |
| 20 | + "#![allow(non_snake_case)]\n".to_string() | |
| 21 | + } | |
| 22 | + | |
| 23 | + fn imports_header(&self, needs_temporal: bool, has_shared: bool) -> String { | |
| 24 | + let mut out = "use serde::{Deserialize, Serialize};\n".to_string(); | |
| 25 | + if needs_temporal { | |
| 26 | + out.push_str("use chrono::{DateTime, NaiveDate, NaiveTime, Utc};\n"); | |
| 27 | + } | |
| 28 | + if has_shared { | |
| 29 | + out.push_str("use super::shared::*;\n"); | |
| 30 | + } | |
| 31 | + out | |
| 32 | + } | |
| 33 | + | |
| 34 | + fn struct_open(&self, name: &str) -> String { | |
| 35 | + format!( | |
| 36 | + "#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {} {{\n", | |
| 37 | + name | |
| 38 | + ) | |
| 39 | + } | |
| 40 | + | |
| 41 | + fn struct_close(&self, _fields: &[(String, String)]) -> String { | |
| 42 | + "}\n".to_string() | |
| 43 | + } | |
| 44 | + | |
| 45 | + fn field_line(&self, code_name: &str, type_name: &str) -> String { | |
| 46 | + format!(" pub {}: {},\n", code_name, type_name) | |
| 47 | + } | |
| 48 | + | |
| 49 | + fn enum_open(&self, name: &str) -> String { | |
| 50 | + format!( | |
| 51 | + "#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum {} {{\n", | |
| 52 | + name | |
| 53 | + ) | |
| 54 | + } | |
| 55 | + | |
| 56 | + fn enum_close(&self) -> String { | |
| 57 | + "}\n".to_string() | |
| 58 | + } | |
| 59 | + | |
| 60 | + fn enum_variant(&self, variant_name: &str, json_value: &str) -> String { | |
| 61 | + if variant_name == json_value { | |
| 62 | + format!(" {},\n", variant_name) | |
| 63 | + } else { | |
| 64 | + format!( | |
| 65 | + " #[serde(rename = \"{}\")]\n {},\n", | |
| 66 | + json_value, variant_name | |
| 67 | + ) | |
| 68 | + } | |
| 69 | + } | |
| 70 | + | |
| 71 | + fn type_name(&self, inferred: &InferredType) -> String { | |
| 72 | + inferred.rust_type() | |
| 73 | + } | |
| 74 | + | |
| 75 | + fn mod_file(&self, file_names: &[&str]) -> Option<String> { | |
| 76 | + let mut out = String::new(); | |
| 77 | + for name in file_names { | |
| 78 | + out.push_str(&format!("mod {};\npub use {}::*;\n\n", name, name)); | |
| 79 | + } | |
| 80 | + Some(out) | |
| 81 | + } | |
| 82 | + | |
| 83 | + fn sanitize_keyword(&self, name: &str) -> String { | |
| 84 | + if RUST_KEYWORDS.contains(&name) { | |
| 85 | + format!("r#{}", name) | |
| 86 | + } else { | |
| 87 | + name.to_string() | |
| 88 | + } | |
| 89 | + } | |
| 90 | + | |
| 91 | + fn field_name(&self, json_name: &str) -> String { | |
| 92 | + self.sanitize_keyword(&to_snake_case(json_name)) | |
| 93 | + } | |
| 94 | + | |
| 95 | + fn file_name(&self, base_name: &str) -> String { | |
| 96 | + format!("{}.rs", base_name) | |
| 97 | + } | |
| 98 | +} |
+280-0src/lang/swift.rs
| @@ -0,0 +1,280 @@ | ||
| 1 | +use crate::lang::LanguageGenerator; | |
| 2 | +use crate::types::InferredType; | |
| 3 | + | |
| 4 | +pub struct SwiftGenerator; | |
| 5 | + | |
| 6 | +const SWIFT_KEYWORDS: &[&str] = &[ | |
| 7 | + "class", | |
| 8 | + "struct", | |
| 9 | + "enum", | |
| 10 | + "protocol", | |
| 11 | + "func", | |
| 12 | + "var", | |
| 13 | + "let", | |
| 14 | + "import", | |
| 15 | + "return", | |
| 16 | + "self", | |
| 17 | + "super", | |
| 18 | + "default", | |
| 19 | + "case", | |
| 20 | + "switch", | |
| 21 | + "where", | |
| 22 | + "in", | |
| 23 | + "is", | |
| 24 | + "as", | |
| 25 | + "try", | |
| 26 | + "throw", | |
| 27 | + "throws", | |
| 28 | + "nil", | |
| 29 | + "true", | |
| 30 | + "false", | |
| 31 | + "do", | |
| 32 | + "catch", | |
| 33 | + "guard", | |
| 34 | + "defer", | |
| 35 | + "repeat", | |
| 36 | + "break", | |
| 37 | + "continue", | |
| 38 | + "fallthrough", | |
| 39 | + "typealias", | |
| 40 | + "associatedtype", | |
| 41 | + "operator", | |
| 42 | + "init", | |
| 43 | + "deinit", | |
| 44 | + "subscript", | |
| 45 | + "private", | |
| 46 | + "public", | |
| 47 | + "internal", | |
| 48 | + "open", | |
| 49 | + "fileprivate", | |
| 50 | + "static", | |
| 51 | + "override", | |
| 52 | + "mutating", | |
| 53 | + "inout", | |
| 54 | + "Any", | |
| 55 | + "Type", | |
| 56 | + "Self", | |
| 57 | +]; | |
| 58 | + | |
| 59 | +fn to_camel_case(s: &str) -> String { | |
| 60 | + let parts: Vec<&str> = s.split(|c| c == '_' || c == '-' || c == ' ').collect(); | |
| 61 | + let mut result = String::new(); | |
| 62 | + for (i, part) in parts.iter().enumerate() { | |
| 63 | + if part.is_empty() { | |
| 64 | + continue; | |
| 65 | + } | |
| 66 | + if i == 0 { | |
| 67 | + result.push_str(part); | |
| 68 | + } else { | |
| 69 | + let mut chars = part.chars(); | |
| 70 | + if let Some(first) = chars.next() { | |
| 71 | + result.extend(first.to_uppercase()); | |
| 72 | + result.push_str(chars.as_str()); | |
| 73 | + } | |
| 74 | + } | |
| 75 | + } | |
| 76 | + result | |
| 77 | +} | |
| 78 | + | |
| 79 | +impl LanguageGenerator for SwiftGenerator { | |
| 80 | + fn file_extension(&self) -> &str { | |
| 81 | + "swift" | |
| 82 | + } | |
| 83 | + | |
| 84 | + fn file_header(&self) -> String { | |
| 85 | + String::new() | |
| 86 | + } | |
| 87 | + | |
| 88 | + fn imports_header(&self, _needs_temporal: bool, _has_shared: bool) -> String { | |
| 89 | + "import Foundation\n".to_string() | |
| 90 | + } | |
| 91 | + | |
| 92 | + fn struct_open(&self, name: &str) -> String { | |
| 93 | + format!("struct {}: Codable {{\n", name) | |
| 94 | + } | |
| 95 | + | |
| 96 | + fn struct_close(&self, fields: &[(String, String)]) -> String { | |
| 97 | + let needs_coding_keys = fields.iter().any(|(code, json)| code != json); | |
| 98 | + if !needs_coding_keys { | |
| 99 | + return "}\n".to_string(); | |
| 100 | + } | |
| 101 | + | |
| 102 | + let mut out = String::new(); | |
| 103 | + out.push_str("\n enum CodingKeys: String, CodingKey {\n"); | |
| 104 | + for (code_name, json_name) in fields { | |
| 105 | + if code_name == json_name { | |
| 106 | + out.push_str(&format!(" case {}\n", code_name)); | |
| 107 | + } else { | |
| 108 | + out.push_str(&format!(" case {} = \"{}\"\n", code_name, json_name)); | |
| 109 | + } | |
| 110 | + } | |
| 111 | + out.push_str(" }\n"); | |
| 112 | + out.push_str("}\n"); | |
| 113 | + out | |
| 114 | + } | |
| 115 | + | |
| 116 | + fn field_line(&self, code_name: &str, type_name: &str) -> String { | |
| 117 | + format!(" let {}: {}\n", code_name, type_name) | |
| 118 | + } | |
| 119 | + | |
| 120 | + fn enum_open(&self, name: &str) -> String { | |
| 121 | + format!("enum {}: String, Codable {{\n", name) | |
| 122 | + } | |
| 123 | + | |
| 124 | + fn enum_close(&self) -> String { | |
| 125 | + "}\n".to_string() | |
| 126 | + } | |
| 127 | + | |
| 128 | + fn enum_variant(&self, variant_name: &str, json_value: &str) -> String { | |
| 129 | + if variant_name == json_value { | |
| 130 | + format!(" case {}\n", variant_name) | |
| 131 | + } else { | |
| 132 | + format!(" case {} = \"{}\"\n", variant_name, json_value) | |
| 133 | + } | |
| 134 | + } | |
| 135 | + | |
| 136 | + fn type_name(&self, inferred: &InferredType) -> String { | |
| 137 | + match inferred { | |
| 138 | + InferredType::Null => "Any?".to_string(), | |
| 139 | + InferredType::Bool => "Bool".to_string(), | |
| 140 | + InferredType::I64 => "Int".to_string(), | |
| 141 | + InferredType::F64 => "Double".to_string(), | |
| 142 | + InferredType::String => "String".to_string(), | |
| 143 | + InferredType::DateTime => "Date".to_string(), | |
| 144 | + InferredType::Date => "Date".to_string(), | |
| 145 | + InferredType::Time => "Date".to_string(), | |
| 146 | + InferredType::Array(inner) => format!("[{}]", self.type_name(inner)), | |
| 147 | + InferredType::Object(_) => "[String: Any]".to_string(), | |
| 148 | + InferredType::Option(inner) => format!("{}?", self.type_name(inner)), | |
| 149 | + InferredType::Mixed(_) => "Any".to_string(), | |
| 150 | + InferredType::Unknown => "Any".to_string(), | |
| 151 | + } | |
| 152 | + } | |
| 153 | + | |
| 154 | + fn mod_file(&self, _file_names: &[&str]) -> Option<String> { | |
| 155 | + None | |
| 156 | + } | |
| 157 | + | |
| 158 | + fn sanitize_keyword(&self, name: &str) -> String { | |
| 159 | + if SWIFT_KEYWORDS.contains(&name) { | |
| 160 | + format!("`{}`", name) | |
| 161 | + } else { | |
| 162 | + name.to_string() | |
| 163 | + } | |
| 164 | + } | |
| 165 | + | |
| 166 | + fn field_name(&self, json_name: &str) -> String { | |
| 167 | + self.sanitize_keyword(&to_camel_case(json_name)) | |
| 168 | + } | |
| 169 | + | |
| 170 | + fn file_name(&self, base_name: &str) -> String { | |
| 171 | + let mut c = base_name.chars(); | |
| 172 | + let capitalized = match c.next() { | |
| 173 | + None => String::new(), | |
| 174 | + Some(f) => f.to_uppercase().collect::<String>() + c.as_str(), | |
| 175 | + }; | |
| 176 | + format!("{}.swift", capitalized) | |
| 177 | + } | |
| 178 | +} | |
| 179 | + | |
| 180 | +#[cfg(test)] | |
| 181 | +mod tests { | |
| 182 | + use super::*; | |
| 183 | + use std::collections::BTreeMap; | |
| 184 | + | |
| 185 | + #[test] | |
| 186 | + fn test_swift_type_names() { | |
| 187 | + let g = SwiftGenerator; | |
| 188 | + assert_eq!(g.type_name(&InferredType::Null), "Any?"); | |
| 189 | + assert_eq!(g.type_name(&InferredType::Bool), "Bool"); | |
| 190 | + assert_eq!(g.type_name(&InferredType::I64), "Int"); | |
| 191 | + assert_eq!(g.type_name(&InferredType::F64), "Double"); | |
| 192 | + assert_eq!(g.type_name(&InferredType::String), "String"); | |
| 193 | + assert_eq!(g.type_name(&InferredType::DateTime), "Date"); | |
| 194 | + assert_eq!(g.type_name(&InferredType::Date), "Date"); | |
| 195 | + assert_eq!(g.type_name(&InferredType::Time), "Date"); | |
| 196 | + assert_eq!(g.type_name(&InferredType::Unknown), "Any"); | |
| 197 | + assert_eq!( | |
| 198 | + g.type_name(&InferredType::Mixed(vec![ | |
| 199 | + InferredType::I64, | |
| 200 | + InferredType::String | |
| 201 | + ])), | |
| 202 | + "Any" | |
| 203 | + ); | |
| 204 | + } | |
| 205 | + | |
| 206 | + #[test] | |
| 207 | + fn test_swift_array_type() { | |
| 208 | + let g = SwiftGenerator; | |
| 209 | + let arr = InferredType::Array(Box::new(InferredType::String)); | |
| 210 | + assert_eq!(g.type_name(&arr), "[String]"); | |
| 211 | + } | |
| 212 | + | |
| 213 | + #[test] | |
| 214 | + fn test_swift_option_type() { | |
| 215 | + let g = SwiftGenerator; | |
| 216 | + let opt = InferredType::Option(Box::new(InferredType::I64)); | |
| 217 | + assert_eq!(g.type_name(&opt), "Int?"); | |
| 218 | + } | |
| 219 | + | |
| 220 | + #[test] | |
| 221 | + fn test_swift_object_fallback() { | |
| 222 | + let g = SwiftGenerator; | |
| 223 | + let obj = InferredType::Object(BTreeMap::new()); | |
| 224 | + assert_eq!(g.type_name(&obj), "[String: Any]"); | |
| 225 | + } | |
| 226 | + | |
| 227 | + #[test] | |
| 228 | + fn test_to_camel_case() { | |
| 229 | + assert_eq!(to_camel_case("departure_date"), "departureDate"); | |
| 230 | + assert_eq!(to_camel_case("id"), "id"); | |
| 231 | + assert_eq!(to_camel_case("seat_abbreviation"), "seatAbbreviation"); | |
| 232 | + } | |
| 233 | + | |
| 234 | + #[test] | |
| 235 | + fn test_swift_field_name() { | |
| 236 | + let g = SwiftGenerator; | |
| 237 | + assert_eq!(g.field_name("departure_date"), "departureDate"); | |
| 238 | + assert_eq!(g.field_name("id"), "id"); | |
| 239 | + assert_eq!(g.field_name("class"), "`class`"); | |
| 240 | + } | |
| 241 | + | |
| 242 | + #[test] | |
| 243 | + fn test_coding_keys() { | |
| 244 | + let g = SwiftGenerator; | |
| 245 | + let fields = vec![ | |
| 246 | + ("origin".to_string(), "origin".to_string()), | |
| 247 | + ("departureDate".to_string(), "departure_date".to_string()), | |
| 248 | + ]; | |
| 249 | + let result = g.struct_close(&fields); | |
| 250 | + assert!(result.contains("enum CodingKeys: String, CodingKey {")); | |
| 251 | + assert!(result.contains("case origin\n")); | |
| 252 | + assert!(result.contains("case departureDate = \"departure_date\"\n")); | |
| 253 | + assert!(result.ends_with("}\n")); | |
| 254 | + } | |
| 255 | + | |
| 256 | + #[test] | |
| 257 | + fn test_coding_keys_omitted_when_all_match() { | |
| 258 | + let g = SwiftGenerator; | |
| 259 | + let fields = vec![ | |
| 260 | + ("origin".to_string(), "origin".to_string()), | |
| 261 | + ("id".to_string(), "id".to_string()), | |
| 262 | + ]; | |
| 263 | + let result = g.struct_close(&fields); | |
| 264 | + assert_eq!(result, "}\n"); | |
| 265 | + assert!(!result.contains("CodingKeys")); | |
| 266 | + } | |
| 267 | + | |
| 268 | + #[test] | |
| 269 | + fn test_swift_keyword_sanitize() { | |
| 270 | + let g = SwiftGenerator; | |
| 271 | + assert_eq!(g.sanitize_keyword("class"), "`class`"); | |
| 272 | + assert_eq!(g.sanitize_keyword("origin"), "origin"); | |
| 273 | + } | |
| 274 | + | |
| 275 | + #[test] | |
| 276 | + fn test_swift_file_name() { | |
| 277 | + let g = SwiftGenerator; | |
| 278 | + assert_eq!(g.file_name("shared"), "Shared.swift"); | |
| 279 | + } | |
| 280 | +} |
+31-0src/main.rs
| @@ -0,0 +1,31 @@ | ||
| 1 | +mod app; | |
| 2 | +mod codegen; | |
| 3 | +mod har; | |
| 4 | +mod jq_engine; | |
| 5 | +mod lang; | |
| 6 | +mod schema; | |
| 7 | +mod session; | |
| 8 | +mod temporal; | |
| 9 | +mod theme; | |
| 10 | +mod types; | |
| 11 | +mod views; | |
| 12 | + | |
| 13 | +use app::JvApp; | |
| 14 | + | |
| 15 | +fn main() -> eframe::Result { | |
| 16 | + tracing_subscriber::fmt::init(); | |
| 17 | + | |
| 18 | + let options = eframe::NativeOptions { | |
| 19 | + viewport: egui::ViewportBuilder::default() | |
| 20 | + .with_inner_size([1200.0, 800.0]) | |
| 21 | + .with_min_inner_size([800.0, 500.0]) | |
| 22 | + .with_title("jv - JSON Viewer"), | |
| 23 | + ..Default::default() | |
| 24 | + }; | |
| 25 | + | |
| 26 | + eframe::run_native( | |
| 27 | + "jv", | |
| 28 | + options, | |
| 29 | + Box::new(|cc| Ok(Box::new(JvApp::new(cc)))), | |
| 30 | + ) | |
| 31 | +} |
+424-0src/schema.rs
| @@ -0,0 +1,424 @@ | ||
| 1 | +use std::collections::{BTreeMap, BTreeSet}; | |
| 2 | + | |
| 3 | +use crate::types::InferredType; | |
| 4 | + | |
| 5 | +#[derive(Debug, Clone)] | |
| 6 | +pub struct SharedStruct { | |
| 7 | + pub name: String, | |
| 8 | + pub fields: BTreeMap<String, InferredType>, | |
| 9 | + pub source_files: Vec<String>, | |
| 10 | + pub occurrence_count: usize, | |
| 11 | +} | |
| 12 | + | |
| 13 | +#[derive(Debug, Clone)] | |
| 14 | +pub struct SchemaOverview { | |
| 15 | + pub structs: Vec<SharedStruct>, | |
| 16 | + pub unique_structs: Vec<SharedStruct>, | |
| 17 | +} | |
| 18 | + | |
| 19 | +impl SchemaOverview { | |
| 20 | + /// All known structs (shared + unique) for type resolution. | |
| 21 | + pub fn all_structs(&self) -> Vec<SharedStruct> { | |
| 22 | + self.structs.iter().chain(self.unique_structs.iter()).cloned().collect() | |
| 23 | + } | |
| 24 | +} | |
| 25 | + | |
| 26 | +/// A struct candidate from Phase 2, tagged with its filename group. | |
| 27 | +struct GroupCandidate { | |
| 28 | + name: String, | |
| 29 | + fields: BTreeMap<String, InferredType>, | |
| 30 | + source_files: Vec<String>, | |
| 31 | + occurrence_count: usize, | |
| 32 | + group_key: String, | |
| 33 | +} | |
| 34 | + | |
| 35 | +impl SchemaOverview { | |
| 36 | + pub fn infer(files: &[(String, serde_json::Value)], jaccard_threshold: f32) -> Self { | |
| 37 | + // Phase 1: Group files by depluralized first word from filename | |
| 38 | + let mut file_groups: BTreeMap<String, Vec<&(String, serde_json::Value)>> = BTreeMap::new(); | |
| 39 | + for file in files { | |
| 40 | + let group_key = crate::codegen::first_normal_word(&file.0) | |
| 41 | + .map(|w| crate::codegen::singularize(&w)) | |
| 42 | + .unwrap_or_else(|| "other".to_string()); | |
| 43 | + file_groups.entry(group_key).or_default().push(file); | |
| 44 | + } | |
| 45 | + | |
| 46 | + // Phase 2: Within each group, find structs via Jaccard similarity. | |
| 47 | + // Single-file groups still produce candidates (needed for cross-group comparison). | |
| 48 | + let mut candidates: Vec<GroupCandidate> = Vec::new(); | |
| 49 | + | |
| 50 | + for (group_key, group_files) in &file_groups { | |
| 51 | + let mut shape_map: BTreeMap<String, Vec<(BTreeMap<String, InferredType>, String)>> = | |
| 52 | + BTreeMap::new(); | |
| 53 | + | |
| 54 | + for (filename, value) in group_files.iter().copied() { | |
| 55 | + collect_objects(value, filename, "", &mut shape_map); | |
| 56 | + } | |
| 57 | + | |
| 58 | + | |
| 59 | + for (context_key, occurrences) in &shape_map { | |
| 60 | + if context_key.starts_with("root::") { | |
| 61 | + continue; | |
| 62 | + } | |
| 63 | + | |
| 64 | + let source_files: Vec<String> = occurrences | |
| 65 | + .iter() | |
| 66 | + .map(|(_, f)| f.clone()) | |
| 67 | + .collect::<BTreeSet<_>>() | |
| 68 | + .into_iter() | |
| 69 | + .collect(); | |
| 70 | + | |
| 71 | + let merged = merge_fields(occurrences); | |
| 72 | + let name = derive_struct_name(context_key); | |
| 73 | + | |
| 74 | + | |
| 75 | + candidates.push(GroupCandidate { | |
| 76 | + name, | |
| 77 | + fields: merged, | |
| 78 | + source_files, | |
| 79 | + occurrence_count: occurrences.len(), | |
| 80 | + group_key: group_key.clone(), | |
| 81 | + }); | |
| 82 | + } | |
| 83 | + } | |
| 84 | + | |
| 85 | + // Phase 3: Merge candidates with similar field name sets (Jaccard on names only, | |
| 86 | + // ignoring types — types get unified via merge_fields anyway). | |
| 87 | + let name_sets: Vec<BTreeSet<String>> = candidates | |
| 88 | + .iter() | |
| 89 | + .map(|c| c.fields.keys().cloned().collect()) | |
| 90 | + .collect(); | |
| 91 | + | |
| 92 | + let mut used = vec![false; candidates.len()]; | |
| 93 | + let mut shared_structs: Vec<(SharedStruct, String)> = Vec::new(); | |
| 94 | + let mut deduplicated_unique: Vec<(SharedStruct, String)> = Vec::new(); | |
| 95 | + | |
| 96 | + for i in 0..candidates.len() { | |
| 97 | + if used[i] { | |
| 98 | + continue; | |
| 99 | + } | |
| 100 | + | |
| 101 | + // Find all candidates that should merge: either by Jaccard similarity | |
| 102 | + // (single-link clustering) or by having the same derived name within the | |
| 103 | + // same group (same parent key = same semantic type with optional fields). | |
| 104 | + let mut cluster = vec![i]; | |
| 105 | + let seed_name = &candidates[i].name; | |
| 106 | + let seed_group = &candidates[i].group_key; | |
| 107 | + for j in (i + 1)..candidates.len() { | |
| 108 | + if used[j] { | |
| 109 | + continue; | |
| 110 | + } | |
| 111 | + // Same name + same group → always merge (same parent key = same type) | |
| 112 | + if candidates[j].name == *seed_name && candidates[j].group_key == *seed_group { | |
| 113 | + cluster.push(j); | |
| 114 | + continue; | |
| 115 | + } | |
| 116 | + let matches_any = cluster.iter().any(|&ci| { | |
| 117 | + let intersection = name_sets[ci].intersection(&name_sets[j]).count(); | |
| 118 | + let union_size = name_sets[ci].union(&name_sets[j]).count(); | |
| 119 | + if union_size == 0 { | |
| 120 | + name_sets[ci].is_empty() && name_sets[j].is_empty() | |
| 121 | + } else { | |
| 122 | + (intersection as f64 / union_size as f64) >= jaccard_threshold as f64 - 1e-6 | |
| 123 | + } | |
| 124 | + }); | |
| 125 | + if matches_any { | |
| 126 | + cluster.push(j); | |
| 127 | + } | |
| 128 | + } | |
| 129 | + | |
| 130 | + // Determine if shared (spans 2+ groups) or just deduplicated | |
| 131 | + let cluster_groups: BTreeSet<&str> = cluster | |
| 132 | + .iter() | |
| 133 | + .map(|&idx| candidates[idx].group_key.as_str()) | |
| 134 | + .collect(); | |
| 135 | + let is_shared = cluster_groups.len() >= 2; | |
| 136 | + | |
| 137 | + if cluster.len() < 2 { | |
| 138 | + continue; | |
| 139 | + } | |
| 140 | + | |
| 141 | + for &idx in &cluster { | |
| 142 | + used[idx] = true; | |
| 143 | + } | |
| 144 | + | |
| 145 | + // Merge fields from all cluster members | |
| 146 | + let mut all_occurrences: Vec<(BTreeMap<String, InferredType>, String)> = Vec::new(); | |
| 147 | + let mut all_source_files: BTreeSet<String> = BTreeSet::new(); | |
| 148 | + let mut total_occurrences: usize = 0; | |
| 149 | + | |
| 150 | + for &idx in &cluster { | |
| 151 | + total_occurrences += candidates[idx].occurrence_count; | |
| 152 | + for f in &candidates[idx].source_files { | |
| 153 | + all_source_files.insert(f.clone()); | |
| 154 | + all_occurrences.push((candidates[idx].fields.clone(), f.clone())); | |
| 155 | + } | |
| 156 | + } | |
| 157 | + | |
| 158 | + // Use name and group_key from the highest-occurrence member | |
| 159 | + let best_idx = *cluster | |
| 160 | + .iter() | |
| 161 | + .max_by_key(|&&idx| candidates[idx].occurrence_count) | |
| 162 | + .unwrap(); | |
| 163 | + let best_name = candidates[best_idx].name.clone(); | |
| 164 | + let best_group_key = candidates[best_idx].group_key.clone(); | |
| 165 | + | |
| 166 | + let merged_fields = merge_fields(&all_occurrences); | |
| 167 | + let merged = ( | |
| 168 | + SharedStruct { | |
| 169 | + name: best_name, | |
| 170 | + fields: merged_fields, | |
| 171 | + source_files: all_source_files.into_iter().collect(), | |
| 172 | + occurrence_count: total_occurrences, | |
| 173 | + }, | |
| 174 | + best_group_key, | |
| 175 | + ); | |
| 176 | + if is_shared { | |
| 177 | + shared_structs.push(merged); | |
| 178 | + } else { | |
| 179 | + deduplicated_unique.push(merged); | |
| 180 | + } | |
| 181 | + } | |
| 182 | + | |
| 183 | + | |
| 184 | + // Disambiguate shared struct names | |
| 185 | + disambiguate_names(&mut shared_structs); | |
| 186 | + shared_structs.sort_by(|a, b| b.0.occurrence_count.cmp(&a.0.occurrence_count)); | |
| 187 | + | |
| 188 | + // Collect unique structs (candidates that weren't part of any cluster) | |
| 189 | + let mut unique_structs: Vec<(SharedStruct, String)> = Vec::new(); | |
| 190 | + for (i, candidate) in candidates.iter().enumerate() { | |
| 191 | + if !used[i] { | |
| 192 | + unique_structs.push(( | |
| 193 | + SharedStruct { | |
| 194 | + name: candidate.name.clone(), | |
| 195 | + fields: candidate.fields.clone(), | |
| 196 | + source_files: candidate.source_files.clone(), | |
| 197 | + occurrence_count: candidate.occurrence_count, | |
| 198 | + }, | |
| 199 | + candidate.group_key.clone(), | |
| 200 | + )); | |
| 201 | + } | |
| 202 | + } | |
| 203 | + // Add back deduplicated same-group clusters | |
| 204 | + unique_structs.extend(deduplicated_unique); | |
| 205 | + | |
| 206 | + disambiguate_names(&mut unique_structs); | |
| 207 | + | |
| 208 | + // Cross-list disambiguation: if a unique struct name collides with a shared | |
| 209 | + // struct name, prefix the unique one with its group key. | |
| 210 | + let shared_names: BTreeSet<String> = | |
| 211 | + shared_structs.iter().map(|(s, _)| s.name.clone()).collect(); | |
| 212 | + for (s, group_key) in unique_structs.iter_mut() { | |
| 213 | + if shared_names.contains(&s.name) { | |
| 214 | + let prefix = | |
| 215 | + crate::codegen::to_pascal_case(&crate::codegen::singularize(group_key)); | |
| 216 | + s.name = format!("{}{}", prefix, s.name); | |
| 217 | + } | |
| 218 | + } | |
| 219 | + | |
| 220 | + unique_structs.sort_by(|a, b| b.0.occurrence_count.cmp(&a.0.occurrence_count)); | |
| 221 | + let shared_structs: Vec<SharedStruct> = | |
| 222 | + shared_structs.into_iter().map(|(s, _)| s).collect(); | |
| 223 | + let unique_structs: Vec<SharedStruct> = | |
| 224 | + unique_structs.into_iter().map(|(s, _)| s).collect(); | |
| 225 | + | |
| 226 | + Self { | |
| 227 | + structs: shared_structs, | |
| 228 | + unique_structs, | |
| 229 | + } | |
| 230 | + } | |
| 231 | +} | |
| 232 | + | |
| 233 | +/// For structs with duplicate names, prepend the PascalCase group key to disambiguate. | |
| 234 | +fn disambiguate_names(structs: &mut [(SharedStruct, String)]) { | |
| 235 | + // Count name occurrences | |
| 236 | + let mut name_counts: BTreeMap<String, usize> = BTreeMap::new(); | |
| 237 | + for (s, _) in structs.iter() { | |
| 238 | + *name_counts.entry(s.name.clone()).or_default() += 1; | |
| 239 | + } | |
| 240 | + | |
| 241 | + // Rename duplicates by prepending group key | |
| 242 | + for (s, group_key) in structs.iter_mut() { | |
| 243 | + if name_counts[&s.name] > 1 { | |
| 244 | + let prefix = crate::codegen::to_pascal_case(&crate::codegen::singularize(group_key)); | |
| 245 | + s.name = format!("{}{}", prefix, s.name); | |
| 246 | + } | |
| 247 | + } | |
| 248 | +} | |
| 249 | + | |
| 250 | +/// Collect all objects, keyed by their sorted field names. | |
| 251 | +/// Objects with identical field name sets share the same bucket regardless of | |
| 252 | +/// parent key or value types. This is O(1) per object (BTreeMap lookup). | |
| 253 | +/// Jaccard-based merging of *similar-but-not-identical* field sets is deferred | |
| 254 | +/// to Phase 3 where the candidate count is small. | |
| 255 | +fn collect_objects( | |
| 256 | + value: &serde_json::Value, | |
| 257 | + filename: &str, | |
| 258 | + parent_key: &str, | |
| 259 | + shapes: &mut BTreeMap<String, Vec<(BTreeMap<String, InferredType>, String)>>, | |
| 260 | +) { | |
| 261 | + match value { | |
| 262 | + serde_json::Value::Object(map) => { | |
| 263 | + let fields: BTreeMap<String, InferredType> = map | |
| 264 | + .iter() | |
| 265 | + .map(|(k, v)| (k.clone(), crate::types::infer_type_plain(v))) | |
| 266 | + .collect(); | |
| 267 | + | |
| 268 | + let context = if parent_key.is_empty() { | |
| 269 | + "root".to_string() | |
| 270 | + } else { | |
| 271 | + parent_key.to_string() | |
| 272 | + }; | |
| 273 | + | |
| 274 | + // Key = context::sorted_field_names — groups exact-same-fields objects | |
| 275 | + let mut key_sig: Vec<String> = fields.keys().cloned().collect(); | |
| 276 | + key_sig.sort(); | |
| 277 | + let sig_key = format!("{}::{}", context, key_sig.join(",")); | |
| 278 | + | |
| 279 | + shapes | |
| 280 | + .entry(sig_key) | |
| 281 | + .or_default() | |
| 282 | + .push((fields, filename.to_string())); | |
| 283 | + | |
| 284 | + for (key, child) in map { | |
| 285 | + collect_objects(child, filename, key, shapes); | |
| 286 | + } | |
| 287 | + } | |
| 288 | + serde_json::Value::Array(arr) => { | |
| 289 | + for item in arr { | |
| 290 | + collect_objects(item, filename, parent_key, shapes); | |
| 291 | + } | |
| 292 | + } | |
| 293 | + _ => {} | |
| 294 | + } | |
| 295 | +} | |
| 296 | + | |
| 297 | +fn merge_fields( | |
| 298 | + occurrences: &[(BTreeMap<String, InferredType>, String)], | |
| 299 | +) -> BTreeMap<String, InferredType> { | |
| 300 | + let all_keys: BTreeSet<String> = occurrences | |
| 301 | + .iter() | |
| 302 | + .flat_map(|(fields, _)| fields.keys().cloned()) | |
| 303 | + .collect(); | |
| 304 | + | |
| 305 | + let total = occurrences.len(); | |
| 306 | + let mut merged = BTreeMap::new(); | |
| 307 | + | |
| 308 | + for key in &all_keys { | |
| 309 | + let mut types: Vec<InferredType> = Vec::new(); | |
| 310 | + let mut present_count = 0; | |
| 311 | + | |
| 312 | + for (fields, _) in occurrences { | |
| 313 | + if let Some(t) = fields.get(key) { | |
| 314 | + types.push(t.clone()); | |
| 315 | + present_count += 1; | |
| 316 | + } | |
| 317 | + } | |
| 318 | + | |
| 319 | + let unified = unify_field_types(types); | |
| 320 | + let field_type = if present_count < total { | |
| 321 | + // Don't double-wrap — if already Option, keep it as-is | |
| 322 | + match unified { | |
| 323 | + InferredType::Option(_) | InferredType::Null => unified, | |
| 324 | + other => InferredType::Option(Box::new(other)), | |
| 325 | + } | |
| 326 | + } else { | |
| 327 | + unified | |
| 328 | + }; | |
| 329 | + | |
| 330 | + merged.insert(key.clone(), field_type); | |
| 331 | + } | |
| 332 | + | |
| 333 | + merged | |
| 334 | +} | |
| 335 | + | |
| 336 | +fn unify_field_types(types: Vec<InferredType>) -> InferredType { | |
| 337 | + if types.is_empty() { | |
| 338 | + return InferredType::Unknown; | |
| 339 | + } | |
| 340 | + | |
| 341 | + let mut unique: Vec<InferredType> = Vec::new(); | |
| 342 | + let mut has_null = false; | |
| 343 | + for t in types { | |
| 344 | + // Unwrap Option<T> → set has_null and use T for dedup. | |
| 345 | + // This handles re-merging already-merged types (e.g. Option<String> + String → Option<String>). | |
| 346 | + let inner = match t { | |
| 347 | + InferredType::Null => { | |
| 348 | + has_null = true; | |
| 349 | + continue; | |
| 350 | + } | |
| 351 | + InferredType::Unknown => continue, // Unknown = no info, absorb it | |
| 352 | + InferredType::Option(inner) => { | |
| 353 | + has_null = true; | |
| 354 | + *inner | |
| 355 | + } | |
| 356 | + other => other, | |
| 357 | + }; | |
| 358 | + if inner == InferredType::Unknown { | |
| 359 | + continue; | |
| 360 | + } | |
| 361 | + if !unique.contains(&inner) { | |
| 362 | + unique.push(inner); | |
| 363 | + } | |
| 364 | + } | |
| 365 | + | |
| 366 | + if unique.is_empty() { | |
| 367 | + return InferredType::Null; | |
| 368 | + } | |
| 369 | + | |
| 370 | + // Merge structurally similar types (Objects together, Arrays together) | |
| 371 | + if unique.len() > 1 { | |
| 372 | + unique = crate::types::merge_structural_types(unique); | |
| 373 | + } | |
| 374 | + | |
| 375 | + if unique.len() == 1 { | |
| 376 | + let inner = unique.into_iter().next().unwrap(); | |
| 377 | + return if has_null { | |
| 378 | + InferredType::Option(Box::new(inner)) | |
| 379 | + } else { | |
| 380 | + inner | |
| 381 | + }; | |
| 382 | + } | |
| 383 | + | |
| 384 | + if has_null { | |
| 385 | + InferredType::Option(Box::new(InferredType::Mixed(unique))) | |
| 386 | + } else { | |
| 387 | + InferredType::Mixed(unique) | |
| 388 | + } | |
| 389 | +} | |
| 390 | + | |
| 391 | +fn derive_struct_name(context_key: &str) -> String { | |
| 392 | + let parts: Vec<&str> = context_key.split("::").collect(); | |
| 393 | + let name_part = parts.first().unwrap_or(&"Unknown"); | |
| 394 | + | |
| 395 | + let singular = crate::codegen::singularize(name_part); | |
| 396 | + | |
| 397 | + let mut result = String::new(); | |
| 398 | + let mut capitalize_next = true; | |
| 399 | + for ch in singular.chars() { | |
| 400 | + if ch == '_' || ch == '-' || ch == ' ' || ch == '.' { | |
| 401 | + capitalize_next = true; | |
| 402 | + } else if capitalize_next { | |
| 403 | + result.push(ch.to_ascii_uppercase()); | |
| 404 | + capitalize_next = false; | |
| 405 | + } else { | |
| 406 | + result.push(ch); | |
| 407 | + } | |
| 408 | + } | |
| 409 | + | |
| 410 | + if result.is_empty() { | |
| 411 | + "Unknown".to_string() | |
| 412 | + } else if result == "Root" { | |
| 413 | + "Root".to_string() | |
| 414 | + } else { | |
| 415 | + result | |
| 416 | + } | |
| 417 | +} | |
| 418 | + | |
| 419 | +#[cfg(test)] | |
| 420 | +mod tests { | |
| 421 | + use super::*; | |
| 422 | + | |
| 423 | + include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/private/schema_tests.rs")); | |
| 424 | +} |
+210-0src/session.rs
| @@ -0,0 +1,210 @@ | ||
| 1 | +use serde::{Deserialize, Serialize}; | |
| 2 | +use std::collections::BTreeMap; | |
| 3 | +use std::path::PathBuf; | |
| 4 | + | |
| 5 | +use crate::schema::SchemaOverview; | |
| 6 | +use crate::types::TemporalOverride; | |
| 7 | + | |
| 8 | +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | |
| 9 | +pub struct EnumConversion { | |
| 10 | + pub field_name: String, | |
| 11 | + pub enum_name: String, | |
| 12 | + pub variants: Vec<String>, | |
| 13 | +} | |
| 14 | + | |
| 15 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 16 | +pub struct Session { | |
| 17 | + pub id: String, | |
| 18 | + pub name: String, | |
| 19 | + pub description: String, | |
| 20 | + pub tags: Vec<String>, | |
| 21 | + pub created_at: String, | |
| 22 | + pub modified_at: String, | |
| 23 | + pub files: Vec<SessionFile>, | |
| 24 | + #[serde(default = "default_jaccard")] | |
| 25 | + pub jaccard_threshold: f32, | |
| 26 | + #[serde(default)] | |
| 27 | + pub enum_conversions: Vec<EnumConversion>, | |
| 28 | + #[serde(default)] | |
| 29 | + pub hidden_fields: Vec<String>, // "StructName.field_name" format | |
| 30 | +} | |
| 31 | + | |
| 32 | +fn default_jaccard() -> f32 { | |
| 33 | + 0.8 | |
| 34 | +} | |
| 35 | + | |
| 36 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 37 | +pub struct SessionFile { | |
| 38 | + pub id: String, | |
| 39 | + pub original_path: String, | |
| 40 | + pub filename: String, | |
| 41 | + pub imported_at: String, | |
| 42 | + pub content: String, | |
| 43 | +} | |
| 44 | + | |
| 45 | +pub struct LoadedSession { | |
| 46 | + pub session: Session, | |
| 47 | + pub parsed_files: Vec<(String, serde_json::Value)>, | |
| 48 | + pub temporal_overrides: BTreeMap<String, TemporalOverride>, | |
| 49 | + pub schema: Option<SchemaOverview>, | |
| 50 | +} | |
| 51 | + | |
| 52 | +impl LoadedSession { | |
| 53 | + pub fn new(session: Session) -> Self { | |
| 54 | + let parsed_files: Vec<(String, serde_json::Value)> = session | |
| 55 | + .files | |
| 56 | + .iter() | |
| 57 | + .filter_map(|f| { | |
| 58 | + serde_json::from_str(&f.content) | |
| 59 | + .ok() | |
| 60 | + .map(|v| (f.filename.clone(), v)) | |
| 61 | + }) | |
| 62 | + .collect(); | |
| 63 | + | |
| 64 | + let mut loaded = Self { | |
| 65 | + schema: None, | |
| 66 | + session, | |
| 67 | + parsed_files, | |
| 68 | + temporal_overrides: BTreeMap::new(), | |
| 69 | + }; | |
| 70 | + loaded.rebuild_schema(); | |
| 71 | + loaded | |
| 72 | + } | |
| 73 | + | |
| 74 | + pub fn rebuild_schema(&mut self) { | |
| 75 | + if !self.parsed_files.is_empty() { | |
| 76 | + let threshold = self.session.jaccard_threshold; | |
| 77 | + self.schema = Some(SchemaOverview::infer(&self.parsed_files, threshold)); | |
| 78 | + } else { | |
| 79 | + self.schema = None; | |
| 80 | + } | |
| 81 | + } | |
| 82 | + | |
| 83 | + pub fn add_file(&mut self, path: &str, content: String) -> Result<(), String> { | |
| 84 | + let value: serde_json::Value = | |
| 85 | + serde_json::from_str(&content).map_err(|e| format!("Invalid JSON: {}", e))?; | |
| 86 | + | |
| 87 | + let filename = PathBuf::from(path) | |
| 88 | + .file_name() | |
| 89 | + .map(|n| n.to_string_lossy().to_string()) | |
| 90 | + .unwrap_or_else(|| "unnamed.json".to_string()); | |
| 91 | + | |
| 92 | + let now = chrono::Utc::now().to_rfc3339(); | |
| 93 | + let file = SessionFile { | |
| 94 | + id: uuid::Uuid::new_v4().to_string(), | |
| 95 | + original_path: path.to_string(), | |
| 96 | + filename: filename.clone(), | |
| 97 | + imported_at: now.clone(), | |
| 98 | + content, | |
| 99 | + }; | |
| 100 | + | |
| 101 | + self.session.files.push(file); | |
| 102 | + self.session.modified_at = now; | |
| 103 | + self.parsed_files.push((filename, value)); | |
| 104 | + | |
| 105 | + Ok(()) | |
| 106 | + } | |
| 107 | + | |
| 108 | + pub fn remove_file(&mut self, index: usize) { | |
| 109 | + if index < self.session.files.len() { | |
| 110 | + self.session.files.remove(index); | |
| 111 | + self.parsed_files.remove(index); | |
| 112 | + self.session.modified_at = chrono::Utc::now().to_rfc3339(); | |
| 113 | + } | |
| 114 | + } | |
| 115 | +} | |
| 116 | + | |
| 117 | +impl Session { | |
| 118 | + pub fn new(name: &str) -> Self { | |
| 119 | + let now = chrono::Utc::now().to_rfc3339(); | |
| 120 | + Self { | |
| 121 | + id: uuid::Uuid::new_v4().to_string(), | |
| 122 | + name: name.to_string(), | |
| 123 | + description: String::new(), | |
| 124 | + tags: Vec::new(), | |
| 125 | + created_at: now.clone(), | |
| 126 | + modified_at: now, | |
| 127 | + files: Vec::new(), | |
| 128 | + jaccard_threshold: default_jaccard(), | |
| 129 | + enum_conversions: Vec::new(), | |
| 130 | + hidden_fields: Vec::new(), | |
| 131 | + } | |
| 132 | + } | |
| 133 | +} | |
| 134 | + | |
| 135 | +pub struct SessionManager { | |
| 136 | + pub sessions: Vec<Session>, | |
| 137 | + data_dir: PathBuf, | |
| 138 | +} | |
| 139 | + | |
| 140 | +impl SessionManager { | |
| 141 | + pub fn new() -> Self { | |
| 142 | + let data_dir = directories::ProjectDirs::from("com", "jv", "jv") | |
| 143 | + .map(|dirs| dirs.data_dir().to_path_buf()) | |
| 144 | + .unwrap_or_else(|| PathBuf::from(".jv_data")); | |
| 145 | + | |
| 146 | + let mut manager = Self { | |
| 147 | + sessions: Vec::new(), | |
| 148 | + data_dir, | |
| 149 | + }; | |
| 150 | + manager.load_sessions(); | |
| 151 | + manager | |
| 152 | + } | |
| 153 | + | |
| 154 | + fn sessions_dir(&self) -> PathBuf { | |
| 155 | + self.data_dir.join("sessions") | |
| 156 | + } | |
| 157 | + | |
| 158 | + fn load_sessions(&mut self) { | |
| 159 | + let dir = self.sessions_dir(); | |
| 160 | + if !dir.exists() { | |
| 161 | + return; | |
| 162 | + } | |
| 163 | + | |
| 164 | + if let Ok(entries) = std::fs::read_dir(&dir) { | |
| 165 | + for entry in entries.flatten() { | |
| 166 | + let path = entry.path(); | |
| 167 | + if path.extension().is_some_and(|e| e == "json") { | |
| 168 | + if let Ok(content) = std::fs::read_to_string(&path) { | |
| 169 | + if let Ok(session) = serde_json::from_str::<Session>(&content) { | |
| 170 | + self.sessions.push(session); | |
| 171 | + } | |
| 172 | + } | |
| 173 | + } | |
| 174 | + } | |
| 175 | + } | |
| 176 | + | |
| 177 | + self.sessions | |
| 178 | + .sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); | |
| 179 | + } | |
| 180 | + | |
| 181 | + pub fn save_session(&self, session: &Session) { | |
| 182 | + let dir = self.sessions_dir(); | |
| 183 | + std::fs::create_dir_all(&dir).ok(); | |
| 184 | + | |
| 185 | + let path = dir.join(format!("{}.json", session.id)); | |
| 186 | + if let Ok(content) = serde_json::to_string_pretty(session) { | |
| 187 | + std::fs::write(path, content).ok(); | |
| 188 | + } | |
| 189 | + } | |
| 190 | + | |
| 191 | + pub fn create_session(&mut self, name: &str) -> Session { | |
| 192 | + let session = Session::new(name); | |
| 193 | + self.save_session(&session); | |
| 194 | + self.sessions.insert(0, session.clone()); | |
| 195 | + session | |
| 196 | + } | |
| 197 | + | |
| 198 | + pub fn delete_session(&mut self, id: &str) { | |
| 199 | + self.sessions.retain(|s| s.id != id); | |
| 200 | + let path = self.sessions_dir().join(format!("{}.json", id)); | |
| 201 | + std::fs::remove_file(path).ok(); | |
| 202 | + } | |
| 203 | + | |
| 204 | + pub fn update_session(&mut self, session: &Session) { | |
| 205 | + self.save_session(session); | |
| 206 | + if let Some(existing) = self.sessions.iter_mut().find(|s| s.id == session.id) { | |
| 207 | + *existing = session.clone(); | |
| 208 | + } | |
| 209 | + } | |
| 210 | +} |
+193-0src/temporal.rs
| @@ -0,0 +1,193 @@ | ||
| 1 | +use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Offset, Utc}; | |
| 2 | + | |
| 3 | +#[derive(Debug, Clone, PartialEq)] | |
| 4 | +pub enum TemporalValue { | |
| 5 | + DateTime(DateTime<chrono::FixedOffset>), | |
| 6 | + DateTimeUtc(DateTime<Utc>), | |
| 7 | + NaiveDateTime(NaiveDateTime), | |
| 8 | + NaiveDate(NaiveDate), | |
| 9 | + NaiveTime(NaiveTime), | |
| 10 | + UnixTimestamp(i64, DateTime<Utc>), | |
| 11 | +} | |
| 12 | + | |
| 13 | +impl TemporalValue { | |
| 14 | + pub fn display(&self) -> String { | |
| 15 | + match self { | |
| 16 | + Self::DateTime(dt) => dt.format("%Y-%m-%d %H:%M:%S %Z").to_string(), | |
| 17 | + Self::DateTimeUtc(dt) => dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(), | |
| 18 | + Self::NaiveDateTime(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(), | |
| 19 | + Self::NaiveDate(d) => d.format("%Y-%m-%d").to_string(), | |
| 20 | + Self::NaiveTime(t) => t.format("%H:%M:%S").to_string(), | |
| 21 | + Self::UnixTimestamp(ts, dt) => { | |
| 22 | + format!("{} (unix: {})", dt.format("%Y-%m-%d %H:%M:%S UTC"), ts) | |
| 23 | + } | |
| 24 | + } | |
| 25 | + } | |
| 26 | + | |
| 27 | + pub fn relative_time(&self) -> String { | |
| 28 | + let now = Utc::now(); | |
| 29 | + let dt_utc = match self { | |
| 30 | + Self::DateTime(dt) => dt.with_timezone(&Utc), | |
| 31 | + Self::DateTimeUtc(dt) => *dt, | |
| 32 | + Self::NaiveDateTime(dt) => dt.and_utc(), | |
| 33 | + Self::NaiveDate(d) => d | |
| 34 | + .and_hms_opt(0, 0, 0) | |
| 35 | + .unwrap_or_default() | |
| 36 | + .and_utc(), | |
| 37 | + Self::NaiveTime(_) => return String::new(), | |
| 38 | + Self::UnixTimestamp(_, dt) => *dt, | |
| 39 | + }; | |
| 40 | + | |
| 41 | + let duration = now.signed_duration_since(dt_utc); | |
| 42 | + let secs = duration.num_seconds().unsigned_abs(); | |
| 43 | + | |
| 44 | + if secs < 60 { | |
| 45 | + "just now".to_string() | |
| 46 | + } else if secs < 3600 { | |
| 47 | + let mins = secs / 60; | |
| 48 | + format!("{}m ago", mins) | |
| 49 | + } else if secs < 86400 { | |
| 50 | + let hours = secs / 3600; | |
| 51 | + format!("{}h ago", hours) | |
| 52 | + } else if secs < 2592000 { | |
| 53 | + let days = secs / 86400; | |
| 54 | + format!("{}d ago", days) | |
| 55 | + } else if secs < 31536000 { | |
| 56 | + let months = secs / 2592000; | |
| 57 | + format!("{}mo ago", months) | |
| 58 | + } else { | |
| 59 | + let years = secs / 31536000; | |
| 60 | + format!("{}y ago", years) | |
| 61 | + } | |
| 62 | + } | |
| 63 | + | |
| 64 | + pub fn to_naive_date(&self) -> Option<NaiveDate> { | |
| 65 | + match self { | |
| 66 | + Self::DateTime(dt) => Some(dt.date_naive()), | |
| 67 | + Self::DateTimeUtc(dt) => Some(dt.date_naive()), | |
| 68 | + Self::NaiveDateTime(dt) => Some(dt.date()), | |
| 69 | + Self::NaiveDate(d) => Some(*d), | |
| 70 | + Self::NaiveTime(_) => None, | |
| 71 | + Self::UnixTimestamp(_, dt) => Some(dt.date_naive()), | |
| 72 | + } | |
| 73 | + } | |
| 74 | + | |
| 75 | + pub fn timezone_info(&self) -> Option<String> { | |
| 76 | + match self { | |
| 77 | + Self::DateTime(dt) => Some(dt.offset().to_string()), | |
| 78 | + Self::DateTimeUtc(_) | Self::UnixTimestamp(_, _) => Some("UTC".to_string()), | |
| 79 | + _ => None, | |
| 80 | + } | |
| 81 | + } | |
| 82 | + | |
| 83 | + /// Returns UTC offset in fractional hours, if timezone is known | |
| 84 | + pub fn utc_offset_hours(&self) -> Option<f32> { | |
| 85 | + match self { | |
| 86 | + Self::DateTime(dt) => Some(dt.offset().local_minus_utc() as f32 / 3600.0), | |
| 87 | + Self::DateTimeUtc(_) | Self::UnixTimestamp(_, _) => Some(0.0), | |
| 88 | + _ => None, | |
| 89 | + } | |
| 90 | + } | |
| 91 | +} | |
| 92 | + | |
| 93 | +pub fn detect_temporal(value: &str) -> Option<TemporalValue> { | |
| 94 | + // ISO 8601 with timezone | |
| 95 | + if let Ok(dt) = DateTime::parse_from_rfc3339(value) { | |
| 96 | + return Some(TemporalValue::DateTime(dt)); | |
| 97 | + } | |
| 98 | + | |
| 99 | + // RFC 2822 | |
| 100 | + if let Ok(dt) = DateTime::parse_from_rfc2822(value) { | |
| 101 | + return Some(TemporalValue::DateTime(dt)); | |
| 102 | + } | |
| 103 | + | |
| 104 | + // ISO 8601 variations | |
| 105 | + for fmt in &[ | |
| 106 | + "%Y-%m-%dT%H:%M:%S%.f%:z", | |
| 107 | + "%Y-%m-%dT%H:%M:%S%:z", | |
| 108 | + "%Y-%m-%dT%H:%M:%SZ", | |
| 109 | + "%Y-%m-%dT%H:%M:%S%.fZ", | |
| 110 | + "%Y-%m-%d %H:%M:%S%:z", | |
| 111 | + "%Y-%m-%d %H:%M:%S%.f%:z", | |
| 112 | + ] { | |
| 113 | + if let Ok(dt) = DateTime::parse_from_str(value, fmt) { | |
| 114 | + return Some(TemporalValue::DateTime(dt)); | |
| 115 | + } | |
| 116 | + } | |
| 117 | + | |
| 118 | + // Naive datetime | |
| 119 | + for fmt in &[ | |
| 120 | + "%Y-%m-%dT%H:%M:%S", | |
| 121 | + "%Y-%m-%dT%H:%M:%S%.f", | |
| 122 | + "%Y-%m-%d %H:%M:%S", | |
| 123 | + "%Y-%m-%d %H:%M:%S%.f", | |
| 124 | + "%Y/%m/%d %H:%M:%S", | |
| 125 | + ] { | |
| 126 | + if let Ok(dt) = NaiveDateTime::parse_from_str(value, fmt) { | |
| 127 | + return Some(TemporalValue::NaiveDateTime(dt)); | |
| 128 | + } | |
| 129 | + } | |
| 130 | + | |
| 131 | + // Naive date | |
| 132 | + for fmt in &["%Y-%m-%d", "%Y/%m/%d", "%m/%d/%Y", "%d/%m/%Y"] { | |
| 133 | + if let Ok(d) = NaiveDate::parse_from_str(value, fmt) { | |
| 134 | + return Some(TemporalValue::NaiveDate(d)); | |
| 135 | + } | |
| 136 | + } | |
| 137 | + | |
| 138 | + // Naive time | |
| 139 | + for fmt in &["%H:%M:%S", "%H:%M:%S%.f", "%H:%M"] { | |
| 140 | + if let Ok(t) = NaiveTime::parse_from_str(value, fmt) { | |
| 141 | + return Some(TemporalValue::NaiveTime(t)); | |
| 142 | + } | |
| 143 | + } | |
| 144 | + | |
| 145 | + None | |
| 146 | +} | |
| 147 | + | |
| 148 | +pub fn detect_unix_timestamp(value: i64) -> Option<TemporalValue> { | |
| 149 | + // Reasonable range: 2000-01-01 to 2099-12-31 | |
| 150 | + if (946684800..=4102444800).contains(&value) { | |
| 151 | + if let Some(dt) = DateTime::from_timestamp(value, 0) { | |
| 152 | + return Some(TemporalValue::UnixTimestamp(value, dt)); | |
| 153 | + } | |
| 154 | + } | |
| 155 | + // Millisecond timestamps | |
| 156 | + if (946684800000..=4102444800000).contains(&value) { | |
| 157 | + let secs = value / 1000; | |
| 158 | + let nanos = ((value % 1000) * 1_000_000) as u32; | |
| 159 | + if let Some(dt) = DateTime::from_timestamp(secs, nanos) { | |
| 160 | + return Some(TemporalValue::UnixTimestamp(value, dt)); | |
| 161 | + } | |
| 162 | + } | |
| 163 | + None | |
| 164 | +} | |
| 165 | + | |
| 166 | +/// Info about an IANA timezone string like "America/Los_Angeles" | |
| 167 | +pub struct TimezoneInfo { | |
| 168 | + pub name: String, | |
| 169 | + pub abbreviation: String, | |
| 170 | + pub offset_hours: f32, | |
| 171 | + pub display: String, | |
| 172 | +} | |
| 173 | + | |
| 174 | +/// Detect IANA timezone names like "America/Los_Angeles", "Europe/London", "UTC" | |
| 175 | +pub fn detect_timezone(value: &str) -> Option<TimezoneInfo> { | |
| 176 | + let tz: chrono_tz::Tz = value.parse().ok()?; | |
| 177 | + let now = Utc::now().with_timezone(&tz); | |
| 178 | + let offset_secs = now.offset().fix().local_minus_utc(); | |
| 179 | + let offset_hours = offset_secs as f32 / 3600.0; | |
| 180 | + let h = offset_secs / 3600; | |
| 181 | + let m = (offset_secs % 3600).abs() / 60; | |
| 182 | + let offset_str = if m == 0 { | |
| 183 | + format!("UTC{:+}", h) | |
| 184 | + } else { | |
| 185 | + format!("UTC{:+}:{:02}", h, m) | |
| 186 | + }; | |
| 187 | + Some(TimezoneInfo { | |
| 188 | + name: value.to_string(), | |
| 189 | + abbreviation: now.format("%Z").to_string(), | |
| 190 | + offset_hours, | |
| 191 | + display: format!("{} ({})", offset_str, now.format("%Z")), | |
| 192 | + }) | |
| 193 | +} |
+91-0src/theme.rs
| @@ -0,0 +1,91 @@ | ||
| 1 | +use egui::Color32; | |
| 2 | + | |
| 3 | +pub struct CatppuccinMocha; | |
| 4 | + | |
| 5 | +impl CatppuccinMocha { | |
| 6 | + // Base colors | |
| 7 | + pub const BASE: Color32 = Color32::from_rgb(30, 30, 46); | |
| 8 | + pub const MANTLE: Color32 = Color32::from_rgb(24, 24, 37); | |
| 9 | + pub const CRUST: Color32 = Color32::from_rgb(17, 17, 27); | |
| 10 | + pub const SURFACE0: Color32 = Color32::from_rgb(49, 50, 68); | |
| 11 | + pub const SURFACE1: Color32 = Color32::from_rgb(69, 71, 90); | |
| 12 | + pub const SURFACE2: Color32 = Color32::from_rgb(88, 91, 112); | |
| 13 | + pub const OVERLAY0: Color32 = Color32::from_rgb(108, 112, 134); | |
| 14 | + pub const OVERLAY1: Color32 = Color32::from_rgb(127, 132, 156); | |
| 15 | + pub const TEXT: Color32 = Color32::from_rgb(205, 214, 244); | |
| 16 | + pub const SUBTEXT0: Color32 = Color32::from_rgb(166, 173, 200); | |
| 17 | + pub const SUBTEXT1: Color32 = Color32::from_rgb(186, 194, 222); | |
| 18 | + | |
| 19 | + // Accent colors | |
| 20 | + pub const BLUE: Color32 = Color32::from_rgb(137, 180, 250); | |
| 21 | + pub const GREEN: Color32 = Color32::from_rgb(166, 227, 161); | |
| 22 | + pub const PEACH: Color32 = Color32::from_rgb(250, 179, 135); | |
| 23 | + pub const MAUVE: Color32 = Color32::from_rgb(203, 166, 247); | |
| 24 | + pub const RED: Color32 = Color32::from_rgb(243, 139, 168); | |
| 25 | + pub const YELLOW: Color32 = Color32::from_rgb(249, 226, 175); | |
| 26 | + pub const TEAL: Color32 = Color32::from_rgb(148, 226, 213); | |
| 27 | + pub const LAVENDER: Color32 = Color32::from_rgb(180, 190, 254); | |
| 28 | + pub const FLAMINGO: Color32 = Color32::from_rgb(242, 205, 205); | |
| 29 | + pub const ROSEWATER: Color32 = Color32::from_rgb(245, 224, 220); | |
| 30 | + pub const SKY: Color32 = Color32::from_rgb(137, 220, 235); | |
| 31 | + pub const SAPPHIRE: Color32 = Color32::from_rgb(116, 199, 236); | |
| 32 | + pub const MAROON: Color32 = Color32::from_rgb(235, 160, 172); | |
| 33 | + pub const PINK: Color32 = Color32::from_rgb(245, 194, 231); | |
| 34 | + | |
| 35 | + pub fn apply(ctx: &egui::Context) { | |
| 36 | + let mut style = (*ctx.style()).clone(); | |
| 37 | + let visuals = &mut style.visuals; | |
| 38 | + | |
| 39 | + visuals.dark_mode = true; | |
| 40 | + visuals.override_text_color = Some(Self::TEXT); | |
| 41 | + visuals.panel_fill = Self::BASE; | |
| 42 | + visuals.window_fill = Self::BASE; | |
| 43 | + visuals.extreme_bg_color = Self::CRUST; | |
| 44 | + visuals.faint_bg_color = Self::MANTLE; | |
| 45 | + visuals.code_bg_color = Self::SURFACE0; | |
| 46 | + | |
| 47 | + visuals.widgets.noninteractive.bg_fill = Self::BASE; | |
| 48 | + visuals.widgets.noninteractive.fg_stroke.color = Self::SUBTEXT0; | |
| 49 | + visuals.widgets.noninteractive.bg_stroke.color = Self::SURFACE1; | |
| 50 | + | |
| 51 | + visuals.widgets.inactive.bg_fill = Self::SURFACE0; | |
| 52 | + visuals.widgets.inactive.fg_stroke.color = Self::TEXT; | |
| 53 | + visuals.widgets.inactive.bg_stroke.color = Self::SURFACE1; | |
| 54 | + | |
| 55 | + visuals.widgets.hovered.bg_fill = Self::SURFACE1; | |
| 56 | + visuals.widgets.hovered.fg_stroke.color = Self::TEXT; | |
| 57 | + visuals.widgets.hovered.bg_stroke.color = Self::BLUE; | |
| 58 | + | |
| 59 | + visuals.widgets.active.bg_fill = Self::SURFACE2; | |
| 60 | + visuals.widgets.active.fg_stroke.color = Self::TEXT; | |
| 61 | + visuals.widgets.active.bg_stroke.color = Self::BLUE; | |
| 62 | + | |
| 63 | + visuals.selection.bg_fill = Color32::from_rgba_premultiplied(137, 180, 250, 40); | |
| 64 | + visuals.selection.stroke.color = Self::BLUE; | |
| 65 | + | |
| 66 | + visuals.hyperlink_color = Self::BLUE; | |
| 67 | + visuals.warn_fg_color = Self::YELLOW; | |
| 68 | + visuals.error_fg_color = Self::RED; | |
| 69 | + | |
| 70 | + visuals.window_stroke.color = Self::SURFACE1; | |
| 71 | + visuals.window_shadow.color = Color32::from_rgba_premultiplied(0, 0, 0, 60); | |
| 72 | + | |
| 73 | + ctx.set_style(style); | |
| 74 | + } | |
| 75 | +} | |
| 76 | + | |
| 77 | +pub fn type_color(type_name: &str) -> Color32 { | |
| 78 | + match type_name { | |
| 79 | + "i64" | "u64" | "f64" => CatppuccinMocha::PEACH, | |
| 80 | + "String" => CatppuccinMocha::GREEN, | |
| 81 | + "bool" => CatppuccinMocha::MAUVE, | |
| 82 | + "null" | "None" => CatppuccinMocha::OVERLAY0, | |
| 83 | + s if s.starts_with("Vec<") => CatppuccinMocha::YELLOW, | |
| 84 | + s if s.starts_with("Option<") || s.ends_with("?") => CatppuccinMocha::FLAMINGO, | |
| 85 | + "Mixed" => CatppuccinMocha::YELLOW, | |
| 86 | + s if s.contains("DateTime") || s.contains("Date") || s.contains("Time") => { | |
| 87 | + CatppuccinMocha::BLUE | |
| 88 | + } | |
| 89 | + _ => CatppuccinMocha::LAVENDER, // structs | |
| 90 | + } | |
| 91 | +} |
+402-0src/types.rs
| @@ -0,0 +1,402 @@ | ||
| 1 | +use serde_json::Value; | |
| 2 | +use std::collections::{BTreeMap, BTreeSet}; | |
| 3 | + | |
| 4 | +use crate::temporal::{detect_temporal, detect_unix_timestamp, TemporalValue}; | |
| 5 | + | |
| 6 | +#[derive(Debug, Clone, PartialEq)] | |
| 7 | +pub enum InferredType { | |
| 8 | + Null, | |
| 9 | + Bool, | |
| 10 | + I64, | |
| 11 | + F64, | |
| 12 | + String, | |
| 13 | + DateTime, | |
| 14 | + Date, | |
| 15 | + Time, | |
| 16 | + Array(Box<InferredType>), | |
| 17 | + Object(BTreeMap<String, InferredType>), | |
| 18 | + Option(Box<InferredType>), | |
| 19 | + Mixed(Vec<InferredType>), | |
| 20 | + Unknown, | |
| 21 | +} | |
| 22 | + | |
| 23 | +impl InferredType { | |
| 24 | + pub fn rust_type(&self) -> String { | |
| 25 | + match self { | |
| 26 | + Self::Null => "Option<serde_json::Value>".to_string(), | |
| 27 | + Self::Bool => "bool".to_string(), | |
| 28 | + Self::I64 => "i64".to_string(), | |
| 29 | + Self::F64 => "f64".to_string(), | |
| 30 | + Self::String => "String".to_string(), | |
| 31 | + Self::DateTime => "DateTime<FixedOffset>".to_string(), | |
| 32 | + Self::Date => "NaiveDate".to_string(), | |
| 33 | + Self::Time => "NaiveTime".to_string(), | |
| 34 | + Self::Array(inner) => format!("Vec<{}>", inner.rust_type()), | |
| 35 | + Self::Object(_) => "serde_json::Value".to_string(), | |
| 36 | + Self::Option(inner) => format!("Option<{}>", inner.rust_type()), | |
| 37 | + Self::Mixed(_) => "serde_json::Value".to_string(), | |
| 38 | + Self::Unknown => "serde_json::Value".to_string(), | |
| 39 | + } | |
| 40 | + } | |
| 41 | + | |
| 42 | + /// Recursive type tag for Jaccard similarity — distinguishes structural shape | |
| 43 | + pub fn type_tag(&self) -> String { | |
| 44 | + match self { | |
| 45 | + Self::Null => "null".into(), | |
| 46 | + Self::Bool => "bool".into(), | |
| 47 | + Self::I64 => "i64".into(), | |
| 48 | + Self::F64 => "f64".into(), | |
| 49 | + Self::String => "str".into(), | |
| 50 | + Self::DateTime => "dt".into(), | |
| 51 | + Self::Date => "date".into(), | |
| 52 | + Self::Time => "time".into(), | |
| 53 | + Self::Array(inner) => format!("[{}]", inner.type_tag()), | |
| 54 | + Self::Object(fields) => { | |
| 55 | + // Recurse into field types so structurally different objects are distinct | |
| 56 | + let mut pairs: Vec<std::string::String> = fields | |
| 57 | + .iter() | |
| 58 | + .map(|(k, v)| format!("{}:{}", k, v.type_tag())) | |
| 59 | + .collect(); | |
| 60 | + pairs.sort(); | |
| 61 | + format!("{{{}}}", pairs.join(",")) | |
| 62 | + } | |
| 63 | + Self::Option(inner) => format!("?{}", inner.type_tag()), | |
| 64 | + Self::Mixed(_) => "mixed".into(), | |
| 65 | + Self::Unknown => "?".into(), | |
| 66 | + } | |
| 67 | + } | |
| 68 | + | |
| 69 | + pub fn display_name(&self) -> String { | |
| 70 | + match self { | |
| 71 | + Self::Null => "null".to_string(), | |
| 72 | + Self::Bool => "bool".to_string(), | |
| 73 | + Self::I64 => "i64".to_string(), | |
| 74 | + Self::F64 => "f64".to_string(), | |
| 75 | + Self::String => "String".to_string(), | |
| 76 | + Self::DateTime => "DateTime".to_string(), | |
| 77 | + Self::Date => "Date".to_string(), | |
| 78 | + Self::Time => "Time".to_string(), | |
| 79 | + Self::Array(inner) => format!("Vec<{}>", inner.display_name()), | |
| 80 | + Self::Object(fields) => format!("Object({})", fields.len()), | |
| 81 | + Self::Option(inner) => format!("Option<{}>", inner.display_name()), | |
| 82 | + Self::Mixed(types) => { | |
| 83 | + let names: Vec<String> = types.iter().map(|t| t.display_name()).collect(); | |
| 84 | + format!("Mixed({})", names.join("|")) | |
| 85 | + } | |
| 86 | + Self::Unknown => "Unknown".to_string(), | |
| 87 | + } | |
| 88 | + } | |
| 89 | + | |
| 90 | + /// Short label for table display — keeps things compact | |
| 91 | + pub fn short_name(&self, structs: &[crate::schema::SharedStruct]) -> String { | |
| 92 | + match self { | |
| 93 | + Self::DateTime => "dt".to_string(), | |
| 94 | + Self::Date => "d".to_string(), | |
| 95 | + Self::Time => "t".to_string(), | |
| 96 | + Self::Option(inner) => format!("{}?", inner.short_name(structs)), | |
| 97 | + Self::Mixed(_) => "Mixed".to_string(), | |
| 98 | + Self::Array(inner) => format!("[{}]", inner.short_name(structs)), | |
| 99 | + Self::Object(fields) => { | |
| 100 | + resolve_struct_name(fields, structs) | |
| 101 | + .unwrap_or_else(|| format!("Object({})", fields.len())) | |
| 102 | + } | |
| 103 | + other => other.display_name(), | |
| 104 | + } | |
| 105 | + } | |
| 106 | + | |
| 107 | + /// Detailed tooltip for complex types, None if short_name is sufficient | |
| 108 | + pub fn tooltip(&self, structs: &[crate::schema::SharedStruct]) -> Option<String> { | |
| 109 | + match self { | |
| 110 | + Self::Mixed(types) => { | |
| 111 | + let names: Vec<String> = types.iter().map(|t| t.short_name(structs)).collect(); | |
| 112 | + Some(names.join(" | ")) | |
| 113 | + } | |
| 114 | + Self::Option(inner) => inner.tooltip(structs), | |
| 115 | + Self::Object(fields) => { | |
| 116 | + let field_names: Vec<&str> = fields.keys().map(|s| s.as_str()).collect(); | |
| 117 | + Some(format!("Fields: {}", field_names.join(", "))) | |
| 118 | + } | |
| 119 | + _ => None, | |
| 120 | + } | |
| 121 | + } | |
| 122 | +} | |
| 123 | + | |
| 124 | +/// Match an Object's field keys against known structs (80%+ key overlap) | |
| 125 | +fn resolve_struct_name( | |
| 126 | + fields: &BTreeMap<String, InferredType>, | |
| 127 | + structs: &[crate::schema::SharedStruct], | |
| 128 | +) -> Option<String> { | |
| 129 | + let obj_keys: std::collections::BTreeSet<&str> = fields.keys().map(|s| s.as_str()).collect(); | |
| 130 | + if obj_keys.is_empty() { | |
| 131 | + return None; | |
| 132 | + } | |
| 133 | + | |
| 134 | + let mut best: Option<(&str, f64)> = None; | |
| 135 | + for s in structs { | |
| 136 | + let struct_keys: std::collections::BTreeSet<&str> = | |
| 137 | + s.fields.keys().map(|k| k.as_str()).collect(); | |
| 138 | + let intersection = obj_keys.intersection(&struct_keys).count(); | |
| 139 | + let union = obj_keys.union(&struct_keys).count(); | |
| 140 | + if union == 0 { | |
| 141 | + continue; | |
| 142 | + } | |
| 143 | + let similarity = intersection as f64 / union as f64; | |
| 144 | + if similarity >= 0.8 { | |
| 145 | + if best.is_none() || similarity > best.unwrap().1 { | |
| 146 | + best = Some((&s.name, similarity)); | |
| 147 | + } | |
| 148 | + } | |
| 149 | + } | |
| 150 | + | |
| 151 | + best.map(|(name, _)| name.to_string()) | |
| 152 | +} | |
| 153 | + | |
| 154 | +/// Override state for temporal detection | |
| 155 | +#[derive(Debug, Clone, PartialEq)] | |
| 156 | +pub enum TemporalOverride { | |
| 157 | + Auto, | |
| 158 | + ForceTemporal, | |
| 159 | + ForcePlain, | |
| 160 | +} | |
| 161 | + | |
| 162 | +impl Default for TemporalOverride { | |
| 163 | + fn default() -> Self { | |
| 164 | + Self::Auto | |
| 165 | + } | |
| 166 | +} | |
| 167 | + | |
| 168 | +/// Infer type from a JSON value without temporal detection — all strings are String. | |
| 169 | +/// Used by schema inference where temporal promotion causes false Mixed types. | |
| 170 | +pub fn infer_type_plain(value: &Value) -> InferredType { | |
| 171 | + match value { | |
| 172 | + Value::Null => InferredType::Null, | |
| 173 | + Value::Bool(_) => InferredType::Bool, | |
| 174 | + Value::Number(n) => { | |
| 175 | + if n.is_i64() { | |
| 176 | + InferredType::I64 | |
| 177 | + } else { | |
| 178 | + InferredType::F64 | |
| 179 | + } | |
| 180 | + } | |
| 181 | + Value::String(_) => InferredType::String, | |
| 182 | + Value::Array(arr) => { | |
| 183 | + if arr.is_empty() { | |
| 184 | + InferredType::Array(Box::new(InferredType::Unknown)) | |
| 185 | + } else { | |
| 186 | + let inner = unify_types(arr.iter().map(infer_type_plain).collect()); | |
| 187 | + InferredType::Array(Box::new(inner)) | |
| 188 | + } | |
| 189 | + } | |
| 190 | + Value::Object(obj) => { | |
| 191 | + let fields: BTreeMap<String, InferredType> = obj | |
| 192 | + .iter() | |
| 193 | + .map(|(k, v)| (k.clone(), infer_type_plain(v))) | |
| 194 | + .collect(); | |
| 195 | + InferredType::Object(fields) | |
| 196 | + } | |
| 197 | + } | |
| 198 | +} | |
| 199 | + | |
| 200 | +pub fn infer_type(value: &Value) -> InferredType { | |
| 201 | + match value { | |
| 202 | + Value::Null => InferredType::Null, | |
| 203 | + Value::Bool(_) => InferredType::Bool, | |
| 204 | + Value::Number(n) => { | |
| 205 | + if n.is_i64() { | |
| 206 | + InferredType::I64 | |
| 207 | + } else { | |
| 208 | + InferredType::F64 | |
| 209 | + } | |
| 210 | + } | |
| 211 | + Value::String(s) => { | |
| 212 | + if let Some(temporal) = detect_temporal(s) { | |
| 213 | + match temporal { | |
| 214 | + TemporalValue::DateTime(_) | |
| 215 | + | TemporalValue::DateTimeUtc(_) | |
| 216 | + | TemporalValue::NaiveDateTime(_) => InferredType::DateTime, | |
| 217 | + TemporalValue::NaiveDate(_) => InferredType::Date, | |
| 218 | + TemporalValue::NaiveTime(_) => InferredType::Time, | |
| 219 | + TemporalValue::UnixTimestamp(_, _) => InferredType::DateTime, | |
| 220 | + } | |
| 221 | + } else { | |
| 222 | + InferredType::String | |
| 223 | + } | |
| 224 | + } | |
| 225 | + Value::Array(arr) => { | |
| 226 | + if arr.is_empty() { | |
| 227 | + InferredType::Array(Box::new(InferredType::Unknown)) | |
| 228 | + } else { | |
| 229 | + let inner = unify_types(arr.iter().map(infer_type).collect()); | |
| 230 | + InferredType::Array(Box::new(inner)) | |
| 231 | + } | |
| 232 | + } | |
| 233 | + Value::Object(obj) => { | |
| 234 | + let fields: BTreeMap<String, InferredType> = obj | |
| 235 | + .iter() | |
| 236 | + .map(|(k, v)| (k.clone(), infer_type(v))) | |
| 237 | + .collect(); | |
| 238 | + InferredType::Object(fields) | |
| 239 | + } | |
| 240 | + } | |
| 241 | +} | |
| 242 | + | |
| 243 | +pub fn infer_type_with_overrides( | |
| 244 | + value: &Value, | |
| 245 | + path: &str, | |
| 246 | + overrides: &BTreeMap<String, TemporalOverride>, | |
| 247 | +) -> InferredType { | |
| 248 | + let ovr = overrides.get(path).unwrap_or(&TemporalOverride::Auto); | |
| 249 | + match value { | |
| 250 | + Value::String(s) => match ovr { | |
| 251 | + TemporalOverride::ForcePlain => InferredType::String, | |
| 252 | + TemporalOverride::ForceTemporal => { | |
| 253 | + if detect_temporal(s).is_some() { | |
| 254 | + InferredType::DateTime | |
| 255 | + } else { | |
| 256 | + InferredType::DateTime | |
| 257 | + } | |
| 258 | + } | |
| 259 | + TemporalOverride::Auto => { | |
| 260 | + if detect_temporal(s).is_some() { | |
| 261 | + InferredType::DateTime | |
| 262 | + } else { | |
| 263 | + InferredType::String | |
| 264 | + } | |
| 265 | + } | |
| 266 | + }, | |
| 267 | + Value::Number(n) => { | |
| 268 | + if let TemporalOverride::ForceTemporal = ovr { | |
| 269 | + if let Some(i) = n.as_i64() { | |
| 270 | + if detect_unix_timestamp(i).is_some() { | |
| 271 | + return InferredType::DateTime; | |
| 272 | + } | |
| 273 | + } | |
| 274 | + } | |
| 275 | + if let TemporalOverride::Auto = ovr { | |
| 276 | + if let Some(i) = n.as_i64() { | |
| 277 | + if detect_unix_timestamp(i).is_some() { | |
| 278 | + return InferredType::DateTime; | |
| 279 | + } | |
| 280 | + } | |
| 281 | + } | |
| 282 | + infer_type(value) | |
| 283 | + } | |
| 284 | + _ => infer_type(value), | |
| 285 | + } | |
| 286 | +} | |
| 287 | + | |
| 288 | +fn unify_types(types: Vec<InferredType>) -> InferredType { | |
| 289 | + if types.is_empty() { | |
| 290 | + return InferredType::Unknown; | |
| 291 | + } | |
| 292 | + | |
| 293 | + let mut has_null = false; | |
| 294 | + let mut non_null: Vec<InferredType> = Vec::new(); | |
| 295 | + | |
| 296 | + for t in types { | |
| 297 | + match t { | |
| 298 | + InferredType::Null => has_null = true, | |
| 299 | + InferredType::Unknown => {} // Unknown is "no info" — absorb it | |
| 300 | + other => { | |
| 301 | + if !non_null.contains(&other) { | |
| 302 | + non_null.push(other); | |
| 303 | + } | |
| 304 | + } | |
| 305 | + } | |
| 306 | + } | |
| 307 | + | |
| 308 | + // If everything was Unknown/Null, fall back to Unknown | |
| 309 | + if non_null.is_empty() && !has_null { | |
| 310 | + return InferredType::Unknown; | |
| 311 | + } | |
| 312 | + | |
| 313 | + // Merge structurally similar types before resorting to Mixed | |
| 314 | + if non_null.len() > 1 { | |
| 315 | + non_null = merge_structural_types(non_null); | |
| 316 | + } | |
| 317 | + | |
| 318 | + let base = if non_null.is_empty() { | |
| 319 | + InferredType::Null | |
| 320 | + } else if non_null.len() == 1 { | |
| 321 | + non_null.into_iter().next().unwrap() | |
| 322 | + } else { | |
| 323 | + InferredType::Mixed(non_null) | |
| 324 | + }; | |
| 325 | + | |
| 326 | + if has_null && base != InferredType::Null { | |
| 327 | + InferredType::Option(Box::new(base)) | |
| 328 | + } else { | |
| 329 | + base | |
| 330 | + } | |
| 331 | +} | |
| 332 | + | |
| 333 | +/// Merge Objects together and Arrays together to reduce false Mixed types. | |
| 334 | +/// E.g., [Object({a,b,c}), Object({a,b,d})] → [Object({a,b,c?,d?})] | |
| 335 | +/// E.g., [Array(X), Array(Y)] → [Array(unify(X,Y))] | |
| 336 | +pub fn merge_structural_types(types: Vec<InferredType>) -> Vec<InferredType> { | |
| 337 | + let mut objects: Vec<BTreeMap<String, InferredType>> = Vec::new(); | |
| 338 | + let mut arrays: Vec<InferredType> = Vec::new(); | |
| 339 | + let mut others: Vec<InferredType> = Vec::new(); | |
| 340 | + | |
| 341 | + for t in types { | |
| 342 | + match t { | |
| 343 | + InferredType::Object(fields) => objects.push(fields), | |
| 344 | + InferredType::Array(inner) => arrays.push(*inner), | |
| 345 | + InferredType::Mixed(inner_types) => { | |
| 346 | + // Flatten nested Mixed — pull out Objects and Arrays | |
| 347 | + for it in inner_types { | |
| 348 | + match it { | |
| 349 | + InferredType::Object(fields) => objects.push(fields), | |
| 350 | + InferredType::Array(inner) => arrays.push(*inner), | |
| 351 | + other => others.push(other), | |
| 352 | + } | |
| 353 | + } | |
| 354 | + } | |
| 355 | + other => others.push(other), | |
| 356 | + } | |
| 357 | + } | |
| 358 | + | |
| 359 | + let mut result = others; | |
| 360 | + | |
| 361 | + if !objects.is_empty() { | |
| 362 | + result.push(merge_objects(objects)); | |
| 363 | + } | |
| 364 | + | |
| 365 | + if !arrays.is_empty() { | |
| 366 | + // Recursively unify all array inner types | |
| 367 | + let unified_inner = unify_types(arrays); | |
| 368 | + result.push(InferredType::Array(Box::new(unified_inner))); | |
| 369 | + } | |
| 370 | + | |
| 371 | + result | |
| 372 | +} | |
| 373 | + | |
| 374 | +/// Merge multiple Object types into one, making fields that don't appear in all objects Optional. | |
| 375 | +fn merge_objects(objects: Vec<BTreeMap<String, InferredType>>) -> InferredType { | |
| 376 | + let all_keys: BTreeSet<String> = objects.iter().flat_map(|o| o.keys().cloned()).collect(); | |
| 377 | + let total = objects.len(); | |
| 378 | + let mut merged = BTreeMap::new(); | |
| 379 | + | |
| 380 | + for key in all_keys { | |
| 381 | + let mut field_types = Vec::new(); | |
| 382 | + let mut present = 0; | |
| 383 | + for obj in &objects { | |
| 384 | + if let Some(t) = obj.get(&key) { | |
| 385 | + field_types.push(t.clone()); | |
| 386 | + present += 1; | |
| 387 | + } | |
| 388 | + } | |
| 389 | + let unified = unify_types(field_types); | |
| 390 | + let field_type = if present < total { | |
| 391 | + match unified { | |
| 392 | + InferredType::Option(_) | InferredType::Null => unified, | |
| 393 | + other => InferredType::Option(Box::new(other)), | |
| 394 | + } | |
| 395 | + } else { | |
| 396 | + unified | |
| 397 | + }; | |
| 398 | + merged.insert(key, field_type); | |
| 399 | + } | |
| 400 | + | |
| 401 | + InferredType::Object(merged) | |
| 402 | +} |
+1945-0src/views/browser.rs
| @@ -0,0 +1,1945 @@ | ||
| 1 | +use egui::{self, RichText, Ui}; | |
| 2 | + | |
| 3 | +use crate::jq_engine::JqEngine; | |
| 4 | +use crate::temporal::{detect_temporal, detect_timezone, detect_unix_timestamp, TemporalValue}; | |
| 5 | +use crate::theme::CatppuccinMocha; | |
| 6 | + | |
| 7 | +#[derive(Debug, Clone)] | |
| 8 | +enum PathSegment { | |
| 9 | + Key(String), | |
| 10 | + Index(usize), | |
| 11 | +} | |
| 12 | + | |
| 13 | +/// An entry at a given level of the JSON tree. | |
| 14 | +struct Entry { | |
| 15 | + label: String, | |
| 16 | + type_icon: &'static str, | |
| 17 | + type_label: String, | |
| 18 | + preview: String, | |
| 19 | + color: egui::Color32, | |
| 20 | + is_container: bool, | |
| 21 | +} | |
| 22 | + | |
| 23 | +pub struct BrowserView { | |
| 24 | + path: Vec<PathSegment>, | |
| 25 | + selection: usize, | |
| 26 | + scroll_to_selection: bool, | |
| 27 | + restore_key: Option<String>, | |
| 28 | + // jq bar | |
| 29 | + jq_input: String, | |
| 30 | + jq_synced: bool, | |
| 31 | + jq_result: Option<String>, | |
| 32 | + jq_error: Option<String>, | |
| 33 | + // completions | |
| 34 | + completions: Vec<String>, | |
| 35 | + completion_index: usize, | |
| 36 | + show_completions: bool, | |
| 37 | + refocus_jq: bool, | |
| 38 | + cache_key: u64, | |
| 39 | +} | |
| 40 | + | |
| 41 | +impl BrowserView { | |
| 42 | + pub fn new() -> Self { | |
| 43 | + Self { | |
| 44 | + path: Vec::new(), | |
| 45 | + selection: 0, | |
| 46 | + scroll_to_selection: false, | |
| 47 | + restore_key: None, | |
| 48 | + jq_input: ".".to_string(), | |
| 49 | + jq_synced: true, | |
| 50 | + jq_result: None, | |
| 51 | + jq_error: None, | |
| 52 | + completions: Vec::new(), | |
| 53 | + completion_index: 0, | |
| 54 | + show_completions: false, | |
| 55 | + refocus_jq: false, | |
| 56 | + cache_key: 0, | |
| 57 | + } | |
| 58 | + } | |
| 59 | + | |
| 60 | + /// Returns the display name of the current file: | |
| 61 | + /// - If inside a file, returns the first path segment (the file key) | |
| 62 | + /// - If at the file list root, returns the selected entry's label | |
| 63 | + pub fn current_file_key(&self, files: &[(String, serde_json::Value)]) -> Option<String> { | |
| 64 | + if let Some(seg) = self.path.first() { | |
| 65 | + match seg { | |
| 66 | + PathSegment::Key(k) => Some(k.clone()), | |
| 67 | + _ => None, | |
| 68 | + } | |
| 69 | + } else { | |
| 70 | + // At root file list — use selection index | |
| 71 | + // The virtual root is a sorted Object, so we need to match by sorted order | |
| 72 | + let mut display_names: Vec<String> = files.iter().map(|(n, _)| { | |
| 73 | + n.strip_suffix(".json").unwrap_or(n).to_string() | |
| 74 | + }).collect(); | |
| 75 | + display_names.sort(); | |
| 76 | + display_names.get(self.selection).cloned() | |
| 77 | + } | |
| 78 | + } | |
| 79 | + | |
| 80 | + /// Navigate the browser to a specific file by display name. | |
| 81 | + /// Stays at the root file list and moves selection to the given file. | |
| 82 | + pub fn navigate_to_file(&mut self, display_name: &str, files: &[(String, serde_json::Value)]) { | |
| 83 | + self.path.clear(); | |
| 84 | + // Find the index in the sorted display names (matching BTreeMap order) | |
| 85 | + let mut display_names: Vec<String> = files.iter().map(|(n, _)| { | |
| 86 | + n.strip_suffix(".json").unwrap_or(n).to_string() | |
| 87 | + }).collect(); | |
| 88 | + display_names.sort(); | |
| 89 | + if let Some(idx) = display_names.iter().position(|n| n == display_name) { | |
| 90 | + self.selection = idx; | |
| 91 | + } | |
| 92 | + self.scroll_to_selection = true; | |
| 93 | + self.sync_jq_from_path(); | |
| 94 | + } | |
| 95 | + | |
| 96 | + pub fn invalidate(&mut self) { | |
| 97 | + self.cache_key = 0; | |
| 98 | + self.path.clear(); | |
| 99 | + self.selection = 0; | |
| 100 | + self.jq_synced = true; | |
| 101 | + self.jq_result = None; | |
| 102 | + self.jq_error = None; | |
| 103 | + } | |
| 104 | + | |
| 105 | + /// files: all parsed files. The browser treats them as the root level of the tree. | |
| 106 | + /// jq operates on whichever file is currently entered (first path segment = file index). | |
| 107 | + pub fn show(&mut self, ui: &mut Ui, files: &[(String, serde_json::Value)]) { | |
| 108 | + // Build a virtual root: an Object keyed by filename | |
| 109 | + let virtual_root = serde_json::Value::Object( | |
| 110 | + files.iter().map(|(name, val)| { | |
| 111 | + let display = name.strip_suffix(".json").unwrap_or(name).to_string(); | |
| 112 | + (display, val.clone()) | |
| 113 | + }).collect() | |
| 114 | + ); | |
| 115 | + | |
| 116 | + let key = files.len() as u64 | |
| 117 | + ^ files.iter().map(|(n, _)| n.len() as u64).sum::<u64>() << 8; | |
| 118 | + if self.cache_key != key { | |
| 119 | + self.cache_key = key; | |
| 120 | + if resolve_path(&virtual_root, &self.path).is_none() { | |
| 121 | + self.path.clear(); | |
| 122 | + self.selection = 0; | |
| 123 | + } | |
| 124 | + self.sync_jq_from_path(); | |
| 125 | + } | |
| 126 | + | |
| 127 | + let current = resolve_path(&virtual_root, &self.path).unwrap_or(&virtual_root); | |
| 128 | + let parent = if self.path.is_empty() { | |
| 129 | + None | |
| 130 | + } else { | |
| 131 | + resolve_path(&virtual_root, &self.path[..self.path.len() - 1]) | |
| 132 | + }; | |
| 133 | + | |
| 134 | + let current_entries = build_entries(current); | |
| 135 | + let parent_entries = parent.map(|p| build_entries(p)); | |
| 136 | + | |
| 137 | + if let Some(key) = self.restore_key.take() { | |
| 138 | + if let Some(idx) = current_entries.iter().position(|e| e.label == key) { | |
| 139 | + self.selection = idx; | |
| 140 | + } | |
| 141 | + } | |
| 142 | + | |
| 143 | + if !current_entries.is_empty() && self.selection >= current_entries.len() { | |
| 144 | + self.selection = current_entries.len() - 1; | |
| 145 | + } | |
| 146 | + | |
| 147 | + let selected_child = current_entries | |
| 148 | + .get(self.selection) | |
| 149 | + .and_then(|e| child_value(current, self.selection, &e.label)); | |
| 150 | + | |
| 151 | + // --- jq bar (operates on the selected file, not the virtual root) --- | |
| 152 | + // The first path segment selects the file; jq runs on that file's value | |
| 153 | + let jq_value = self.path.first().and_then(|seg| { | |
| 154 | + match seg { | |
| 155 | + PathSegment::Key(k) => files.iter().find(|(n, _)| { | |
| 156 | + n.strip_suffix(".json").unwrap_or(n) == k | |
| 157 | + }).map(|(_, v)| v), | |
| 158 | + _ => None, | |
| 159 | + } | |
| 160 | + }); | |
| 161 | + if let Some(val) = jq_value { | |
| 162 | + self.show_jq_bar(ui, val); | |
| 163 | + } else { | |
| 164 | + // At file list root — show jq bar disabled/empty | |
| 165 | + ui.horizontal(|ui| { | |
| 166 | + ui.label( | |
| 167 | + RichText::new(egui_phosphor::regular::FUNNEL) | |
| 168 | + .color(CatppuccinMocha::SURFACE1) | |
| 169 | + .size(14.0), | |
| 170 | + ); | |
| 171 | + ui.label( | |
| 172 | + RichText::new("select a file to use jq") | |
| 173 | + .color(CatppuccinMocha::OVERLAY0) | |
| 174 | + .small(), | |
| 175 | + ); | |
| 176 | + }); | |
| 177 | + } | |
| 178 | + | |
| 179 | + ui.add_space(4.0); | |
| 180 | + | |
| 181 | + // --- Keyboard handling (only when jq input NOT focused) --- | |
| 182 | + let jq_id = ui.id().with("browser_jq_input"); | |
| 183 | + let jq_has_focus = ui.ctx().memory(|m| { | |
| 184 | + m.focused().map_or(false, |f| f == jq_id) | |
| 185 | + }); | |
| 186 | + | |
| 187 | + if !jq_has_focus { | |
| 188 | + let down = ui.input(|i| { | |
| 189 | + i.key_pressed(egui::Key::J) || i.key_pressed(egui::Key::ArrowDown) | |
| 190 | + }); | |
| 191 | + let up = ui.input(|i| { | |
| 192 | + i.key_pressed(egui::Key::K) || i.key_pressed(egui::Key::ArrowUp) | |
| 193 | + }); | |
| 194 | + let right = ui.input(|i| { | |
| 195 | + i.key_pressed(egui::Key::L) | |
| 196 | + || i.key_pressed(egui::Key::ArrowRight) | |
| 197 | + || i.key_pressed(egui::Key::Enter) | |
| 198 | + }); | |
| 199 | + let left = ui.input(|i| { | |
| 200 | + i.key_pressed(egui::Key::H) | |
| 201 | + || i.key_pressed(egui::Key::ArrowLeft) | |
| 202 | + || i.key_pressed(egui::Key::Backspace) | |
| 203 | + }); | |
| 204 | + let go_top = ui.input(|i| i.key_pressed(egui::Key::G)); | |
| 205 | + let go_bottom = ui.input(|i| i.key_pressed(egui::Key::End)); | |
| 206 | + let go_home = ui.input(|i| i.key_pressed(egui::Key::Home)); | |
| 207 | + | |
| 208 | + if down && !current_entries.is_empty() { | |
| 209 | + self.selection = (self.selection + 1).min(current_entries.len() - 1); | |
| 210 | + self.scroll_to_selection = true; | |
| 211 | + self.sync_jq_from_path(); | |
| 212 | + } | |
| 213 | + if up { | |
| 214 | + self.selection = self.selection.saturating_sub(1); | |
| 215 | + self.scroll_to_selection = true; | |
| 216 | + self.sync_jq_from_path(); | |
| 217 | + } | |
| 218 | + if right { | |
| 219 | + if let Some(entry) = current_entries.get(self.selection) { | |
| 220 | + if entry.is_container { | |
| 221 | + self.enter_selected(current, ¤t_entries); | |
| 222 | + } | |
| 223 | + } | |
| 224 | + } | |
| 225 | + if left && !self.path.is_empty() { | |
| 226 | + self.go_up(); | |
| 227 | + } | |
| 228 | + if go_top || go_home { | |
| 229 | + self.selection = 0; | |
| 230 | + self.scroll_to_selection = true; | |
| 231 | + self.sync_jq_from_path(); | |
| 232 | + } | |
| 233 | + if go_bottom && !current_entries.is_empty() { | |
| 234 | + self.selection = current_entries.len() - 1; | |
| 235 | + self.scroll_to_selection = true; | |
| 236 | + self.sync_jq_from_path(); | |
| 237 | + } | |
| 238 | + | |
| 239 | + // Copy selected value: c or Ctrl+C | |
| 240 | + let copy = ui.input(|i| i.key_pressed(egui::Key::C)) | |
| 241 | + || ui.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::C)); | |
| 242 | + if copy { | |
| 243 | + if let Some(entry) = current_entries.get(self.selection) { | |
| 244 | + if let Some(v) = child_value(current, self.selection, &entry.label) { | |
| 245 | + ui.ctx().copy_text(copy_value_str(v)); | |
| 246 | + } | |
| 247 | + } | |
| 248 | + } | |
| 249 | + } | |
| 250 | + | |
| 251 | + // --- Three-column miller layout: parent | current | preview --- | |
| 252 | + let avail = ui.available_rect_before_wrap(); | |
| 253 | + let total_w = avail.width() - 12.0; | |
| 254 | + let col_widths = [total_w * 0.22, total_w * 0.38, total_w * 0.38]; | |
| 255 | + | |
| 256 | + let mut clicked_entry: Option<usize> = None; | |
| 257 | + let mut dbl_clicked_entry: Option<usize> = None; | |
| 258 | + | |
| 259 | + let col_height = avail.height(); | |
| 260 | + ui.horizontal(|ui| { | |
| 261 | + ui.set_height(col_height); | |
| 262 | + | |
| 263 | + // Left: parent | |
| 264 | + ui.vertical(|ui| { | |
| 265 | + ui.set_width(col_widths[0]); | |
| 266 | + ui.set_height(col_height); | |
| 267 | + self.render_parent_column(ui, &parent_entries, col_height); | |
| 268 | + }); | |
| 269 | + | |
| 270 | + Self::draw_separator(ui, col_height); | |
| 271 | + | |
| 272 | + // Middle: current | |
| 273 | + ui.vertical(|ui| { | |
| 274 | + ui.set_width(col_widths[1]); | |
| 275 | + ui.set_height(col_height); | |
| 276 | + let (c, d) = self.render_current_column(ui, ¤t_entries, current, col_height); | |
| 277 | + clicked_entry = c; | |
| 278 | + dbl_clicked_entry = d; | |
| 279 | + }); | |
| 280 | + | |
| 281 | + Self::draw_separator(ui, col_height); | |
| 282 | + | |
| 283 | + // Right: preview | |
| 284 | + ui.vertical(|ui| { | |
| 285 | + let remaining = ui.available_width(); | |
| 286 | + ui.set_width(remaining); | |
| 287 | + ui.set_height(col_height); | |
| 288 | + self.render_preview_column(ui, selected_child, ¤t_entries, col_height); | |
| 289 | + }); | |
| 290 | + }); | |
| 291 | + | |
| 292 | + // Handle click actions | |
| 293 | + if let Some(idx) = clicked_entry { | |
| 294 | + self.selection = idx; | |
| 295 | + self.scroll_to_selection = false; | |
| 296 | + self.sync_jq_from_path(); | |
| 297 | + } | |
| 298 | + if let Some(idx) = dbl_clicked_entry { | |
| 299 | + self.selection = idx; | |
| 300 | + if let Some(entry) = current_entries.get(idx) { | |
| 301 | + if entry.is_container { | |
| 302 | + self.enter_selected(current, ¤t_entries); | |
| 303 | + } | |
| 304 | + } | |
| 305 | + } | |
| 306 | + } | |
| 307 | + | |
| 308 | + fn show_jq_bar(&mut self, ui: &mut Ui, root: &serde_json::Value) { | |
| 309 | + let mut run_jq = false; | |
| 310 | + let mut accepted_completion: Option<String> = None; | |
| 311 | + | |
| 312 | + ui.horizontal(|ui| { | |
| 313 | + ui.label( | |
| 314 | + RichText::new(egui_phosphor::regular::FUNNEL) | |
| 315 | + .color(CatppuccinMocha::MAUVE) | |
| 316 | + .size(14.0), | |
| 317 | + ); | |
| 318 | + | |
| 319 | + let jq_id = ui.id().with("browser_jq_input"); | |
| 320 | + let response = ui.add( | |
| 321 | + egui::TextEdit::singleline(&mut self.jq_input) | |
| 322 | + .id(jq_id) | |
| 323 | + .font(egui::FontId::monospace(14.0)) | |
| 324 | + .desired_width(ui.available_width() - 10.0) | |
| 325 | + .text_color(CatppuccinMocha::GREEN), | |
| 326 | + ); | |
| 327 | + | |
| 328 | + if self.refocus_jq { | |
| 329 | + response.request_focus(); | |
| 330 | + if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), response.id) { | |
| 331 | + let ccursor = egui::text::CCursor::new(self.jq_input.len()); | |
| 332 | + state | |
| 333 | + .cursor | |
| 334 | + .set_char_range(Some(egui::text::CCursorRange::one(ccursor))); | |
| 335 | + state.store(ui.ctx(), response.id); | |
| 336 | + } | |
| 337 | + self.refocus_jq = false; | |
| 338 | + } | |
| 339 | + | |
| 340 | + let has_focus = response.has_focus(); | |
| 341 | + | |
| 342 | + if has_focus { | |
| 343 | + // Mark as manually edited | |
| 344 | + if response.changed() { | |
| 345 | + self.jq_synced = false; | |
| 346 | + self.jq_result = None; | |
| 347 | + self.jq_error = None; | |
| 348 | + self.rebuild_completions(root); | |
| 349 | + } | |
| 350 | + | |
| 351 | + let tab = | |
| 352 | + ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Tab)); | |
| 353 | + let ctrl_space = | |
| 354 | + ui.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::Space)); | |
| 355 | + | |
| 356 | + if tab && self.show_completions && !self.completions.is_empty() { | |
| 357 | + accepted_completion = | |
| 358 | + Some(self.completions[self.completion_index].clone()); | |
| 359 | + } | |
| 360 | + if tab { | |
| 361 | + response.request_focus(); | |
| 362 | + } | |
| 363 | + | |
| 364 | + if ui.input(|i| i.key_pressed(egui::Key::Escape)) { | |
| 365 | + self.show_completions = false; | |
| 366 | + self.jq_synced = true; | |
| 367 | + self.sync_jq_from_path(); | |
| 368 | + self.jq_result = None; | |
| 369 | + self.jq_error = None; | |
| 370 | + response.surrender_focus(); | |
| 371 | + } | |
| 372 | + | |
| 373 | + if ctrl_space { | |
| 374 | + if self.show_completions { | |
| 375 | + self.show_completions = false; | |
| 376 | + } else { | |
| 377 | + self.rebuild_completions(root); | |
| 378 | + } | |
| 379 | + } | |
| 380 | + | |
| 381 | + // Enter: try to navigate to path, or run as jq query | |
| 382 | + if response.lost_focus() | |
| 383 | + && ui.input(|i| i.key_pressed(egui::Key::Enter)) | |
| 384 | + { | |
| 385 | + run_jq = true; | |
| 386 | + } | |
| 387 | + | |
| 388 | + // Navigate completions | |
| 389 | + if self.show_completions && !self.completions.is_empty() { | |
| 390 | + let down = ui.input(|i| i.key_pressed(egui::Key::ArrowDown)); | |
| 391 | + let up = ui.input(|i| i.key_pressed(egui::Key::ArrowUp)); | |
| 392 | + if down { | |
| 393 | + self.completion_index = | |
| 394 | + (self.completion_index + 1).min(self.completions.len() - 1); | |
| 395 | + } | |
| 396 | + if up { | |
| 397 | + self.completion_index = self.completion_index.saturating_sub(1); | |
| 398 | + } | |
| 399 | + if ui.input(|i| i.key_pressed(egui::Key::Enter)) { | |
| 400 | + accepted_completion = | |
| 401 | + Some(self.completions[self.completion_index].clone()); | |
| 402 | + } | |
| 403 | + } | |
| 404 | + } | |
| 405 | + }); | |
| 406 | + | |
| 407 | + // Apply completion | |
| 408 | + if let Some(comp) = accepted_completion { | |
| 409 | + self.apply_completion(&comp); | |
| 410 | + self.show_completions = false; | |
| 411 | + self.refocus_jq = true; | |
| 412 | + // Try to navigate to the completed path | |
| 413 | + if let Some(path) = jq_path_to_segments(&self.jq_input) { | |
| 414 | + self.path = path; | |
| 415 | + self.selection = 0; | |
| 416 | + self.jq_synced = true; | |
| 417 | + self.jq_result = None; | |
| 418 | + self.jq_error = None; | |
| 419 | + } | |
| 420 | + } | |
| 421 | + | |
| 422 | + // Show completion popup | |
| 423 | + if self.show_completions && !self.completions.is_empty() { | |
| 424 | + let mut clicked: Option<String> = None; | |
| 425 | + egui::Frame::new() | |
| 426 | + .fill(CatppuccinMocha::SURFACE0) | |
| 427 | + .inner_margin(6.0) | |
| 428 | + .corner_radius(4.0) | |
| 429 | + .stroke(egui::Stroke::new(1.0, CatppuccinMocha::SURFACE1)) | |
| 430 | + .show(ui, |ui| { | |
| 431 | + ui.set_max_height(200.0); | |
| 432 | + egui::ScrollArea::vertical() | |
| 433 | + .id_salt("browser_completions") | |
| 434 | + .show(ui, |ui| { | |
| 435 | + for (i, comp) in self.completions.iter().enumerate() { | |
| 436 | + let selected = i == self.completion_index; | |
| 437 | + let text_color = if selected { | |
| 438 | + CatppuccinMocha::BLUE | |
| 439 | + } else { | |
| 440 | + CatppuccinMocha::TEXT | |
| 441 | + }; | |
| 442 | + let bg = if selected { | |
| 443 | + CatppuccinMocha::SURFACE1 | |
| 444 | + } else { | |
| 445 | + CatppuccinMocha::SURFACE0 | |
| 446 | + }; | |
| 447 | + let r = ui.add( | |
| 448 | + egui::Label::new( | |
| 449 | + RichText::new(comp) | |
| 450 | + .color(text_color) | |
| 451 | + .family(egui::FontFamily::Monospace) | |
| 452 | + .background_color(bg), | |
| 453 | + ) | |
| 454 | + .sense(egui::Sense::click()), | |
| 455 | + ); | |
| 456 | + if selected { | |
| 457 | + r.scroll_to_me(Some(egui::Align::Center)); | |
| 458 | + } | |
| 459 | + if r.clicked() { | |
| 460 | + clicked = Some(comp.clone()); | |
| 461 | + } | |
| 462 | + } | |
| 463 | + }); | |
| 464 | + }); | |
| 465 | + if let Some(c) = clicked { | |
| 466 | + self.apply_completion(&c); | |
| 467 | + self.show_completions = false; | |
| 468 | + self.refocus_jq = true; | |
| 469 | + if let Some(path) = jq_path_to_segments(&self.jq_input) { | |
| 470 | + self.path = path; | |
| 471 | + self.selection = 0; | |
| 472 | + self.jq_synced = true; | |
| 473 | + self.jq_result = None; | |
| 474 | + self.jq_error = None; | |
| 475 | + } | |
| 476 | + } | |
| 477 | + } | |
| 478 | + | |
| 479 | + // Execute jq query on Enter | |
| 480 | + if run_jq { | |
| 481 | + self.show_completions = false; | |
| 482 | + // First try as navigation path | |
| 483 | + if let Some(path) = jq_path_to_segments(&self.jq_input) { | |
| 484 | + if resolve_path(root, &path).is_some() { | |
| 485 | + self.path = path; | |
| 486 | + self.selection = 0; | |
| 487 | + self.jq_synced = true; | |
| 488 | + self.jq_result = None; | |
| 489 | + self.jq_error = None; | |
| 490 | + self.scroll_to_selection = true; | |
| 491 | + return; | |
| 492 | + } | |
| 493 | + } | |
| 494 | + // Otherwise run as jq query | |
| 495 | + let result = JqEngine::execute(&self.jq_input, root); | |
| 496 | + if let Some(err) = &result.error { | |
| 497 | + self.jq_error = Some(err.clone()); | |
| 498 | + self.jq_result = None; | |
| 499 | + } else { | |
| 500 | + self.jq_error = None; | |
| 501 | + self.jq_result = Some(result.output.join("\n")); | |
| 502 | + } | |
| 503 | + } | |
| 504 | + | |
| 505 | + // Show jq error/result inline | |
| 506 | + if let Some(err) = &self.jq_error { | |
| 507 | + ui.label( | |
| 508 | + RichText::new(err) | |
| 509 | + .color(CatppuccinMocha::RED) | |
| 510 | + .family(egui::FontFamily::Monospace) | |
| 511 | + .small(), | |
| 512 | + ); | |
| 513 | + } | |
| 514 | + } | |
| 515 | + | |
| 516 | + fn draw_separator(ui: &mut Ui, height: f32) { | |
| 517 | + let sep_rect = ui.available_rect_before_wrap(); | |
| 518 | + ui.painter().line_segment( | |
| 519 | + [ | |
| 520 | + egui::pos2(sep_rect.left(), sep_rect.top()), | |
| 521 | + egui::pos2(sep_rect.left(), sep_rect.top() + height), | |
| 522 | + ], | |
| 523 | + egui::Stroke::new(1.0, CatppuccinMocha::SURFACE0), | |
| 524 | + ); | |
| 525 | + ui.add_space(4.0); | |
| 526 | + } | |
| 527 | + | |
| 528 | + fn render_parent_column( | |
| 529 | + &self, | |
| 530 | + ui: &mut Ui, | |
| 531 | + parent_entries: &Option<Vec<Entry>>, | |
| 532 | + height: f32, | |
| 533 | + ) { | |
| 534 | + let row_height = ui.text_style_height(&egui::TextStyle::Monospace) + 4.0; | |
| 535 | + | |
| 536 | + if let Some(entries) = parent_entries { | |
| 537 | + // Which entry in the parent corresponds to our current path segment? | |
| 538 | + let active_idx = self | |
| 539 | + .path | |
| 540 | + .last() | |
| 541 | + .map(|seg| match seg { | |
| 542 | + PathSegment::Key(k) => entries | |
| 543 | + .iter() | |
| 544 | + .position(|e| e.label == *k) | |
| 545 | + .unwrap_or(0), | |
| 546 | + PathSegment::Index(i) => *i, | |
| 547 | + }) | |
| 548 | + .unwrap_or(0); | |
| 549 | + | |
| 550 | + egui::ScrollArea::vertical() | |
| 551 | + .id_salt("browser_parent") | |
| 552 | + .auto_shrink(false) | |
| 553 | + .max_height(height) | |
| 554 | + .show_rows(ui, row_height, entries.len(), |ui, range| { | |
| 555 | + for i in range { | |
| 556 | + let entry = &entries[i]; | |
| 557 | + let is_active = i == active_idx; | |
| 558 | + let bg = if is_active { | |
| 559 | + CatppuccinMocha::SURFACE0 | |
| 560 | + } else { | |
| 561 | + egui::Color32::TRANSPARENT | |
| 562 | + }; | |
| 563 | + let text_color = if is_active { | |
| 564 | + CatppuccinMocha::TEXT | |
| 565 | + } else { | |
| 566 | + CatppuccinMocha::OVERLAY0 | |
| 567 | + }; | |
| 568 | + egui::Frame::new() | |
| 569 | + .fill(bg) | |
| 570 | + .corner_radius(4.0) | |
| 571 | + .inner_margin(egui::Margin::symmetric(6, 1)) | |
| 572 | + .show(ui, |ui| { | |
| 573 | + ui.horizontal(|ui| { | |
| 574 | + ui.label( | |
| 575 | + RichText::new(entry.type_icon) | |
| 576 | + .color(entry.color) | |
| 577 | + .family(egui::FontFamily::Monospace) | |
| 578 | + .size(12.0), | |
| 579 | + ); | |
| 580 | + ui.label( | |
| 581 | + RichText::new(&entry.label) | |
| 582 | + .color(text_color) | |
| 583 | + .family(egui::FontFamily::Monospace) | |
| 584 | + .size(12.0), | |
| 585 | + ); | |
| 586 | + }); | |
| 587 | + }); | |
| 588 | + } | |
| 589 | + }); | |
| 590 | + } else { | |
| 591 | + // At root — left column is blank (files are in the sidebar) | |
| 592 | + } | |
| 593 | + } | |
| 594 | + | |
| 595 | + fn render_current_column( | |
| 596 | + &mut self, | |
| 597 | + ui: &mut Ui, | |
| 598 | + entries: &[Entry], | |
| 599 | + current_value: &serde_json::Value, | |
| 600 | + height: f32, | |
| 601 | + ) -> (Option<usize>, Option<usize>) { | |
| 602 | + let mut clicked = None; | |
| 603 | + let mut dbl_clicked = None; | |
| 604 | + | |
| 605 | + if entries.is_empty() { | |
| 606 | + ui.centered_and_justified(|ui| { | |
| 607 | + ui.label( | |
| 608 | + RichText::new("(empty)") | |
| 609 | + .color(CatppuccinMocha::OVERLAY0) | |
| 610 | + .family(egui::FontFamily::Monospace), | |
| 611 | + ); | |
| 612 | + }); | |
| 613 | + return (None, None); | |
| 614 | + } | |
| 615 | + | |
| 616 | + let scroll_sel = self.scroll_to_selection; | |
| 617 | + self.scroll_to_selection = false; | |
| 618 | + let selection = self.selection; | |
| 619 | + | |
| 620 | + egui::ScrollArea::vertical() | |
| 621 | + .id_salt("browser_current") | |
| 622 | + .auto_shrink(false) | |
| 623 | + .max_height(height) | |
| 624 | + .show(ui, |ui| { | |
| 625 | + for i in 0..entries.len() { | |
| 626 | + let entry = &entries[i]; | |
| 627 | + let is_selected = i == selection; | |
| 628 | + | |
| 629 | + // Check hover from previous frame | |
| 630 | + let row_id = ui.id().with(("row", i)); | |
| 631 | + let is_hovered = ui.ctx().data(|d| d.get_temp::<bool>(row_id)).unwrap_or(false); | |
| 632 | + | |
| 633 | + let bg = if is_selected { | |
| 634 | + CatppuccinMocha::SURFACE0 | |
| 635 | + } else if is_hovered { | |
| 636 | + egui::Color32::from_rgba_unmultiplied( | |
| 637 | + entry.color.r(), entry.color.g(), entry.color.b(), 15, | |
| 638 | + ) | |
| 639 | + } else { | |
| 640 | + egui::Color32::TRANSPARENT | |
| 641 | + }; | |
| 642 | + | |
| 643 | + let font_size = if is_hovered && !is_selected { 12.5 } else { 12.0 }; | |
| 644 | + let type_size = if is_hovered && !is_selected { 11.5 } else { 11.0 }; | |
| 645 | + let preview_size = if is_hovered && !is_selected { 11.5 } else { 11.0 }; | |
| 646 | + | |
| 647 | + let mut copy_clicked = false; | |
| 648 | + | |
| 649 | + // Hit-test copy button from previous frame's stored rect | |
| 650 | + let copy_btn_id = ui.id().with(("copy_btn", i)); | |
| 651 | + if let Some(prev_rect) = ui.ctx().data(|d| d.get_temp::<egui::Rect>(copy_btn_id)) { | |
| 652 | + if ui.rect_contains_pointer(prev_rect) { | |
| 653 | + ui.painter().rect_filled(prev_rect, 3.0, CatppuccinMocha::SURFACE1); | |
| 654 | + ui.painter().text( | |
| 655 | + prev_rect.center(), | |
| 656 | + egui::Align2::CENTER_CENTER, | |
| 657 | + egui_phosphor::regular::COPY, | |
| 658 | + egui::FontId::proportional(12.0), | |
| 659 | + CatppuccinMocha::TEXT, | |
| 660 | + ); | |
| 661 | + egui::show_tooltip_at_pointer( | |
| 662 | + ui.ctx(), | |
| 663 | + ui.layer_id(), | |
| 664 | + ui.id().with(("copy_tip", i)), | |
| 665 | + |ui| { | |
| 666 | + ui.label("Copy value (c or Ctrl+C)"); | |
| 667 | + }, | |
| 668 | + ); | |
| 669 | + if ui.input(|inp| inp.pointer.any_click()) { | |
| 670 | + copy_clicked = true; | |
| 671 | + if let Some(v) = child_value(current_value, i, &entry.label) { | |
| 672 | + ui.ctx().copy_text(copy_value_str(v)); | |
| 673 | + } | |
| 674 | + } | |
| 675 | + } | |
| 676 | + } | |
| 677 | + | |
| 678 | + let r = egui::Frame::new() | |
| 679 | + .fill(bg) | |
| 680 | + .corner_radius(4.0) | |
| 681 | + .inner_margin(egui::Margin::symmetric(6, 1)) | |
| 682 | + .show(ui, |ui| { | |
| 683 | + ui.horizontal(|ui| { | |
| 684 | + // Copy icon — paint only, no space allocation | |
| 685 | + let icon_rect = egui::Rect::from_min_size( | |
| 686 | + ui.cursor().left_top(), | |
| 687 | + egui::vec2(16.0, ui.min_rect().height().max(14.0)), | |
| 688 | + ); | |
| 689 | + ui.painter().text( | |
| 690 | + icon_rect.center(), | |
| 691 | + egui::Align2::CENTER_CENTER, | |
| 692 | + egui_phosphor::regular::COPY, | |
| 693 | + egui::FontId::proportional(12.0), | |
| 694 | + CatppuccinMocha::OVERLAY0, | |
| 695 | + ); | |
| 696 | + ui.add_space(16.0); | |
| 697 | + ui.ctx().data_mut(|d| d.insert_temp(copy_btn_id, icon_rect)); | |
| 698 | + | |
| 699 | + // Type icon | |
| 700 | + ui.label( | |
| 701 | + RichText::new(entry.type_icon) | |
| 702 | + .color(entry.color) | |
| 703 | + .family(egui::FontFamily::Monospace) | |
| 704 | + .size(font_size), | |
| 705 | + ); | |
| 706 | + // Label (key or index) | |
| 707 | + let label_color = if is_selected || is_hovered { | |
| 708 | + entry.color | |
| 709 | + } else { | |
| 710 | + CatppuccinMocha::SUBTEXT0 | |
| 711 | + }; | |
| 712 | + ui.label( | |
| 713 | + RichText::new(&entry.label) | |
| 714 | + .color(label_color) | |
| 715 | + .family(egui::FontFamily::Monospace) | |
| 716 | + .size(font_size), | |
| 717 | + ); | |
| 718 | + // Type label | |
| 719 | + ui.label( | |
| 720 | + RichText::new(&entry.type_label) | |
| 721 | + .color(CatppuccinMocha::OVERLAY0) | |
| 722 | + .family(egui::FontFamily::Monospace) | |
| 723 | + .size(type_size), | |
| 724 | + ); | |
| 725 | + // Value preview (right-aligned) | |
| 726 | + ui.with_layout( | |
| 727 | + egui::Layout::right_to_left(egui::Align::Center), | |
| 728 | + |ui| { | |
| 729 | + let preview = if entry.preview.len() > 24 { | |
| 730 | + format!("{}...", &entry.preview[..21]) | |
| 731 | + } else { | |
| 732 | + entry.preview.clone() | |
| 733 | + }; | |
| 734 | + ui.label( | |
| 735 | + RichText::new(preview) | |
| 736 | + .color(entry.color) | |
| 737 | + .family(egui::FontFamily::Monospace) | |
| 738 | + .size(preview_size), | |
| 739 | + ); | |
| 740 | + }, | |
| 741 | + ); | |
| 742 | + }); | |
| 743 | + }); | |
| 744 | + | |
| 745 | + let response = r.response.interact(egui::Sense::click()); | |
| 746 | + ui.ctx().data_mut(|d| d.insert_temp(row_id, response.hovered())); | |
| 747 | + | |
| 748 | + if response.clicked() && !copy_clicked { | |
| 749 | + clicked = Some(i); | |
| 750 | + } | |
| 751 | + if response.double_clicked() && !copy_clicked { | |
| 752 | + dbl_clicked = Some(i); | |
| 753 | + } | |
| 754 | + | |
| 755 | + if is_selected && scroll_sel { | |
| 756 | + response.scroll_to_me(Some(egui::Align::Center)); | |
| 757 | + } | |
| 758 | + } | |
| 759 | + }); | |
| 760 | + | |
| 761 | + (clicked, dbl_clicked) | |
| 762 | + } | |
| 763 | + | |
| 764 | + fn render_preview_column( | |
| 765 | + &self, | |
| 766 | + ui: &mut Ui, | |
| 767 | + selected_child: Option<&serde_json::Value>, | |
| 768 | + entries: &[Entry], | |
| 769 | + height: f32, | |
| 770 | + ) { | |
| 771 | + // Clip content to column bounds | |
| 772 | + let clip = ui.available_rect_before_wrap(); | |
| 773 | + ui.set_clip_rect(clip); | |
| 774 | + let row_height = ui.text_style_height(&egui::TextStyle::Monospace) + 2.0; | |
| 775 | + | |
| 776 | + // If we have a jq result (non-synced query), show that instead | |
| 777 | + if !self.jq_synced { | |
| 778 | + if let Some(result) = &self.jq_result { | |
| 779 | + let lines: Vec<&str> = result.lines().collect(); | |
| 780 | + ui.label( | |
| 781 | + RichText::new(format!("{} jq result — {} lines", egui_phosphor::regular::FUNNEL, lines.len())) | |
| 782 | + .color(CatppuccinMocha::MAUVE) | |
| 783 | + .small(), | |
| 784 | + ); | |
| 785 | + ui.add_space(4.0); | |
| 786 | + egui::ScrollArea::vertical() | |
| 787 | + .id_salt("browser_jq_preview") | |
| 788 | + .auto_shrink(false) | |
| 789 | + .max_height(height - 24.0) | |
| 790 | + .show_rows(ui, row_height, lines.len(), |ui, range| { | |
| 791 | + for i in range { | |
| 792 | + let line = lines[i]; | |
| 793 | + let color = colorize_json_token(line.trim()); | |
| 794 | + ui.label( | |
| 795 | + RichText::new(line) | |
| 796 | + .color(color) | |
| 797 | + .family(egui::FontFamily::Monospace) | |
| 798 | + .size(12.0), | |
| 799 | + ); | |
| 800 | + } | |
| 801 | + }); | |
| 802 | + return; | |
| 803 | + } | |
| 804 | + } | |
| 805 | + | |
| 806 | + let Some(val) = selected_child else { | |
| 807 | + if entries.is_empty() { | |
| 808 | + ui.centered_and_justified(|ui| { | |
| 809 | + ui.label( | |
| 810 | + RichText::new("Nothing selected") | |
| 811 | + .color(CatppuccinMocha::OVERLAY0), | |
| 812 | + ); | |
| 813 | + }); | |
| 814 | + } | |
| 815 | + return; | |
| 816 | + }; | |
| 817 | + | |
| 818 | + match val { | |
| 819 | + serde_json::Value::Object(map) => { | |
| 820 | + ui.label( | |
| 821 | + RichText::new(format!( | |
| 822 | + "{} Object — {} fields", | |
| 823 | + egui_phosphor::regular::BRACKETS_CURLY, | |
| 824 | + map.len() | |
| 825 | + )) | |
| 826 | + .color(CatppuccinMocha::LAVENDER) | |
| 827 | + .small(), | |
| 828 | + ); | |
| 829 | + ui.add_space(4.0); | |
| 830 | + let keys: Vec<(&String, &serde_json::Value)> = map.iter().collect(); | |
| 831 | + egui::ScrollArea::vertical() | |
| 832 | + .id_salt("browser_preview") | |
| 833 | + .auto_shrink(false) | |
| 834 | + .max_height(height - 24.0) | |
| 835 | + .show_rows(ui, row_height, keys.len(), |ui, range| { | |
| 836 | + for i in range { | |
| 837 | + let (k, v) = keys[i]; | |
| 838 | + let (icon, color) = type_icon_color(v); | |
| 839 | + let preview = value_preview(v); | |
| 840 | + ui.horizontal(|ui| { | |
| 841 | + ui.label( | |
| 842 | + RichText::new(icon) | |
| 843 | + .color(color) | |
| 844 | + .family(egui::FontFamily::Monospace) | |
| 845 | + .size(11.0), | |
| 846 | + ); | |
| 847 | + ui.label( | |
| 848 | + RichText::new(k) | |
| 849 | + .color(CatppuccinMocha::SUBTEXT0) | |
| 850 | + .family(egui::FontFamily::Monospace) | |
| 851 | + .size(11.0), | |
| 852 | + ); | |
| 853 | + let p = if preview.len() > 30 { | |
| 854 | + format!("{}...", &preview[..27]) | |
| 855 | + } else { | |
| 856 | + preview | |
| 857 | + }; | |
| 858 | + ui.label( | |
| 859 | + RichText::new(p) | |
| 860 | + .color(color) | |
| 861 | + .family(egui::FontFamily::Monospace) | |
| 862 | + .size(11.0), | |
| 863 | + ); | |
| 864 | + }); | |
| 865 | + } | |
| 866 | + }); | |
| 867 | + } | |
| 868 | + serde_json::Value::Array(arr) => { | |
| 869 | + ui.label( | |
| 870 | + RichText::new(format!( | |
| 871 | + "{} Array — {} items", | |
| 872 | + egui_phosphor::regular::BRACKETS_SQUARE, | |
| 873 | + arr.len() | |
| 874 | + )) | |
| 875 | + .color(CatppuccinMocha::YELLOW) | |
| 876 | + .small(), | |
| 877 | + ); | |
| 878 | + ui.add_space(4.0); | |
| 879 | + let show_count = arr.len().min(200); | |
| 880 | + egui::ScrollArea::vertical() | |
| 881 | + .id_salt("browser_preview") | |
| 882 | + .auto_shrink(false) | |
| 883 | + .max_height(height - 24.0) | |
| 884 | + .show_rows(ui, row_height, show_count, |ui, range| { | |
| 885 | + for i in range { | |
| 886 | + let v = &arr[i]; | |
| 887 | + let (icon, color) = type_icon_color(v); | |
| 888 | + let preview = value_preview(v); | |
| 889 | + ui.horizontal(|ui| { | |
| 890 | + ui.label( | |
| 891 | + RichText::new(icon) | |
| 892 | + .color(color) | |
| 893 | + .family(egui::FontFamily::Monospace) | |
| 894 | + .size(11.0), | |
| 895 | + ); | |
| 896 | + ui.label( | |
| 897 | + RichText::new(format!("[{}]", i)) | |
| 898 | + .color(CatppuccinMocha::OVERLAY0) | |
| 899 | + .family(egui::FontFamily::Monospace) | |
| 900 | + .size(11.0), | |
| 901 | + ); | |
| 902 | + let p = if preview.len() > 30 { | |
| 903 | + format!("{}...", &preview[..27]) | |
| 904 | + } else { | |
| 905 | + preview | |
| 906 | + }; | |
| 907 | + ui.label( | |
| 908 | + RichText::new(p) | |
| 909 | + .color(color) | |
| 910 | + .family(egui::FontFamily::Monospace) | |
| 911 | + .size(11.0), | |
| 912 | + ); | |
| 913 | + }); | |
| 914 | + } | |
| 915 | + }); | |
| 916 | + } | |
| 917 | + _ => { | |
| 918 | + // Scalar value — show full content with smart detection | |
| 919 | + let color = match val { | |
| 920 | + serde_json::Value::String(_) => CatppuccinMocha::GREEN, | |
| 921 | + serde_json::Value::Number(_) => CatppuccinMocha::PEACH, | |
| 922 | + serde_json::Value::Bool(_) => CatppuccinMocha::MAUVE, | |
| 923 | + serde_json::Value::Null => CatppuccinMocha::OVERLAY0, | |
| 924 | + _ => CatppuccinMocha::TEXT, | |
| 925 | + }; | |
| 926 | + | |
| 927 | + // Image preview — full width, no raw text | |
| 928 | + if let serde_json::Value::String(s) = val { | |
| 929 | + if let Some(img) = detect_image_data(s) { | |
| 930 | + egui::ScrollArea::vertical() | |
| 931 | + .id_salt("browser_preview") | |
| 932 | + .auto_shrink(false) | |
| 933 | + .max_height(height) | |
| 934 | + .show(ui, |ui| { | |
| 935 | + render_image_previews(ui, &[(&String::new(), img)]); | |
| 936 | + ui.add_space(4.0); | |
| 937 | + if ui | |
| 938 | + .add( | |
| 939 | + egui::Button::new( | |
| 940 | + RichText::new(format!( | |
| 941 | + " {} Copy raw ", | |
| 942 | + egui_phosphor::regular::COPY | |
| 943 | + )) | |
| 944 | + .size(11.0), | |
| 945 | + ) | |
| 946 | + .fill(CatppuccinMocha::SURFACE0) | |
| 947 | + .corner_radius(4.0), | |
| 948 | + ) | |
| 949 | + .clicked() | |
| 950 | + { | |
| 951 | + ui.ctx().copy_text(s.clone()); | |
| 952 | + } | |
| 953 | + }); | |
| 954 | + return; | |
| 955 | + } | |
| 956 | + } | |
| 957 | + | |
| 958 | + // Datetime preview | |
| 959 | + let mut showed_smart = false; | |
| 960 | + | |
| 961 | + if let serde_json::Value::String(s) = val { | |
| 962 | + if let Some(tv) = detect_temporal(s) { | |
| 963 | + showed_smart = true; | |
| 964 | + render_datetime_widgets(ui, &[(&String::new(), tv)]); | |
| 965 | + } else if let Some(tz) = detect_timezone(s) { | |
| 966 | + showed_smart = true; | |
| 967 | + render_timezone_globe(ui, tz.offset_hours, &format!("{} ({})", tz.name, tz.display)); | |
| 968 | + } | |
| 969 | + } | |
| 970 | + if let serde_json::Value::Number(n) = val { | |
| 971 | + if let Some(i) = n.as_i64() { | |
| 972 | + if let Some(tv) = detect_unix_timestamp(i) { | |
| 973 | + showed_smart = true; | |
| 974 | + render_datetime_widgets(ui, &[(&String::new(), tv)]); | |
| 975 | + } | |
| 976 | + } | |
| 977 | + } | |
| 978 | + | |
| 979 | + if showed_smart { | |
| 980 | + ui.add_space(4.0); | |
| 981 | + let sep_rect = ui.available_rect_before_wrap(); | |
| 982 | + ui.painter().line_segment( | |
| 983 | + [ | |
| 984 | + egui::pos2(sep_rect.left(), sep_rect.top()), | |
| 985 | + egui::pos2(sep_rect.right(), sep_rect.top()), | |
| 986 | + ], | |
| 987 | + egui::Stroke::new(1.0, CatppuccinMocha::SURFACE0), | |
| 988 | + ); | |
| 989 | + ui.add_space(4.0); | |
| 990 | + } | |
| 991 | + | |
| 992 | + // Raw value text | |
| 993 | + let pretty = match val { | |
| 994 | + serde_json::Value::String(s) => s.clone(), | |
| 995 | + other => serde_json::to_string_pretty(other).unwrap_or_default(), | |
| 996 | + }; | |
| 997 | + let lines: Vec<&str> = pretty.lines().collect(); | |
| 998 | + egui::ScrollArea::vertical() | |
| 999 | + .id_salt("browser_preview") | |
| 1000 | + .auto_shrink(false) | |
| 1001 | + .max_height(height) | |
| 1002 | + .show_rows(ui, row_height, lines.len(), |ui, range| { | |
| 1003 | + for i in range { | |
| 1004 | + ui.label( | |
| 1005 | + RichText::new(lines[i]) | |
| 1006 | + .color(color) | |
| 1007 | + .family(egui::FontFamily::Monospace) | |
| 1008 | + .size(12.0), | |
| 1009 | + ); | |
| 1010 | + } | |
| 1011 | + }); | |
| 1012 | + } | |
| 1013 | + } | |
| 1014 | + } | |
| 1015 | + | |
| 1016 | + fn enter_selected(&mut self, current: &serde_json::Value, entries: &[Entry]) { | |
| 1017 | + if let Some(entry) = entries.get(self.selection) { | |
| 1018 | + if !entry.is_container { | |
| 1019 | + return; | |
| 1020 | + } | |
| 1021 | + // Don't enter empty containers | |
| 1022 | + let child = child_value(current, self.selection, &entry.label); | |
| 1023 | + let is_empty = child.map_or(true, |v| match v { | |
| 1024 | + serde_json::Value::Object(m) => m.is_empty(), | |
| 1025 | + serde_json::Value::Array(a) => a.is_empty(), | |
| 1026 | + _ => true, | |
| 1027 | + }); | |
| 1028 | + if is_empty { | |
| 1029 | + return; | |
| 1030 | + } | |
| 1031 | + match current { | |
| 1032 | + serde_json::Value::Object(_) => { | |
| 1033 | + self.path.push(PathSegment::Key(entry.label.clone())); | |
| 1034 | + } | |
| 1035 | + serde_json::Value::Array(_) => { | |
| 1036 | + self.path.push(PathSegment::Index(self.selection)); | |
| 1037 | + } | |
| 1038 | + _ => return, | |
| 1039 | + } | |
| 1040 | + self.selection = 0; | |
| 1041 | + self.scroll_to_selection = true; | |
| 1042 | + self.sync_jq_from_path(); | |
| 1043 | + } | |
| 1044 | + } | |
| 1045 | + | |
| 1046 | + fn go_up(&mut self) { | |
| 1047 | + if let Some(seg) = self.path.pop() { | |
| 1048 | + match seg { | |
| 1049 | + PathSegment::Index(i) => { | |
| 1050 | + self.selection = i; | |
| 1051 | + self.restore_key = None; | |
| 1052 | + } | |
| 1053 | + PathSegment::Key(k) => { | |
| 1054 | + self.selection = 0; | |
| 1055 | + self.restore_key = Some(k); | |
| 1056 | + } | |
| 1057 | + } | |
| 1058 | + self.scroll_to_selection = true; | |
| 1059 | + self.sync_jq_from_path(); | |
| 1060 | + } | |
| 1061 | + } | |
| 1062 | + | |
| 1063 | + fn sync_jq_from_path(&mut self) { | |
| 1064 | + if self.jq_synced { | |
| 1065 | + // Skip the first path segment (file name) for jq display | |
| 1066 | + let jq_path = if self.path.len() > 1 { | |
| 1067 | + &self.path[1..] | |
| 1068 | + } else { | |
| 1069 | + &[] | |
| 1070 | + }; | |
| 1071 | + self.jq_input = path_to_jq(jq_path); | |
| 1072 | + self.jq_result = None; | |
| 1073 | + self.jq_error = None; | |
| 1074 | + } | |
| 1075 | + } | |
| 1076 | + | |
| 1077 | + fn rebuild_completions(&mut self, root: &serde_json::Value) { | |
| 1078 | + let segment = extract_current_segment(&self.jq_input); | |
| 1079 | + let all_paths = collect_all_paths(root); | |
| 1080 | + | |
| 1081 | + let needle = segment.trim(); | |
| 1082 | + if needle.is_empty() || needle == "." { | |
| 1083 | + self.completions = all_paths; | |
| 1084 | + } else { | |
| 1085 | + let needle_lower = needle.to_lowercase(); | |
| 1086 | + let mut scored: Vec<(i64, String)> = all_paths | |
| 1087 | + .into_iter() | |
| 1088 | + .filter_map(|path| { | |
| 1089 | + let score = fuzzy_score(&needle_lower, &path.to_lowercase()); | |
| 1090 | + if score > 0 { | |
| 1091 | + Some((score, path)) | |
| 1092 | + } else { | |
| 1093 | + None | |
| 1094 | + } | |
| 1095 | + }) | |
| 1096 | + .collect(); | |
| 1097 | + scored.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1))); | |
| 1098 | + self.completions = scored.into_iter().map(|(_, p)| p).collect(); | |
| 1099 | + } | |
| 1100 | + self.completion_index = 0; | |
| 1101 | + self.show_completions = !self.completions.is_empty(); | |
| 1102 | + } | |
| 1103 | + | |
| 1104 | + fn apply_completion(&mut self, completion: &str) { | |
| 1105 | + let (before, _) = split_at_current_segment(&self.jq_input); | |
| 1106 | + self.jq_input = format!("{}{}", before, completion); | |
| 1107 | + } | |
| 1108 | +} | |
| 1109 | + | |
| 1110 | +// --- Helper functions --- | |
| 1111 | + | |
| 1112 | +fn resolve_path<'a>( | |
| 1113 | + root: &'a serde_json::Value, | |
| 1114 | + path: &[PathSegment], | |
| 1115 | +) -> Option<&'a serde_json::Value> { | |
| 1116 | + let mut current = root; | |
| 1117 | + for seg in path { | |
| 1118 | + match seg { | |
| 1119 | + PathSegment::Key(k) => { | |
| 1120 | + current = current.as_object()?.get(k)?; | |
| 1121 | + } | |
| 1122 | + PathSegment::Index(i) => { | |
| 1123 | + current = current.as_array()?.get(*i)?; | |
| 1124 | + } | |
| 1125 | + } | |
| 1126 | + } | |
| 1127 | + Some(current) | |
| 1128 | +} | |
| 1129 | + | |
| 1130 | +fn path_to_jq(path: &[PathSegment]) -> String { | |
| 1131 | + if path.is_empty() { | |
| 1132 | + return ".".to_string(); | |
| 1133 | + } | |
| 1134 | + let mut s = String::new(); | |
| 1135 | + for seg in path { | |
| 1136 | + match seg { | |
| 1137 | + PathSegment::Key(k) => { | |
| 1138 | + // Use bracket notation if key has special chars | |
| 1139 | + if k.chars() | |
| 1140 | + .all(|c| c.is_ascii_alphanumeric() || c == '_') | |
| 1141 | + && !k.is_empty() | |
| 1142 | + && !k.chars().next().unwrap().is_ascii_digit() | |
| 1143 | + { | |
| 1144 | + s.push('.'); | |
| 1145 | + s.push_str(k); | |
| 1146 | + } else { | |
| 1147 | + s.push_str(&format!(".\"{}\"", k)); | |
| 1148 | + } | |
| 1149 | + } | |
| 1150 | + PathSegment::Index(i) => { | |
| 1151 | + s.push_str(&format!("[{}]", i)); | |
| 1152 | + } | |
| 1153 | + } | |
| 1154 | + } | |
| 1155 | + s | |
| 1156 | +} | |
| 1157 | + | |
| 1158 | +/// Try to parse a jq path expression into path segments. | |
| 1159 | +/// Only handles simple dot-access and index patterns. | |
| 1160 | +fn jq_path_to_segments(expr: &str) -> Option<Vec<PathSegment>> { | |
| 1161 | + let expr = expr.trim(); | |
| 1162 | + if expr == "." { | |
| 1163 | + return Some(Vec::new()); | |
| 1164 | + } | |
| 1165 | + if !expr.starts_with('.') { | |
| 1166 | + return None; | |
| 1167 | + } | |
| 1168 | + | |
| 1169 | + let mut segments = Vec::new(); | |
| 1170 | + let mut rest = &expr[1..]; // skip leading dot | |
| 1171 | + | |
| 1172 | + while !rest.is_empty() { | |
| 1173 | + if rest.starts_with('[') { | |
| 1174 | + // Index access: [N] | |
| 1175 | + let end = rest.find(']')?; | |
| 1176 | + let idx_str = &rest[1..end]; | |
| 1177 | + let idx: usize = idx_str.parse().ok()?; | |
| 1178 | + segments.push(PathSegment::Index(idx)); | |
| 1179 | + rest = &rest[end + 1..]; | |
| 1180 | + } else if rest.starts_with('.') { | |
| 1181 | + rest = &rest[1..]; | |
| 1182 | + } else if rest.starts_with('"') { | |
| 1183 | + // Quoted key: ."key" | |
| 1184 | + let end = rest[1..].find('"')? + 1; | |
| 1185 | + let key = &rest[1..end]; | |
| 1186 | + segments.push(PathSegment::Key(key.to_string())); | |
| 1187 | + rest = &rest[end + 1..]; | |
| 1188 | + } else { | |
| 1189 | + // Simple key: letters until next . or [ | |
| 1190 | + let end = rest | |
| 1191 | + .find(|c: char| c == '.' || c == '[') | |
| 1192 | + .unwrap_or(rest.len()); | |
| 1193 | + let key = &rest[..end]; | |
| 1194 | + if key.is_empty() { | |
| 1195 | + return None; | |
| 1196 | + } | |
| 1197 | + segments.push(PathSegment::Key(key.to_string())); | |
| 1198 | + rest = &rest[end..]; | |
| 1199 | + } | |
| 1200 | + } | |
| 1201 | + | |
| 1202 | + Some(segments) | |
| 1203 | +} | |
| 1204 | + | |
| 1205 | +fn copy_value_str(v: &serde_json::Value) -> String { | |
| 1206 | + match v { | |
| 1207 | + serde_json::Value::String(s) => s.clone(), | |
| 1208 | + serde_json::Value::Null => "null".to_string(), | |
| 1209 | + other => serde_json::to_string(other).unwrap_or_default(), | |
| 1210 | + } | |
| 1211 | +} | |
| 1212 | + | |
| 1213 | +fn build_entries(value: &serde_json::Value) -> Vec<Entry> { | |
| 1214 | + match value { | |
| 1215 | + serde_json::Value::Object(map) => map | |
| 1216 | + .iter() | |
| 1217 | + .map(|(k, v)| { | |
| 1218 | + let (icon, color) = type_icon_color(v); | |
| 1219 | + Entry { | |
| 1220 | + label: k.clone(), | |
| 1221 | + type_icon: icon, | |
| 1222 | + type_label: type_label(v), | |
| 1223 | + preview: value_preview(v), | |
| 1224 | + color, | |
| 1225 | + is_container: v.is_object() || v.is_array(), | |
| 1226 | + } | |
| 1227 | + }) | |
| 1228 | + .collect(), | |
| 1229 | + serde_json::Value::Array(arr) => arr | |
| 1230 | + .iter() | |
| 1231 | + .enumerate() | |
| 1232 | + .map(|(i, v)| { | |
| 1233 | + let (icon, color) = type_icon_color(v); | |
| 1234 | + Entry { | |
| 1235 | + label: format!("[{}]", i), | |
| 1236 | + type_icon: icon, | |
| 1237 | + type_label: type_label(v), | |
| 1238 | + preview: value_preview(v), | |
| 1239 | + color, | |
| 1240 | + is_container: v.is_object() || v.is_array(), | |
| 1241 | + } | |
| 1242 | + }) | |
| 1243 | + .collect(), | |
| 1244 | + _ => Vec::new(), | |
| 1245 | + } | |
| 1246 | +} | |
| 1247 | + | |
| 1248 | +fn child_value<'a>( | |
| 1249 | + parent: &'a serde_json::Value, | |
| 1250 | + index: usize, | |
| 1251 | + label: &str, | |
| 1252 | +) -> Option<&'a serde_json::Value> { | |
| 1253 | + match parent { | |
| 1254 | + serde_json::Value::Object(map) => map.get(label), | |
| 1255 | + serde_json::Value::Array(arr) => arr.get(index), | |
| 1256 | + _ => None, | |
| 1257 | + } | |
| 1258 | +} | |
| 1259 | + | |
| 1260 | +fn type_icon_color(v: &serde_json::Value) -> (&'static str, egui::Color32) { | |
| 1261 | + match v { | |
| 1262 | + serde_json::Value::Object(_) => ("{}", CatppuccinMocha::LAVENDER), | |
| 1263 | + serde_json::Value::Array(_) => ("[]", CatppuccinMocha::YELLOW), | |
| 1264 | + serde_json::Value::String(s) => { | |
| 1265 | + if let Some(tv) = detect_temporal(s) { | |
| 1266 | + let icon = match tv { | |
| 1267 | + TemporalValue::NaiveDate(_) => "d", | |
| 1268 | + TemporalValue::NaiveTime(_) => "t", | |
| 1269 | + _ => "dt", | |
| 1270 | + }; | |
| 1271 | + (icon, CatppuccinMocha::BLUE) | |
| 1272 | + } else if detect_timezone(s).is_some() { | |
| 1273 | + ("tz", CatppuccinMocha::BLUE) | |
| 1274 | + } else { | |
| 1275 | + ("\"\"", CatppuccinMocha::GREEN) | |
| 1276 | + } | |
| 1277 | + } | |
| 1278 | + serde_json::Value::Number(_) => ("#", CatppuccinMocha::PEACH), | |
| 1279 | + serde_json::Value::Bool(_) => ("?", CatppuccinMocha::MAUVE), | |
| 1280 | + serde_json::Value::Null => ("~", CatppuccinMocha::OVERLAY0), | |
| 1281 | + } | |
| 1282 | +} | |
| 1283 | + | |
| 1284 | +fn type_label(v: &serde_json::Value) -> String { | |
| 1285 | + match v { | |
| 1286 | + serde_json::Value::Object(m) => format!("{} fields", m.len()), | |
| 1287 | + serde_json::Value::Array(a) => format!("{} items", a.len()), | |
| 1288 | + serde_json::Value::String(s) => { | |
| 1289 | + match detect_temporal(s) { | |
| 1290 | + Some(TemporalValue::NaiveDate(_)) => "date".to_string(), | |
| 1291 | + Some(TemporalValue::NaiveTime(_)) => "time".to_string(), | |
| 1292 | + Some(_) => "datetime".to_string(), | |
| 1293 | + None if detect_timezone(s).is_some() => "timezone".to_string(), | |
| 1294 | + None => "str".to_string(), | |
| 1295 | + } | |
| 1296 | + } | |
| 1297 | + serde_json::Value::Number(n) => { | |
| 1298 | + if n.is_f64() && !n.is_i64() { | |
| 1299 | + "f64".to_string() | |
| 1300 | + } else { | |
| 1301 | + "i64".to_string() | |
| 1302 | + } | |
| 1303 | + } | |
| 1304 | + serde_json::Value::Bool(_) => "bool".to_string(), | |
| 1305 | + serde_json::Value::Null => "null".to_string(), | |
| 1306 | + } | |
| 1307 | +} | |
| 1308 | + | |
| 1309 | +fn value_preview(v: &serde_json::Value) -> String { | |
| 1310 | + match v { | |
| 1311 | + serde_json::Value::Object(m) => { | |
| 1312 | + if m.is_empty() { | |
| 1313 | + "{}".to_string() | |
| 1314 | + } else { | |
| 1315 | + format!("{{{} fields}}", m.len()) | |
| 1316 | + } | |
| 1317 | + } | |
| 1318 | + serde_json::Value::Array(a) => { | |
| 1319 | + if a.is_empty() { | |
| 1320 | + "[]".to_string() | |
| 1321 | + } else { | |
| 1322 | + format!("[{} items]", a.len()) | |
| 1323 | + } | |
| 1324 | + } | |
| 1325 | + serde_json::Value::String(s) => { | |
| 1326 | + if s.starts_with("data:image/") { | |
| 1327 | + "image".to_string() | |
| 1328 | + } else if let Some(tv) = detect_temporal(s) { | |
| 1329 | + tv.display() | |
| 1330 | + } else if let Some(tz) = detect_timezone(s) { | |
| 1331 | + tz.display | |
| 1332 | + } else if s.len() > 60 { | |
| 1333 | + format!("\"{}...\"", &s[..57]) | |
| 1334 | + } else { | |
| 1335 | + format!("\"{}\"", s) | |
| 1336 | + } | |
| 1337 | + } | |
| 1338 | + serde_json::Value::Number(n) => n.to_string(), | |
| 1339 | + serde_json::Value::Bool(b) => b.to_string(), | |
| 1340 | + serde_json::Value::Null => "null".to_string(), | |
| 1341 | + } | |
| 1342 | +} | |
| 1343 | + | |
| 1344 | +fn colorize_json_token(s: &str) -> egui::Color32 { | |
| 1345 | + if s.starts_with('"') { | |
| 1346 | + if s.contains(':') { | |
| 1347 | + CatppuccinMocha::BLUE | |
| 1348 | + } else { | |
| 1349 | + CatppuccinMocha::GREEN | |
| 1350 | + } | |
| 1351 | + } else if s == "null" || s == "null," { | |
| 1352 | + CatppuccinMocha::OVERLAY0 | |
| 1353 | + } else if s == "true" || s == "true," || s == "false" || s == "false," { | |
| 1354 | + CatppuccinMocha::MAUVE | |
| 1355 | + } else if s.starts_with(|c: char| c.is_ascii_digit() || c == '-') { | |
| 1356 | + CatppuccinMocha::PEACH | |
| 1357 | + } else { | |
| 1358 | + CatppuccinMocha::TEXT | |
| 1359 | + } | |
| 1360 | +} | |
| 1361 | + | |
| 1362 | +// --- Reused from jq.rs --- | |
| 1363 | + | |
| 1364 | +fn collect_all_paths(root: &serde_json::Value) -> Vec<String> { | |
| 1365 | + let mut all_paths = Vec::new(); | |
| 1366 | + match root { | |
| 1367 | + serde_json::Value::Object(map) => { | |
| 1368 | + for (key, val) in map { | |
| 1369 | + let path = format!(".{}", key); | |
| 1370 | + all_paths.push(path.clone()); | |
| 1371 | + collect_deep_paths(val, &path, 6, &mut all_paths); | |
| 1372 | + } | |
| 1373 | + } | |
| 1374 | + serde_json::Value::Array(arr) => { | |
| 1375 | + all_paths.push(".[]".to_string()); | |
| 1376 | + if let Some(first) = arr.first() { | |
| 1377 | + if first.is_object() { | |
| 1378 | + collect_deep_paths(first, ".[]", 6, &mut all_paths); | |
| 1379 | + } | |
| 1380 | + } | |
| 1381 | + } | |
| 1382 | + _ => {} | |
| 1383 | + } | |
| 1384 | + all_paths.sort(); | |
| 1385 | + all_paths.dedup(); | |
| 1386 | + all_paths | |
| 1387 | +} | |
| 1388 | + | |
| 1389 | +fn collect_deep_paths( | |
| 1390 | + value: &serde_json::Value, | |
| 1391 | + prefix: &str, | |
| 1392 | + max_depth: usize, | |
| 1393 | + out: &mut Vec<String>, | |
| 1394 | +) { | |
| 1395 | + if max_depth == 0 { | |
| 1396 | + return; | |
| 1397 | + } | |
| 1398 | + match value { | |
| 1399 | + serde_json::Value::Object(map) => { | |
| 1400 | + for (key, val) in map { | |
| 1401 | + let path = format!("{}.{}", prefix, key); | |
| 1402 | + out.push(path.clone()); | |
| 1403 | + match val { | |
| 1404 | + serde_json::Value::Object(_) => { | |
| 1405 | + collect_deep_paths(val, &path, max_depth - 1, out); | |
| 1406 | + } | |
| 1407 | + serde_json::Value::Array(arr) => { | |
| 1408 | + let arr_path = format!("{}[]", path); | |
| 1409 | + out.push(arr_path.clone()); | |
| 1410 | + if let Some(first) = arr.first() { | |
| 1411 | + if first.is_object() { | |
| 1412 | + collect_deep_paths(first, &arr_path, max_depth - 1, out); | |
| 1413 | + } | |
| 1414 | + } | |
| 1415 | + } | |
| 1416 | + _ => {} | |
| 1417 | + } | |
| 1418 | + } | |
| 1419 | + } | |
| 1420 | + serde_json::Value::Array(arr) => { | |
| 1421 | + let arr_path = format!("{}[]", prefix); | |
| 1422 | + out.push(arr_path.clone()); | |
| 1423 | + if let Some(first) = arr.first() { | |
| 1424 | + if first.is_object() { | |
| 1425 | + collect_deep_paths(first, &arr_path, max_depth - 1, out); | |
| 1426 | + } | |
| 1427 | + } | |
| 1428 | + } | |
| 1429 | + _ => {} | |
| 1430 | + } | |
| 1431 | +} | |
| 1432 | + | |
| 1433 | +fn fuzzy_score(needle: &str, haystack: &str) -> i64 { | |
| 1434 | + let needle_chars: Vec<char> = needle.chars().collect(); | |
| 1435 | + let haystack_chars: Vec<char> = haystack.chars().collect(); | |
| 1436 | + if needle_chars.is_empty() { | |
| 1437 | + return 1; | |
| 1438 | + } | |
| 1439 | + if needle_chars.len() > haystack_chars.len() { | |
| 1440 | + return 0; | |
| 1441 | + } | |
| 1442 | + let mut score: i64 = 0; | |
| 1443 | + let mut ni = 0; | |
| 1444 | + let mut prev_match: Option<usize> = None; | |
| 1445 | + for (hi, &hc) in haystack_chars.iter().enumerate() { | |
| 1446 | + if ni < needle_chars.len() && hc == needle_chars[ni] { | |
| 1447 | + score += 1; | |
| 1448 | + if let Some(prev) = prev_match { | |
| 1449 | + if hi == prev + 1 { | |
| 1450 | + score += 2; | |
| 1451 | + } | |
| 1452 | + } | |
| 1453 | + if hi > 0 && matches!(haystack_chars[hi - 1], '.' | '[' | ']') { | |
| 1454 | + score += 1; | |
| 1455 | + } | |
| 1456 | + prev_match = Some(hi); | |
| 1457 | + ni += 1; | |
| 1458 | + } | |
| 1459 | + } | |
| 1460 | + if ni == needle_chars.len() { | |
| 1461 | + score | |
| 1462 | + } else { | |
| 1463 | + 0 | |
| 1464 | + } | |
| 1465 | +} | |
| 1466 | + | |
| 1467 | +fn extract_current_segment(query: &str) -> &str { | |
| 1468 | + let start = query | |
| 1469 | + .rfind(|c: char| c == '|' || c == '(' || c == ';') | |
| 1470 | + .map(|i| i + 1) | |
| 1471 | + .unwrap_or(0); | |
| 1472 | + query[start..].trim_start() | |
| 1473 | +} | |
| 1474 | + | |
| 1475 | +fn split_at_current_segment(query: &str) -> (&str, &str) { | |
| 1476 | + let start = query | |
| 1477 | + .rfind(|c: char| c == '|' || c == '(' || c == ';') | |
| 1478 | + .map(|i| i + 1) | |
| 1479 | + .unwrap_or(0); | |
| 1480 | + let segment_start = query[start..] | |
| 1481 | + .find(|c: char| !c.is_whitespace()) | |
| 1482 | + .map(|offset| start + offset) | |
| 1483 | + .unwrap_or(query.len()); | |
| 1484 | + (&query[..segment_start], &query[segment_start..]) | |
| 1485 | +} | |
| 1486 | + | |
| 1487 | +// --- Smart preview helpers --- | |
| 1488 | + | |
| 1489 | +enum ImageData { | |
| 1490 | + Base64 { data: Vec<u8> }, | |
| 1491 | + Url(String), | |
| 1492 | +} | |
| 1493 | + | |
| 1494 | +fn detect_image_data(s: &str) -> Option<ImageData> { | |
| 1495 | + use base64::Engine; | |
| 1496 | + | |
| 1497 | + // data:image/... base64 | |
| 1498 | + if s.starts_with("data:image/") && s.contains("base64,") { | |
| 1499 | + let b64 = s.split("base64,").nth(1)?; | |
| 1500 | + if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(b64) { | |
| 1501 | + return Some(ImageData::Base64 { data: bytes }); | |
| 1502 | + } | |
| 1503 | + } | |
| 1504 | + | |
| 1505 | + // Raw base64 with image magic bytes | |
| 1506 | + if s.len() > 20 && !s.contains(' ') && !s.contains('\n') { | |
| 1507 | + if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(s) { | |
| 1508 | + if is_image_magic(&bytes) { | |
| 1509 | + return Some(ImageData::Base64 { data: bytes }); | |
| 1510 | + } | |
| 1511 | + } | |
| 1512 | + } | |
| 1513 | + | |
| 1514 | + // URL pointing to an image | |
| 1515 | + let lower = s.to_lowercase(); | |
| 1516 | + if (lower.starts_with("http://") || lower.starts_with("https://")) | |
| 1517 | + && (lower.ends_with(".png") | |
| 1518 | + || lower.ends_with(".jpg") | |
| 1519 | + || lower.ends_with(".jpeg") | |
| 1520 | + || lower.ends_with(".gif") | |
| 1521 | + || lower.ends_with(".webp") | |
| 1522 | + || lower.ends_with(".svg") | |
| 1523 | + || lower.contains("/image") | |
| 1524 | + || lower.contains("imgur.com")) | |
| 1525 | + { | |
| 1526 | + return Some(ImageData::Url(s.to_string())); | |
| 1527 | + } | |
| 1528 | + | |
| 1529 | + None | |
| 1530 | +} | |
| 1531 | + | |
| 1532 | +fn is_image_magic(bytes: &[u8]) -> bool { | |
| 1533 | + if bytes.len() < 4 { | |
| 1534 | + return false; | |
| 1535 | + } | |
| 1536 | + // PNG | |
| 1537 | + if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) { | |
| 1538 | + return true; | |
| 1539 | + } | |
| 1540 | + // JPEG | |
| 1541 | + if bytes.starts_with(&[0xFF, 0xD8]) { | |
| 1542 | + return true; | |
| 1543 | + } | |
| 1544 | + // GIF | |
| 1545 | + if bytes.starts_with(b"GIF8") { | |
| 1546 | + return true; | |
| 1547 | + } | |
| 1548 | + // WEBP (RIFF....WEBP) | |
| 1549 | + if bytes.len() >= 12 && bytes.starts_with(b"RIFF") && &bytes[8..12] == b"WEBP" { | |
| 1550 | + return true; | |
| 1551 | + } | |
| 1552 | + false | |
| 1553 | +} | |
| 1554 | + | |
| 1555 | +fn render_datetime_widgets(ui: &mut Ui, fields: &[(&String, TemporalValue)]) -> f32 { | |
| 1556 | + let start_y = ui.cursor().top(); | |
| 1557 | + | |
| 1558 | + for (key, tv) in fields { | |
| 1559 | + ui.horizontal(|ui| { | |
| 1560 | + ui.label( | |
| 1561 | + RichText::new(egui_phosphor::regular::CALENDAR) | |
| 1562 | + .color(CatppuccinMocha::TEAL) | |
| 1563 | + .size(12.0), | |
| 1564 | + ); | |
| 1565 | + ui.label( | |
| 1566 | + RichText::new(*key) | |
| 1567 | + .color(CatppuccinMocha::SUBTEXT0) | |
| 1568 | + .family(egui::FontFamily::Monospace) | |
| 1569 | + .size(11.0), | |
| 1570 | + ); | |
| 1571 | + ui.label( | |
| 1572 | + RichText::new(tv.relative_time()) | |
| 1573 | + .color(CatppuccinMocha::OVERLAY1) | |
| 1574 | + .size(10.0), | |
| 1575 | + ); | |
| 1576 | + }); | |
| 1577 | + | |
| 1578 | + // Show formatted datetime | |
| 1579 | + ui.horizontal(|ui| { | |
| 1580 | + ui.add_space(20.0); | |
| 1581 | + ui.label( | |
| 1582 | + RichText::new(tv.display()) | |
| 1583 | + .color(CatppuccinMocha::TEAL) | |
| 1584 | + .family(egui::FontFamily::Monospace) | |
| 1585 | + .size(11.0), | |
| 1586 | + ); | |
| 1587 | + }); | |
| 1588 | + | |
| 1589 | + // Timezone globe if we have tz info | |
| 1590 | + if let Some(offset_h) = tv.utc_offset_hours() { | |
| 1591 | + let tz_label = tv.timezone_info().unwrap_or_default(); | |
| 1592 | + ui.add_space(2.0); | |
| 1593 | + render_timezone_globe(ui, offset_h, &tz_label); | |
| 1594 | + } | |
| 1595 | + | |
| 1596 | + // Mini calendar if we have a date | |
| 1597 | + if let Some(date) = tv.to_naive_date() { | |
| 1598 | + ui.add_space(2.0); | |
| 1599 | + render_mini_calendar(ui, date); | |
| 1600 | + } | |
| 1601 | + ui.add_space(4.0); | |
| 1602 | + } | |
| 1603 | + | |
| 1604 | + ui.cursor().top() - start_y | |
| 1605 | +} | |
| 1606 | + | |
| 1607 | +fn render_mini_calendar(ui: &mut Ui, date: chrono::NaiveDate) { | |
| 1608 | + use chrono::Datelike; | |
| 1609 | + | |
| 1610 | + let year = date.year(); | |
| 1611 | + let month = date.month(); | |
| 1612 | + let first_of_month = chrono::NaiveDate::from_ymd_opt(year, month, 1).unwrap(); | |
| 1613 | + let days_in_month = if month == 12 { | |
| 1614 | + chrono::NaiveDate::from_ymd_opt(year + 1, 1, 1) | |
| 1615 | + } else { | |
| 1616 | + chrono::NaiveDate::from_ymd_opt(year, month + 1, 1) | |
| 1617 | + } | |
| 1618 | + .unwrap() | |
| 1619 | + .signed_duration_since(first_of_month) | |
| 1620 | + .num_days() as u32; | |
| 1621 | + | |
| 1622 | + let start_weekday = first_of_month.weekday().num_days_from_monday(); // Mon=0 | |
| 1623 | + | |
| 1624 | + let month_name = match month { | |
| 1625 | + 1 => "Jan", | |
| 1626 | + 2 => "Feb", | |
| 1627 | + 3 => "Mar", | |
| 1628 | + 4 => "Apr", | |
| 1629 | + 5 => "May", | |
| 1630 | + 6 => "Jun", | |
| 1631 | + 7 => "Jul", | |
| 1632 | + 8 => "Aug", | |
| 1633 | + 9 => "Sep", | |
| 1634 | + 10 => "Oct", | |
| 1635 | + 11 => "Nov", | |
| 1636 | + 12 => "Dec", | |
| 1637 | + _ => "", | |
| 1638 | + }; | |
| 1639 | + | |
| 1640 | + let avail_w = ui.available_width(); | |
| 1641 | + let padding = 16.0; // inner_margin symmetric(8, 6) = 8 each side | |
| 1642 | + let grid_w = avail_w - padding; | |
| 1643 | + let cell_size = (grid_w / 7.0).floor(); | |
| 1644 | + let font_size = (cell_size * 0.5).clamp(9.0, 14.0); | |
| 1645 | + | |
| 1646 | + egui::Frame::new() | |
| 1647 | + .fill(CatppuccinMocha::SURFACE0) | |
| 1648 | + .corner_radius(6.0) | |
| 1649 | + .inner_margin(egui::Margin::symmetric(8, 6)) | |
| 1650 | + .show(ui, |ui| { | |
| 1651 | + ui.set_width(avail_w - padding); | |
| 1652 | + | |
| 1653 | + // Month/year header | |
| 1654 | + ui.horizontal(|ui| { | |
| 1655 | + ui.label( | |
| 1656 | + RichText::new(format!("{} {}", month_name, year)) | |
| 1657 | + .color(CatppuccinMocha::TEXT) | |
| 1658 | + .size(font_size + 1.0) | |
| 1659 | + .strong(), | |
| 1660 | + ); | |
| 1661 | + }); | |
| 1662 | + ui.add_space(2.0); | |
| 1663 | + | |
| 1664 | + // Day-of-week headers | |
| 1665 | + ui.horizontal(|ui| { | |
| 1666 | + ui.spacing_mut().item_spacing.x = 0.0; | |
| 1667 | + for day in &["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] { | |
| 1668 | + ui.allocate_ui(egui::vec2(cell_size, cell_size), |ui| { | |
| 1669 | + ui.centered_and_justified(|ui| { | |
| 1670 | + ui.label( | |
| 1671 | + RichText::new(*day) | |
| 1672 | + .color(CatppuccinMocha::OVERLAY0) | |
| 1673 | + .size(font_size), | |
| 1674 | + ); | |
| 1675 | + }); | |
| 1676 | + }); | |
| 1677 | + } | |
| 1678 | + }); | |
| 1679 | + | |
| 1680 | + // Day grid | |
| 1681 | + let mut day = 1u32; | |
| 1682 | + let mut cell = 0u32; | |
| 1683 | + let total_cells = start_weekday + days_in_month; | |
| 1684 | + let rows = (total_cells + 6) / 7; | |
| 1685 | + | |
| 1686 | + for _row in 0..rows { | |
| 1687 | + ui.horizontal(|ui| { | |
| 1688 | + ui.spacing_mut().item_spacing.x = 0.0; | |
| 1689 | + for _col in 0..7u32 { | |
| 1690 | + let in_month = cell >= start_weekday && day <= days_in_month; | |
| 1691 | + ui.allocate_ui(egui::vec2(cell_size, cell_size), |ui| { | |
| 1692 | + if in_month { | |
| 1693 | + let is_today = day == date.day(); | |
| 1694 | + let (bg, fg) = if is_today { | |
| 1695 | + (CatppuccinMocha::TEAL, CatppuccinMocha::CRUST) | |
| 1696 | + } else { | |
| 1697 | + (egui::Color32::TRANSPARENT, CatppuccinMocha::SUBTEXT0) | |
| 1698 | + }; | |
| 1699 | + if is_today { | |
| 1700 | + let rect = ui.available_rect_before_wrap(); | |
| 1701 | + let center = rect.center(); | |
| 1702 | + ui.painter() | |
| 1703 | + .circle_filled(center, cell_size * 0.4, bg); | |
| 1704 | + } | |
| 1705 | + ui.centered_and_justified(|ui| { | |
| 1706 | + ui.label( | |
| 1707 | + RichText::new(format!("{}", day)) | |
| 1708 | + .color(fg) | |
| 1709 | + .size(font_size), | |
| 1710 | + ); | |
| 1711 | + }); | |
| 1712 | + day += 1; | |
| 1713 | + } | |
| 1714 | + }); | |
| 1715 | + cell += 1; | |
| 1716 | + } | |
| 1717 | + }); | |
| 1718 | + } | |
| 1719 | + }); | |
| 1720 | +} | |
| 1721 | + | |
| 1722 | +fn render_timezone_globe(ui: &mut Ui, offset_hours: f32, tz_label: &str) { | |
| 1723 | + let avail_w = ui.available_width(); | |
| 1724 | + let padding = 16.0; | |
| 1725 | + let globe_w = avail_w - padding; | |
| 1726 | + let globe_h = globe_w * 0.5; // ellipse aspect | |
| 1727 | + | |
| 1728 | + egui::Frame::new() | |
| 1729 | + .fill(CatppuccinMocha::SURFACE0) | |
| 1730 | + .corner_radius(6.0) | |
| 1731 | + .inner_margin(egui::Margin::symmetric(8, 6)) | |
| 1732 | + .show(ui, |ui| { | |
| 1733 | + ui.set_width(globe_w); | |
| 1734 | + | |
| 1735 | + // Label: tz +05:30 | |
| 1736 | + ui.horizontal(|ui| { | |
| 1737 | + ui.label( | |
| 1738 | + RichText::new("tz") | |
| 1739 | + .color(CatppuccinMocha::BLUE) | |
| 1740 | + .size(11.0) | |
| 1741 | + .strong(), | |
| 1742 | + ); | |
| 1743 | + ui.label( | |
| 1744 | + RichText::new(tz_label) | |
| 1745 | + .color(CatppuccinMocha::TEXT) | |
| 1746 | + .family(egui::FontFamily::Monospace) | |
| 1747 | + .size(11.0), | |
| 1748 | + ); | |
| 1749 | + }); | |
| 1750 | + | |
| 1751 | + ui.add_space(2.0); | |
| 1752 | + | |
| 1753 | + // Allocate space for globe | |
| 1754 | + let (rect, _) = ui.allocate_exact_size( | |
| 1755 | + egui::vec2(globe_w, globe_h), | |
| 1756 | + egui::Sense::hover(), | |
| 1757 | + ); | |
| 1758 | + let painter = ui.painter_at(rect); | |
| 1759 | + let center = rect.center(); | |
| 1760 | + let rx = globe_w * 0.45; // horizontal radius | |
| 1761 | + let ry = globe_h * 0.45; // vertical radius | |
| 1762 | + | |
| 1763 | + // Globe outline | |
| 1764 | + let outline_color = CatppuccinMocha::SURFACE2; | |
| 1765 | + let n_pts = 64; | |
| 1766 | + let ellipse_pts: Vec<egui::Pos2> = (0..=n_pts) | |
| 1767 | + .map(|i| { | |
| 1768 | + let angle = std::f32::consts::TAU * i as f32 / n_pts as f32; | |
| 1769 | + egui::pos2( | |
| 1770 | + center.x + rx * angle.cos(), | |
| 1771 | + center.y + ry * angle.sin(), | |
| 1772 | + ) | |
| 1773 | + }) | |
| 1774 | + .collect(); | |
| 1775 | + | |
| 1776 | + // Timezone band: offset_hours maps to longitude fraction | |
| 1777 | + // UTC=0 is center, -12 is left edge, +12 is right edge | |
| 1778 | + // Band is ~1 hour wide = 1/24 of the full width | |
| 1779 | + let frac = offset_hours / 24.0; // -0.5 to 0.5 | |
| 1780 | + let band_center_x = center.x + frac * 2.0 * rx; | |
| 1781 | + let band_half_w = rx / 12.0; // 1 hour = 1/24 of diameter = 1/12 of radius | |
| 1782 | + | |
| 1783 | + // Draw the timezone band (clipped to ellipse via vertical strips) | |
| 1784 | + let band_left = band_center_x - band_half_w; | |
| 1785 | + let band_right = band_center_x + band_half_w; | |
| 1786 | + let steps = 20; | |
| 1787 | + for i in 0..steps { | |
| 1788 | + let x0 = band_left + (band_right - band_left) * i as f32 / steps as f32; | |
| 1789 | + let x1 = band_left + (band_right - band_left) * (i + 1) as f32 / steps as f32; | |
| 1790 | + // Compute ellipse y at these x positions | |
| 1791 | + let dx0 = (x0 - center.x) / rx; | |
| 1792 | + let dx1 = (x1 - center.x) / rx; | |
| 1793 | + if dx0.abs() > 1.0 || dx1.abs() > 1.0 { | |
| 1794 | + continue; | |
| 1795 | + } | |
| 1796 | + let y_half_0 = ry * (1.0 - dx0 * dx0).sqrt(); | |
| 1797 | + let y_half_1 = ry * (1.0 - dx1 * dx1).sqrt(); | |
| 1798 | + let points = vec![ | |
| 1799 | + egui::pos2(x0, center.y - y_half_0), | |
| 1800 | + egui::pos2(x1, center.y - y_half_1), | |
| 1801 | + egui::pos2(x1, center.y + y_half_1), | |
| 1802 | + egui::pos2(x0, center.y + y_half_0), | |
| 1803 | + ]; | |
| 1804 | + painter.add(egui::Shape::convex_polygon( | |
| 1805 | + points, | |
| 1806 | + egui::Color32::from_rgba_unmultiplied(137, 180, 250, 40), | |
| 1807 | + egui::Stroke::NONE, | |
| 1808 | + )); | |
| 1809 | + } | |
| 1810 | + | |
| 1811 | + // Longitude lines (every 3 hours = 8 lines) | |
| 1812 | + for i in -4..=4 { | |
| 1813 | + let f = i as f32 / 12.0; // fraction of full width | |
| 1814 | + let x = center.x + f * 2.0 * rx; | |
| 1815 | + let dx = (x - center.x) / rx; | |
| 1816 | + if dx.abs() >= 1.0 { | |
| 1817 | + continue; | |
| 1818 | + } | |
| 1819 | + let y_half = ry * (1.0 - dx * dx).sqrt(); | |
| 1820 | + let color = if i == 0 { | |
| 1821 | + CatppuccinMocha::OVERLAY0 // prime meridian slightly brighter | |
| 1822 | + } else { | |
| 1823 | + egui::Color32::from_rgba_unmultiplied(108, 112, 134, 80) | |
| 1824 | + }; | |
| 1825 | + painter.line_segment( | |
| 1826 | + [ | |
| 1827 | + egui::pos2(x, center.y - y_half), | |
| 1828 | + egui::pos2(x, center.y + y_half), | |
| 1829 | + ], | |
| 1830 | + egui::Stroke::new(1.0, color), | |
| 1831 | + ); | |
| 1832 | + } | |
| 1833 | + | |
| 1834 | + // Latitude lines (equator + 2 tropics) | |
| 1835 | + for f in [-0.4_f32, 0.0, 0.4] { | |
| 1836 | + let y = center.y + f * ry; | |
| 1837 | + // Compute x range at this y | |
| 1838 | + let dy = f.abs(); | |
| 1839 | + let x_half = rx * (1.0 - dy * dy).sqrt(); | |
| 1840 | + painter.line_segment( | |
| 1841 | + [ | |
| 1842 | + egui::pos2(center.x - x_half, y), | |
| 1843 | + egui::pos2(center.x + x_half, y), | |
| 1844 | + ], | |
| 1845 | + egui::Stroke::new( | |
| 1846 | + if f == 0.0 { 1.0 } else { 0.5 }, | |
| 1847 | + if f == 0.0 { CatppuccinMocha::OVERLAY0 } else { egui::Color32::from_rgba_unmultiplied(108, 112, 134, 80) }, | |
| 1848 | + ), | |
| 1849 | + ); | |
| 1850 | + } | |
| 1851 | + | |
| 1852 | + // Globe outline on top | |
| 1853 | + painter.add(egui::Shape::closed_line( | |
| 1854 | + ellipse_pts, | |
| 1855 | + egui::Stroke::new(1.5, outline_color), | |
| 1856 | + )); | |
| 1857 | + | |
| 1858 | + // Timezone center marker | |
| 1859 | + let dx_m = (band_center_x - center.x) / rx; | |
| 1860 | + if dx_m.abs() < 1.0 { | |
| 1861 | + painter.circle_filled( | |
| 1862 | + egui::pos2(band_center_x, center.y), | |
| 1863 | + 3.0, | |
| 1864 | + CatppuccinMocha::BLUE, | |
| 1865 | + ); | |
| 1866 | + } | |
| 1867 | + }); | |
| 1868 | +} | |
| 1869 | + | |
| 1870 | +fn render_image_previews(ui: &mut Ui, fields: &[(&String, ImageData)]) -> f32 { | |
| 1871 | + let start_y = ui.cursor().top(); | |
| 1872 | + let max_width = ui.available_width(); | |
| 1873 | + | |
| 1874 | + for (key, img) in fields { | |
| 1875 | + if !key.is_empty() { | |
| 1876 | + ui.horizontal(|ui| { | |
| 1877 | + ui.label( | |
| 1878 | + RichText::new(egui_phosphor::regular::IMAGE) | |
| 1879 | + .color(CatppuccinMocha::PINK) | |
| 1880 | + .size(12.0), | |
| 1881 | + ); | |
| 1882 | + ui.label( | |
| 1883 | + RichText::new(*key) | |
| 1884 | + .color(CatppuccinMocha::SUBTEXT0) | |
| 1885 | + .family(egui::FontFamily::Monospace) | |
| 1886 | + .size(11.0), | |
| 1887 | + ); | |
| 1888 | + }); | |
| 1889 | + } | |
| 1890 | + | |
| 1891 | + match img { | |
| 1892 | + ImageData::Base64 { data } => { | |
| 1893 | + // Use a stable ID based on first bytes to cache the texture | |
| 1894 | + let hash = data.len() as u64 | |
| 1895 | + ^ data.iter().take(32).enumerate().fold(0u64, |acc, (i, b)| { | |
| 1896 | + acc ^ ((*b as u64) << (i % 8 * 8)) | |
| 1897 | + }); | |
| 1898 | + let tex_id = egui::Id::new(("img_preview", hash)); | |
| 1899 | + | |
| 1900 | + let texture: Option<egui::TextureHandle> = | |
| 1901 | + ui.ctx().data(|d| d.get_temp(tex_id)); | |
| 1902 | + | |
| 1903 | + let texture = if let Some(t) = texture { | |
| 1904 | + t | |
| 1905 | + } else if let Ok(dyn_img) = image::load_from_memory(data) { | |
| 1906 | + let rgba = dyn_img.to_rgba8(); | |
| 1907 | + let size = [rgba.width() as usize, rgba.height() as usize]; | |
| 1908 | + let color_image = | |
| 1909 | + egui::ColorImage::from_rgba_unmultiplied(size, rgba.as_raw()); | |
| 1910 | + let t = ui.ctx().load_texture( | |
| 1911 | + format!("img_{}", hash), | |
| 1912 | + color_image, | |
| 1913 | + egui::TextureOptions::LINEAR, | |
| 1914 | + ); | |
| 1915 | + ui.ctx().data_mut(|d| d.insert_temp(tex_id, t.clone())); | |
| 1916 | + t | |
| 1917 | + } else { | |
| 1918 | + ui.label( | |
| 1919 | + RichText::new("Failed to decode image") | |
| 1920 | + .color(CatppuccinMocha::RED) | |
| 1921 | + .size(11.0), | |
| 1922 | + ); | |
| 1923 | + continue; | |
| 1924 | + }; | |
| 1925 | + | |
| 1926 | + let tex_size = texture.size_vec2(); | |
| 1927 | + let scale = (max_width / tex_size.x).min(1.0); | |
| 1928 | + let display_size = tex_size * scale; | |
| 1929 | + ui.add(egui::Image::new(&texture).fit_to_exact_size(display_size)); | |
| 1930 | + } | |
| 1931 | + ImageData::Url(url) => { | |
| 1932 | + ui.add( | |
| 1933 | + egui::Image::from_uri(url) | |
| 1934 | + .max_width(max_width) | |
| 1935 | + .maintain_aspect_ratio(true) | |
| 1936 | + .corner_radius(4.0), | |
| 1937 | + ); | |
| 1938 | + } | |
| 1939 | + } | |
| 1940 | + | |
| 1941 | + ui.add_space(4.0); | |
| 1942 | + } | |
| 1943 | + | |
| 1944 | + ui.cursor().top() - start_y | |
| 1945 | +} |
+2228-0src/views/code.rs
| @@ -0,0 +1,2228 @@ | ||
| 1 | +use std::collections::{BTreeMap, BTreeSet}; | |
| 2 | + | |
| 3 | +use egui::{self, RichText, Ui}; | |
| 4 | + | |
| 5 | +use crate::codegen::CodeGenerator; | |
| 6 | +use crate::theme::CatppuccinMocha; | |
| 7 | + | |
| 8 | +struct StructBlock { | |
| 9 | + name: String, | |
| 10 | + start_line: usize, | |
| 11 | + end_line: usize, | |
| 12 | + text: String, | |
| 13 | +} | |
| 14 | + | |
| 15 | +/// A generated .rs file | |
| 16 | +struct GeneratedFile { | |
| 17 | + name: String, | |
| 18 | + code: String, | |
| 19 | + lines: Vec<String>, | |
| 20 | + structs: Vec<StructBlock>, | |
| 21 | + is_group: bool, | |
| 22 | +} | |
| 23 | + | |
| 24 | +/// State for the enum conversion popup | |
| 25 | +struct EnumPopup { | |
| 26 | + open: bool, | |
| 27 | + field_name: String, | |
| 28 | + enum_name: String, | |
| 29 | + values: Vec<String>, | |
| 30 | + // Existing enum to tag with (None = create new) | |
| 31 | + selected_existing: Option<String>, | |
| 32 | + // Similar existing enums sorted by similarity | |
| 33 | + similar_enums: Vec<(String, Vec<String>, f32)>, // (name, variants, similarity 0..1) | |
| 34 | + // Structs that have this field, with checkbox state | |
| 35 | + affected_structs: Vec<(String, bool)>, // (struct_name, checked) | |
| 36 | + // Preview of merged variants (when tagging existing) | |
| 37 | + preview_variants: Vec<String>, | |
| 38 | +} | |
| 39 | + | |
| 40 | +impl EnumPopup { | |
| 41 | + fn new() -> Self { | |
| 42 | + Self { | |
| 43 | + open: false, | |
| 44 | + field_name: String::new(), | |
| 45 | + enum_name: String::new(), | |
| 46 | + values: Vec::new(), | |
| 47 | + selected_existing: None, | |
| 48 | + similar_enums: Vec::new(), | |
| 49 | + affected_structs: Vec::new(), | |
| 50 | + preview_variants: Vec::new(), | |
| 51 | + } | |
| 52 | + } | |
| 53 | +} | |
| 54 | + | |
| 55 | +pub struct CodeView { | |
| 56 | + files: Vec<GeneratedFile>, | |
| 57 | + selected_file: usize, | |
| 58 | + cache_key: u64, | |
| 59 | + // Search | |
| 60 | + search_query: String, | |
| 61 | + search_matches: Vec<(usize, usize)>, // (line_idx, col_offset) | |
| 62 | + current_match: usize, | |
| 63 | + scroll_to_match: Option<usize>, // line to scroll to | |
| 64 | + // Navigation: struct_name -> (file_index, line) | |
| 65 | + struct_index: BTreeMap<String, (usize, usize)>, | |
| 66 | + // Pending navigation from type click | |
| 67 | + nav_target: Option<(usize, usize)>, // (file_index, line) | |
| 68 | + // Sample string values per field name (for tooltips) | |
| 69 | + field_values: BTreeMap<String, Vec<String>>, | |
| 70 | + // Enum candidates: field names where values look like a finite set | |
| 71 | + enum_candidates: BTreeSet<String>, | |
| 72 | + // User-converted enums: field_name -> (enum_name, variants) | |
| 73 | + converted_enums: BTreeMap<String, (String, Vec<String>)>, | |
| 74 | + // Hidden fields: "StructName.field_name" set | |
| 75 | + hidden_fields: BTreeSet<String>, | |
| 76 | + // Enum popup state | |
| 77 | + enum_popup: EnumPopup, | |
| 78 | + // Selected code language | |
| 79 | + selected_language: crate::lang::CodeLanguage, | |
| 80 | +} | |
| 81 | + | |
| 82 | +impl CodeView { | |
| 83 | + pub fn new() -> Self { | |
| 84 | + Self { | |
| 85 | + files: Vec::new(), | |
| 86 | + selected_file: 0, | |
| 87 | + cache_key: 0, | |
| 88 | + search_query: String::new(), | |
| 89 | + search_matches: Vec::new(), | |
| 90 | + current_match: 0, | |
| 91 | + scroll_to_match: None, | |
| 92 | + struct_index: BTreeMap::new(), | |
| 93 | + nav_target: None, | |
| 94 | + field_values: BTreeMap::new(), | |
| 95 | + enum_candidates: BTreeSet::new(), | |
| 96 | + converted_enums: BTreeMap::new(), | |
| 97 | + hidden_fields: BTreeSet::new(), | |
| 98 | + enum_popup: EnumPopup::new(), | |
| 99 | + selected_language: crate::lang::CodeLanguage::Rust, | |
| 100 | + } | |
| 101 | + } | |
| 102 | + | |
| 103 | + pub fn invalidate(&mut self) { | |
| 104 | + self.cache_key = 0; | |
| 105 | + } | |
| 106 | + | |
| 107 | + /// Get list of generated file names and group flags for sidebar | |
| 108 | + pub fn file_names(&self) -> Vec<(&str, bool)> { | |
| 109 | + self.files.iter().map(|f| (f.name.as_str(), f.is_group)).collect() | |
| 110 | + } | |
| 111 | + | |
| 112 | + pub fn selected_file_index(&self) -> usize { | |
| 113 | + self.selected_file | |
| 114 | + } | |
| 115 | + | |
| 116 | + pub fn select_file(&mut self, idx: usize) { | |
| 117 | + if idx < self.files.len() { | |
| 118 | + self.selected_file = idx; | |
| 119 | + } | |
| 120 | + } | |
| 121 | + | |
| 122 | + pub fn selected_language(&self) -> crate::lang::CodeLanguage { | |
| 123 | + self.selected_language | |
| 124 | + } | |
| 125 | + | |
| 126 | + pub fn set_language(&mut self, lang: crate::lang::CodeLanguage) { | |
| 127 | + if self.selected_language != lang { | |
| 128 | + self.selected_language = lang; | |
| 129 | + self.cache_key = 0; // force rebuild | |
| 130 | + } | |
| 131 | + } | |
| 132 | + | |
| 133 | + /// Generate code: per-file .rs + shared.rs | |
| 134 | + pub fn show( | |
| 135 | + &mut self, | |
| 136 | + ui: &mut Ui, | |
| 137 | + parsed_files: &[(String, serde_json::Value)], | |
| 138 | + schema: Option<&crate::schema::SchemaOverview>, | |
| 139 | + enum_conversions: &mut Vec<crate::session::EnumConversion>, | |
| 140 | + hidden_fields: &mut Vec<String>, | |
| 141 | + ) { | |
| 142 | + // Rebuild converted_enums from session state | |
| 143 | + self.converted_enums.clear(); | |
| 144 | + for ec in enum_conversions.iter() { | |
| 145 | + self.converted_enums.insert( | |
| 146 | + ec.field_name.clone(), | |
| 147 | + (ec.enum_name.clone(), ec.variants.clone()), | |
| 148 | + ); | |
| 149 | + } | |
| 150 | + // Rebuild hidden_fields from session state | |
| 151 | + self.hidden_fields = hidden_fields.iter().cloned().collect(); | |
| 152 | + | |
| 153 | + let enum_count = enum_conversions.len(); | |
| 154 | + let hidden_count = hidden_fields.len(); | |
| 155 | + let key = { | |
| 156 | + use std::hash::{Hash, Hasher}; | |
| 157 | + let mut h = std::collections::hash_map::DefaultHasher::new(); | |
| 158 | + parsed_files.len().hash(&mut h); | |
| 159 | + for (name, _) in parsed_files { | |
| 160 | + name.hash(&mut h); | |
| 161 | + } | |
| 162 | + enum_count.hash(&mut h); | |
| 163 | + hidden_count.hash(&mut h); | |
| 164 | + (self.selected_language as u64).hash(&mut h); | |
| 165 | + h.finish() | |
| 166 | + }; | |
| 167 | + if self.cache_key != key { | |
| 168 | + self.rebuild_file_mode(parsed_files, schema); | |
| 169 | + self.build_struct_index(); | |
| 170 | + self.cache_key = key; | |
| 171 | + } | |
| 172 | + // Handle pending navigation from type click | |
| 173 | + if let Some((file_idx, line)) = self.nav_target.take() { | |
| 174 | + self.selected_file = file_idx; | |
| 175 | + // Scroll a couple lines above so the struct has breathing room | |
| 176 | + self.scroll_to_match = Some(line.saturating_sub(2)); | |
| 177 | + } | |
| 178 | + let prev_enums = self.converted_enums.clone(); | |
| 179 | + let prev_hidden_count = self.hidden_fields.len(); | |
| 180 | + | |
| 181 | + // Internal layout: file sidebar + code content | |
| 182 | + let avail = ui.available_rect_before_wrap(); | |
| 183 | + let sidebar_w = 180.0; | |
| 184 | + | |
| 185 | + ui.horizontal(|ui| { | |
| 186 | + ui.set_height(avail.height()); | |
| 187 | + | |
| 188 | + // -- Internal file sidebar -- | |
| 189 | + ui.vertical(|ui| { | |
| 190 | + ui.set_width(sidebar_w); | |
| 191 | + ui.set_height(avail.height()); | |
| 192 | + | |
| 193 | + // Language toggle | |
| 194 | + ui.horizontal(|ui| { | |
| 195 | + ui.spacing_mut().item_spacing.x = 4.0; | |
| 196 | + use egui_phosphor::regular; | |
| 197 | + for (lang_option, icon) in [ | |
| 198 | + (crate::lang::CodeLanguage::Rust, regular::FILE_RS), | |
| 199 | + (crate::lang::CodeLanguage::Swift, regular::BIRD), | |
| 200 | + ] { | |
| 201 | + let is_selected = self.selected_language == lang_option; | |
| 202 | + let bg = if is_selected { | |
| 203 | + crate::theme::CatppuccinMocha::SURFACE0 | |
| 204 | + } else { | |
| 205 | + egui::Color32::TRANSPARENT | |
| 206 | + }; | |
| 207 | + let text_color = if is_selected { | |
| 208 | + crate::theme::CatppuccinMocha::BLUE | |
| 209 | + } else { | |
| 210 | + crate::theme::CatppuccinMocha::OVERLAY0 | |
| 211 | + }; | |
| 212 | + let btn = egui::Frame::new() | |
| 213 | + .fill(bg) | |
| 214 | + .corner_radius(6.0) | |
| 215 | + .inner_margin(egui::Margin::symmetric(6, 3)) | |
| 216 | + .show(ui, |ui| { | |
| 217 | + ui.add( | |
| 218 | + egui::Label::new( | |
| 219 | + RichText::new(format!("{} {}", icon, lang_option.display_name())) | |
| 220 | + .color(text_color) | |
| 221 | + .size(11.0), | |
| 222 | + ) | |
| 223 | + .sense(egui::Sense::click()), | |
| 224 | + ) | |
| 225 | + }); | |
| 226 | + if btn.inner.clicked() { | |
| 227 | + self.selected_language = lang_option; | |
| 228 | + self.cache_key = 0; | |
| 229 | + } | |
| 230 | + } | |
| 231 | + }); | |
| 232 | + ui.add_space(4.0); | |
| 233 | + | |
| 234 | + // File list | |
| 235 | + let mut clicked_file = None; | |
| 236 | + egui::ScrollArea::vertical() | |
| 237 | + .id_salt("code_file_list") | |
| 238 | + .auto_shrink(false) | |
| 239 | + .max_height(avail.height() - 30.0) | |
| 240 | + .show(ui, |ui| { | |
| 241 | + let mut prev_was_group = false; | |
| 242 | + for (idx, file) in self.files.iter().enumerate() { | |
| 243 | + if file.is_group && !prev_was_group { | |
| 244 | + ui.add_space(4.0); | |
| 245 | + ui.separator(); | |
| 246 | + ui.add_space(2.0); | |
| 247 | + } | |
| 248 | + prev_was_group = file.is_group; | |
| 249 | + | |
| 250 | + let is_selected = idx == self.selected_file; | |
| 251 | + let row_id = ui.id().with(("code_file", idx)); | |
| 252 | + let is_hovered = ui.ctx().data(|d| d.get_temp::<bool>(row_id)).unwrap_or(false); | |
| 253 | + | |
| 254 | + let bg = if is_selected { | |
| 255 | + crate::theme::CatppuccinMocha::SURFACE0 | |
| 256 | + } else if is_hovered { | |
| 257 | + egui::Color32::from_rgba_premultiplied(40, 40, 55, 255) | |
| 258 | + } else { | |
| 259 | + egui::Color32::TRANSPARENT | |
| 260 | + }; | |
| 261 | + let text_color = if is_selected || is_hovered { | |
| 262 | + crate::theme::CatppuccinMocha::TEXT | |
| 263 | + } else { | |
| 264 | + crate::theme::CatppuccinMocha::SUBTEXT0 | |
| 265 | + }; | |
| 266 | + let icon_color = if is_selected || is_hovered { | |
| 267 | + crate::theme::CatppuccinMocha::TEXT | |
| 268 | + } else { | |
| 269 | + crate::theme::CatppuccinMocha::SURFACE2 | |
| 270 | + }; | |
| 271 | + | |
| 272 | + let r = egui::Frame::new() | |
| 273 | + .fill(bg) | |
| 274 | + .corner_radius(6.0) | |
| 275 | + .inner_margin(egui::Margin::symmetric(6, 2)) | |
| 276 | + .show(ui, |ui| { | |
| 277 | + ui.horizontal(|ui| { | |
| 278 | + ui.label( | |
| 279 | + RichText::new(egui_phosphor::regular::FILE_CODE) | |
| 280 | + .color(icon_color) | |
| 281 | + .size(12.0), | |
| 282 | + ); | |
| 283 | + ui.label( | |
| 284 | + RichText::new(&file.name) | |
| 285 | + .color(text_color) | |
| 286 | + .size(11.0), | |
| 287 | + ); | |
| 288 | + }); | |
| 289 | + }) | |
| 290 | + .response | |
| 291 | + .interact(egui::Sense::click()); | |
| 292 | + | |
| 293 | + ui.ctx().data_mut(|d| d.insert_temp(row_id, r.hovered())); | |
| 294 | + if r.clicked() { | |
| 295 | + clicked_file = Some(idx); | |
| 296 | + } | |
| 297 | + } | |
| 298 | + }); | |
| 299 | + | |
| 300 | + if let Some(idx) = clicked_file { | |
| 301 | + self.selected_file = idx; | |
| 302 | + } | |
| 303 | + }); | |
| 304 | + | |
| 305 | + // Separator | |
| 306 | + let sep_rect = ui.available_rect_before_wrap(); | |
| 307 | + ui.painter().line_segment( | |
| 308 | + [ | |
| 309 | + egui::pos2(sep_rect.left(), sep_rect.top()), | |
| 310 | + egui::pos2(sep_rect.left(), sep_rect.bottom()), | |
| 311 | + ], | |
| 312 | + egui::Stroke::new(1.0, crate::theme::CatppuccinMocha::SURFACE0), | |
| 313 | + ); | |
| 314 | + ui.add_space(4.0); | |
| 315 | + | |
| 316 | + // -- Code content -- | |
| 317 | + ui.vertical(|ui| { | |
| 318 | + self.show_selected(ui, "code_all_scroll"); | |
| 319 | + }); | |
| 320 | + }); | |
| 321 | + self.show_enum_popup(ui.ctx()); | |
| 322 | + | |
| 323 | + // Sync enum conversions back to session if changed | |
| 324 | + if self.converted_enums != prev_enums { | |
| 325 | + *enum_conversions = self | |
| 326 | + .converted_enums | |
| 327 | + .iter() | |
| 328 | + .map(|(field_name, (enum_name, variants))| crate::session::EnumConversion { | |
| 329 | + field_name: field_name.clone(), | |
| 330 | + enum_name: enum_name.clone(), | |
| 331 | + variants: variants.clone(), | |
| 332 | + }) | |
| 333 | + .collect(); | |
| 334 | + } | |
| 335 | + // Sync hidden fields back to session if changed | |
| 336 | + if self.hidden_fields.len() != prev_hidden_count { | |
| 337 | + *hidden_fields = self.hidden_fields.iter().cloned().collect(); | |
| 338 | + } | |
| 339 | + } | |
| 340 | + | |
| 341 | + fn rebuild_file_mode( | |
| 342 | + &mut self, | |
| 343 | + parsed_files: &[(String, serde_json::Value)], | |
| 344 | + schema: Option<&crate::schema::SchemaOverview>, | |
| 345 | + ) { | |
| 346 | + self.files.clear(); | |
| 347 | + let lang = self.selected_language.generator(); | |
| 348 | + | |
| 349 | + // shared file from schema (if available) | |
| 350 | + let shared_struct_names: std::collections::BTreeSet<String> = schema | |
| 351 | + .map(|s| s.structs.iter().map(|st| st.name.clone()).collect()) | |
| 352 | + .unwrap_or_default(); | |
| 353 | + | |
| 354 | + // unique struct names from schema — used to match disambiguated names | |
| 355 | + let unique_struct_names: std::collections::BTreeSet<String> = schema | |
| 356 | + .map(|s| s.unique_structs.iter().map(|st| st.name.clone()).collect()) | |
| 357 | + .unwrap_or_default(); | |
| 358 | + | |
| 359 | + if let Some(s) = schema { | |
| 360 | + if !s.structs.is_empty() { | |
| 361 | + let code = CodeGenerator::from_schema(&s.structs).generate_code(lang.as_ref()); | |
| 362 | + let lines: Vec<String> = code.lines().map(|l| l.to_string()).collect(); | |
| 363 | + let struct_blocks = extract_struct_blocks(&lines, self.selected_language); | |
| 364 | + self.files.push(GeneratedFile { | |
| 365 | + name: lang.file_name("shared"), | |
| 366 | + code, | |
| 367 | + lines, | |
| 368 | + structs: struct_blocks, | |
| 369 | + is_group: false, | |
| 370 | + }); | |
| 371 | + } | |
| 372 | + } | |
| 373 | + | |
| 374 | + // Group files by depluralized first word | |
| 375 | + let mut groups: std::collections::BTreeMap<String, Vec<(&str, &serde_json::Value)>> = | |
| 376 | + std::collections::BTreeMap::new(); | |
| 377 | + for (filename, value) in parsed_files { | |
| 378 | + let word = first_normal_word(filename).unwrap_or_else(|| "other".to_string()); | |
| 379 | + let key = crate::codegen::singularize(&word.to_ascii_lowercase()); | |
| 380 | + groups.entry(key).or_default().push((filename.as_str(), value)); | |
| 381 | + } | |
| 382 | + | |
| 383 | + // Generate one file per group, merging struct definitions | |
| 384 | + for (group_key, files) in &groups { | |
| 385 | + let mut seen_structs: std::collections::BTreeSet<String> = std::collections::BTreeSet::new(); | |
| 386 | + let mut struct_blocks: Vec<String> = Vec::new(); | |
| 387 | + | |
| 388 | + for (filename, value) in files { | |
| 389 | + let raw = generate_file_code_structs(value, &shared_struct_names, &unique_struct_names, filename, lang.as_ref()); | |
| 390 | + for block in &raw { | |
| 391 | + if !seen_structs.contains(&block.name) { | |
| 392 | + seen_structs.insert(block.name.clone()); | |
| 393 | + struct_blocks.push(block.code.clone()); | |
| 394 | + } | |
| 395 | + } | |
| 396 | + } | |
| 397 | + | |
| 398 | + let mut merged_code = String::new(); | |
| 399 | + let header = lang.file_header(); | |
| 400 | + if !header.is_empty() { | |
| 401 | + merged_code.push_str(&header); | |
| 402 | + merged_code.push('\n'); | |
| 403 | + } | |
| 404 | + let needs_temporal = struct_blocks.iter().any(|b| { | |
| 405 | + b.contains("DateTime<") || b.contains("NaiveDate") || b.contains("NaiveTime") | |
| 406 | + || b.contains("Date") // Swift uses Date | |
| 407 | + }); | |
| 408 | + merged_code.push_str(&lang.imports_header(needs_temporal, !shared_struct_names.is_empty())); | |
| 409 | + merged_code.push('\n'); | |
| 410 | + | |
| 411 | + for block in &struct_blocks { | |
| 412 | + merged_code.push_str(block); | |
| 413 | + merged_code.push('\n'); | |
| 414 | + } | |
| 415 | + | |
| 416 | + let code = merged_code.trim_end().to_string() + "\n"; | |
| 417 | + let lines: Vec<String> = code.lines().map(|l| l.to_string()).collect(); | |
| 418 | + let struct_blocks = extract_struct_blocks(&lines, self.selected_language); | |
| 419 | + self.files.push(GeneratedFile { | |
| 420 | + name: lang.file_name(group_key), | |
| 421 | + code, | |
| 422 | + lines, | |
| 423 | + structs: struct_blocks, | |
| 424 | + is_group: true, | |
| 425 | + }); | |
| 426 | + } | |
| 427 | + | |
| 428 | + let enums_file_name = lang.file_name("enums"); | |
| 429 | + let shared_file_name = lang.file_name("shared"); | |
| 430 | + | |
| 431 | + // Generate enums file from converted enums and rewrite types in all files | |
| 432 | + if !self.converted_enums.is_empty() { | |
| 433 | + let mut enums_code = String::new(); | |
| 434 | + let header = lang.file_header(); | |
| 435 | + if !header.is_empty() { | |
| 436 | + enums_code.push_str(&header); | |
| 437 | + enums_code.push('\n'); | |
| 438 | + } | |
| 439 | + enums_code.push_str(&lang.imports_header(false, false)); | |
| 440 | + enums_code.push('\n'); | |
| 441 | + for (_, (enum_name, variants)) in &self.converted_enums { | |
| 442 | + enums_code.push_str(&lang.enum_open(enum_name)); | |
| 443 | + for v in variants { | |
| 444 | + let variant_name = to_enum_variant(v); | |
| 445 | + enums_code.push_str(&lang.enum_variant(&variant_name, v)); | |
| 446 | + } | |
| 447 | + enums_code.push_str(&lang.enum_close()); | |
| 448 | + enums_code.push('\n'); | |
| 449 | + } | |
| 450 | + let enums_code = enums_code.trim_end().to_string() + "\n"; | |
| 451 | + let lines: Vec<String> = enums_code.lines().map(|l| l.to_string()).collect(); | |
| 452 | + let struct_blocks = extract_struct_blocks(&lines, self.selected_language); | |
| 453 | + self.files.push(GeneratedFile { | |
| 454 | + name: enums_file_name.clone(), | |
| 455 | + code: enums_code, | |
| 456 | + lines, | |
| 457 | + structs: struct_blocks, | |
| 458 | + is_group: false, | |
| 459 | + }); | |
| 460 | + | |
| 461 | + // Rewrite String -> EnumName in all files for converted fields | |
| 462 | + let is_rust = self.selected_language == crate::lang::CodeLanguage::Rust; | |
| 463 | + for file in &mut self.files { | |
| 464 | + if file.name == enums_file_name { | |
| 465 | + continue; | |
| 466 | + } | |
| 467 | + let mut new_code = file.code.clone(); | |
| 468 | + for (field_name, (enum_name, _)) in &self.converted_enums { | |
| 469 | + let code_field = lang.field_name(field_name); | |
| 470 | + if is_rust { | |
| 471 | + let patterns = [ | |
| 472 | + (format!("pub {}: String,", field_name), format!("pub {}: {},", field_name, enum_name)), | |
| 473 | + (format!("pub {}: String,", code_field), format!("pub {}: {},", code_field, enum_name)), | |
| 474 | + (format!("pub {}: Option<String>,", field_name), format!("pub {}: Option<{}>,", field_name, enum_name)), | |
| 475 | + (format!("pub {}: Option<String>,", code_field), format!("pub {}: Option<{}>,", code_field, enum_name)), | |
| 476 | + ]; | |
| 477 | + for (from, to) in &patterns { | |
| 478 | + new_code = new_code.replace(from, to); | |
| 479 | + } | |
| 480 | + } else { | |
| 481 | + // Swift: `let field: String` and `let field: String?` | |
| 482 | + let patterns = [ | |
| 483 | + (format!("let {}: String", code_field), format!("let {}: {}", code_field, enum_name)), | |
| 484 | + (format!("let {}: String?", code_field), format!("let {}: {}?", code_field, enum_name)), | |
| 485 | + ]; | |
| 486 | + for (from, to) in &patterns { | |
| 487 | + new_code = new_code.replace(from, to); | |
| 488 | + } | |
| 489 | + } | |
| 490 | + } | |
| 491 | + if new_code != file.code { | |
| 492 | + // Add enum import for Rust only (Swift doesn't need imports) | |
| 493 | + if is_rust && !new_code.contains("use crate::enums::") { | |
| 494 | + new_code = new_code.replace( | |
| 495 | + "use serde::{Deserialize, Serialize};\n", | |
| 496 | + "use serde::{Deserialize, Serialize};\nuse crate::enums::*;\n", | |
| 497 | + ); | |
| 498 | + } | |
| 499 | + file.code = new_code; | |
| 500 | + file.lines = file.code.lines().map(|l| l.to_string()).collect(); | |
| 501 | + file.structs = extract_struct_blocks(&file.lines, self.selected_language); | |
| 502 | + } | |
| 503 | + } | |
| 504 | + } | |
| 505 | + | |
| 506 | + // Reorder: move enums file right after shared file (before group files) | |
| 507 | + if let Some(enum_pos) = self.files.iter().position(|f| f.name == enums_file_name) { | |
| 508 | + let enums_file = self.files.remove(enum_pos); | |
| 509 | + let insert_at = self.files.iter().position(|f| f.name == shared_file_name) | |
| 510 | + .map(|i| i + 1) | |
| 511 | + .unwrap_or(0); | |
| 512 | + self.files.insert(insert_at, enums_file); | |
| 513 | + } | |
| 514 | + | |
| 515 | + // Generate mod file (Rust only — Swift returns None) | |
| 516 | + { | |
| 517 | + let names: Vec<&str> = self.files.iter().map(|f| f.name.as_str()).collect(); | |
| 518 | + if let Some(mod_code) = lang.mod_file(&names) { | |
| 519 | + let lines: Vec<String> = mod_code.lines().map(|l| l.to_string()).collect(); | |
| 520 | + let struct_blocks = extract_struct_blocks(&lines, self.selected_language); | |
| 521 | + self.files.insert(0, GeneratedFile { | |
| 522 | + name: "mod.rs".to_string(), | |
| 523 | + code: mod_code, | |
| 524 | + lines, | |
| 525 | + structs: struct_blocks, | |
| 526 | + is_group: false, | |
| 527 | + }); | |
| 528 | + } | |
| 529 | + } | |
| 530 | + | |
| 531 | + if self.selected_file >= self.files.len() { | |
| 532 | + self.selected_file = 0; | |
| 533 | + } | |
| 534 | + | |
| 535 | + // Collect sample string values per field name for tooltips + enum detection | |
| 536 | + self.field_values.clear(); | |
| 537 | + self.enum_candidates.clear(); | |
| 538 | + let mut value_sets: BTreeMap<String, BTreeSet<String>> = BTreeMap::new(); | |
| 539 | + let mut occurrence_counts: BTreeMap<String, usize> = BTreeMap::new(); | |
| 540 | + for (_, value) in parsed_files { | |
| 541 | + collect_string_values(value, &mut value_sets, &mut occurrence_counts); | |
| 542 | + } | |
| 543 | + for (key, vals) in value_sets { | |
| 544 | + let is_candidate = !vals.is_empty() | |
| 545 | + && vals.len() <= 30 | |
| 546 | + && vals.iter().all(|v| v.len() <= 60 && !v.contains(' ') || looks_like_enum_value(v)); | |
| 547 | + let mut sorted: Vec<String> = vals.into_iter().collect(); | |
| 548 | + sorted.sort(); | |
| 549 | + if is_candidate && !self.converted_enums.contains_key(&key) { | |
| 550 | + self.enum_candidates.insert(key.clone()); | |
| 551 | + } | |
| 552 | + sorted.truncate(30); | |
| 553 | + self.field_values.insert(key, sorted); | |
| 554 | + } | |
| 555 | + } | |
| 556 | + | |
| 557 | + fn build_struct_index(&mut self) { | |
| 558 | + self.struct_index.clear(); | |
| 559 | + for (file_idx, file) in self.files.iter().enumerate() { | |
| 560 | + for line_idx in 0..file.lines.len() { | |
| 561 | + let trimmed = file.lines[line_idx].trim(); | |
| 562 | + // Detect struct lines: "pub struct X" (Rust) or "struct X:" (Swift) | |
| 563 | + let struct_name = trimmed.strip_prefix("pub struct ") | |
| 564 | + .or_else(|| trimmed.strip_prefix("struct ")) | |
| 565 | + .and_then(|rest| rest.split(|c: char| c == ' ' || c == '{' || c == '<' || c == ':').next()) | |
| 566 | + .filter(|n| !n.is_empty()); | |
| 567 | + if let Some(name) = struct_name { | |
| 568 | + let target = if line_idx > 0 | |
| 569 | + && file.lines[line_idx - 1].trim().starts_with("#[derive") | |
| 570 | + { | |
| 571 | + line_idx - 1 | |
| 572 | + } else { | |
| 573 | + line_idx | |
| 574 | + }; | |
| 575 | + self.struct_index.entry(name.to_string()).or_insert((file_idx, target)); | |
| 576 | + } | |
| 577 | + // Detect enum lines: "pub enum X" (Rust) or "enum X:" (Swift) | |
| 578 | + let enum_name = trimmed.strip_prefix("pub enum ") | |
| 579 | + .or_else(|| trimmed.strip_prefix("enum ")) | |
| 580 | + .and_then(|rest| rest.split(|c: char| c == ' ' || c == '{' || c == '<' || c == ':').next()) | |
| 581 | + .filter(|n| !n.is_empty()); | |
| 582 | + if let Some(name) = enum_name { | |
| 583 | + let target = if line_idx > 0 | |
| 584 | + && file.lines[line_idx - 1].trim().starts_with("#[derive") | |
| 585 | + { | |
| 586 | + line_idx - 1 | |
| 587 | + } else { | |
| 588 | + line_idx | |
| 589 | + }; | |
| 590 | + self.struct_index.entry(name.to_string()).or_insert((file_idx, target)); | |
| 591 | + } | |
| 592 | + if let Some(rest) = trimmed.strip_prefix("pub type ") { | |
| 593 | + let name = rest | |
| 594 | + .split(|c: char| c == ' ' || c == '=') | |
| 595 | + .next() | |
| 596 | + .unwrap_or("") | |
| 597 | + .to_string(); | |
| 598 | + if !name.is_empty() { | |
| 599 | + self.struct_index.entry(name).or_insert((file_idx, line_idx)); | |
| 600 | + } | |
| 601 | + } | |
| 602 | + } | |
| 603 | + } | |
| 604 | + } | |
| 605 | + | |
| 606 | + fn rebuild_search(&mut self) { | |
| 607 | + self.search_matches.clear(); | |
| 608 | + self.current_match = 0; | |
| 609 | + if self.search_query.is_empty() { | |
| 610 | + return; | |
| 611 | + } | |
| 612 | + if let Some(file) = self.files.get(self.selected_file) { | |
| 613 | + let query = self.search_query.to_ascii_lowercase(); | |
| 614 | + for (line_idx, line) in file.lines.iter().enumerate() { | |
| 615 | + let lower = line.to_ascii_lowercase(); | |
| 616 | + let mut start = 0; | |
| 617 | + while let Some(pos) = lower[start..].find(&query) { | |
| 618 | + self.search_matches.push((line_idx, start + pos)); | |
| 619 | + start += pos + query.len(); | |
| 620 | + } | |
| 621 | + } | |
| 622 | + } | |
| 623 | + } | |
| 624 | + | |
| 625 | + fn show_selected(&mut self, ui: &mut Ui, scroll_id: &str) { | |
| 626 | + if self.files.is_empty() { | |
| 627 | + ui.centered_and_justified(|ui| { | |
| 628 | + ui.label( | |
| 629 | + RichText::new("No code generated") | |
| 630 | + .color(CatppuccinMocha::OVERLAY0) | |
| 631 | + .size(16.0), | |
| 632 | + ); | |
| 633 | + }); | |
| 634 | + return; | |
| 635 | + } | |
| 636 | + | |
| 637 | + // Top bar: Copy + file info on left, search on right | |
| 638 | + ui.horizontal(|ui| { | |
| 639 | + if ui.button("Copy All").clicked() { | |
| 640 | + let code = filter_hidden_fields_from_code( | |
| 641 | + &self.files[self.selected_file], | |
| 642 | + &self.hidden_fields, | |
| 643 | + ); | |
| 644 | + ui.ctx().copy_text(code); | |
| 645 | + } | |
| 646 | + if ui.button( | |
| 647 | + RichText::new(format!("{} Download", egui_phosphor::regular::DOWNLOAD_SIMPLE)) | |
| 648 | + ).clicked() { | |
| 649 | + if let Some(dir) = rfd::FileDialog::new().pick_folder() { | |
| 650 | + let dl_lang = self.selected_language.generator(); | |
| 651 | + // Write all generated files | |
| 652 | + for file in &self.files { | |
| 653 | + let code = filter_hidden_fields_from_code(file, &self.hidden_fields); | |
| 654 | + let _ = std::fs::write(dir.join(&file.name), code); | |
| 655 | + } | |
| 656 | + // Generate mod file if the language supports it | |
| 657 | + let names: Vec<&str> = self.files.iter() | |
| 658 | + .filter(|f| f.name != "mod.rs") | |
| 659 | + .map(|f| f.name.as_str()) | |
| 660 | + .collect(); | |
| 661 | + if let Some(mod_code) = dl_lang.mod_file(&names) { | |
| 662 | + let _ = std::fs::write(dir.join("mod.rs"), mod_code); | |
| 663 | + } | |
| 664 | + } | |
| 665 | + } | |
| 666 | + ui.label( | |
| 667 | + RichText::new(format!( | |
| 668 | + "{} — {} lines", | |
| 669 | + self.files[self.selected_file].name, | |
| 670 | + self.files[self.selected_file].lines.len() | |
| 671 | + )) | |
| 672 | + .color(CatppuccinMocha::OVERLAY0) | |
| 673 | + .small(), | |
| 674 | + ); | |
| 675 | + | |
| 676 | + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { | |
| 677 | + // Next/Prev buttons | |
| 678 | + let has_matches = !self.search_matches.is_empty(); | |
| 679 | + if ui.add_enabled(has_matches, egui::Button::new( | |
| 680 | + RichText::new(egui_phosphor::regular::CARET_DOWN).size(14.0), | |
| 681 | + ).frame(false)).on_hover_text("Next match").clicked() { | |
| 682 | + self.current_match = (self.current_match + 1) % self.search_matches.len(); | |
| 683 | + self.scroll_to_match = Some(self.search_matches[self.current_match].0); | |
| 684 | + } | |
| 685 | + if ui.add_enabled(has_matches, egui::Button::new( | |
| 686 | + RichText::new(egui_phosphor::regular::CARET_UP).size(14.0), | |
| 687 | + ).frame(false)).on_hover_text("Previous match").clicked() { | |
| 688 | + if self.current_match == 0 { | |
| 689 | + self.current_match = self.search_matches.len().saturating_sub(1); | |
| 690 | + } else { | |
| 691 | + self.current_match -= 1; | |
| 692 | + } | |
| 693 | + self.scroll_to_match = Some(self.search_matches[self.current_match].0); | |
| 694 | + } | |
| 695 | + | |
| 696 | + // Match count (always reserve space so TextEdit ID stays stable) | |
| 697 | + let count_text = if self.search_query.is_empty() { | |
| 698 | + String::new() | |
| 699 | + } else if self.search_matches.is_empty() { | |
| 700 | + "0/0".to_string() | |
| 701 | + } else { | |
| 702 | + format!("{}/{}", self.current_match + 1, self.search_matches.len()) | |
| 703 | + }; | |
| 704 | + ui.label( | |
| 705 | + RichText::new(count_text) | |
| 706 | + .color(if !self.search_query.is_empty() && self.search_matches.is_empty() { | |
| 707 | + CatppuccinMocha::RED | |
| 708 | + } else { | |
| 709 | + CatppuccinMocha::OVERLAY0 | |
| 710 | + }) | |
| 711 | + .small() | |
| 712 | + .family(egui::FontFamily::Monospace), | |
| 713 | + ); | |
| 714 | + | |
| 715 | + // Search input — stable ID so focus persists across frames | |
| 716 | + let search_id = ui.id().with("code_search"); | |
| 717 | + let prev_query = self.search_query.clone(); | |
| 718 | + let response = ui.add( | |
| 719 | + egui::TextEdit::singleline(&mut self.search_query) | |
| 720 | + .id(search_id) | |
| 721 | + .desired_width(250.0) | |
| 722 | + .hint_text("Search…") | |
| 723 | + .font(egui::TextStyle::Body), | |
| 724 | + ); | |
| 725 | + if self.search_query != prev_query { | |
| 726 | + self.rebuild_search(); | |
| 727 | + if !self.search_matches.is_empty() { | |
| 728 | + self.scroll_to_match = Some(self.search_matches[0].0); | |
| 729 | + } | |
| 730 | + response.request_focus(); | |
| 731 | + } | |
| 732 | + // Enter navigates to next match | |
| 733 | + if response.lost_focus() | |
| 734 | + && ui.input(|i| i.key_pressed(egui::Key::Enter)) | |
| 735 | + && !self.search_matches.is_empty() | |
| 736 | + { | |
| 737 | + self.current_match = (self.current_match + 1) % self.search_matches.len(); | |
| 738 | + self.scroll_to_match = Some(self.search_matches[self.current_match].0); | |
| 739 | + response.request_focus(); | |
| 740 | + } | |
| 741 | + }); | |
| 742 | + }); | |
| 743 | + | |
| 744 | + ui.add_space(8.0); | |
| 745 | + | |
| 746 | + // Collect search state for rendering | |
| 747 | + let current_match_line = self.search_matches | |
| 748 | + .get(self.current_match) | |
| 749 | + .map(|(line, _)| *line); | |
| 750 | + let match_lines: std::collections::BTreeSet<usize> = self.search_matches | |
| 751 | + .iter() | |
| 752 | + .map(|(line, _)| *line) | |
| 753 | + .collect(); | |
| 754 | + let scroll_target: Option<usize> = self.scroll_to_match.take(); | |
| 755 | + | |
| 756 | + let file = &self.files[self.selected_file]; | |
| 757 | + let row_height = ui.text_style_height(&egui::TextStyle::Monospace) + 2.0; | |
| 758 | + let num_rows = file.lines.len(); | |
| 759 | + let struct_index = &self.struct_index; | |
| 760 | + let field_values = &self.field_values; | |
| 761 | + | |
| 762 | + let struct_start_lines: Vec<(usize, usize)> = file | |
| 763 | + .structs | |
| 764 | + .iter() | |
| 765 | + .enumerate() | |
| 766 | + .map(|(i, s)| (s.start_line, i)) | |
| 767 | + .collect(); | |
| 768 | + | |
| 769 | + let scroll_id_salt = format!("{}_{}", scroll_id, self.selected_file); | |
| 770 | + let spacing_y = ui.spacing().item_spacing.y; | |
| 771 | + let scroll_offset = scroll_target.map(|line| (line as f32 * (row_height + spacing_y)).max(0.0)); | |
| 772 | + | |
| 773 | + let mut area = egui::ScrollArea::vertical() | |
| 774 | + .id_salt(&scroll_id_salt) | |
| 775 | + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) | |
| 776 | + .auto_shrink(false) | |
| 777 | + .animated(false); | |
| 778 | + | |
| 779 | + if let Some(y) = scroll_offset { | |
| 780 | + area = area.vertical_scroll_offset(y); | |
| 781 | + } | |
| 782 | + | |
| 783 | + let mut nav_clicked: Option<(usize, usize)> = None; | |
| 784 | + let mut pending_enum_convert: Option<String> = None; | |
| 785 | + let mut pending_enum_revert: Option<String> = None; | |
| 786 | + let mut pending_toggle_field: Option<String> = None; | |
| 787 | + let mut pending_enum_edit: Option<String> = None; | |
| 788 | + let enum_candidates = &self.enum_candidates; | |
| 789 | + let converted_enums = &self.converted_enums; | |
| 790 | + let hidden_fields = &self.hidden_fields; | |
| 791 | + | |
| 792 | + // Build reverse mapping: enum_name -> Vec<(struct_name, field_name)> | |
| 793 | + let lang = self.selected_language.generator(); | |
| 794 | + let enums_file_name = lang.file_name("enums"); | |
| 795 | + let is_rust = self.selected_language == crate::lang::CodeLanguage::Rust; | |
| 796 | + let enum_usages: BTreeMap<String, Vec<(String, String, String)>> = { | |
| 797 | + let mut usages: BTreeMap<String, Vec<(String, String, String)>> = BTreeMap::new(); | |
| 798 | + // Scan all non-enums files for fields that reference enum types | |
| 799 | + for f in &self.files { | |
| 800 | + if f.name == enums_file_name || f.name == "mod.rs" { | |
| 801 | + continue; | |
| 802 | + } | |
| 803 | + let mut cur_struct: Option<String> = None; | |
| 804 | + for line in &f.lines { | |
| 805 | + let t = line.trim(); | |
| 806 | + // Detect struct start | |
| 807 | + let struct_start = t.strip_prefix("pub struct ") | |
| 808 | + .or_else(|| t.strip_prefix("struct ")); | |
| 809 | + if let Some(rest) = struct_start { | |
| 810 | + cur_struct = rest | |
| 811 | + .split(|c: char| c == ' ' || c == '{' || c == ':') | |
| 812 | + .next() | |
| 813 | + .map(|s| s.to_string()); | |
| 814 | + } else if t == "}" { | |
| 815 | + cur_struct = None; | |
| 816 | + } else { | |
| 817 | + let is_field = if is_rust { | |
| 818 | + t.starts_with("pub ") && t.contains(':') | |
| 819 | + } else { | |
| 820 | + t.starts_with("let ") && t.contains(':') | |
| 821 | + }; | |
| 822 | + if is_field { | |
| 823 | + if let Some(ref sname) = cur_struct { | |
| 824 | + if let Some(colon) = t.find(':') { | |
| 825 | + let field = if is_rust { | |
| 826 | + t[..colon].trim().trim_start_matches("pub ").trim_start_matches("r#") | |
| 827 | + } else { | |
| 828 | + t[..colon].trim().trim_start_matches("let ").trim_start_matches('`').trim_end_matches('`') | |
| 829 | + }; | |
| 830 | + let typ = t[colon + 1..].trim().trim_end_matches(','); | |
| 831 | + let bare = extract_inner_type(typ); | |
| 832 | + for (_, (enum_name, _)) in converted_enums.iter() { | |
| 833 | + if bare == enum_name { | |
| 834 | + usages.entry(enum_name.clone()) | |
| 835 | + .or_default() | |
| 836 | + .push((f.name.clone(), sname.clone(), field.to_string())); | |
| 837 | + } | |
| 838 | + } | |
| 839 | + } | |
| 840 | + } | |
| 841 | + } | |
| 842 | + } | |
| 843 | + } | |
| 844 | + } | |
| 845 | + usages | |
| 846 | + }; | |
| 847 | + | |
| 848 | + // Precompute struct name ranges for current file | |
| 849 | + let struct_ranges: Vec<(usize, usize, &str)> = file | |
| 850 | + .structs | |
| 851 | + .iter() | |
| 852 | + .map(|s| (s.start_line, s.end_line, s.name.as_str())) | |
| 853 | + .collect(); | |
| 854 | + | |
| 855 | + // Precompute enum name ranges for current file (for current_enum tracking) | |
| 856 | + let enum_ranges: Vec<(usize, usize, String)> = { | |
| 857 | + let mut ranges = Vec::new(); | |
| 858 | + let mut i = 0; | |
| 859 | + while i < file.lines.len() { | |
| 860 | + let t = file.lines[i].trim(); | |
| 861 | + let enum_name = t.strip_prefix("pub enum ") | |
| 862 | + .or_else(|| t.strip_prefix("enum ")) | |
| 863 | + .and_then(|rest| rest.split(|c: char| c == ' ' || c == '{' || c == ':').next()) | |
| 864 | + .filter(|n| !n.is_empty()); | |
| 865 | + if let Some(name) = enum_name { | |
| 866 | + let name = name.to_string(); | |
| 867 | + let start = i; | |
| 868 | + while i < file.lines.len() && file.lines[i].trim() != "}" { | |
| 869 | + i += 1; | |
| 870 | + } | |
| 871 | + ranges.push((start, i, name)); | |
| 872 | + } | |
| 873 | + i += 1; | |
| 874 | + } | |
| 875 | + ranges | |
| 876 | + }; | |
| 877 | + | |
| 878 | + area.show_rows(ui, row_height, num_rows, |ui, row_range| { | |
| 879 | + for row in row_range { | |
| 880 | + let line = &file.lines[row]; | |
| 881 | + let is_match_line = match_lines.contains(&row); | |
| 882 | + let is_current = current_match_line == Some(row); | |
| 883 | + | |
| 884 | + // Find which struct this row belongs to | |
| 885 | + let current_struct = struct_ranges | |
| 886 | + .iter() | |
| 887 | + .find(|(start, end, _)| row >= *start && row <= *end) | |
| 888 | + .map(|(_, _, name)| *name); | |
| 889 | + | |
| 890 | + // Find which enum this row belongs to (if any) | |
| 891 | + let current_enum = enum_ranges | |
| 892 | + .iter() | |
| 893 | + .find(|(start, end, _)| row >= *start && row <= *end) | |
| 894 | + .map(|(_, _, name)| name.as_str()); | |
| 895 | + | |
| 896 | + // Highlight match lines — subtle bg + left accent bar | |
| 897 | + if is_current || is_match_line { | |
| 898 | + let rect = ui.available_rect_before_wrap(); | |
| 899 | + let row_rect = egui::Rect::from_min_size( | |
| 900 | + rect.min, | |
| 901 | + egui::vec2(rect.width(), row_height), | |
| 902 | + ); | |
| 903 | + // Subtle background tint | |
| 904 | + let bg = if is_current { | |
| 905 | + egui::Color32::from_rgba_unmultiplied(137, 180, 250, 25) | |
| 906 | + } else { | |
| 907 | + egui::Color32::from_rgba_unmultiplied(249, 226, 175, 15) | |
| 908 | + }; | |
| 909 | + ui.painter().rect_filled(row_rect, 0.0, bg); | |
| 910 | + // Left accent bar | |
| 911 | + let bar = egui::Rect::from_min_size( | |
| 912 | + rect.min, | |
| 913 | + egui::vec2(3.0, row_height), | |
| 914 | + ); | |
| 915 | + let bar_color = if is_current { | |
| 916 | + CatppuccinMocha::BLUE | |
| 917 | + } else { | |
| 918 | + CatppuccinMocha::YELLOW | |
| 919 | + }; | |
| 920 | + ui.painter().rect_filled(bar, 0.0, bar_color); | |
| 921 | + } | |
| 922 | + | |
| 923 | + ui.horizontal(|ui| { | |
| 924 | + if let Some((_, struct_idx)) = | |
| 925 | + struct_start_lines.iter().find(|(l, _)| *l == row) | |
| 926 | + { | |
| 927 | + let btn = ui.add( | |
| 928 | + egui::Button::new( | |
| 929 | + RichText::new(egui_phosphor::regular::COPY) | |
| 930 | + .color(CatppuccinMocha::OVERLAY0), | |
| 931 | + ) | |
| 932 | + .frame(false), | |
| 933 | + ); | |
| 934 | + if btn.hovered() { | |
| 935 | + ui.painter().text( | |
| 936 | + btn.rect.center(), | |
| 937 | + egui::Align2::CENTER_CENTER, | |
| 938 | + egui_phosphor::regular::COPY, | |
| 939 | + egui::FontId::proportional(14.0), | |
| 940 | + CatppuccinMocha::BLUE, | |
| 941 | + ); | |
| 942 | + } | |
| 943 | + if btn.clicked() { | |
| 944 | + // Copy struct with hidden fields filtered out | |
| 945 | + let text = filter_hidden_fields_from_block( | |
| 946 | + &file.structs[*struct_idx].text, | |
| 947 | + &file.structs[*struct_idx].name, | |
| 948 | + hidden_fields, | |
| 949 | + ); | |
| 950 | + ui.ctx().copy_text(text); | |
| 951 | + } | |
| 952 | + btn.on_hover_ui_at_pointer(|ui| { | |
| 953 | + ui.label( | |
| 954 | + RichText::new("Copy struct") | |
| 955 | + .color(CatppuccinMocha::TEXT) | |
| 956 | + .small(), | |
| 957 | + ); | |
| 958 | + }); | |
| 959 | + } else { | |
| 960 | + ui.label( | |
| 961 | + RichText::new(" ") | |
| 962 | + .family(egui::FontFamily::Monospace), | |
| 963 | + ); | |
| 964 | + } | |
| 965 | + | |
| 966 | + ui.label( | |
| 967 | + RichText::new(format!("{:>4} ", row + 1)) | |
| 968 | + .color(CatppuccinMocha::SURFACE2) | |
| 969 | + .family(egui::FontFamily::Monospace), | |
| 970 | + ); | |
| 971 | + let action = render_code_line( | |
| 972 | + ui, line, struct_index, field_values, enum_candidates, | |
| 973 | + converted_enums, hidden_fields, current_struct, | |
| 974 | + &enum_usages, current_enum, self.selected_language, | |
| 975 | + ); | |
| 976 | + match action { | |
| 977 | + LineAction::NavTo(target) => nav_clicked = Some(target), | |
| 978 | + LineAction::ConvertEnum(field) => pending_enum_convert = Some(field), | |
| 979 | + LineAction::RevertEnum(field) => pending_enum_revert = Some(field), | |
| 980 | + LineAction::ToggleField(key) => pending_toggle_field = Some(key), | |
| 981 | + LineAction::EditEnum(field) => pending_enum_edit = Some(field), | |
| 982 | + LineAction::None => {} | |
| 983 | + } | |
| 984 | + }); | |
| 985 | + } | |
| 986 | + }); | |
| 987 | + | |
| 988 | + if let Some(target) = nav_clicked { | |
| 989 | + self.nav_target = Some(target); | |
| 990 | + } | |
| 991 | + if let Some(field_name) = pending_enum_convert { | |
| 992 | + if let Some(values) = self.field_values.get(&field_name) { | |
| 993 | + self.open_enum_popup(&field_name, values.clone()); | |
| 994 | + } | |
| 995 | + } | |
| 996 | + if let Some(field_name) = pending_enum_revert { | |
| 997 | + self.converted_enums.remove(&field_name); | |
| 998 | + self.enum_candidates.insert(field_name); | |
| 999 | + self.cache_key = 0; | |
| 1000 | + } | |
| 1001 | + if let Some(key) = pending_toggle_field { | |
| 1002 | + if self.hidden_fields.contains(&key) { | |
| 1003 | + self.hidden_fields.remove(&key); | |
| 1004 | + } else { | |
| 1005 | + self.hidden_fields.insert(key); | |
| 1006 | + } | |
| 1007 | + } | |
| 1008 | + if let Some(field_name) = pending_enum_edit { | |
| 1009 | + let values = self | |
| 1010 | + .field_values | |
| 1011 | + .get(&field_name) | |
| 1012 | + .cloned() | |
| 1013 | + .or_else(|| self.converted_enums.get(&field_name).map(|(_, v)| v.clone())) | |
| 1014 | + .unwrap_or_default(); | |
| 1015 | + self.open_enum_popup(&field_name, values); | |
| 1016 | + } | |
| 1017 | + } | |
| 1018 | + | |
| 1019 | + fn open_enum_popup(&mut self, field_name: &str, values: Vec<String>) { | |
| 1020 | + let existing = self.converted_enums.get(field_name); | |
| 1021 | + let popup = &mut self.enum_popup; | |
| 1022 | + popup.open = true; | |
| 1023 | + popup.field_name = field_name.to_string(); | |
| 1024 | + popup.enum_name = existing | |
| 1025 | + .map(|(name, _)| name.clone()) | |
| 1026 | + .unwrap_or_else(|| field_to_enum_name(field_name)); | |
| 1027 | + popup.values = values.clone(); | |
| 1028 | + popup.selected_existing = None; | |
| 1029 | + popup.preview_variants = existing | |
| 1030 | + .map(|(_, v)| v.clone()) | |
| 1031 | + .unwrap_or_else(|| values.clone()); | |
| 1032 | + | |
| 1033 | + // Find similar existing enums by Jaccard similarity of variants | |
| 1034 | + let value_set: BTreeSet<&str> = values.iter().map(|s| s.as_str()).collect(); | |
| 1035 | + let mut similar: Vec<(String, Vec<String>, f32)> = self | |
| 1036 | + .converted_enums | |
| 1037 | + .iter() | |
| 1038 | + .map(|(_, (name, variants))| { | |
| 1039 | + let existing_set: BTreeSet<&str> = variants.iter().map(|s| s.as_str()).collect(); | |
| 1040 | + let intersection = value_set.intersection(&existing_set).count(); | |
| 1041 | + let union = value_set.union(&existing_set).count(); | |
| 1042 | + let sim = if union > 0 { | |
| 1043 | + intersection as f32 / union as f32 | |
| 1044 | + } else { | |
| 1045 | + 0.0 | |
| 1046 | + }; | |
| 1047 | + (name.clone(), variants.clone(), sim) | |
| 1048 | + }) | |
| 1049 | + .collect(); | |
| 1050 | + similar.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); | |
| 1051 | + popup.similar_enums = similar; | |
| 1052 | + | |
| 1053 | + // Find all structs that contain this field name | |
| 1054 | + let mut affected = Vec::new(); | |
| 1055 | + for file in &self.files { | |
| 1056 | + for s in &file.structs { | |
| 1057 | + for line in s.text.lines() { | |
| 1058 | + let trimmed = line.trim(); | |
| 1059 | + // Extract field name from Rust ("pub field:") or Swift ("let field:") | |
| 1060 | + let fname_opt = if let Some(rest) = trimmed.strip_prefix("pub ") { | |
| 1061 | + rest.find(':').map(|colon| rest[..colon].trim().trim_start_matches("r#")) | |
| 1062 | + } else if let Some(rest) = trimmed.strip_prefix("let ") { | |
| 1063 | + rest.find(':').map(|colon| rest[..colon].trim().trim_start_matches('`').trim_end_matches('`')) | |
| 1064 | + } else { | |
| 1065 | + None | |
| 1066 | + }; | |
| 1067 | + if let Some(fname) = fname_opt { | |
| 1068 | + if fname == field_name { | |
| 1069 | + affected.push((s.name.clone(), true)); | |
| 1070 | + break; | |
| 1071 | + } | |
| 1072 | + } | |
| 1073 | + } | |
| 1074 | + } | |
| 1075 | + } | |
| 1076 | + popup.affected_structs = affected; | |
| 1077 | + } | |
| 1078 | + | |
| 1079 | + fn show_enum_popup(&mut self, ctx: &egui::Context) { | |
| 1080 | + if !self.enum_popup.open { | |
| 1081 | + return; | |
| 1082 | + } | |
| 1083 | + | |
| 1084 | + let mut open = true; | |
| 1085 | + let mut confirmed = false; | |
| 1086 | + let mut cancelled = false; | |
| 1087 | + | |
| 1088 | + egui::Window::new("Convert to Enum") | |
| 1089 | + .open(&mut open) | |
| 1090 | + .collapsible(false) | |
| 1091 | + .resizable(true) | |
| 1092 | + .default_width(420.0) | |
| 1093 | + .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) | |
| 1094 | + .show(ctx, |ui| { | |
| 1095 | + ui.spacing_mut().item_spacing.y = 6.0; | |
| 1096 | + | |
| 1097 | + // Enum name | |
| 1098 | + ui.horizontal(|ui| { | |
| 1099 | + ui.label( | |
| 1100 | + RichText::new("Enum name") | |
| 1101 | + .color(CatppuccinMocha::SUBTEXT0) | |
| 1102 | + .small(), | |
| 1103 | + ); | |
| 1104 | + ui.text_edit_singleline(&mut self.enum_popup.enum_name); | |
| 1105 | + }); | |
| 1106 | + | |
| 1107 | + ui.add_space(4.0); | |
| 1108 | + | |
| 1109 | + // Tag with existing enum | |
| 1110 | + if !self.enum_popup.similar_enums.is_empty() { | |
| 1111 | + ui.label( | |
| 1112 | + RichText::new("Tag with existing enum") | |
| 1113 | + .color(CatppuccinMocha::SUBTEXT0) | |
| 1114 | + .small(), | |
| 1115 | + ); | |
| 1116 | + ui.add_space(2.0); | |
| 1117 | + | |
| 1118 | + let mut new_selection = self.enum_popup.selected_existing.clone(); | |
| 1119 | + | |
| 1120 | + // "New enum" option | |
| 1121 | + let is_new = self.enum_popup.selected_existing.is_none(); | |
| 1122 | + if ui.add(egui::RadioButton::new(is_new, | |
| 1123 | + RichText::new("Create new enum").size(12.0) | |
| 1124 | + )).clicked() { | |
| 1125 | + new_selection = None; | |
| 1126 | + } | |
| 1127 | + | |
| 1128 | + for (name, variants, sim) in &self.enum_popup.similar_enums { | |
| 1129 | + let is_selected = self.enum_popup.selected_existing.as_ref() == Some(name); | |
| 1130 | + let pct = (sim * 100.0) as u32; | |
| 1131 | + let label = format!("{} ({} variants, {}% similar)", name, variants.len(), pct); | |
| 1132 | + if ui.add(egui::RadioButton::new(is_selected, | |
| 1133 | + RichText::new(label).size(12.0) | |
| 1134 | + )).clicked() { | |
| 1135 | + new_selection = Some(name.clone()); | |
| 1136 | + } | |
| 1137 | + } | |
| 1138 | + | |
| 1139 | + if new_selection != self.enum_popup.selected_existing { | |
| 1140 | + self.enum_popup.selected_existing = new_selection.clone(); | |
| 1141 | + // Update enum name and preview | |
| 1142 | + if let Some(ref existing_name) = new_selection { | |
| 1143 | + self.enum_popup.enum_name = existing_name.clone(); | |
| 1144 | + // Merge variants | |
| 1145 | + if let Some((_, existing_variants, _)) = self.enum_popup.similar_enums | |
| 1146 | + .iter() | |
| 1147 | + .find(|(n, _, _)| n == existing_name) | |
| 1148 | + { | |
| 1149 | + let mut merged: BTreeSet<String> = existing_variants.iter().cloned().collect(); | |
| 1150 | + for v in &self.enum_popup.values { | |
| 1151 | + merged.insert(v.clone()); | |
| 1152 | + } | |
| 1153 | + self.enum_popup.preview_variants = merged.into_iter().collect(); | |
| 1154 | + } | |
| 1155 | + } else { | |
| 1156 | + self.enum_popup.enum_name = field_to_enum_name(&self.enum_popup.field_name); | |
| 1157 | + self.enum_popup.preview_variants = self.enum_popup.values.clone(); | |
| 1158 | + } | |
| 1159 | + } | |
| 1160 | + | |
| 1161 | + ui.add_space(4.0); | |
| 1162 | + } | |
| 1163 | + | |
| 1164 | + // Affected structs | |
| 1165 | + ui.label( | |
| 1166 | + RichText::new("Apply to structs") | |
| 1167 | + .color(CatppuccinMocha::SUBTEXT0) | |
| 1168 | + .small(), | |
| 1169 | + ); | |
| 1170 | + ui.add_space(2.0); | |
| 1171 | + for (name, checked) in &mut self.enum_popup.affected_structs { | |
| 1172 | + ui.checkbox(checked, | |
| 1173 | + RichText::new(format!("{}.{}", name, self.enum_popup.field_name)) | |
| 1174 | + .size(12.0) | |
| 1175 | + .family(egui::FontFamily::Monospace) | |
| 1176 | + ); | |
| 1177 | + } | |
| 1178 | + | |
| 1179 | + ui.add_space(6.0); | |
| 1180 | + | |
| 1181 | + // Preview | |
| 1182 | + ui.label( | |
| 1183 | + RichText::new("Preview") | |
| 1184 | + .color(CatppuccinMocha::SUBTEXT0) | |
| 1185 | + .small(), | |
| 1186 | + ); | |
| 1187 | + ui.add_space(2.0); | |
| 1188 | + egui::Frame::new() | |
| 1189 | + .fill(CatppuccinMocha::CRUST) | |
| 1190 | + .corner_radius(6.0) | |
| 1191 | + .inner_margin(8.0) | |
| 1192 | + .show(ui, |ui| { | |
| 1193 | + let enum_name = &self.enum_popup.enum_name; | |
| 1194 | + ui.label( | |
| 1195 | + RichText::new(format!("pub enum {} {{", enum_name)) | |
| 1196 | + .color(CatppuccinMocha::MAUVE) | |
| 1197 | + .family(egui::FontFamily::Monospace) | |
| 1198 | + .size(11.0), | |
| 1199 | + ); | |
| 1200 | + for v in &self.enum_popup.preview_variants { | |
| 1201 | + let variant = to_enum_variant(v); | |
| 1202 | + let is_new = self.enum_popup.selected_existing.is_some() | |
| 1203 | + && !self.enum_popup.similar_enums | |
| 1204 | + .iter() | |
| 1205 | + .find(|(n, _, _)| Some(n) == self.enum_popup.selected_existing.as_ref()) | |
| 1206 | + .map(|(_, existing, _)| existing.contains(v)) | |
| 1207 | + .unwrap_or(false); | |
| 1208 | + let color = if is_new { | |
| 1209 | + CatppuccinMocha::GREEN | |
| 1210 | + } else { | |
| 1211 | + CatppuccinMocha::TEXT | |
| 1212 | + }; | |
| 1213 | + ui.label( | |
| 1214 | + RichText::new(format!(" {},", variant)) | |
| 1215 | + .color(color) | |
| 1216 | + .family(egui::FontFamily::Monospace) | |
| 1217 | + .size(11.0), | |
| 1218 | + ); | |
| 1219 | + } | |
| 1220 | + ui.label( | |
| 1221 | + RichText::new("}") | |
| 1222 | + .color(CatppuccinMocha::MAUVE) | |
| 1223 | + .family(egui::FontFamily::Monospace) | |
| 1224 | + .size(11.0), | |
| 1225 | + ); | |
| 1226 | + }); | |
| 1227 | + | |
| 1228 | + ui.add_space(8.0); | |
| 1229 | + | |
| 1230 | + // Buttons | |
| 1231 | + ui.horizontal(|ui| { | |
| 1232 | + if ui.add( | |
| 1233 | + egui::Button::new( | |
| 1234 | + RichText::new("Convert") | |
| 1235 | + .color(CatppuccinMocha::BASE) | |
| 1236 | + .strong(), | |
| 1237 | + ) | |
| 1238 | + .fill(CatppuccinMocha::GREEN) | |
| 1239 | + .corner_radius(6.0), | |
| 1240 | + ).clicked() { | |
| 1241 | + confirmed = true; | |
| 1242 | + } | |
| 1243 | + if ui.add( | |
| 1244 | + egui::Button::new("Cancel") | |
| 1245 | + .fill(CatppuccinMocha::SURFACE0) | |
| 1246 | + .corner_radius(6.0), | |
| 1247 | + ).clicked() { | |
| 1248 | + cancelled = true; | |
| 1249 | + } | |
| 1250 | + }); | |
| 1251 | + }); | |
| 1252 | + | |
| 1253 | + if confirmed { | |
| 1254 | + // Apply the conversion | |
| 1255 | + let field_name = self.enum_popup.field_name.clone(); | |
| 1256 | + let enum_name = self.enum_popup.enum_name.clone(); | |
| 1257 | + let variants = self.enum_popup.preview_variants.clone(); | |
| 1258 | + | |
| 1259 | + // If tagging with existing, update that enum's variants | |
| 1260 | + if let Some(ref existing) = self.enum_popup.selected_existing { | |
| 1261 | + // Find the field_name(s) that use this existing enum and update variants | |
| 1262 | + let field_keys: Vec<String> = self.converted_enums | |
| 1263 | + .iter() | |
| 1264 | + .filter(|(_, (name, _))| name == existing) | |
| 1265 | + .map(|(k, _)| k.clone()) | |
| 1266 | + .collect(); | |
| 1267 | + for k in field_keys { | |
| 1268 | + self.converted_enums.get_mut(&k).unwrap().1 = variants.clone(); | |
| 1269 | + } | |
| 1270 | + } | |
| 1271 | + | |
| 1272 | + self.converted_enums.insert( | |
| 1273 | + field_name.clone(), | |
| 1274 | + (enum_name, variants), | |
| 1275 | + ); | |
| 1276 | + self.enum_candidates.remove(&field_name); | |
| 1277 | + self.cache_key = 0; | |
| 1278 | + self.enum_popup.open = false; | |
| 1279 | + } else if !open || cancelled { | |
| 1280 | + self.enum_popup.open = false; | |
| 1281 | + } | |
| 1282 | + } | |
| 1283 | +} | |
| 1284 | + | |
| 1285 | +/// Wrapper around codegen::first_normal_word that singularizes + PascalCases the result, | |
| 1286 | +/// matching the naming scheme used by schema disambiguation. | |
| 1287 | +fn first_normal_word(filename: &str) -> Option<String> { | |
| 1288 | + crate::codegen::first_normal_word(filename).map(|w| { | |
| 1289 | + let singular = crate::codegen::singularize(&w); | |
| 1290 | + crate::codegen::to_pascal_case(&singular) | |
| 1291 | + }) | |
| 1292 | +} | |
| 1293 | + | |
| 1294 | +struct NamedCodeBlock { | |
| 1295 | + name: String, | |
| 1296 | + code: String, | |
| 1297 | +} | |
| 1298 | + | |
| 1299 | +/// Generate individual struct blocks for a file (used for grouping/merging) | |
| 1300 | +fn generate_file_code_structs( | |
| 1301 | + value: &serde_json::Value, | |
| 1302 | + shared_names: &std::collections::BTreeSet<String>, | |
| 1303 | + unique_names: &std::collections::BTreeSet<String>, | |
| 1304 | + filename: &str, | |
| 1305 | + lang: &dyn crate::lang::LanguageGenerator, | |
| 1306 | +) -> Vec<NamedCodeBlock> { | |
| 1307 | + let prefix = first_normal_word(filename).unwrap_or_default(); | |
| 1308 | + let is_root_array = value.is_array(); | |
| 1309 | + | |
| 1310 | + let (root_name, array_item_name) = if is_root_array { | |
| 1311 | + let singular = crate::codegen::singularize(&prefix); | |
| 1312 | + let item_name = if singular.is_empty() { | |
| 1313 | + "Item".to_string() | |
| 1314 | + } else { | |
| 1315 | + let mut s = String::new(); | |
| 1316 | + s.push(singular.chars().next().unwrap().to_ascii_uppercase()); | |
| 1317 | + s.extend(singular.chars().skip(1)); | |
| 1318 | + s | |
| 1319 | + }; | |
| 1320 | + (item_name.clone(), Some(item_name)) | |
| 1321 | + } else { | |
| 1322 | + let name = if prefix.is_empty() { | |
| 1323 | + "Root".to_string() | |
| 1324 | + } else { | |
| 1325 | + format!("{}Root", prefix) | |
| 1326 | + }; | |
| 1327 | + (name, None) | |
| 1328 | + }; | |
| 1329 | + | |
| 1330 | + let gen = CodeGenerator::from_value_named(value, &root_name); | |
| 1331 | + let mut blocks = Vec::new(); | |
| 1332 | + | |
| 1333 | + // Type alias for array roots | |
| 1334 | + if let Some(ref item_name) = array_item_name { | |
| 1335 | + let aliased = if shared_names.contains(item_name) { | |
| 1336 | + item_name.clone() | |
| 1337 | + } else if !prefix.is_empty() && !item_name.starts_with(&prefix) { | |
| 1338 | + format!("{}{}", prefix, item_name) | |
| 1339 | + } else { | |
| 1340 | + item_name.clone() | |
| 1341 | + }; | |
| 1342 | + blocks.push(NamedCodeBlock { | |
| 1343 | + name: format!("{}Root", prefix), | |
| 1344 | + code: format!("pub type {}Root = Vec<{}>;\n", prefix, aliased), | |
| 1345 | + }); | |
| 1346 | + } | |
| 1347 | + | |
| 1348 | + // Combine shared + unique names for type resolution | |
| 1349 | + let all_schema_names: std::collections::BTreeSet<String> = shared_names | |
| 1350 | + .iter() | |
| 1351 | + .chain(unique_names.iter()) | |
| 1352 | + .cloned() | |
| 1353 | + .collect(); | |
| 1354 | + | |
| 1355 | + for s in gen.structs.iter().rev() { | |
| 1356 | + if shared_names.contains(&s.name) { | |
| 1357 | + continue; | |
| 1358 | + } | |
| 1359 | + | |
| 1360 | + // Use schema-disambiguated name if available, otherwise prefix as before | |
| 1361 | + let prefixed = format!("{}{}", prefix, s.name); | |
| 1362 | + let struct_name = if unique_names.contains(&prefixed) { | |
| 1363 | + prefixed | |
| 1364 | + } else if unique_names.contains(&s.name) { | |
| 1365 | + s.name.clone() | |
| 1366 | + } else if s.name != root_name && !prefix.is_empty() && !s.name.starts_with(&prefix) { | |
| 1367 | + prefixed | |
| 1368 | + } else { | |
| 1369 | + s.name.clone() | |
| 1370 | + }; | |
| 1371 | + | |
| 1372 | + let mut code = String::new(); | |
| 1373 | + code.push_str(&lang.struct_open(&struct_name)); | |
| 1374 | + let mut field_pairs: Vec<(String, String)> = Vec::new(); | |
| 1375 | + for field in &s.fields { | |
| 1376 | + let code_name = lang.field_name(&field.json_name); | |
| 1377 | + let base_type = field.resolved_type.clone().unwrap_or_else(|| lang.type_name(&field.inferred_type)); | |
| 1378 | + let resolved_type = if !prefix.is_empty() && !all_schema_names.contains(&base_type) { | |
| 1379 | + prefix_type(&base_type, &prefix, &all_schema_names, &root_name) | |
| 1380 | + } else { | |
| 1381 | + base_type | |
| 1382 | + }; | |
| 1383 | + code.push_str(&lang.field_line(&code_name, &resolved_type)); | |
| 1384 | + field_pairs.push((code_name, field.json_name.clone())); | |
| 1385 | + } | |
| 1386 | + code.push_str(&lang.struct_close(&field_pairs)); | |
| 1387 | + | |
| 1388 | + blocks.push(NamedCodeBlock { | |
| 1389 | + name: struct_name, | |
| 1390 | + code, | |
| 1391 | + }); | |
| 1392 | + } | |
| 1393 | + | |
| 1394 | + blocks | |
| 1395 | +} | |
| 1396 | + | |
| 1397 | +/// Prefix struct references in type strings (handles Vec<T>, Option<T>, bare T) | |
| 1398 | +fn prefix_type( | |
| 1399 | + rust_type: &str, | |
| 1400 | + prefix: &str, | |
| 1401 | + shared_names: &std::collections::BTreeSet<String>, | |
| 1402 | + root_name: &str, | |
| 1403 | +) -> String { | |
| 1404 | + if rust_type.starts_with("Vec<") && rust_type.ends_with('>') { | |
| 1405 | + let inner = &rust_type[4..rust_type.len() - 1]; | |
| 1406 | + let prefixed_inner = prefix_type(inner, prefix, shared_names, root_name); | |
| 1407 | + format!("Vec<{}>", prefixed_inner) | |
| 1408 | + } else if rust_type.starts_with("Option<") && rust_type.ends_with('>') { | |
| 1409 | + let inner = &rust_type[7..rust_type.len() - 1]; | |
| 1410 | + let prefixed_inner = prefix_type(inner, prefix, shared_names, root_name); | |
| 1411 | + format!("Option<{}>", prefixed_inner) | |
| 1412 | + } else if is_struct_name(rust_type) | |
| 1413 | + && !shared_names.contains(rust_type) | |
| 1414 | + && rust_type != root_name | |
| 1415 | + && !rust_type.starts_with(prefix) | |
| 1416 | + { | |
| 1417 | + format!("{}{}", prefix, rust_type) | |
| 1418 | + } else { | |
| 1419 | + rust_type.to_string() | |
| 1420 | + } | |
| 1421 | +} | |
| 1422 | + | |
| 1423 | +/// Check if a type name looks like a struct (PascalCase, not a primitive) | |
| 1424 | +fn is_struct_name(s: &str) -> bool { | |
| 1425 | + let first = s.chars().next().unwrap_or('a'); | |
| 1426 | + first.is_ascii_uppercase() | |
| 1427 | + && !s.contains('<') | |
| 1428 | + && !matches!( | |
| 1429 | + s, | |
| 1430 | + "String" | "Vec" | "Option" | "bool" | "i64" | "u64" | "f64" | "i32" | "u32" | "f32" | |
| 1431 | + | "NaiveDate" | "NaiveTime" | |
| 1432 | + ) | |
| 1433 | +} | |
| 1434 | + | |
| 1435 | +enum LineAction { | |
| 1436 | + None, | |
| 1437 | + NavTo((usize, usize)), | |
| 1438 | + ConvertEnum(String), | |
| 1439 | + RevertEnum(String), | |
| 1440 | + ToggleField(String), // "StructName.field_name" | |
| 1441 | + EditEnum(String), // field_name key into converted_enums | |
| 1442 | +} | |
| 1443 | + | |
| 1444 | +/// Render a line of code with interactive elements, language-aware. | |
| 1445 | +fn render_code_line( | |
| 1446 | + ui: &mut Ui, | |
| 1447 | + line: &str, | |
| 1448 | + struct_index: &BTreeMap<String, (usize, usize)>, | |
| 1449 | + field_values: &BTreeMap<String, Vec<String>>, | |
| 1450 | + enum_candidates: &BTreeSet<String>, | |
| 1451 | + converted_enums: &BTreeMap<String, (String, Vec<String>)>, | |
| 1452 | + hidden_fields: &BTreeSet<String>, | |
| 1453 | + current_struct: Option<&str>, | |
| 1454 | + enum_usages: &BTreeMap<String, Vec<(String, String, String)>>, | |
| 1455 | + current_enum: Option<&str>, | |
| 1456 | + language: crate::lang::CodeLanguage, | |
| 1457 | +) -> LineAction { | |
| 1458 | + let trimmed = line.trim(); | |
| 1459 | + let is_swift = language == crate::lang::CodeLanguage::Swift; | |
| 1460 | + | |
| 1461 | + if trimmed.is_empty() { | |
| 1462 | + ui.label(""); | |
| 1463 | + return LineAction::None; | |
| 1464 | + } | |
| 1465 | + | |
| 1466 | + if trimmed.starts_with("//") { | |
| 1467 | + ui.label( | |
| 1468 | + RichText::new(line) | |
| 1469 | + .color(CatppuccinMocha::OVERLAY0) | |
| 1470 | + .family(egui::FontFamily::Monospace), | |
| 1471 | + ); | |
| 1472 | + return LineAction::None; | |
| 1473 | + } | |
| 1474 | + | |
| 1475 | + // Attributes: #[...] / #![...] for Rust | |
| 1476 | + if trimmed.starts_with("#[") || trimmed.starts_with("#!") { | |
| 1477 | + ui.label( | |
| 1478 | + RichText::new(line) | |
| 1479 | + .color(CatppuccinMocha::YELLOW) | |
| 1480 | + .family(egui::FontFamily::Monospace), | |
| 1481 | + ); | |
| 1482 | + return LineAction::None; | |
| 1483 | + } | |
| 1484 | + | |
| 1485 | + // Swift CodingKeys enum line | |
| 1486 | + if is_swift && trimmed.starts_with("enum CodingKeys") { | |
| 1487 | + ui.label( | |
| 1488 | + RichText::new(line) | |
| 1489 | + .color(CatppuccinMocha::YELLOW) | |
| 1490 | + .family(egui::FontFamily::Monospace), | |
| 1491 | + ); | |
| 1492 | + return LineAction::None; | |
| 1493 | + } | |
| 1494 | + | |
| 1495 | + if trimmed == "}" || trimmed == "}," { | |
| 1496 | + ui.label( | |
| 1497 | + RichText::new(line) | |
| 1498 | + .color(CatppuccinMocha::TEXT) | |
| 1499 | + .family(egui::FontFamily::Monospace), | |
| 1500 | + ); | |
| 1501 | + return LineAction::None; | |
| 1502 | + } | |
| 1503 | + | |
| 1504 | + // Handle enum declaration lines: "pub enum Name {" (Rust) or "enum Name: String, Codable {" (Swift) | |
| 1505 | + let enum_prefix = if is_swift { "enum " } else { "pub enum " }; | |
| 1506 | + if trimmed.starts_with(enum_prefix) && !trimmed.starts_with("enum CodingKeys") { | |
| 1507 | + ui.spacing_mut().item_spacing.x = 0.0; | |
| 1508 | + let indent = &line[..line.len() - line.trim_start().len()]; | |
| 1509 | + if !indent.is_empty() { | |
| 1510 | + ui.label(RichText::new(indent).family(egui::FontFamily::Monospace)); | |
| 1511 | + } | |
| 1512 | + ui.label( | |
| 1513 | + RichText::new(enum_prefix) | |
| 1514 | + .color(CatppuccinMocha::MAUVE) | |
| 1515 | + .family(egui::FontFamily::Monospace), | |
| 1516 | + ); | |
| 1517 | + let rest = &trimmed[enum_prefix.len()..]; | |
| 1518 | + let name = rest.split(|c: char| c == ' ' || c == '{' || c == ':').next().unwrap_or(""); | |
| 1519 | + let after = &rest[name.len()..]; | |
| 1520 | + | |
| 1521 | + // Check if this enum is a converted enum (editable) | |
| 1522 | + let enum_field_key = converted_enums | |
| 1523 | + .iter() | |
| 1524 | + .find(|(_, (en, _))| en == name) | |
| 1525 | + .map(|(field_name, _)| field_name.clone()); | |
| 1526 | + | |
| 1527 | + let mut action = LineAction::None; | |
| 1528 | + let label = if enum_field_key.is_some() { | |
| 1529 | + // Check previous-frame hover state via cached rect | |
| 1530 | + let hover_id = egui::Id::new("enum_hover").with(name); | |
| 1531 | + let prev_rect: Option<egui::Rect> = ui.ctx().data(|d| d.get_temp(hover_id)); | |
| 1532 | + let is_hovered = prev_rect.map_or(false, |r| { | |
| 1533 | + ui.input(|i| i.pointer.hover_pos().map_or(false, |p| r.contains(p))) | |
| 1534 | + }); | |
| 1535 | + | |
| 1536 | + let mut text = RichText::new(name) | |
| 1537 | + .color(CatppuccinMocha::YELLOW) | |
| 1538 | + .family(egui::FontFamily::Monospace); | |
| 1539 | + if is_hovered { | |
| 1540 | + text = text.size(ui.text_style_height(&egui::TextStyle::Body) + 1.5); | |
| 1541 | + } | |
| 1542 | + | |
| 1543 | + let link = ui.add(egui::Label::new(text).sense(egui::Sense::click())); | |
| 1544 | + | |
| 1545 | + // Cache rect for next frame | |
| 1546 | + ui.ctx().data_mut(|d| d.insert_temp(hover_id, link.rect)); | |
| 1547 | + | |
| 1548 | + if is_hovered { | |
| 1549 | + // Light green background pill | |
| 1550 | + ui.painter().rect_filled( | |
| 1551 | + link.rect.expand(1.0), | |
| 1552 | + 3.0, | |
| 1553 | + egui::Color32::from_rgba_unmultiplied(166, 227, 161, 35), | |
| 1554 | + ); | |
| 1555 | + ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); | |
| 1556 | + } | |
| 1557 | + if link.clicked() { | |
| 1558 | + action = LineAction::EditEnum(enum_field_key.unwrap()); | |
| 1559 | + } | |
| 1560 | + link | |
| 1561 | + } else { | |
| 1562 | + ui.label( | |
| 1563 | + RichText::new(name) | |
| 1564 | + .color(CatppuccinMocha::YELLOW) | |
| 1565 | + .family(egui::FontFamily::Monospace), | |
| 1566 | + ) | |
| 1567 | + }; | |
| 1568 | + // Usage tooltip | |
| 1569 | + if let Some(usages) = enum_usages.get(name) { | |
| 1570 | + if !usages.is_empty() { | |
| 1571 | + label.on_hover_ui(|ui| { | |
| 1572 | + ui.style_mut().spacing.item_spacing.y = 2.0; | |
| 1573 | + ui.label( | |
| 1574 | + RichText::new("Used by") | |
| 1575 | + .color(CatppuccinMocha::BLUE) | |
| 1576 | + .small() | |
| 1577 | + .strong(), | |
| 1578 | + ); | |
| 1579 | + ui.separator(); | |
| 1580 | + for (file_name, struct_name, field_name) in usages { | |
| 1581 | + ui.label( | |
| 1582 | + RichText::new(format!("{}::{}.{}", file_name.trim_end_matches(".rs").trim_end_matches(".swift"), struct_name, field_name)) | |
| 1583 | + .color(CatppuccinMocha::TEXT) | |
| 1584 | + .family(egui::FontFamily::Monospace) | |
| 1585 | + .small(), | |
| 1586 | + ); | |
| 1587 | + } | |
| 1588 | + }); | |
| 1589 | + } | |
| 1590 | + } | |
| 1591 | + if !after.is_empty() { | |
| 1592 | + ui.label( | |
| 1593 | + RichText::new(after) | |
| 1594 | + .color(CatppuccinMocha::TEXT) | |
| 1595 | + .family(egui::FontFamily::Monospace), | |
| 1596 | + ); | |
| 1597 | + } | |
| 1598 | + return action; | |
| 1599 | + } | |
| 1600 | + | |
| 1601 | + // Handle enum variant lines (inside an enum block) | |
| 1602 | + if let Some(enum_name) = current_enum { | |
| 1603 | + let is_variant = if is_swift { | |
| 1604 | + trimmed.starts_with("case ") && !trimmed.starts_with("case ") || trimmed.starts_with("case ") | |
| 1605 | + } else { | |
| 1606 | + let stripped = trimmed.trim_end_matches(','); | |
| 1607 | + !stripped.is_empty() | |
| 1608 | + && !stripped.starts_with("#[") | |
| 1609 | + && !stripped.starts_with("//") | |
| 1610 | + && stripped != "}" | |
| 1611 | + && !stripped.starts_with("pub ") | |
| 1612 | + && !stripped.starts_with("use ") | |
| 1613 | + }; | |
| 1614 | + if is_variant { | |
| 1615 | + let indent = &line[..line.len() - line.trim_start().len()]; | |
| 1616 | + ui.spacing_mut().item_spacing.x = 0.0; | |
| 1617 | + let label = ui.label( | |
| 1618 | + RichText::new(format!("{}{}", indent, trimmed)) | |
| 1619 | + .color(CatppuccinMocha::GREEN) | |
| 1620 | + .family(egui::FontFamily::Monospace), | |
| 1621 | + ); | |
| 1622 | + // Usage tooltip on variants too | |
| 1623 | + if let Some(usages) = enum_usages.get(enum_name) { | |
| 1624 | + if !usages.is_empty() { | |
| 1625 | + label.on_hover_ui(|ui| { | |
| 1626 | + ui.style_mut().spacing.item_spacing.y = 2.0; | |
| 1627 | + ui.label( | |
| 1628 | + RichText::new(format!("{} used by", enum_name)) | |
| 1629 | + .color(CatppuccinMocha::BLUE) | |
| 1630 | + .small() | |
| 1631 | + .strong(), | |
| 1632 | + ); | |
| 1633 | + ui.separator(); | |
| 1634 | + for (file_name, struct_name, field_name) in usages { | |
| 1635 | + ui.label( | |
| 1636 | + RichText::new(format!("{}::{}.{}", file_name.trim_end_matches(".rs").trim_end_matches(".swift"), struct_name, field_name)) | |
| 1637 | + .color(CatppuccinMocha::TEXT) | |
| 1638 | + .family(egui::FontFamily::Monospace) | |
| 1639 | + .small(), | |
| 1640 | + ); | |
| 1641 | + } | |
| 1642 | + }); | |
| 1643 | + } | |
| 1644 | + } | |
| 1645 | + return LineAction::None; | |
| 1646 | + } | |
| 1647 | + } | |
| 1648 | + | |
| 1649 | + let color = if trimmed.starts_with("use ") || trimmed.starts_with("import ") { | |
| 1650 | + CatppuccinMocha::MAUVE | |
| 1651 | + } else if trimmed.starts_with("pub struct ") || trimmed.starts_with("struct ") { | |
| 1652 | + CatppuccinMocha::MAUVE | |
| 1653 | + } else { | |
| 1654 | + CatppuccinMocha::TEXT | |
| 1655 | + }; | |
| 1656 | + | |
| 1657 | + ui.spacing_mut().item_spacing.x = 0.0; | |
| 1658 | + | |
| 1659 | + // Field lines: "pub field: Type," (Rust) or "let field: Type" (Swift) | |
| 1660 | + let is_field_line = if is_swift { | |
| 1661 | + trimmed.starts_with("let ") && trimmed.contains(':') | |
| 1662 | + } else { | |
| 1663 | + trimmed.starts_with("pub ") && trimmed.contains(':') | |
| 1664 | + }; | |
| 1665 | + | |
| 1666 | + if is_field_line { | |
| 1667 | + let indent = &line[..line.len() - line.trim_start().len()]; | |
| 1668 | + if let Some(colon_pos) = trimmed.find(':') { | |
| 1669 | + let field_part = &trimmed[..colon_pos]; | |
| 1670 | + let type_part = trimmed[colon_pos + 1..].trim(); | |
| 1671 | + | |
| 1672 | + // Extract the JSON field name | |
| 1673 | + let json_field_name = if is_swift { | |
| 1674 | + field_part.trim_start_matches("let ").trim_start_matches('`').trim_end_matches('`') | |
| 1675 | + } else { | |
| 1676 | + field_part.trim_start_matches("pub ").trim_start_matches("r#") | |
| 1677 | + }; | |
| 1678 | + | |
| 1679 | + // Check if this field is hidden | |
| 1680 | + let field_key = current_struct | |
| 1681 | + .map(|s| format!("{}.{}", s, json_field_name)) | |
| 1682 | + .unwrap_or_default(); | |
| 1683 | + let is_hidden = !field_key.is_empty() && hidden_fields.contains(&field_key); | |
| 1684 | + | |
| 1685 | + // Dim color for hidden fields | |
| 1686 | + let text_color = if is_hidden { | |
| 1687 | + CatppuccinMocha::SURFACE2 | |
| 1688 | + } else { | |
| 1689 | + CatppuccinMocha::TEXT | |
| 1690 | + }; | |
| 1691 | + | |
| 1692 | + // Toggle button on the left | |
| 1693 | + ui.label( | |
| 1694 | + RichText::new(indent) | |
| 1695 | + .family(egui::FontFamily::Monospace), | |
| 1696 | + ); | |
| 1697 | + if !field_key.is_empty() { | |
| 1698 | + if is_hidden { | |
| 1699 | + let btn = ui.add( | |
| 1700 | + egui::Button::new( | |
| 1701 | + RichText::new(egui_phosphor::regular::PLUS_CIRCLE) | |
| 1702 | + .color(CatppuccinMocha::GREEN) | |
| 1703 | + .size(14.0), | |
| 1704 | + ) | |
| 1705 | + .frame(false), | |
| 1706 | + ); | |
| 1707 | + if btn.clicked() { | |
| 1708 | + return LineAction::ToggleField(field_key); | |
| 1709 | + } | |
| 1710 | + btn.on_hover_text("Restore field"); | |
| 1711 | + } else { | |
| 1712 | + let btn = ui.add( | |
| 1713 | + egui::Button::new( | |
| 1714 | + RichText::new(egui_phosphor::regular::TRASH) | |
| 1715 | + .color(CatppuccinMocha::RED) | |
| 1716 | + .size(14.0), | |
| 1717 | + ) | |
| 1718 | + .frame(false), | |
| 1719 | + ); | |
| 1720 | + if btn.clicked() { | |
| 1721 | + return LineAction::ToggleField(field_key); | |
| 1722 | + } | |
| 1723 | + btn.on_hover_text("Exclude field"); | |
| 1724 | + } | |
| 1725 | + ui.spacing_mut().item_spacing.x = 2.0; | |
| 1726 | + } | |
| 1727 | + | |
| 1728 | + ui.label( | |
| 1729 | + RichText::new(format!("{}: ", field_part)) | |
| 1730 | + .color(text_color) | |
| 1731 | + .family(egui::FontFamily::Monospace), | |
| 1732 | + ); | |
| 1733 | + | |
| 1734 | + // Make type names clickable if they resolve to a known struct | |
| 1735 | + let type_for_lookup = if is_swift { | |
| 1736 | + type_part.trim_end_matches('?') | |
| 1737 | + } else { | |
| 1738 | + type_part.trim_end_matches(',') | |
| 1739 | + }; | |
| 1740 | + let bare_type = extract_inner_type(type_for_lookup); | |
| 1741 | + if !is_hidden { | |
| 1742 | + if let Some(&(file_idx, line_idx)) = struct_index.get(bare_type) { | |
| 1743 | + let nav = render_clickable_type(ui, type_part, bare_type, file_idx, line_idx); | |
| 1744 | + // Show revert button for converted enum fields (before early return) | |
| 1745 | + if converted_enums.contains_key(json_field_name) { | |
| 1746 | + ui.spacing_mut().item_spacing.x = 4.0; | |
| 1747 | + let btn = ui.add( | |
| 1748 | + egui::Button::new( | |
| 1749 | + RichText::new(format!("{} revert", egui_phosphor::regular::ARROW_U_UP_LEFT)) | |
| 1750 | + .color(CatppuccinMocha::PEACH) | |
| 1751 | + .small(), | |
| 1752 | + ) | |
| 1753 | + .frame(false), | |
| 1754 | + ); | |
| 1755 | + if btn.clicked() { | |
| 1756 | + return LineAction::RevertEnum(json_field_name.to_string()); | |
| 1757 | + } | |
| 1758 | + btn.on_hover_text("Revert to String"); | |
| 1759 | + } | |
| 1760 | + if let Some(target) = nav { | |
| 1761 | + return LineAction::NavTo(target); | |
| 1762 | + } | |
| 1763 | + return LineAction::None; | |
| 1764 | + } | |
| 1765 | + } | |
| 1766 | + | |
| 1767 | + let type_color = if is_hidden { | |
| 1768 | + CatppuccinMocha::SURFACE2 | |
| 1769 | + } else { | |
| 1770 | + classify_type_color(type_for_lookup) | |
| 1771 | + }; | |
| 1772 | + let type_label = ui.label( | |
| 1773 | + RichText::new(type_part) | |
| 1774 | + .color(type_color) | |
| 1775 | + .family(egui::FontFamily::Monospace), | |
| 1776 | + ); | |
| 1777 | + | |
| 1778 | + // Show sample values tooltip for String fields | |
| 1779 | + let clean_type = type_for_lookup; | |
| 1780 | + let is_string_type = clean_type == "String" || clean_type == "Option<String>" || clean_type == "String?"; | |
| 1781 | + if !is_hidden && is_string_type && !json_field_name.is_empty() { | |
| 1782 | + if let Some(values) = field_values.get(json_field_name) { | |
| 1783 | + if !values.is_empty() { | |
| 1784 | + type_label.on_hover_ui(|ui| { | |
| 1785 | + ui.style_mut().spacing.item_spacing.y = 2.0; | |
| 1786 | + ui.label( | |
| 1787 | + RichText::new(format!("Values for \"{}\"", json_field_name)) | |
| 1788 | + .color(CatppuccinMocha::BLUE) | |
| 1789 | + .small() | |
| 1790 | + .strong(), | |
| 1791 | + ); | |
| 1792 | + ui.separator(); | |
| 1793 | + for val in values { | |
| 1794 | + let display = if val.len() > 60 { | |
| 1795 | + format!("{}...", &val[..57]) | |
| 1796 | + } else { | |
| 1797 | + val.clone() | |
| 1798 | + }; | |
| 1799 | + ui.label( | |
| 1800 | + RichText::new(display) | |
| 1801 | + .color(CatppuccinMocha::GREEN) | |
| 1802 | + .family(egui::FontFamily::Monospace) | |
| 1803 | + .small(), | |
| 1804 | + ); | |
| 1805 | + } | |
| 1806 | + if values.len() >= 30 { | |
| 1807 | + ui.label( | |
| 1808 | + RichText::new("...and more") | |
| 1809 | + .color(CatppuccinMocha::OVERLAY0) | |
| 1810 | + .small(), | |
| 1811 | + ); | |
| 1812 | + } | |
| 1813 | + }); | |
| 1814 | + } | |
| 1815 | + } | |
| 1816 | + | |
| 1817 | + // Show "-> enum" button for candidates | |
| 1818 | + if enum_candidates.contains(json_field_name) { | |
| 1819 | + ui.spacing_mut().item_spacing.x = 4.0; | |
| 1820 | + let btn = ui.add( | |
| 1821 | + egui::Button::new( | |
| 1822 | + RichText::new(format!("{} enum", egui_phosphor::regular::ARROW_RIGHT)) | |
| 1823 | + .color(CatppuccinMocha::TEAL) | |
| 1824 | + .small(), | |
| 1825 | + ) | |
| 1826 | + .frame(false), | |
| 1827 | + ); | |
| 1828 | + if btn.clicked() { | |
| 1829 | + return LineAction::ConvertEnum(json_field_name.to_string()); | |
| 1830 | + } | |
| 1831 | + btn.on_hover_text("Convert to enum"); | |
| 1832 | + } | |
| 1833 | + } | |
| 1834 | + | |
| 1835 | + // Show revert button for converted enum fields | |
| 1836 | + if !is_hidden && converted_enums.contains_key(json_field_name) { | |
| 1837 | + ui.spacing_mut().item_spacing.x = 4.0; | |
| 1838 | + let btn = ui.add( | |
| 1839 | + egui::Button::new( | |
| 1840 | + RichText::new(format!("{} revert", egui_phosphor::regular::ARROW_U_UP_LEFT)) | |
| 1841 | + .color(CatppuccinMocha::PEACH) | |
| 1842 | + .small(), | |
| 1843 | + ) | |
| 1844 | + .frame(false), | |
| 1845 | + ); | |
| 1846 | + if btn.clicked() { | |
| 1847 | + return LineAction::RevertEnum(json_field_name.to_string()); | |
| 1848 | + } | |
| 1849 | + btn.on_hover_text("Revert to String"); | |
| 1850 | + } | |
| 1851 | + } else { | |
| 1852 | + ui.label( | |
| 1853 | + RichText::new(line) | |
| 1854 | + .color(color) | |
| 1855 | + .family(egui::FontFamily::Monospace), | |
| 1856 | + ); | |
| 1857 | + } | |
| 1858 | + } else { | |
| 1859 | + ui.label( | |
| 1860 | + RichText::new(line) | |
| 1861 | + .color(color) | |
| 1862 | + .family(egui::FontFamily::Monospace), | |
| 1863 | + ); | |
| 1864 | + } | |
| 1865 | + LineAction::None | |
| 1866 | +} | |
| 1867 | + | |
| 1868 | +/// Extract the innermost struct name from a type like "Vec<Option<Foo>>," -> "Foo" | |
| 1869 | +/// Also handles Swift: "[Foo]" -> "Foo", "Foo?" -> "Foo" | |
| 1870 | +fn extract_inner_type(t: &str) -> &str { | |
| 1871 | + let t = t.trim().trim_end_matches(','); | |
| 1872 | + if let Some(rest) = t.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) { | |
| 1873 | + extract_inner_type(rest) | |
| 1874 | + } else if let Some(rest) = t.strip_prefix("Option<").and_then(|s| s.strip_suffix('>')) { | |
| 1875 | + extract_inner_type(rest) | |
| 1876 | + } else if let Some(rest) = t.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { | |
| 1877 | + // Swift array [Foo] | |
| 1878 | + extract_inner_type(rest) | |
| 1879 | + } else if let Some(rest) = t.strip_suffix('?') { | |
| 1880 | + // Swift optional Foo? | |
| 1881 | + extract_inner_type(rest) | |
| 1882 | + } else { | |
| 1883 | + t | |
| 1884 | + } | |
| 1885 | +} | |
| 1886 | + | |
| 1887 | +/// Render a type string where the struct name portion is clickable. | |
| 1888 | +/// Returns Some((file_idx, line_idx)) if the user clicked. | |
| 1889 | +fn render_clickable_type( | |
| 1890 | + ui: &mut Ui, | |
| 1891 | + full_type: &str, | |
| 1892 | + struct_name: &str, | |
| 1893 | + file_idx: usize, | |
| 1894 | + line_idx: usize, | |
| 1895 | +) -> Option<(usize, usize)> { | |
| 1896 | + let mut clicked = None; | |
| 1897 | + | |
| 1898 | + // Find where the struct name appears in the full type string | |
| 1899 | + if let Some(pos) = full_type.find(struct_name) { | |
| 1900 | + let before = &full_type[..pos]; | |
| 1901 | + let after = &full_type[pos + struct_name.len()..]; | |
| 1902 | + | |
| 1903 | + if !before.is_empty() { | |
| 1904 | + let wrapper_color = classify_type_color(before.trim_end_matches(|c: char| c == '<')); | |
| 1905 | + ui.label( | |
| 1906 | + RichText::new(before) | |
| 1907 | + .color(wrapper_color) | |
| 1908 | + .family(egui::FontFamily::Monospace), | |
| 1909 | + ); | |
| 1910 | + } | |
| 1911 | + | |
| 1912 | + let type_color = classify_type_color(struct_name); | |
| 1913 | + let r = ui.add( | |
| 1914 | + egui::Label::new( | |
| 1915 | + RichText::new(struct_name) | |
| 1916 | + .color(type_color) | |
| 1917 | + .family(egui::FontFamily::Monospace) | |
| 1918 | + .underline(), | |
| 1919 | + ) | |
| 1920 | + .sense(egui::Sense::click()), | |
| 1921 | + ); | |
| 1922 | + if r.clicked() { | |
| 1923 | + clicked = Some((file_idx, line_idx)); | |
| 1924 | + } | |
| 1925 | + if r.hovered() { | |
| 1926 | + ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); | |
| 1927 | + } | |
| 1928 | + | |
| 1929 | + if !after.is_empty() { | |
| 1930 | + let suffix_color = if after.trim_end_matches(',').chars().all(|c| c == '>') { | |
| 1931 | + classify_type_color(before.trim_end_matches(|c: char| c == '<')) | |
| 1932 | + } else { | |
| 1933 | + CatppuccinMocha::TEXT | |
| 1934 | + }; | |
| 1935 | + ui.label( | |
| 1936 | + RichText::new(after) | |
| 1937 | + .color(suffix_color) | |
| 1938 | + .family(egui::FontFamily::Monospace), | |
| 1939 | + ); | |
| 1940 | + } | |
| 1941 | + } else { | |
| 1942 | + // Fallback: render as plain | |
| 1943 | + let type_color = classify_type_color(full_type.trim_end_matches(',')); | |
| 1944 | + ui.label( | |
| 1945 | + RichText::new(full_type) | |
| 1946 | + .color(type_color) | |
| 1947 | + .family(egui::FontFamily::Monospace), | |
| 1948 | + ); | |
| 1949 | + } | |
| 1950 | + | |
| 1951 | + clicked | |
| 1952 | +} | |
| 1953 | + | |
| 1954 | +fn extract_struct_blocks(lines: &[String], language: crate::lang::CodeLanguage) -> Vec<StructBlock> { | |
| 1955 | + let is_swift = language == crate::lang::CodeLanguage::Swift; | |
| 1956 | + let mut blocks = Vec::new(); | |
| 1957 | + let mut i = 0; | |
| 1958 | + while i < lines.len() { | |
| 1959 | + let trimmed = lines[i].trim(); | |
| 1960 | + let is_block_start = if is_swift { | |
| 1961 | + trimmed.starts_with("struct ") || (trimmed.starts_with("enum ") && !trimmed.starts_with("enum CodingKeys")) | |
| 1962 | + } else { | |
| 1963 | + trimmed.starts_with("#[derive") || trimmed.starts_with("pub struct") || trimmed.starts_with("pub enum") | |
| 1964 | + }; | |
| 1965 | + if is_block_start { | |
| 1966 | + let start = i; | |
| 1967 | + let mut block_lines = Vec::new(); | |
| 1968 | + let mut name = String::new(); | |
| 1969 | + let mut brace_depth: i32 = 0; | |
| 1970 | + while i < lines.len() { | |
| 1971 | + let line_trimmed = lines[i].trim(); | |
| 1972 | + if name.is_empty() { | |
| 1973 | + let found_name = line_trimmed.strip_prefix("pub struct ") | |
| 1974 | + .or_else(|| line_trimmed.strip_prefix("pub enum ")) | |
| 1975 | + .or_else(|| line_trimmed.strip_prefix("struct ")) | |
| 1976 | + .or_else(|| { | |
| 1977 | + if line_trimmed.starts_with("enum ") && !line_trimmed.starts_with("enum CodingKeys") { | |
| 1978 | + line_trimmed.strip_prefix("enum ") | |
| 1979 | + } else { | |
| 1980 | + None | |
| 1981 | + } | |
| 1982 | + }); | |
| 1983 | + if let Some(rest) = found_name { | |
| 1984 | + name = rest | |
| 1985 | + .split(|c: char| c == ' ' || c == '{' || c == '<' || c == ':') | |
| 1986 | + .next() | |
| 1987 | + .unwrap_or("") | |
| 1988 | + .to_string(); | |
| 1989 | + } | |
| 1990 | + } | |
| 1991 | + for ch in lines[i].chars() { | |
| 1992 | + if ch == '{' { brace_depth += 1; } | |
| 1993 | + if ch == '}' { brace_depth -= 1; } | |
| 1994 | + } | |
| 1995 | + block_lines.push(lines[i].as_str()); | |
| 1996 | + if brace_depth <= 0 && block_lines.len() > 1 { | |
| 1997 | + i += 1; | |
| 1998 | + break; | |
| 1999 | + } | |
| 2000 | + i += 1; | |
| 2001 | + } | |
| 2002 | + blocks.push(StructBlock { | |
| 2003 | + name, | |
| 2004 | + start_line: start, | |
| 2005 | + end_line: i.saturating_sub(1), | |
| 2006 | + text: block_lines.join("\n"), | |
| 2007 | + }); | |
| 2008 | + } else { | |
| 2009 | + i += 1; | |
| 2010 | + } | |
| 2011 | + } | |
| 2012 | + blocks | |
| 2013 | +} | |
| 2014 | + | |
| 2015 | +fn classify_type_color(type_str: &str) -> egui::Color32 { | |
| 2016 | + let t = type_str.trim(); | |
| 2017 | + // Rust wrappers | |
| 2018 | + if t.starts_with("Vec<") || t.starts_with('[') { | |
| 2019 | + CatppuccinMocha::YELLOW | |
| 2020 | + } else if t.starts_with("Option<") || t.ends_with('?') { | |
| 2021 | + CatppuccinMocha::FLAMINGO | |
| 2022 | + } else if matches!(t, "i64" | "u64" | "f64" | "i32" | "u32" | "f32" | "usize" | "Int" | "Double") { | |
| 2023 | + CatppuccinMocha::PEACH | |
| 2024 | + } else if t == "String" { | |
| 2025 | + CatppuccinMocha::GREEN | |
| 2026 | + } else if matches!(t, "bool" | "Bool") { | |
| 2027 | + CatppuccinMocha::MAUVE | |
| 2028 | + } else { | |
| 2029 | + CatppuccinMocha::LAVENDER | |
| 2030 | + } | |
| 2031 | +} | |
| 2032 | + | |
| 2033 | +/// Recursively collect string values per JSON field name, plus occurrence counts. | |
| 2034 | +fn collect_string_values( | |
| 2035 | + value: &serde_json::Value, | |
| 2036 | + out: &mut BTreeMap<String, BTreeSet<String>>, | |
| 2037 | + counts: &mut BTreeMap<String, usize>, | |
| 2038 | +) { | |
| 2039 | + match value { | |
| 2040 | + serde_json::Value::Object(map) => { | |
| 2041 | + for (key, val) in map { | |
| 2042 | + if let serde_json::Value::String(s) = val { | |
| 2043 | + out.entry(key.clone()).or_default().insert(s.clone()); | |
| 2044 | + *counts.entry(key.clone()).or_default() += 1; | |
| 2045 | + } | |
| 2046 | + collect_string_values(val, out, counts); | |
| 2047 | + } | |
| 2048 | + } | |
| 2049 | + serde_json::Value::Array(arr) => { | |
| 2050 | + for item in arr { | |
| 2051 | + collect_string_values(item, out, counts); | |
| 2052 | + } | |
| 2053 | + } | |
| 2054 | + _ => {} | |
| 2055 | + } | |
| 2056 | +} | |
| 2057 | + | |
| 2058 | +/// Convert a string value to a valid Rust enum variant name (PascalCase). | |
| 2059 | +fn to_enum_variant(s: &str) -> String { | |
| 2060 | + let mut result = String::new(); | |
| 2061 | + let mut capitalize_next = true; | |
| 2062 | + for ch in s.chars() { | |
| 2063 | + if ch == '_' || ch == '-' || ch == ' ' || ch == '.' || ch == '/' { | |
| 2064 | + capitalize_next = true; | |
| 2065 | + } else if capitalize_next { | |
| 2066 | + result.push(ch.to_ascii_uppercase()); | |
| 2067 | + capitalize_next = false; | |
| 2068 | + } else { | |
| 2069 | + result.push(ch); | |
| 2070 | + } | |
| 2071 | + } | |
| 2072 | + if result.is_empty() { | |
| 2073 | + "Unknown".to_string() | |
| 2074 | + } else if result.chars().next().unwrap().is_ascii_digit() { | |
| 2075 | + format!("V{}", result) | |
| 2076 | + } else { | |
| 2077 | + result | |
| 2078 | + } | |
| 2079 | +} | |
| 2080 | + | |
| 2081 | +/// Convert a field name to a PascalCase enum type name. | |
| 2082 | +fn field_to_enum_name(field_name: &str) -> String { | |
| 2083 | + let mut result = String::new(); | |
| 2084 | + let mut capitalize_next = true; | |
| 2085 | + for ch in field_name.chars() { | |
| 2086 | + if ch == '_' || ch == '-' { | |
| 2087 | + capitalize_next = true; | |
| 2088 | + } else if capitalize_next { | |
| 2089 | + result.push(ch.to_ascii_uppercase()); | |
| 2090 | + capitalize_next = false; | |
| 2091 | + } else { | |
| 2092 | + result.push(ch); | |
| 2093 | + } | |
| 2094 | + } | |
| 2095 | + result | |
| 2096 | +} | |
| 2097 | + | |
| 2098 | +/// Heuristic: does this string look like an enum variant? | |
| 2099 | +/// Short, no newlines, looks like an identifier or code. | |
| 2100 | +fn looks_like_enum_value(s: &str) -> bool { | |
| 2101 | + if s.is_empty() || s.len() > 60 { | |
| 2102 | + return false; | |
| 2103 | + } | |
| 2104 | + // Allow PascalCase, camelCase, SCREAMING_SNAKE, kebab-case, or short codes | |
| 2105 | + let has_only_valid = s.chars().all(|c| { | |
| 2106 | + c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/' | |
| 2107 | + }); | |
| 2108 | + if has_only_valid { | |
| 2109 | + return true; | |
| 2110 | + } | |
| 2111 | + // Allow short phrases (≤3 words) like "In Progress" | |
| 2112 | + let words: Vec<&str> = s.split_whitespace().collect(); | |
| 2113 | + words.len() <= 3 && words.iter().all(|w| w.len() <= 20) | |
| 2114 | +} | |
| 2115 | + | |
| 2116 | +/// Filter hidden fields from a struct block text for copying. | |
| 2117 | +fn filter_hidden_fields_from_block( | |
| 2118 | + block_text: &str, | |
| 2119 | + struct_name: &str, | |
| 2120 | + hidden_fields: &BTreeSet<String>, | |
| 2121 | +) -> String { | |
| 2122 | + let mut result = Vec::new(); | |
| 2123 | + let mut skip_serde_rename = false; | |
| 2124 | + for line in block_text.lines() { | |
| 2125 | + let trimmed = line.trim(); | |
| 2126 | + // Check if this is a field line: "pub field: Type," (Rust) or "let field: Type" (Swift) | |
| 2127 | + let field_name = if trimmed.starts_with("pub ") && trimmed.contains(':') { | |
| 2128 | + trimmed.strip_prefix("pub ") | |
| 2129 | + .and_then(|s| s.split(':').next()) | |
| 2130 | + .map(|s| s.trim().trim_start_matches("r#")) | |
| 2131 | + } else if trimmed.starts_with("let ") && trimmed.contains(':') { | |
| 2132 | + trimmed.strip_prefix("let ") | |
| 2133 | + .and_then(|s| s.split(':').next()) | |
| 2134 | + .map(|s| s.trim().trim_start_matches('`').trim_end_matches('`')) | |
| 2135 | + } else { | |
| 2136 | + None | |
| 2137 | + }; | |
| 2138 | + if let Some(fname) = field_name { | |
| 2139 | + let key = format!("{}.{}", struct_name, fname); | |
| 2140 | + if hidden_fields.contains(&key) { | |
| 2141 | + skip_serde_rename = false; | |
| 2142 | + continue; | |
| 2143 | + } | |
| 2144 | + } | |
| 2145 | + // Skip #[serde(rename = ...)] lines that precede a hidden field | |
| 2146 | + if trimmed.starts_with("#[serde(rename") { | |
| 2147 | + skip_serde_rename = true; | |
| 2148 | + result.push(line.to_string()); | |
| 2149 | + continue; | |
| 2150 | + } | |
| 2151 | + if skip_serde_rename { | |
| 2152 | + skip_serde_rename = false; | |
| 2153 | + let next_field = if trimmed.starts_with("pub ") && trimmed.contains(':') { | |
| 2154 | + trimmed.strip_prefix("pub ") | |
| 2155 | + .and_then(|s| s.split(':').next()) | |
| 2156 | + .map(|s| s.trim().trim_start_matches("r#")) | |
| 2157 | + } else { | |
| 2158 | + None | |
| 2159 | + }; | |
| 2160 | + if let Some(fname) = next_field { | |
| 2161 | + let key = format!("{}.{}", struct_name, fname); | |
| 2162 | + if hidden_fields.contains(&key) { | |
| 2163 | + result.pop(); | |
| 2164 | + continue; | |
| 2165 | + } | |
| 2166 | + } | |
| 2167 | + } | |
| 2168 | + result.push(line.to_string()); | |
| 2169 | + } | |
| 2170 | + result.join("\n") | |
| 2171 | +} | |
| 2172 | + | |
| 2173 | +/// Filter hidden fields from an entire generated file for "Copy All". | |
| 2174 | +fn filter_hidden_fields_from_code( | |
| 2175 | + file: &GeneratedFile, | |
| 2176 | + hidden_fields: &BTreeSet<String>, | |
| 2177 | +) -> String { | |
| 2178 | + if hidden_fields.is_empty() { | |
| 2179 | + return file.code.clone(); | |
| 2180 | + } | |
| 2181 | + let mut result = Vec::new(); | |
| 2182 | + let mut skip_serde_rename = false; | |
| 2183 | + | |
| 2184 | + for (i, line) in file.lines.iter().enumerate() { | |
| 2185 | + let trimmed = line.trim(); | |
| 2186 | + | |
| 2187 | + // Track which struct we're in | |
| 2188 | + let current_struct: Option<&str> = file | |
| 2189 | + .structs | |
| 2190 | + .iter() | |
| 2191 | + .find(|s| i >= s.start_line && i <= s.end_line) | |
| 2192 | + .map(|s| s.name.as_str()); | |
| 2193 | + | |
| 2194 | + if let Some(struct_name) = current_struct { | |
| 2195 | + // Detect field lines for both Rust and Swift | |
| 2196 | + let field_name_opt = if trimmed.starts_with("pub ") && trimmed.contains(':') && !trimmed.contains("struct ") { | |
| 2197 | + trimmed.strip_prefix("pub ") | |
| 2198 | + .and_then(|s| s.split(':').next()) | |
| 2199 | + .map(|s| s.trim().trim_start_matches("r#")) | |
| 2200 | + } else if trimmed.starts_with("let ") && trimmed.contains(':') { | |
| 2201 | + trimmed.strip_prefix("let ") | |
| 2202 | + .and_then(|s| s.split(':').next()) | |
| 2203 | + .map(|s| s.trim().trim_start_matches('`').trim_end_matches('`')) | |
| 2204 | + } else { | |
| 2205 | + None | |
| 2206 | + }; | |
| 2207 | + if let Some(field_name) = field_name_opt { | |
| 2208 | + let key = format!("{}.{}", struct_name, field_name); | |
| 2209 | + if hidden_fields.contains(&key) { | |
| 2210 | + if skip_serde_rename { | |
| 2211 | + result.pop(); | |
| 2212 | + } | |
| 2213 | + skip_serde_rename = false; | |
| 2214 | + continue; | |
| 2215 | + } | |
| 2216 | + } | |
| 2217 | + if trimmed.starts_with("#[serde(rename") { | |
| 2218 | + skip_serde_rename = true; | |
| 2219 | + result.push(line.clone()); | |
| 2220 | + continue; | |
| 2221 | + } | |
| 2222 | + } | |
| 2223 | + skip_serde_rename = false; | |
| 2224 | + result.push(line.clone()); | |
| 2225 | + } | |
| 2226 | + result.join("\n") + "\n" | |
| 2227 | +} | |
| 2228 | + |
+626-0src/views/jq.rs
| @@ -0,0 +1,626 @@ | ||
| 1 | +use egui::{self, RichText, Ui}; | |
| 2 | + | |
| 3 | +use crate::jq_engine::JqEngine; | |
| 4 | +use crate::theme::CatppuccinMocha; | |
| 5 | + | |
| 6 | +struct ResultLine { | |
| 7 | + text: String, | |
| 8 | + color: egui::Color32, | |
| 9 | +} | |
| 10 | + | |
| 11 | +pub struct JqView { | |
| 12 | + pub query: String, | |
| 13 | + pub history: Vec<String>, | |
| 14 | + previous_query: Option<String>, | |
| 15 | + last_executed_query: String, | |
| 16 | + cached_result: Option<crate::jq_engine::JqResult>, | |
| 17 | + cached_lines: Vec<ResultLine>, | |
| 18 | + completions: Vec<String>, | |
| 19 | + completion_index: usize, | |
| 20 | + show_completions: bool, | |
| 21 | + refocus: bool, | |
| 22 | +} | |
| 23 | + | |
| 24 | +impl JqView { | |
| 25 | + pub fn new() -> Self { | |
| 26 | + Self { | |
| 27 | + query: ".".to_string(), | |
| 28 | + history: Vec::new(), | |
| 29 | + previous_query: None, | |
| 30 | + last_executed_query: String::new(), | |
| 31 | + cached_result: None, | |
| 32 | + cached_lines: Vec::new(), | |
| 33 | + completions: Vec::new(), | |
| 34 | + completion_index: 0, | |
| 35 | + show_completions: false, | |
| 36 | + refocus: false, | |
| 37 | + } | |
| 38 | + } | |
| 39 | + | |
| 40 | + pub fn invalidate(&mut self) { | |
| 41 | + self.cached_result = None; | |
| 42 | + self.cached_lines.clear(); | |
| 43 | + } | |
| 44 | + | |
| 45 | + pub fn show(&mut self, ui: &mut Ui, value: &serde_json::Value) { | |
| 46 | + // Auto-execute on first show or when query changes | |
| 47 | + if self.cached_result.is_none() || self.query != self.last_executed_query { | |
| 48 | + // Save previous query for Ctrl-L swap (skip initial load) | |
| 49 | + if !self.last_executed_query.is_empty() && self.last_executed_query != self.query { | |
| 50 | + self.previous_query = Some(self.last_executed_query.clone()); | |
| 51 | + } | |
| 52 | + let result = JqEngine::execute(&self.query, value); | |
| 53 | + self.rebuild_result_lines(&result); | |
| 54 | + self.cached_result = Some(result); | |
| 55 | + self.last_executed_query = self.query.clone(); | |
| 56 | + } | |
| 57 | + | |
| 58 | + let mut accepted_completion: Option<String> = None; | |
| 59 | + | |
| 60 | + ui.horizontal(|ui| { | |
| 61 | + ui.label(RichText::new("jq").color(CatppuccinMocha::MAUVE).strong()); | |
| 62 | + | |
| 63 | + let response = ui.add( | |
| 64 | + egui::TextEdit::singleline(&mut self.query) | |
| 65 | + .font(egui::FontId::monospace(14.0)) | |
| 66 | + .desired_width(ui.available_width() - 80.0) | |
| 67 | + .text_color(CatppuccinMocha::GREEN), | |
| 68 | + ); | |
| 69 | + | |
| 70 | + // Re-focus and move cursor to end after completion was applied | |
| 71 | + if self.refocus { | |
| 72 | + response.request_focus(); | |
| 73 | + if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), response.id) { | |
| 74 | + let ccursor = egui::text::CCursor::new(self.query.len()); | |
| 75 | + state.cursor.set_char_range(Some(egui::text::CCursorRange::one(ccursor))); | |
| 76 | + state.store(ui.ctx(), response.id); | |
| 77 | + } | |
| 78 | + self.refocus = false; | |
| 79 | + } | |
| 80 | + | |
| 81 | + let has_focus = response.has_focus() || response.lost_focus(); | |
| 82 | + | |
| 83 | + if has_focus { | |
| 84 | + // Consume keybindings only when jq input has focus | |
| 85 | + let tab = ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Tab)); | |
| 86 | + let ctrl_space = ui.ctx().input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::Space)); | |
| 87 | + let ctrl_period = ui.ctx().input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::Period)); | |
| 88 | + let ctrl_h = ui.ctx().input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::H)); | |
| 89 | + let ctrl_l = ui.ctx().input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::L)); | |
| 90 | + | |
| 91 | + if tab && self.show_completions && !self.completions.is_empty() { | |
| 92 | + accepted_completion = | |
| 93 | + Some(self.completions[self.completion_index].clone()); | |
| 94 | + } | |
| 95 | + if tab { | |
| 96 | + response.request_focus(); | |
| 97 | + } | |
| 98 | + | |
| 99 | + if ui.input(|i| i.key_pressed(egui::Key::Escape)) { | |
| 100 | + self.show_completions = false; | |
| 101 | + } | |
| 102 | + | |
| 103 | + // Ctrl-Space / Ctrl-Period: toggle autocompletion | |
| 104 | + if ctrl_space || ctrl_period { | |
| 105 | + if self.show_completions { | |
| 106 | + self.show_completions = false; | |
| 107 | + } else { | |
| 108 | + self.rebuild_completions(value); | |
| 109 | + } | |
| 110 | + } | |
| 111 | + | |
| 112 | + // Ctrl-H: delete rightmost path segment | |
| 113 | + if ctrl_h { | |
| 114 | + if let Some(pos) = self.query.rfind(|c: char| c == '.' || c == '|') { | |
| 115 | + self.query.truncate(pos); | |
| 116 | + } else { | |
| 117 | + self.query.clear(); | |
| 118 | + } | |
| 119 | + self.show_completions = false; | |
| 120 | + self.refocus = true; | |
| 121 | + } | |
| 122 | + | |
| 123 | + // Ctrl-L: swap with previous query | |
| 124 | + if ctrl_l { | |
| 125 | + if let Some(prev) = self.previous_query.take() { | |
| 126 | + let current = self.query.clone(); | |
| 127 | + self.query = prev; | |
| 128 | + self.previous_query = Some(current); | |
| 129 | + } | |
| 130 | + self.refocus = true; | |
| 131 | + } | |
| 132 | + | |
| 133 | + if self.show_completions && !self.completions.is_empty() { | |
| 134 | + let down = ui.input(|i| i.key_pressed(egui::Key::ArrowDown)) | |
| 135 | + || ui.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::N)); | |
| 136 | + let up = ui.input(|i| i.key_pressed(egui::Key::ArrowUp)) | |
| 137 | + || ui.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::P)); | |
| 138 | + if down { | |
| 139 | + self.completion_index = (self.completion_index + 1) | |
| 140 | + .min(self.completions.len() - 1); | |
| 141 | + } | |
| 142 | + if up { | |
| 143 | + self.completion_index = self.completion_index.saturating_sub(1); | |
| 144 | + } | |
| 145 | + if ui.input(|i| i.key_pressed(egui::Key::Enter)) { | |
| 146 | + accepted_completion = | |
| 147 | + Some(self.completions[self.completion_index].clone()); | |
| 148 | + } | |
| 149 | + } | |
| 150 | + } | |
| 151 | + | |
| 152 | + // Auto-show completions as user types | |
| 153 | + if response.changed() { | |
| 154 | + self.rebuild_completions(value); | |
| 155 | + } | |
| 156 | + | |
| 157 | + if ui.button("Run").clicked() { | |
| 158 | + self.show_completions = false; | |
| 159 | + let result = JqEngine::execute(&self.query, value); | |
| 160 | + self.rebuild_result_lines(&result); | |
| 161 | + self.cached_result = Some(result); | |
| 162 | + self.last_executed_query = self.query.clone(); | |
| 163 | + if !self.query.is_empty() && !self.history.contains(&self.query) { | |
| 164 | + self.history.push(self.query.clone()); | |
| 165 | + } | |
| 166 | + } | |
| 167 | + }); | |
| 168 | + | |
| 169 | + // Apply accepted completion | |
| 170 | + if let Some(comp) = accepted_completion { | |
| 171 | + self.apply_completion(&comp); | |
| 172 | + self.show_completions = false; | |
| 173 | + self.refocus = true; | |
| 174 | + let result = JqEngine::execute(&self.query, value); | |
| 175 | + self.rebuild_result_lines(&result); | |
| 176 | + self.cached_result = Some(result); | |
| 177 | + self.last_executed_query = self.query.clone(); | |
| 178 | + } | |
| 179 | + | |
| 180 | + // --- Completion popup --- | |
| 181 | + if self.show_completions && !self.completions.is_empty() { | |
| 182 | + let mut clicked: Option<String> = None; | |
| 183 | + egui::Frame::new() | |
| 184 | + .fill(CatppuccinMocha::SURFACE0) | |
| 185 | + .inner_margin(6.0) | |
| 186 | + .corner_radius(4.0) | |
| 187 | + .stroke(egui::Stroke::new(1.0, CatppuccinMocha::SURFACE1)) | |
| 188 | + .show(ui, |ui| { | |
| 189 | + ui.set_max_height(200.0); | |
| 190 | + egui::ScrollArea::vertical() | |
| 191 | + .id_salt("jq_completions") | |
| 192 | + .show(ui, |ui| { | |
| 193 | + for (i, comp) in self.completions.iter().enumerate() { | |
| 194 | + let selected = i == self.completion_index; | |
| 195 | + let text_color = if selected { | |
| 196 | + CatppuccinMocha::BLUE | |
| 197 | + } else { | |
| 198 | + CatppuccinMocha::TEXT | |
| 199 | + }; | |
| 200 | + let bg = if selected { | |
| 201 | + CatppuccinMocha::SURFACE1 | |
| 202 | + } else { | |
| 203 | + CatppuccinMocha::SURFACE0 | |
| 204 | + }; | |
| 205 | + let r = ui.add( | |
| 206 | + egui::Label::new( | |
| 207 | + RichText::new(comp) | |
| 208 | + .color(text_color) | |
| 209 | + .family(egui::FontFamily::Monospace) | |
| 210 | + .background_color(bg), | |
| 211 | + ) | |
| 212 | + .sense(egui::Sense::click()), | |
| 213 | + ); | |
| 214 | + if selected { | |
| 215 | + r.scroll_to_me(Some(egui::Align::Center)); | |
| 216 | + } | |
| 217 | + if r.clicked() { | |
| 218 | + clicked = Some(comp.clone()); | |
| 219 | + } | |
| 220 | + } | |
| 221 | + }); | |
| 222 | + }); | |
| 223 | + | |
| 224 | + if let Some(c) = clicked { | |
| 225 | + self.apply_completion(&c); | |
| 226 | + self.show_completions = false; | |
| 227 | + let result = JqEngine::execute(&self.query, value); | |
| 228 | + self.rebuild_result_lines(&result); | |
| 229 | + self.cached_result = Some(result); | |
| 230 | + self.last_executed_query = self.query.clone(); | |
| 231 | + } | |
| 232 | + } | |
| 233 | + | |
| 234 | + // --- History --- | |
| 235 | + if !self.history.is_empty() && !self.show_completions { | |
| 236 | + ui.horizontal(|ui| { | |
| 237 | + ui.label( | |
| 238 | + RichText::new("History:") | |
| 239 | + .color(CatppuccinMocha::SUBTEXT0) | |
| 240 | + .small(), | |
| 241 | + ); | |
| 242 | + let mut clicked_history: Option<String> = None; | |
| 243 | + for q in self.history.iter().rev().take(10) { | |
| 244 | + if ui | |
| 245 | + .add( | |
| 246 | + egui::Label::new( | |
| 247 | + RichText::new(q) | |
| 248 | + .color(CatppuccinMocha::BLUE) | |
| 249 | + .small() | |
| 250 | + .family(egui::FontFamily::Monospace), | |
| 251 | + ) | |
| 252 | + .sense(egui::Sense::click()), | |
| 253 | + ) | |
| 254 | + .clicked() | |
| 255 | + { | |
| 256 | + clicked_history = Some(q.clone()); | |
| 257 | + } | |
| 258 | + ui.label( | |
| 259 | + RichText::new("·").color(CatppuccinMocha::SURFACE2).small(), | |
| 260 | + ); | |
| 261 | + } | |
| 262 | + if let Some(q) = clicked_history { | |
| 263 | + self.query = q; | |
| 264 | + let result = JqEngine::execute(&self.query, value); | |
| 265 | + self.rebuild_result_lines(&result); | |
| 266 | + self.cached_result = Some(result); | |
| 267 | + self.last_executed_query = self.query.clone(); | |
| 268 | + } | |
| 269 | + }); | |
| 270 | + } | |
| 271 | + | |
| 272 | + ui.add_space(8.0); | |
| 273 | + | |
| 274 | + // --- Results --- | |
| 275 | + if let Some(result) = &self.cached_result { | |
| 276 | + if let Some(err) = &result.error { | |
| 277 | + ui.label( | |
| 278 | + RichText::new(format!("Error: {}", err)) | |
| 279 | + .color(CatppuccinMocha::RED) | |
| 280 | + .family(egui::FontFamily::Monospace), | |
| 281 | + ); | |
| 282 | + } | |
| 283 | + | |
| 284 | + if !self.cached_lines.is_empty() { | |
| 285 | + let row_height = ui.text_style_height(&egui::TextStyle::Monospace) + 2.0; | |
| 286 | + let num_rows = self.cached_lines.len(); | |
| 287 | + | |
| 288 | + ui.label( | |
| 289 | + RichText::new(format!("{} lines", num_rows)) | |
| 290 | + .color(CatppuccinMocha::OVERLAY0) | |
| 291 | + .small(), | |
| 292 | + ); | |
| 293 | + | |
| 294 | + egui::ScrollArea::vertical() | |
| 295 | + .id_salt("jq_results") | |
| 296 | + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) | |
| 297 | + .auto_shrink(false) | |
| 298 | + .show_rows(ui, row_height, num_rows, |ui, row_range| { | |
| 299 | + for row in row_range { | |
| 300 | + let line = &self.cached_lines[row]; | |
| 301 | + ui.label( | |
| 302 | + RichText::new(&line.text) | |
| 303 | + .color(line.color) | |
| 304 | + .family(egui::FontFamily::Monospace), | |
| 305 | + ); | |
| 306 | + } | |
| 307 | + }); | |
| 308 | + } | |
| 309 | + } | |
| 310 | + } | |
| 311 | + | |
| 312 | + /// Pre-flatten jq result output into colored lines for virtual scrolling. | |
| 313 | + fn rebuild_result_lines(&mut self, result: &crate::jq_engine::JqResult) { | |
| 314 | + self.cached_lines.clear(); | |
| 315 | + for (i, output) in result.output.iter().enumerate() { | |
| 316 | + if i > 0 { | |
| 317 | + self.cached_lines.push(ResultLine { | |
| 318 | + text: "───".to_string(), | |
| 319 | + color: CatppuccinMocha::SURFACE2, | |
| 320 | + }); | |
| 321 | + } | |
| 322 | + if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(output) { | |
| 323 | + let pretty = serde_json::to_string_pretty(&parsed).unwrap_or_default(); | |
| 324 | + for line in pretty.lines() { | |
| 325 | + let (color, text) = colorize_json_line(line); | |
| 326 | + self.cached_lines.push(ResultLine { text, color }); | |
| 327 | + } | |
| 328 | + } else { | |
| 329 | + self.cached_lines.push(ResultLine { | |
| 330 | + text: output.clone(), | |
| 331 | + color: CatppuccinMocha::TEXT, | |
| 332 | + }); | |
| 333 | + } | |
| 334 | + } | |
| 335 | + } | |
| 336 | + | |
| 337 | + /// Rebuild completions based on current query. | |
| 338 | + /// Collect all valid jq paths from root value. | |
| 339 | + fn collect_all_paths(root: &serde_json::Value) -> Vec<String> { | |
| 340 | + let mut all_paths = Vec::new(); | |
| 341 | + match root { | |
| 342 | + serde_json::Value::Object(map) => { | |
| 343 | + for (key, val) in map { | |
| 344 | + let path = format!(".{}", key); | |
| 345 | + all_paths.push(path.clone()); | |
| 346 | + collect_deep_paths(val, &path, 6, &mut all_paths); | |
| 347 | + } | |
| 348 | + } | |
| 349 | + serde_json::Value::Array(arr) => { | |
| 350 | + all_paths.push(".[]".to_string()); | |
| 351 | + if let Some(first) = arr.first() { | |
| 352 | + if first.is_object() { | |
| 353 | + collect_deep_paths(first, ".[]", 6, &mut all_paths); | |
| 354 | + } | |
| 355 | + } | |
| 356 | + } | |
| 357 | + _ => {} | |
| 358 | + } | |
| 359 | + all_paths.sort(); | |
| 360 | + all_paths.dedup(); | |
| 361 | + all_paths | |
| 362 | + } | |
| 363 | + | |
| 364 | + /// Rebuild completions using fuzzy matching against all possible paths. | |
| 365 | + fn rebuild_completions(&mut self, root: &serde_json::Value) { | |
| 366 | + let segment = extract_current_segment(&self.query); | |
| 367 | + let all_paths = Self::collect_all_paths(root); | |
| 368 | + | |
| 369 | + let needle = segment.trim(); | |
| 370 | + if needle.is_empty() || needle == "." { | |
| 371 | + // Show all paths | |
| 372 | + self.completions = all_paths; | |
| 373 | + } else { | |
| 374 | + // Fuzzy filter: every char in needle must appear in order in the path | |
| 375 | + let needle_lower = needle.to_lowercase(); | |
| 376 | + let mut scored: Vec<(i64, String)> = all_paths | |
| 377 | + .into_iter() | |
| 378 | + .filter_map(|path| { | |
| 379 | + let score = fuzzy_score(&needle_lower, &path.to_lowercase()); | |
| 380 | + if score > 0 { Some((score, path)) } else { None } | |
| 381 | + }) | |
| 382 | + .collect(); | |
| 383 | + scored.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1))); | |
| 384 | + self.completions = scored.into_iter().map(|(_, p)| p).collect(); | |
| 385 | + } | |
| 386 | + | |
| 387 | + self.completion_index = 0; | |
| 388 | + self.show_completions = !self.completions.is_empty(); | |
| 389 | + } | |
| 390 | + | |
| 391 | + /// Replace the current segment of the query with the completion. | |
| 392 | + fn apply_completion(&mut self, completion: &str) { | |
| 393 | + let (before, _) = split_at_current_segment(&self.query); | |
| 394 | + self.query = format!("{}{}", before, completion); | |
| 395 | + } | |
| 396 | +} | |
| 397 | + | |
| 398 | +/// Recursively collect all dot-paths from a JSON value. | |
| 399 | +fn collect_deep_paths( | |
| 400 | + value: &serde_json::Value, | |
| 401 | + prefix: &str, | |
| 402 | + max_depth: usize, | |
| 403 | + out: &mut Vec<String>, | |
| 404 | +) { | |
| 405 | + if max_depth == 0 { | |
| 406 | + return; | |
| 407 | + } | |
| 408 | + match value { | |
| 409 | + serde_json::Value::Object(map) => { | |
| 410 | + for (key, val) in map { | |
| 411 | + let path = format!("{}.{}", prefix, key); | |
| 412 | + out.push(path.clone()); | |
| 413 | + match val { | |
| 414 | + serde_json::Value::Object(_) => { | |
| 415 | + collect_deep_paths(val, &path, max_depth - 1, out); | |
| 416 | + } | |
| 417 | + serde_json::Value::Array(arr) => { | |
| 418 | + let arr_path = format!("{}[]", path); | |
| 419 | + out.push(arr_path.clone()); | |
| 420 | + if let Some(first) = arr.first() { | |
| 421 | + if first.is_object() { | |
| 422 | + collect_deep_paths(first, &arr_path, max_depth - 1, out); | |
| 423 | + } | |
| 424 | + } | |
| 425 | + } | |
| 426 | + _ => {} | |
| 427 | + } | |
| 428 | + } | |
| 429 | + } | |
| 430 | + serde_json::Value::Array(arr) => { | |
| 431 | + let arr_path = format!("{}[]", prefix); | |
| 432 | + out.push(arr_path.clone()); | |
| 433 | + if let Some(first) = arr.first() { | |
| 434 | + if first.is_object() { | |
| 435 | + collect_deep_paths(first, &arr_path, max_depth - 1, out); | |
| 436 | + } | |
| 437 | + } | |
| 438 | + } | |
| 439 | + _ => {} | |
| 440 | + } | |
| 441 | +} | |
| 442 | + | |
| 443 | +/// Fuzzy match: returns a score > 0 if all chars of needle appear in haystack in order. | |
| 444 | +/// Higher score = better match (consecutive chars, earlier positions). | |
| 445 | +fn fuzzy_score(needle: &str, haystack: &str) -> i64 { | |
| 446 | + let needle_chars: Vec<char> = needle.chars().collect(); | |
| 447 | + let haystack_chars: Vec<char> = haystack.chars().collect(); | |
| 448 | + | |
| 449 | + if needle_chars.is_empty() { | |
| 450 | + return 1; | |
| 451 | + } | |
| 452 | + if needle_chars.len() > haystack_chars.len() { | |
| 453 | + return 0; | |
| 454 | + } | |
| 455 | + | |
| 456 | + let mut score: i64 = 0; | |
| 457 | + let mut ni = 0; | |
| 458 | + let mut prev_match: Option<usize> = None; | |
| 459 | + | |
| 460 | + for (hi, &hc) in haystack_chars.iter().enumerate() { | |
| 461 | + if ni < needle_chars.len() && hc == needle_chars[ni] { | |
| 462 | + score += 1; | |
| 463 | + // Bonus for consecutive matches | |
| 464 | + if let Some(prev) = prev_match { | |
| 465 | + if hi == prev + 1 { | |
| 466 | + score += 2; | |
| 467 | + } | |
| 468 | + } | |
| 469 | + // Bonus for matching after separator (., [, ]) | |
| 470 | + if hi > 0 && matches!(haystack_chars[hi - 1], '.' | '[' | ']') { | |
| 471 | + score += 1; | |
| 472 | + } | |
| 473 | + prev_match = Some(hi); | |
| 474 | + ni += 1; | |
| 475 | + } | |
| 476 | + } | |
| 477 | + | |
| 478 | + if ni == needle_chars.len() { score } else { 0 } | |
| 479 | +} | |
| 480 | + | |
| 481 | +/// Extract the current jq segment being typed (after last `|`, `(`, `;`). | |
| 482 | +fn extract_current_segment(query: &str) -> &str { | |
| 483 | + let start = query | |
| 484 | + .rfind(|c: char| c == '|' || c == '(' || c == ';') | |
| 485 | + .map(|i| i + 1) | |
| 486 | + .unwrap_or(0); | |
| 487 | + query[start..].trim_start() | |
| 488 | +} | |
| 489 | + | |
| 490 | +/// Split query into (everything before current segment, current segment). | |
| 491 | +fn split_at_current_segment(query: &str) -> (&str, &str) { | |
| 492 | + let start = query | |
| 493 | + .rfind(|c: char| c == '|' || c == '(' || c == ';') | |
| 494 | + .map(|i| i + 1) | |
| 495 | + .unwrap_or(0); | |
| 496 | + | |
| 497 | + // Include any whitespace after the delimiter in "before" | |
| 498 | + let segment_start = query[start..] | |
| 499 | + .find(|c: char| !c.is_whitespace()) | |
| 500 | + .map(|offset| start + offset) | |
| 501 | + .unwrap_or(query.len()); | |
| 502 | + | |
| 503 | + (&query[..segment_start], &query[segment_start..]) | |
| 504 | +} | |
| 505 | + | |
| 506 | +fn colorize_json_line(line: &str) -> (egui::Color32, String) { | |
| 507 | + let trimmed = line.trim(); | |
| 508 | + let color = if trimmed.starts_with('"') { | |
| 509 | + if trimmed.contains(':') { | |
| 510 | + CatppuccinMocha::BLUE | |
| 511 | + } else { | |
| 512 | + CatppuccinMocha::GREEN | |
| 513 | + } | |
| 514 | + } else if trimmed == "null" || trimmed == "null," { | |
| 515 | + CatppuccinMocha::OVERLAY0 | |
| 516 | + } else if trimmed == "true" | |
| 517 | + || trimmed == "true," | |
| 518 | + || trimmed == "false" | |
| 519 | + || trimmed == "false," | |
| 520 | + { | |
| 521 | + CatppuccinMocha::MAUVE | |
| 522 | + } else if trimmed.starts_with(|c: char| c.is_ascii_digit() || c == '-') { | |
| 523 | + CatppuccinMocha::PEACH | |
| 524 | + } else { | |
| 525 | + CatppuccinMocha::TEXT | |
| 526 | + }; | |
| 527 | + | |
| 528 | + (color, line.to_string()) | |
| 529 | +} | |
| 530 | + | |
| 531 | +#[cfg(test)] | |
| 532 | +mod tests { | |
| 533 | + use super::*; | |
| 534 | + | |
| 535 | + #[test] | |
| 536 | + fn test_extract_segment_simple() { | |
| 537 | + assert_eq!(extract_current_segment(".users"), ".users"); | |
| 538 | + assert_eq!(extract_current_segment(".users.na"), ".users.na"); | |
| 539 | + } | |
| 540 | + | |
| 541 | + #[test] | |
| 542 | + fn test_extract_segment_pipe() { | |
| 543 | + assert_eq!(extract_current_segment(".users[] | .na"), ".na"); | |
| 544 | + } | |
| 545 | + | |
| 546 | + #[test] | |
| 547 | + fn test_split_at_segment() { | |
| 548 | + let (before, seg) = split_at_current_segment(".users[] | .na"); | |
| 549 | + assert_eq!(before, ".users[] | "); | |
| 550 | + assert_eq!(seg, ".na"); | |
| 551 | + } | |
| 552 | + | |
| 553 | + #[test] | |
| 554 | + fn test_split_at_segment_simple() { | |
| 555 | + let (before, seg) = split_at_current_segment(".users"); | |
| 556 | + assert_eq!(before, ""); | |
| 557 | + assert_eq!(seg, ".users"); | |
| 558 | + } | |
| 559 | + | |
| 560 | + #[test] | |
| 561 | + fn test_completions_from_root_dot() { | |
| 562 | + let root: serde_json::Value = serde_json::json!({"users": [], "metadata": {}}); | |
| 563 | + let mut view = JqView::new(); | |
| 564 | + view.query = ".".to_string(); | |
| 565 | + view.rebuild_completions(&root); | |
| 566 | + assert!(!view.completions.is_empty(), "rebuild: completions should not be empty for '.'"); | |
| 567 | + assert!(view.completions.iter().any(|c| c.contains("users")), "should contain .users"); | |
| 568 | + } | |
| 569 | + | |
| 570 | + #[test] | |
| 571 | + fn test_completions_from_empty() { | |
| 572 | + let root: serde_json::Value = serde_json::json!({"foo": 1, "bar": 2}); | |
| 573 | + let mut view = JqView::new(); | |
| 574 | + view.query = String::new(); | |
| 575 | + view.rebuild_completions(&root); | |
| 576 | + assert!(!view.completions.is_empty(), "rebuild: completions should not be empty for empty query"); | |
| 577 | + } | |
| 578 | + | |
| 579 | + #[test] | |
| 580 | + fn test_apply_completion_from_dot() { | |
| 581 | + let root: serde_json::Value = serde_json::json!({"users": [{"id": 1}]}); | |
| 582 | + let mut view = JqView::new(); | |
| 583 | + view.query = ".".to_string(); | |
| 584 | + view.rebuild_completions(&root); | |
| 585 | + assert!(!view.completions.is_empty()); | |
| 586 | + let comp = view.completions[0].clone(); | |
| 587 | + view.apply_completion(&comp); | |
| 588 | + assert!(view.query.starts_with('.'), "query should start with dot, got: {:?}", view.query); | |
| 589 | + } | |
| 590 | + | |
| 591 | + #[test] | |
| 592 | + fn test_completions_all_start_with_dot() { | |
| 593 | + let root: serde_json::Value = serde_json::json!({"users": [{"id": 1}], "metadata": {"version": "1.0"}}); | |
| 594 | + let mut view = JqView::new(); | |
| 595 | + view.query = ".".to_string(); | |
| 596 | + view.rebuild_completions(&root); | |
| 597 | + assert!(!view.completions.is_empty()); | |
| 598 | + for c in &view.completions { | |
| 599 | + assert!(c.starts_with('.'), "completion should start with dot: {:?}", c); | |
| 600 | + } | |
| 601 | + } | |
| 602 | + | |
| 603 | + #[test] | |
| 604 | + fn test_completions_array_root() { | |
| 605 | + let root: serde_json::Value = serde_json::json!([{"alternate2": "val", "name": "test"}]); | |
| 606 | + let mut view = JqView::new(); | |
| 607 | + view.query = ".".to_string(); | |
| 608 | + view.rebuild_completions(&root); | |
| 609 | + assert!(!view.completions.is_empty()); | |
| 610 | + assert!(view.completions.iter().any(|c| c == ".[]"), "should contain .[], got: {:?}", view.completions); | |
| 611 | + assert!(view.completions.iter().any(|c| c == ".[].alternate2"), "should contain .[].alternate2, got: {:?}", view.completions); | |
| 612 | + for c in &view.completions { | |
| 613 | + assert!(c.starts_with('.'), "completion should start with dot: {:?}", c); | |
| 614 | + } | |
| 615 | + } | |
| 616 | + | |
| 617 | + #[test] | |
| 618 | + fn test_fuzzy_search() { | |
| 619 | + let root: serde_json::Value = serde_json::json!({"users": [{"id": 1, "username": "test"}], "metadata": {}}); | |
| 620 | + let mut view = JqView::new(); | |
| 621 | + view.query = ".usrn".to_string(); // fuzzy for "username" | |
| 622 | + view.rebuild_completions(&root); | |
| 623 | + assert!(view.completions.iter().any(|c| c.contains("username")), | |
| 624 | + "fuzzy should match username, got: {:?}", view.completions); | |
| 625 | + } | |
| 626 | +} |
+399-0src/views/json.rs
| @@ -0,0 +1,399 @@ | ||
| 1 | +use egui::{self, RichText, Ui}; | |
| 2 | +use std::collections::BTreeSet; | |
| 3 | + | |
| 4 | +use crate::theme::CatppuccinMocha; | |
| 5 | + | |
| 6 | +/// A pre-rendered line in the JSON view. | |
| 7 | +struct JsonLine { | |
| 8 | + text: String, | |
| 9 | + color: egui::Color32, | |
| 10 | + /// If Some, clicking this line toggles the path. | |
| 11 | + toggle_path: Option<String>, | |
| 12 | +} | |
| 13 | + | |
| 14 | +pub struct JsonView { | |
| 15 | + expanded: BTreeSet<String>, | |
| 16 | + lines: Vec<JsonLine>, | |
| 17 | + dirty: bool, | |
| 18 | +} | |
| 19 | + | |
| 20 | +impl JsonView { | |
| 21 | + pub fn new() -> Self { | |
| 22 | + Self { | |
| 23 | + expanded: BTreeSet::new(), | |
| 24 | + lines: Vec::new(), | |
| 25 | + dirty: true, | |
| 26 | + } | |
| 27 | + } | |
| 28 | + | |
| 29 | + pub fn mark_dirty(&mut self) { | |
| 30 | + self.dirty = true; | |
| 31 | + } | |
| 32 | + | |
| 33 | + pub fn show(&mut self, ui: &mut Ui, value: &serde_json::Value, filename: &str) { | |
| 34 | + ui.horizontal(|ui| { | |
| 35 | + if ui.button("Expand All").clicked() { | |
| 36 | + self.expand_all(value, "root"); | |
| 37 | + self.dirty = true; | |
| 38 | + } | |
| 39 | + if ui.button("Collapse All").clicked() { | |
| 40 | + self.expanded.clear(); | |
| 41 | + self.dirty = true; | |
| 42 | + } | |
| 43 | + if ui.button("Copy JSON").clicked() { | |
| 44 | + let text = serde_json::to_string_pretty(value).unwrap_or_default(); | |
| 45 | + ui.ctx().copy_text(text); | |
| 46 | + } | |
| 47 | + ui.label( | |
| 48 | + RichText::new(format!("{} lines", self.lines.len())) | |
| 49 | + .color(CatppuccinMocha::OVERLAY0) | |
| 50 | + .small(), | |
| 51 | + ); | |
| 52 | + }); | |
| 53 | + | |
| 54 | + ui.add_space(8.0); | |
| 55 | + | |
| 56 | + // Rebuild line buffer when dirty | |
| 57 | + if self.dirty { | |
| 58 | + self.lines.clear(); | |
| 59 | + flatten_value(value, "root", 0, false, &self.expanded, &mut self.lines); | |
| 60 | + self.dirty = false; | |
| 61 | + } | |
| 62 | + | |
| 63 | + let row_height = ui.text_style_height(&egui::TextStyle::Monospace) + 2.0; | |
| 64 | + let num_rows = self.lines.len(); | |
| 65 | + let mut toggle: Option<String> = None; | |
| 66 | + | |
| 67 | + egui::ScrollArea::vertical() | |
| 68 | + .id_salt(format!("json_scroll_{}", filename)) | |
| 69 | + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) | |
| 70 | + .auto_shrink(false) | |
| 71 | + .show_rows(ui, row_height, num_rows, |ui, row_range| { | |
| 72 | + for row in row_range { | |
| 73 | + let line = &self.lines[row]; | |
| 74 | + ui.horizontal(|ui| { | |
| 75 | + ui.label( | |
| 76 | + RichText::new(format!("{:>4} ", row + 1)) | |
| 77 | + .color(CatppuccinMocha::SURFACE2) | |
| 78 | + .family(egui::FontFamily::Monospace), | |
| 79 | + ); | |
| 80 | + | |
| 81 | + if line.toggle_path.is_some() { | |
| 82 | + let r = ui.add( | |
| 83 | + egui::Label::new( | |
| 84 | + RichText::new(&line.text) | |
| 85 | + .color(line.color) | |
| 86 | + .family(egui::FontFamily::Monospace), | |
| 87 | + ) | |
| 88 | + .sense(egui::Sense::click()), | |
| 89 | + ); | |
| 90 | + if r.clicked() { | |
| 91 | + toggle = line.toggle_path.clone(); | |
| 92 | + } | |
| 93 | + } else { | |
| 94 | + ui.label( | |
| 95 | + RichText::new(&line.text) | |
| 96 | + .color(line.color) | |
| 97 | + .family(egui::FontFamily::Monospace), | |
| 98 | + ); | |
| 99 | + } | |
| 100 | + }); | |
| 101 | + } | |
| 102 | + }); | |
| 103 | + | |
| 104 | + if let Some(path) = toggle { | |
| 105 | + if self.expanded.contains(&path) { | |
| 106 | + self.expanded.remove(&path); | |
| 107 | + } else { | |
| 108 | + self.expanded.insert(path); | |
| 109 | + } | |
| 110 | + self.dirty = true; | |
| 111 | + } | |
| 112 | + } | |
| 113 | + | |
| 114 | + fn expand_all(&mut self, value: &serde_json::Value, path: &str) { | |
| 115 | + match value { | |
| 116 | + serde_json::Value::Object(map) => { | |
| 117 | + self.expanded.insert(path.to_string()); | |
| 118 | + for (key, val) in map { | |
| 119 | + self.expand_all(val, &format!("{}.{}", path, key)); | |
| 120 | + } | |
| 121 | + } | |
| 122 | + serde_json::Value::Array(arr) => { | |
| 123 | + self.expanded.insert(path.to_string()); | |
| 124 | + for (i, val) in arr.iter().enumerate() { | |
| 125 | + self.expand_all(val, &format!("{}[{}]", path, i)); | |
| 126 | + } | |
| 127 | + } | |
| 128 | + _ => {} | |
| 129 | + } | |
| 130 | + } | |
| 131 | +} | |
| 132 | + | |
| 133 | +/// Flatten the JSON tree into a vec of lines based on current expand state. | |
| 134 | +fn flatten_value( | |
| 135 | + value: &serde_json::Value, | |
| 136 | + path: &str, | |
| 137 | + indent: usize, | |
| 138 | + trailing_comma: bool, | |
| 139 | + expanded: &BTreeSet<String>, | |
| 140 | + out: &mut Vec<JsonLine>, | |
| 141 | +) { | |
| 142 | + let pad = " ".repeat(indent); | |
| 143 | + let comma = if trailing_comma { "," } else { "" }; | |
| 144 | + | |
| 145 | + match value { | |
| 146 | + serde_json::Value::Null => { | |
| 147 | + out.push(JsonLine { | |
| 148 | + text: format!("{}null{}", pad, comma), | |
| 149 | + color: CatppuccinMocha::OVERLAY0, | |
| 150 | + toggle_path: None, | |
| 151 | + }); | |
| 152 | + } | |
| 153 | + serde_json::Value::Bool(b) => { | |
| 154 | + out.push(JsonLine { | |
| 155 | + text: format!("{}{}{}", pad, b, comma), | |
| 156 | + color: CatppuccinMocha::MAUVE, | |
| 157 | + toggle_path: None, | |
| 158 | + }); | |
| 159 | + } | |
| 160 | + serde_json::Value::Number(n) => { | |
| 161 | + out.push(JsonLine { | |
| 162 | + text: format!("{}{}{}", pad, n, comma), | |
| 163 | + color: CatppuccinMocha::PEACH, | |
| 164 | + toggle_path: None, | |
| 165 | + }); | |
| 166 | + } | |
| 167 | + serde_json::Value::String(s) => { | |
| 168 | + out.push(JsonLine { | |
| 169 | + text: format!("{}\"{}\"{}", pad, s, comma), | |
| 170 | + color: CatppuccinMocha::GREEN, | |
| 171 | + toggle_path: None, | |
| 172 | + }); | |
| 173 | + } | |
| 174 | + serde_json::Value::Object(map) => { | |
| 175 | + if map.is_empty() { | |
| 176 | + out.push(JsonLine { | |
| 177 | + text: format!("{}{{}}{}", pad, comma), | |
| 178 | + color: CatppuccinMocha::TEXT, | |
| 179 | + toggle_path: None, | |
| 180 | + }); | |
| 181 | + return; | |
| 182 | + } | |
| 183 | + | |
| 184 | + let is_expanded = expanded.contains(path); | |
| 185 | + if is_expanded { | |
| 186 | + out.push(JsonLine { | |
| 187 | + text: format!("{}v {{", pad), | |
| 188 | + color: CatppuccinMocha::TEXT, | |
| 189 | + toggle_path: Some(path.to_string()), | |
| 190 | + }); | |
| 191 | + | |
| 192 | + let entries: Vec<_> = map.iter().collect(); | |
| 193 | + let len = entries.len(); | |
| 194 | + for (i, (key, val)) in entries.iter().enumerate() { | |
| 195 | + let child_path = format!("{}.{}", path, key); | |
| 196 | + let has_comma = i < len - 1; | |
| 197 | + let inner_pad = " ".repeat(indent + 1); | |
| 198 | + | |
| 199 | + if val.is_object() || val.is_array() { | |
| 200 | + // Key prefix on same line as opener | |
| 201 | + let prefix = format!("{}\"{}\": ", inner_pad, key); | |
| 202 | + flatten_value_with_prefix(val, &child_path, indent + 1, has_comma, expanded, out, &prefix); | |
| 203 | + } else { | |
| 204 | + let c = if has_comma { "," } else { "" }; | |
| 205 | + let val_text = format_inline_value(val); | |
| 206 | + let color = inline_value_color(val); | |
| 207 | + out.push(JsonLine { | |
| 208 | + text: format!("{}\"{}\": {}{}", inner_pad, key, val_text, c), | |
| 209 | + color, | |
| 210 | + toggle_path: None, | |
| 211 | + }); | |
| 212 | + } | |
| 213 | + } | |
| 214 | + | |
| 215 | + out.push(JsonLine { | |
| 216 | + text: format!("{}}}{}", pad, comma), | |
| 217 | + color: CatppuccinMocha::TEXT, | |
| 218 | + toggle_path: None, | |
| 219 | + }); | |
| 220 | + } else { | |
| 221 | + out.push(JsonLine { | |
| 222 | + text: format!("{}> {{ {} fields }}{}", pad, map.len(), comma), | |
| 223 | + color: CatppuccinMocha::TEXT, | |
| 224 | + toggle_path: Some(path.to_string()), | |
| 225 | + }); | |
| 226 | + } | |
| 227 | + } | |
| 228 | + serde_json::Value::Array(arr) => { | |
| 229 | + if arr.is_empty() { | |
| 230 | + out.push(JsonLine { | |
| 231 | + text: format!("{}[]{}", pad, comma), | |
| 232 | + color: CatppuccinMocha::TEXT, | |
| 233 | + toggle_path: None, | |
| 234 | + }); | |
| 235 | + return; | |
| 236 | + } | |
| 237 | + | |
| 238 | + let is_expanded = expanded.contains(path); | |
| 239 | + if is_expanded { | |
| 240 | + out.push(JsonLine { | |
| 241 | + text: format!("{}v [", pad), | |
| 242 | + color: CatppuccinMocha::TEXT, | |
| 243 | + toggle_path: Some(path.to_string()), | |
| 244 | + }); | |
| 245 | + | |
| 246 | + let len = arr.len(); | |
| 247 | + for (i, val) in arr.iter().enumerate() { | |
| 248 | + let child_path = format!("{}[{}]", path, i); | |
| 249 | + let has_comma = i < len - 1; | |
| 250 | + flatten_value(val, &child_path, indent + 1, has_comma, expanded, out); | |
| 251 | + } | |
| 252 | + | |
| 253 | + out.push(JsonLine { | |
| 254 | + text: format!("{}]{}", pad, comma), | |
| 255 | + color: CatppuccinMocha::TEXT, | |
| 256 | + toggle_path: None, | |
| 257 | + }); | |
| 258 | + } else { | |
| 259 | + out.push(JsonLine { | |
| 260 | + text: format!("{}> [ {} items ]{}", pad, arr.len(), comma), | |
| 261 | + color: CatppuccinMocha::TEXT, | |
| 262 | + toggle_path: Some(path.to_string()), | |
| 263 | + }); | |
| 264 | + } | |
| 265 | + } | |
| 266 | + } | |
| 267 | +} | |
| 268 | + | |
| 269 | +/// Like flatten_value but prepends a prefix (key name) to the first line. | |
| 270 | +fn flatten_value_with_prefix( | |
| 271 | + value: &serde_json::Value, | |
| 272 | + path: &str, | |
| 273 | + indent: usize, | |
| 274 | + trailing_comma: bool, | |
| 275 | + expanded: &BTreeSet<String>, | |
| 276 | + out: &mut Vec<JsonLine>, | |
| 277 | + prefix: &str, | |
| 278 | +) { | |
| 279 | + let pad = " ".repeat(indent); | |
| 280 | + let comma = if trailing_comma { "," } else { "" }; | |
| 281 | + | |
| 282 | + match value { | |
| 283 | + serde_json::Value::Object(map) => { | |
| 284 | + if map.is_empty() { | |
| 285 | + out.push(JsonLine { | |
| 286 | + text: format!("{}{{}}{}", prefix, comma), | |
| 287 | + color: CatppuccinMocha::TEXT, | |
| 288 | + toggle_path: None, | |
| 289 | + }); | |
| 290 | + return; | |
| 291 | + } | |
| 292 | + | |
| 293 | + let is_expanded = expanded.contains(path); | |
| 294 | + if is_expanded { | |
| 295 | + out.push(JsonLine { | |
| 296 | + text: format!("{}v {{", prefix), | |
| 297 | + color: CatppuccinMocha::BLUE, | |
| 298 | + toggle_path: Some(path.to_string()), | |
| 299 | + }); | |
| 300 | + | |
| 301 | + let entries: Vec<_> = map.iter().collect(); | |
| 302 | + let len = entries.len(); | |
| 303 | + for (i, (key, val)) in entries.iter().enumerate() { | |
| 304 | + let child_path = format!("{}.{}", path, key); | |
| 305 | + let has_comma = i < len - 1; | |
| 306 | + let inner_pad = " ".repeat(indent + 1); | |
| 307 | + | |
| 308 | + if val.is_object() || val.is_array() { | |
| 309 | + let child_prefix = format!("{}\"{}\": ", inner_pad, key); | |
| 310 | + flatten_value_with_prefix(val, &child_path, indent + 1, has_comma, expanded, out, &child_prefix); | |
| 311 | + } else { | |
| 312 | + let c = if has_comma { "," } else { "" }; | |
| 313 | + let val_text = format_inline_value(val); | |
| 314 | + let color = inline_value_color(val); | |
| 315 | + out.push(JsonLine { | |
| 316 | + text: format!("{}\"{}\": {}{}", inner_pad, key, val_text, c), | |
| 317 | + color, | |
| 318 | + toggle_path: None, | |
| 319 | + }); | |
| 320 | + } | |
| 321 | + } | |
| 322 | + | |
| 323 | + out.push(JsonLine { | |
| 324 | + text: format!("{}}}{}", pad, comma), | |
| 325 | + color: CatppuccinMocha::TEXT, | |
| 326 | + toggle_path: None, | |
| 327 | + }); | |
| 328 | + } else { | |
| 329 | + out.push(JsonLine { | |
| 330 | + text: format!("{}> {{ {} fields }}{}", prefix, map.len(), comma), | |
| 331 | + color: CatppuccinMocha::BLUE, | |
| 332 | + toggle_path: Some(path.to_string()), | |
| 333 | + }); | |
| 334 | + } | |
| 335 | + } | |
| 336 | + serde_json::Value::Array(arr) => { | |
| 337 | + if arr.is_empty() { | |
| 338 | + out.push(JsonLine { | |
| 339 | + text: format!("{}[]{}", prefix, comma), | |
| 340 | + color: CatppuccinMocha::TEXT, | |
| 341 | + toggle_path: None, | |
| 342 | + }); | |
| 343 | + return; | |
| 344 | + } | |
| 345 | + | |
| 346 | + let is_expanded = expanded.contains(path); | |
| 347 | + if is_expanded { | |
| 348 | + out.push(JsonLine { | |
| 349 | + text: format!("{}v [", prefix), | |
| 350 | + color: CatppuccinMocha::BLUE, | |
| 351 | + toggle_path: Some(path.to_string()), | |
| 352 | + }); | |
| 353 | + | |
| 354 | + let len = arr.len(); | |
| 355 | + for (i, val) in arr.iter().enumerate() { | |
| 356 | + let child_path = format!("{}[{}]", path, i); | |
| 357 | + let has_comma = i < len - 1; | |
| 358 | + flatten_value(val, &child_path, indent + 1, has_comma, expanded, out); | |
| 359 | + } | |
| 360 | + | |
| 361 | + out.push(JsonLine { | |
| 362 | + text: format!("{}]{}", pad, comma), | |
| 363 | + color: CatppuccinMocha::TEXT, | |
| 364 | + toggle_path: None, | |
| 365 | + }); | |
| 366 | + } else { | |
| 367 | + out.push(JsonLine { | |
| 368 | + text: format!("{}> [ {} items ]{}", prefix, arr.len(), comma), | |
| 369 | + color: CatppuccinMocha::BLUE, | |
| 370 | + toggle_path: Some(path.to_string()), | |
| 371 | + }); | |
| 372 | + } | |
| 373 | + } | |
| 374 | + _ => { | |
| 375 | + // Scalar with prefix — shouldn't happen but handle gracefully | |
| 376 | + flatten_value(value, path, indent, trailing_comma, expanded, out); | |
| 377 | + } | |
| 378 | + } | |
| 379 | +} | |
| 380 | + | |
| 381 | +fn format_inline_value(value: &serde_json::Value) -> String { | |
| 382 | + match value { | |
| 383 | + serde_json::Value::Null => "null".to_string(), | |
| 384 | + serde_json::Value::Bool(b) => b.to_string(), | |
| 385 | + serde_json::Value::Number(n) => n.to_string(), | |
| 386 | + serde_json::Value::String(s) => format!("\"{}\"", s), | |
| 387 | + _ => String::new(), | |
| 388 | + } | |
| 389 | +} | |
| 390 | + | |
| 391 | +fn inline_value_color(value: &serde_json::Value) -> egui::Color32 { | |
| 392 | + match value { | |
| 393 | + serde_json::Value::Null => CatppuccinMocha::OVERLAY0, | |
| 394 | + serde_json::Value::Bool(_) => CatppuccinMocha::MAUVE, | |
| 395 | + serde_json::Value::Number(_) => CatppuccinMocha::PEACH, | |
| 396 | + serde_json::Value::String(_) => CatppuccinMocha::GREEN, | |
| 397 | + _ => CatppuccinMocha::TEXT, | |
| 398 | + } | |
| 399 | +} |
+7-0src/views/mod.rs
| @@ -0,0 +1,7 @@ | ||
| 1 | +pub mod table; | |
| 2 | +pub mod json; | |
| 3 | +pub mod code; | |
| 4 | +pub mod jq; | |
| 5 | +pub mod browser; | |
| 6 | +pub mod schema_diagram; | |
| 7 | +pub mod shared_browser; |
+900-0src/views/schema_diagram.rs
| @@ -0,0 +1,900 @@ | ||
| 1 | +use std::collections::{BTreeMap, BTreeSet}; | |
| 2 | + | |
| 3 | +use egui::{self, pos2, vec2, Color32, Pos2, Rect, RichText, Ui, Vec2}; | |
| 4 | + | |
| 5 | +use crate::schema::SharedStruct; | |
| 6 | +use crate::session::EnumConversion; | |
| 7 | +use crate::theme::CatppuccinMocha; | |
| 8 | + | |
| 9 | +/// A node in the diagram representing a struct or enum | |
| 10 | +#[derive(Clone)] | |
| 11 | +struct DiagramNode { | |
| 12 | + name: String, | |
| 13 | + fields: Vec<(String, String, Color32)>, // (field_name, type_display, color) | |
| 14 | + kind: NodeKind, | |
| 15 | + pos: Pos2, | |
| 16 | + size: Vec2, | |
| 17 | + is_shared: bool, | |
| 18 | +} | |
| 19 | + | |
| 20 | +#[derive(Clone, PartialEq)] | |
| 21 | +enum NodeKind { | |
| 22 | + Struct, | |
| 23 | + Enum, | |
| 24 | +} | |
| 25 | + | |
| 26 | +/// An edge connecting two nodes | |
| 27 | +#[derive(Clone)] | |
| 28 | +struct DiagramEdge { | |
| 29 | + from: String, | |
| 30 | + to: String, | |
| 31 | + label: String, | |
| 32 | +} | |
| 33 | + | |
| 34 | +pub struct SchemaDiagramView { | |
| 35 | + // Diagram state | |
| 36 | + nodes: Vec<DiagramNode>, | |
| 37 | + edges: Vec<DiagramEdge>, | |
| 38 | + mermaid_code: String, | |
| 39 | + | |
| 40 | + // Interaction state | |
| 41 | + pan_offset: Vec2, | |
| 42 | + zoom: f32, | |
| 43 | + dragging_node: Option<usize>, | |
| 44 | + drag_start: Option<Pos2>, | |
| 45 | + node_drag_origin: Option<Pos2>, | |
| 46 | + is_panning: bool, | |
| 47 | + pan_start: Option<Pos2>, | |
| 48 | + pan_origin: Option<Vec2>, | |
| 49 | + | |
| 50 | + // Cache | |
| 51 | + cache_key: u64, | |
| 52 | + | |
| 53 | + // Initial layout stored for reset | |
| 54 | + initial_positions: Vec<Pos2>, | |
| 55 | + initial_pan: Vec2, | |
| 56 | + initial_zoom: f32, | |
| 57 | +} | |
| 58 | + | |
| 59 | +impl SchemaDiagramView { | |
| 60 | + pub fn new() -> Self { | |
| 61 | + Self { | |
| 62 | + nodes: Vec::new(), | |
| 63 | + edges: Vec::new(), | |
| 64 | + mermaid_code: String::new(), | |
| 65 | + pan_offset: Vec2::ZERO, | |
| 66 | + zoom: 1.0, | |
| 67 | + dragging_node: None, | |
| 68 | + drag_start: None, | |
| 69 | + node_drag_origin: None, | |
| 70 | + is_panning: false, | |
| 71 | + pan_start: None, | |
| 72 | + pan_origin: None, | |
| 73 | + cache_key: 0, | |
| 74 | + initial_positions: Vec::new(), | |
| 75 | + initial_pan: Vec2::ZERO, | |
| 76 | + initial_zoom: 1.0, | |
| 77 | + } | |
| 78 | + } | |
| 79 | + | |
| 80 | + pub fn invalidate(&mut self) { | |
| 81 | + self.cache_key = 0; | |
| 82 | + } | |
| 83 | + | |
| 84 | + pub fn show( | |
| 85 | + &mut self, | |
| 86 | + ui: &mut Ui, | |
| 87 | + files: &[(String, serde_json::Value)], | |
| 88 | + structs: &[SharedStruct], | |
| 89 | + unique_structs: &[SharedStruct], | |
| 90 | + enum_conversions: &[EnumConversion], | |
| 91 | + hidden_fields: &[String], | |
| 92 | + ) { | |
| 93 | + let key = Self::compute_cache_key(files, structs, enum_conversions, hidden_fields); | |
| 94 | + if key != self.cache_key { | |
| 95 | + self.rebuild(files, structs, unique_structs, enum_conversions, hidden_fields); | |
| 96 | + self.cache_key = key; | |
| 97 | + } | |
| 98 | + | |
| 99 | + self.show_toolbar(ui); | |
| 100 | + ui.add_space(4.0); | |
| 101 | + self.show_diagram(ui); | |
| 102 | + } | |
| 103 | + | |
| 104 | + fn compute_cache_key( | |
| 105 | + files: &[(String, serde_json::Value)], | |
| 106 | + structs: &[SharedStruct], | |
| 107 | + enum_conversions: &[EnumConversion], | |
| 108 | + hidden_fields: &[String], | |
| 109 | + ) -> u64 { | |
| 110 | + use std::hash::{Hash, Hasher}; | |
| 111 | + let mut hasher = std::collections::hash_map::DefaultHasher::new(); | |
| 112 | + files.len().hash(&mut hasher); | |
| 113 | + for (name, _) in files { | |
| 114 | + name.hash(&mut hasher); | |
| 115 | + } | |
| 116 | + structs.len().hash(&mut hasher); | |
| 117 | + for s in structs { | |
| 118 | + s.name.hash(&mut hasher); | |
| 119 | + s.fields.len().hash(&mut hasher); | |
| 120 | + s.occurrence_count.hash(&mut hasher); | |
| 121 | + } | |
| 122 | + enum_conversions.len().hash(&mut hasher); | |
| 123 | + for e in enum_conversions { | |
| 124 | + e.field_name.hash(&mut hasher); | |
| 125 | + e.enum_name.hash(&mut hasher); | |
| 126 | + } | |
| 127 | + hidden_fields.len().hash(&mut hasher); | |
| 128 | + for h in hidden_fields { | |
| 129 | + h.hash(&mut hasher); | |
| 130 | + } | |
| 131 | + hasher.finish() | |
| 132 | + } | |
| 133 | + | |
| 134 | + fn rebuild( | |
| 135 | + &mut self, | |
| 136 | + files: &[(String, serde_json::Value)], | |
| 137 | + structs: &[SharedStruct], | |
| 138 | + unique_structs: &[SharedStruct], | |
| 139 | + enum_conversions: &[EnumConversion], | |
| 140 | + hidden_fields: &[String], | |
| 141 | + ) { | |
| 142 | + let hidden_set: BTreeSet<&str> = hidden_fields.iter().map(|s| s.as_str()).collect(); | |
| 143 | + let enum_map: BTreeMap<&str, &EnumConversion> = enum_conversions | |
| 144 | + .iter() | |
| 145 | + .map(|e| (e.field_name.as_str(), e)) | |
| 146 | + .collect(); | |
| 147 | + | |
| 148 | + self.nodes.clear(); | |
| 149 | + self.edges.clear(); | |
| 150 | + | |
| 151 | + let shared_names: BTreeSet<String> = structs.iter().map(|s| s.name.clone()).collect(); | |
| 152 | + let unique_names: BTreeSet<String> = unique_structs.iter().map(|s| s.name.clone()).collect(); | |
| 153 | + | |
| 154 | + // Combine all structs for type resolution | |
| 155 | + let all_structs: Vec<SharedStruct> = structs.iter().chain(unique_structs).cloned().collect(); | |
| 156 | + | |
| 157 | + // Build unique struct nodes from schema (using disambiguated names) | |
| 158 | + for s in unique_structs { | |
| 159 | + let mut fields = Vec::new(); | |
| 160 | + for (field_name, field_type) in &s.fields { | |
| 161 | + let hidden_key = format!("{}.{}", s.name, field_name); | |
| 162 | + if hidden_set.contains(hidden_key.as_str()) { | |
| 163 | + continue; | |
| 164 | + } | |
| 165 | + let (type_display, color) = if let Some(ec) = enum_map.get(field_name.as_str()) { | |
| 166 | + (ec.enum_name.clone(), CatppuccinMocha::MAUVE) | |
| 167 | + } else { | |
| 168 | + let display = field_type.short_name(&all_structs); | |
| 169 | + let color = crate::theme::type_color(&display); | |
| 170 | + (display, color) | |
| 171 | + }; | |
| 172 | + fields.push((field_name.clone(), type_display, color)); | |
| 173 | + } | |
| 174 | + | |
| 175 | + self.nodes.push(DiagramNode { | |
| 176 | + name: s.name.clone(), | |
| 177 | + fields, | |
| 178 | + kind: NodeKind::Struct, | |
| 179 | + pos: Pos2::ZERO, | |
| 180 | + size: Vec2::ZERO, | |
| 181 | + is_shared: false, | |
| 182 | + }); | |
| 183 | + } | |
| 184 | + | |
| 185 | + // Build per-file struct nodes using CodeGenerator (only for structs not in schema) | |
| 186 | + let mut added_names: BTreeSet<String> = unique_names.clone(); | |
| 187 | + for (filename, value) in files { | |
| 188 | + let root_name = crate::codegen::first_normal_word(filename) | |
| 189 | + .map(|w| to_pascal_case(&crate::codegen::singularize(&w))) | |
| 190 | + .unwrap_or_else(|| "Root".to_string()); | |
| 191 | + | |
| 192 | + let gen = crate::codegen::CodeGenerator::from_value_named(value, &root_name); | |
| 193 | + | |
| 194 | + for gs in &gen.structs { | |
| 195 | + // Skip if this struct name matches a shared or unique struct | |
| 196 | + if shared_names.contains(&gs.name) || unique_names.contains(&gs.name) { | |
| 197 | + continue; | |
| 198 | + } | |
| 199 | + // Skip duplicates from other files | |
| 200 | + if added_names.contains(&gs.name) { | |
| 201 | + continue; | |
| 202 | + } | |
| 203 | + added_names.insert(gs.name.clone()); | |
| 204 | + | |
| 205 | + let fields: Vec<(String, String, Color32)> = gs | |
| 206 | + .fields | |
| 207 | + .iter() | |
| 208 | + .filter(|f| { | |
| 209 | + let hidden_key = format!("{}.{}", gs.name, f.json_name); | |
| 210 | + !hidden_set.contains(hidden_key.as_str()) | |
| 211 | + }) | |
| 212 | + .map(|f| { | |
| 213 | + let type_display = if let Some(ec) = enum_map.get(f.json_name.as_str()) { | |
| 214 | + ec.enum_name.clone() | |
| 215 | + } else { | |
| 216 | + f.resolved_type.clone().unwrap_or_else(|| f.inferred_type.rust_type()) | |
| 217 | + }; | |
| 218 | + let color = crate::theme::type_color(&type_display); | |
| 219 | + (f.json_name.clone(), type_display, color) | |
| 220 | + }) | |
| 221 | + .collect(); | |
| 222 | + | |
| 223 | + self.nodes.push(DiagramNode { | |
| 224 | + name: gs.name.clone(), | |
| 225 | + fields, | |
| 226 | + kind: NodeKind::Struct, | |
| 227 | + pos: Pos2::ZERO, | |
| 228 | + size: Vec2::ZERO, | |
| 229 | + is_shared: false, | |
| 230 | + }); | |
| 231 | + } | |
| 232 | + } | |
| 233 | + | |
| 234 | + // Build shared struct nodes | |
| 235 | + for s in structs { | |
| 236 | + let mut fields = Vec::new(); | |
| 237 | + for (field_name, field_type) in &s.fields { | |
| 238 | + let hidden_key = format!("{}.{}", s.name, field_name); | |
| 239 | + if hidden_set.contains(hidden_key.as_str()) { | |
| 240 | + continue; | |
| 241 | + } | |
| 242 | + | |
| 243 | + let (type_display, color) = if let Some(ec) = enum_map.get(field_name.as_str()) { | |
| 244 | + (ec.enum_name.clone(), CatppuccinMocha::MAUVE) | |
| 245 | + } else { | |
| 246 | + let display = field_type.short_name(&all_structs); | |
| 247 | + let color = crate::theme::type_color(&display); | |
| 248 | + (display, color) | |
| 249 | + }; | |
| 250 | + | |
| 251 | + fields.push((field_name.clone(), type_display, color)); | |
| 252 | + } | |
| 253 | + | |
| 254 | + self.nodes.push(DiagramNode { | |
| 255 | + name: s.name.clone(), | |
| 256 | + fields, | |
| 257 | + kind: NodeKind::Struct, | |
| 258 | + pos: Pos2::ZERO, | |
| 259 | + size: Vec2::ZERO, | |
| 260 | + is_shared: s.source_files.len() > 1, | |
| 261 | + }); | |
| 262 | + } | |
| 263 | + | |
| 264 | + // Build enum nodes from conversions | |
| 265 | + let mut enum_names_added: BTreeSet<String> = BTreeSet::new(); | |
| 266 | + for ec in enum_conversions { | |
| 267 | + if enum_names_added.contains(&ec.enum_name) { | |
| 268 | + continue; | |
| 269 | + } | |
| 270 | + enum_names_added.insert(ec.enum_name.clone()); | |
| 271 | + | |
| 272 | + let fields: Vec<(String, String, Color32)> = ec | |
| 273 | + .variants | |
| 274 | + .iter() | |
| 275 | + .map(|v| (v.clone(), String::new(), CatppuccinMocha::GREEN)) | |
| 276 | + .collect(); | |
| 277 | + | |
| 278 | + self.nodes.push(DiagramNode { | |
| 279 | + name: ec.enum_name.clone(), | |
| 280 | + fields, | |
| 281 | + kind: NodeKind::Enum, | |
| 282 | + pos: Pos2::ZERO, | |
| 283 | + size: Vec2::ZERO, | |
| 284 | + is_shared: false, | |
| 285 | + }); | |
| 286 | + } | |
| 287 | + | |
| 288 | + // Build edges: struct field -> other struct or enum | |
| 289 | + let node_names: BTreeSet<String> = self.nodes.iter().map(|n| n.name.clone()).collect(); | |
| 290 | + for node in &self.nodes { | |
| 291 | + if node.kind != NodeKind::Struct { | |
| 292 | + continue; | |
| 293 | + } | |
| 294 | + for (field_name, type_display, _) in &node.fields { | |
| 295 | + let target = extract_inner_type(type_display); | |
| 296 | + if target != node.name && node_names.contains(&target) { | |
| 297 | + self.edges.push(DiagramEdge { | |
| 298 | + from: node.name.clone(), | |
| 299 | + to: target, | |
| 300 | + label: field_name.clone(), | |
| 301 | + }); | |
| 302 | + } | |
| 303 | + } | |
| 304 | + } | |
| 305 | + | |
| 306 | + // Layout nodes | |
| 307 | + self.layout_nodes(); | |
| 308 | + | |
| 309 | + // Store initial state for reset | |
| 310 | + self.initial_positions = self.nodes.iter().map(|n| n.pos).collect(); | |
| 311 | + self.initial_pan = Vec2::ZERO; | |
| 312 | + self.initial_zoom = 1.0; | |
| 313 | + self.pan_offset = Vec2::ZERO; | |
| 314 | + self.zoom = 1.0; | |
| 315 | + | |
| 316 | + // Generate mermaid | |
| 317 | + self.mermaid_code = | |
| 318 | + self.generate_mermaid(files, structs, enum_conversions, hidden_fields); | |
| 319 | + } | |
| 320 | + | |
| 321 | + fn layout_nodes(&mut self) { | |
| 322 | + if self.nodes.is_empty() { | |
| 323 | + return; | |
| 324 | + } | |
| 325 | + | |
| 326 | + // Compute node sizes based on content | |
| 327 | + for node in &mut self.nodes { | |
| 328 | + let title_width = node.name.len() as f32 * 8.0 + 40.0; | |
| 329 | + let max_field_width = node | |
| 330 | + .fields | |
| 331 | + .iter() | |
| 332 | + .map(|(name, typ, _)| { | |
| 333 | + if typ.is_empty() { | |
| 334 | + name.len() as f32 * 7.0 + 24.0 | |
| 335 | + } else { | |
| 336 | + (name.len() + typ.len() + 2) as f32 * 7.0 + 24.0 | |
| 337 | + } | |
| 338 | + }) | |
| 339 | + .fold(0.0f32, f32::max); | |
| 340 | + | |
| 341 | + let width = title_width.max(max_field_width).max(140.0); | |
| 342 | + let header_height = 32.0; | |
| 343 | + let field_height = node.fields.len() as f32 * 22.0; | |
| 344 | + let height = header_height + field_height + 12.0; | |
| 345 | + | |
| 346 | + node.size = vec2(width, height); | |
| 347 | + } | |
| 348 | + | |
| 349 | + // Grid layout: structs first, then enums | |
| 350 | + let padding = 40.0; | |
| 351 | + let cols = ((self.nodes.len() as f32).sqrt().ceil() as usize).max(1); | |
| 352 | + | |
| 353 | + let mut x = 0.0f32; | |
| 354 | + let mut y = 0.0f32; | |
| 355 | + let mut row_height = 0.0f32; | |
| 356 | + let mut col = 0; | |
| 357 | + | |
| 358 | + for node in &mut self.nodes { | |
| 359 | + node.pos = pos2(x, y); | |
| 360 | + row_height = row_height.max(node.size.y); | |
| 361 | + col += 1; | |
| 362 | + if col >= cols { | |
| 363 | + col = 0; | |
| 364 | + x = 0.0; | |
| 365 | + y += row_height + padding; | |
| 366 | + row_height = 0.0; | |
| 367 | + } else { | |
| 368 | + x += node.size.x + padding; | |
| 369 | + } | |
| 370 | + } | |
| 371 | + } | |
| 372 | + | |
| 373 | + fn generate_mermaid( | |
| 374 | + &self, | |
| 375 | + _files: &[(String, serde_json::Value)], | |
| 376 | + _structs: &[SharedStruct], | |
| 377 | + _enum_conversions: &[EnumConversion], | |
| 378 | + _hidden_fields: &[String], | |
| 379 | + ) -> String { | |
| 380 | + let mut lines = vec!["classDiagram".to_string()]; | |
| 381 | + | |
| 382 | + // All nodes (per-file + shared + enums) are already built — use them | |
| 383 | + for node in &self.nodes { | |
| 384 | + lines.push(format!(" class {} {{", node.name)); | |
| 385 | + if node.kind == NodeKind::Enum { | |
| 386 | + lines.push(" <<enumeration>>".to_string()); | |
| 387 | + } | |
| 388 | + for (field_name, type_display, _) in &node.fields { | |
| 389 | + if node.kind == NodeKind::Enum { | |
| 390 | + lines.push(format!(" {}", field_name)); | |
| 391 | + } else { | |
| 392 | + lines.push(format!(" +{} {}", type_display, field_name)); | |
| 393 | + } | |
| 394 | + } | |
| 395 | + lines.push(" }".to_string()); | |
| 396 | + } | |
| 397 | + | |
| 398 | + // Relationships from pre-built edges | |
| 399 | + for edge in &self.edges { | |
| 400 | + // Check if it's an array relationship by looking at the source node's field type | |
| 401 | + let is_array = self.nodes.iter() | |
| 402 | + .find(|n| n.name == edge.from) | |
| 403 | + .and_then(|n| n.fields.iter().find(|(fname, _, _)| fname == &edge.label)) | |
| 404 | + .map(|(_, typ, _)| typ.starts_with('[') || typ.starts_with("Vec<")) | |
| 405 | + .unwrap_or(false); | |
| 406 | + | |
| 407 | + let arrow = if is_array { | |
| 408 | + format!("{} \"1\" --> \"*\" {} : {}", edge.from, edge.to, edge.label) | |
| 409 | + } else { | |
| 410 | + format!("{} --> {} : {}", edge.from, edge.to, edge.label) | |
| 411 | + }; | |
| 412 | + lines.push(format!(" {}", arrow)); | |
| 413 | + } | |
| 414 | + | |
| 415 | + lines.join("\n") | |
| 416 | + } | |
| 417 | + | |
| 418 | + fn show_toolbar(&mut self, ui: &mut Ui) { | |
| 419 | + use egui_phosphor::regular; | |
| 420 | + | |
| 421 | + ui.horizontal(|ui| { | |
| 422 | + ui.spacing_mut().item_spacing.x = 6.0; | |
| 423 | + | |
| 424 | + // Copy Mermaid button | |
| 425 | + if ui | |
| 426 | + .add( | |
| 427 | + egui::Button::new( | |
| 428 | + RichText::new(format!(" {} Copy Mermaid ", regular::CLIPBOARD_TEXT)) | |
| 429 | + .size(12.0), | |
| 430 | + ) | |
| 431 | + .fill(CatppuccinMocha::SURFACE0) | |
| 432 | + .corner_radius(6.0), | |
| 433 | + ) | |
| 434 | + .clicked() | |
| 435 | + { | |
| 436 | + ui.ctx().copy_text(self.mermaid_code.clone()); | |
| 437 | + } | |
| 438 | + | |
| 439 | + // Reset view button | |
| 440 | + if ui | |
| 441 | + .add( | |
| 442 | + egui::Button::new( | |
| 443 | + RichText::new(format!(" {} Reset View ", regular::ARROWS_COUNTER_CLOCKWISE)) | |
| 444 | + .size(12.0), | |
| 445 | + ) | |
| 446 | + .fill(CatppuccinMocha::SURFACE0) | |
| 447 | + .corner_radius(6.0), | |
| 448 | + ) | |
| 449 | + .clicked() | |
| 450 | + { | |
| 451 | + self.pan_offset = self.initial_pan; | |
| 452 | + self.zoom = self.initial_zoom; | |
| 453 | + for (i, node) in self.nodes.iter_mut().enumerate() { | |
| 454 | + if let Some(pos) = self.initial_positions.get(i) { | |
| 455 | + node.pos = *pos; | |
| 456 | + } | |
| 457 | + } | |
| 458 | + } | |
| 459 | + | |
| 460 | + // Zoom display | |
| 461 | + ui.label( | |
| 462 | + RichText::new(format!("{:.0}%", self.zoom * 100.0)) | |
| 463 | + .color(CatppuccinMocha::OVERLAY0) | |
| 464 | + .size(11.0), | |
| 465 | + ); | |
| 466 | + | |
| 467 | + // Node count info | |
| 468 | + let struct_count = self.nodes.iter().filter(|n| n.kind == NodeKind::Struct).count(); | |
| 469 | + let enum_count = self.nodes.iter().filter(|n| n.kind == NodeKind::Enum).count(); | |
| 470 | + let edge_count = self.edges.len(); | |
| 471 | + let mut info = format!("{} structs", struct_count); | |
| 472 | + if enum_count > 0 { | |
| 473 | + info.push_str(&format!(", {} enums", enum_count)); | |
| 474 | + } | |
| 475 | + if edge_count > 0 { | |
| 476 | + info.push_str(&format!(", {} relations", edge_count)); | |
| 477 | + } | |
| 478 | + ui.label( | |
| 479 | + RichText::new(info) | |
| 480 | + .color(CatppuccinMocha::OVERLAY0) | |
| 481 | + .size(11.0), | |
| 482 | + ); | |
| 483 | + }); | |
| 484 | + } | |
| 485 | + | |
| 486 | + fn show_diagram(&mut self, ui: &mut Ui) { | |
| 487 | + let avail = ui.available_size(); | |
| 488 | + let (response, mut painter) = | |
| 489 | + ui.allocate_painter(avail, egui::Sense::click_and_drag()); | |
| 490 | + let canvas_rect = response.rect; | |
| 491 | + | |
| 492 | + // Handle zoom with scroll wheel | |
| 493 | + if ui.rect_contains_pointer(canvas_rect) { | |
| 494 | + let scroll_delta = ui.input(|i| i.smooth_scroll_delta.y); | |
| 495 | + if scroll_delta != 0.0 { | |
| 496 | + let factor = 1.0 + scroll_delta * 0.002; | |
| 497 | + let new_zoom = (self.zoom * factor).clamp(0.2, 3.0); | |
| 498 | + // Zoom toward pointer position | |
| 499 | + if let Some(pointer) = ui.input(|i| i.pointer.hover_pos()) { | |
| 500 | + let pointer_in_canvas = pointer - canvas_rect.left_top().to_vec2(); | |
| 501 | + let old_world = (pointer_in_canvas - self.pan_offset) / self.zoom; | |
| 502 | + self.zoom = new_zoom; | |
| 503 | + self.pan_offset = pointer_in_canvas - old_world * self.zoom; | |
| 504 | + } else { | |
| 505 | + self.zoom = new_zoom; | |
| 506 | + } | |
| 507 | + } | |
| 508 | + } | |
| 509 | + | |
| 510 | + // Handle drag interactions | |
| 511 | + let pointer_pos = ui.input(|i| i.pointer.hover_pos()); | |
| 512 | + | |
| 513 | + if response.drag_started() { | |
| 514 | + if let Some(pos) = pointer_pos { | |
| 515 | + // Check if we're clicking on a node | |
| 516 | + let world_pos = self.screen_to_world(pos, canvas_rect); | |
| 517 | + let mut found_node = None; | |
| 518 | + for (i, node) in self.nodes.iter().enumerate().rev() { | |
| 519 | + let node_rect = Rect::from_min_size(node.pos, node.size); | |
| 520 | + if node_rect.contains(world_pos) { | |
| 521 | + found_node = Some(i); | |
| 522 | + break; | |
| 523 | + } | |
| 524 | + } | |
| 525 | + | |
| 526 | + if let Some(idx) = found_node { | |
| 527 | + self.dragging_node = Some(idx); | |
| 528 | + self.drag_start = Some(pos); | |
| 529 | + self.node_drag_origin = Some(self.nodes[idx].pos); | |
| 530 | + } else { | |
| 531 | + // Panning | |
| 532 | + self.is_panning = true; | |
| 533 | + self.pan_start = Some(pos); | |
| 534 | + self.pan_origin = Some(self.pan_offset); | |
| 535 | + } | |
| 536 | + } | |
| 537 | + } | |
| 538 | + | |
| 539 | + if response.dragged() { | |
| 540 | + if let Some(pos) = pointer_pos { | |
| 541 | + if let Some(node_idx) = self.dragging_node { | |
| 542 | + if let (Some(start), Some(origin)) = (self.drag_start, self.node_drag_origin) { | |
| 543 | + let delta = (pos - start) / self.zoom; | |
| 544 | + self.nodes[node_idx].pos = origin + delta; | |
| 545 | + } | |
| 546 | + } else if self.is_panning { | |
| 547 | + if let (Some(start), Some(origin)) = (self.pan_start, self.pan_origin) { | |
| 548 | + let delta = pos - start; | |
| 549 | + self.pan_offset = origin + delta; | |
| 550 | + } | |
| 551 | + } | |
| 552 | + } | |
| 553 | + } | |
| 554 | + | |
| 555 | + if response.drag_stopped() { | |
| 556 | + self.dragging_node = None; | |
| 557 | + self.drag_start = None; | |
| 558 | + self.node_drag_origin = None; | |
| 559 | + self.is_panning = false; | |
| 560 | + self.pan_start = None; | |
| 561 | + self.pan_origin = None; | |
| 562 | + } | |
| 563 | + | |
| 564 | + // Draw background | |
| 565 | + painter.rect_filled(canvas_rect, 0.0, CatppuccinMocha::CRUST); | |
| 566 | + | |
| 567 | + // Draw grid dots | |
| 568 | + self.draw_grid(&painter, canvas_rect); | |
| 569 | + | |
| 570 | + // Clip to canvas | |
| 571 | + let clip = painter.clip_rect(); | |
| 572 | + painter.set_clip_rect(canvas_rect); | |
| 573 | + | |
| 574 | + // Draw edges first (behind nodes) | |
| 575 | + for edge in &self.edges { | |
| 576 | + self.draw_edge(&painter, canvas_rect, edge); | |
| 577 | + } | |
| 578 | + | |
| 579 | + // Draw nodes | |
| 580 | + let hover_world = pointer_pos.map(|p| self.screen_to_world(p, canvas_rect)); | |
| 581 | + for node in &self.nodes { | |
| 582 | + self.draw_node(&painter, canvas_rect, node, hover_world); | |
| 583 | + } | |
| 584 | + | |
| 585 | + // Restore clip | |
| 586 | + painter.set_clip_rect(clip); | |
| 587 | + } | |
| 588 | + | |
| 589 | + fn screen_to_world(&self, screen_pos: Pos2, canvas_rect: Rect) -> Pos2 { | |
| 590 | + let local = screen_pos - canvas_rect.left_top().to_vec2(); | |
| 591 | + pos2( | |
| 592 | + (local.x - self.pan_offset.x) / self.zoom, | |
| 593 | + (local.y - self.pan_offset.y) / self.zoom, | |
| 594 | + ) | |
| 595 | + } | |
| 596 | + | |
| 597 | + fn world_to_screen(&self, world_pos: Pos2, canvas_rect: Rect) -> Pos2 { | |
| 598 | + pos2( | |
| 599 | + world_pos.x * self.zoom + self.pan_offset.x + canvas_rect.left(), | |
| 600 | + world_pos.y * self.zoom + self.pan_offset.y + canvas_rect.top(), | |
| 601 | + ) | |
| 602 | + } | |
| 603 | + | |
| 604 | + fn draw_grid(&self, painter: &egui::Painter, canvas_rect: Rect) { | |
| 605 | + let grid_spacing = 30.0 * self.zoom; | |
| 606 | + if grid_spacing < 8.0 { | |
| 607 | + return; // Too zoomed out for grid | |
| 608 | + } | |
| 609 | + | |
| 610 | + let dot_color = Color32::from_rgba_premultiplied(255, 255, 255, 12); | |
| 611 | + let offset_x = self.pan_offset.x % grid_spacing; | |
| 612 | + let offset_y = self.pan_offset.y % grid_spacing; | |
| 613 | + | |
| 614 | + let mut x = canvas_rect.left() + offset_x; | |
| 615 | + while x < canvas_rect.right() { | |
| 616 | + let mut y = canvas_rect.top() + offset_y; | |
| 617 | + while y < canvas_rect.bottom() { | |
| 618 | + painter.circle_filled(pos2(x, y), 1.0, dot_color); | |
| 619 | + y += grid_spacing; | |
| 620 | + } | |
| 621 | + x += grid_spacing; | |
| 622 | + } | |
| 623 | + } | |
| 624 | + | |
| 625 | + fn draw_edge(&self, painter: &egui::Painter, canvas_rect: Rect, edge: &DiagramEdge) { | |
| 626 | + let from_node = self.nodes.iter().find(|n| n.name == edge.from); | |
| 627 | + let to_node = self.nodes.iter().find(|n| n.name == edge.to); | |
| 628 | + | |
| 629 | + let (Some(from), Some(to)) = (from_node, to_node) else { | |
| 630 | + return; | |
| 631 | + }; | |
| 632 | + | |
| 633 | + let from_center = from.pos + from.size / 2.0; | |
| 634 | + let to_center = to.pos + to.size / 2.0; | |
| 635 | + | |
| 636 | + // Find best connection points on node edges | |
| 637 | + let from_screen = self.world_to_screen( | |
| 638 | + edge_point(from.pos, from.size, to_center), | |
| 639 | + canvas_rect, | |
| 640 | + ); | |
| 641 | + let to_screen = self.world_to_screen( | |
| 642 | + edge_point(to.pos, to.size, from_center), | |
| 643 | + canvas_rect, | |
| 644 | + ); | |
| 645 | + | |
| 646 | + let edge_color = Color32::from_rgba_premultiplied(137, 180, 250, 100); | |
| 647 | + painter.line_segment( | |
| 648 | + [from_screen, to_screen], | |
| 649 | + egui::Stroke::new(1.5 * self.zoom, edge_color), | |
| 650 | + ); | |
| 651 | + | |
| 652 | + // Arrowhead | |
| 653 | + let dir = (to_screen - from_screen).normalized(); | |
| 654 | + let arrow_size = 8.0 * self.zoom; | |
| 655 | + let perp = vec2(-dir.y, dir.x); | |
| 656 | + let tip = to_screen; | |
| 657 | + let left = tip - dir * arrow_size + perp * arrow_size * 0.4; | |
| 658 | + let right = tip - dir * arrow_size - perp * arrow_size * 0.4; | |
| 659 | + painter.add(egui::Shape::convex_polygon( | |
| 660 | + vec![tip, left, right], | |
| 661 | + edge_color, | |
| 662 | + egui::Stroke::NONE, | |
| 663 | + )); | |
| 664 | + | |
| 665 | + // Edge label | |
| 666 | + let mid = pos2( | |
| 667 | + (from_screen.x + to_screen.x) / 2.0, | |
| 668 | + (from_screen.y + to_screen.y) / 2.0, | |
| 669 | + ); | |
| 670 | + let font = egui::FontId::proportional(10.0 * self.zoom); | |
| 671 | + painter.text( | |
| 672 | + mid, | |
| 673 | + egui::Align2::CENTER_CENTER, | |
| 674 | + &edge.label, | |
| 675 | + font, | |
| 676 | + CatppuccinMocha::OVERLAY0, | |
| 677 | + ); | |
| 678 | + } | |
| 679 | + | |
| 680 | + fn draw_node( | |
| 681 | + &self, | |
| 682 | + painter: &egui::Painter, | |
| 683 | + canvas_rect: Rect, | |
| 684 | + node: &DiagramNode, | |
| 685 | + hover_world: Option<Pos2>, | |
| 686 | + ) { | |
| 687 | + let screen_pos = self.world_to_screen(node.pos, canvas_rect); | |
| 688 | + let screen_size = node.size * self.zoom; | |
| 689 | + let node_rect = Rect::from_min_size(screen_pos, screen_size); | |
| 690 | + | |
| 691 | + // Check if visible | |
| 692 | + if !canvas_rect.intersects(node_rect) { | |
| 693 | + return; | |
| 694 | + } | |
| 695 | + | |
| 696 | + let world_rect = Rect::from_min_size(node.pos, node.size); | |
| 697 | + let is_hovered = hover_world | |
| 698 | + .map(|p| world_rect.contains(p)) | |
| 699 | + .unwrap_or(false); | |
| 700 | + | |
| 701 | + // Node background | |
| 702 | + let bg = if is_hovered { | |
| 703 | + CatppuccinMocha::SURFACE1 | |
| 704 | + } else { | |
| 705 | + CatppuccinMocha::SURFACE0 | |
| 706 | + }; | |
| 707 | + let border_color = match node.kind { | |
| 708 | + NodeKind::Struct if node.is_shared => CatppuccinMocha::BLUE, | |
| 709 | + NodeKind::Struct => CatppuccinMocha::SURFACE2, | |
| 710 | + NodeKind::Enum => CatppuccinMocha::MAUVE, | |
| 711 | + }; | |
| 712 | + | |
| 713 | + let corner = 8.0 * self.zoom; | |
| 714 | + painter.rect_filled(node_rect, corner, bg); | |
| 715 | + painter.rect_stroke( | |
| 716 | + node_rect, | |
| 717 | + corner, | |
| 718 | + egui::Stroke::new(1.5 * self.zoom, border_color), | |
| 719 | + egui::StrokeKind::Outside, | |
| 720 | + ); | |
| 721 | + | |
| 722 | + // Header | |
| 723 | + let header_height = 32.0 * self.zoom; | |
| 724 | + let header_rect = Rect::from_min_size(screen_pos, vec2(screen_size.x, header_height)); | |
| 725 | + | |
| 726 | + // Header background | |
| 727 | + let header_bg = match node.kind { | |
| 728 | + NodeKind::Struct if node.is_shared => Color32::from_rgba_premultiplied(137, 180, 250, 25), | |
| 729 | + NodeKind::Struct => Color32::from_rgba_premultiplied(69, 71, 90, 180), | |
| 730 | + NodeKind::Enum => Color32::from_rgba_premultiplied(203, 166, 247, 25), | |
| 731 | + }; | |
| 732 | + let corner_u8 = corner as u8; | |
| 733 | + painter.rect_filled( | |
| 734 | + Rect::from_min_size(screen_pos, vec2(screen_size.x, header_height)), | |
| 735 | + egui::CornerRadius { | |
| 736 | + nw: corner_u8, | |
| 737 | + ne: corner_u8, | |
| 738 | + sw: 0, | |
| 739 | + se: 0, | |
| 740 | + }, | |
| 741 | + header_bg, | |
| 742 | + ); | |
| 743 | + | |
| 744 | + // Kind badge | |
| 745 | + let badge_text = match node.kind { | |
| 746 | + NodeKind::Struct if node.is_shared => "shared", | |
| 747 | + NodeKind::Struct => "struct", | |
| 748 | + NodeKind::Enum => "enum", | |
| 749 | + }; | |
| 750 | + let badge_color = match node.kind { | |
| 751 | + NodeKind::Struct if node.is_shared => CatppuccinMocha::BLUE, | |
| 752 | + NodeKind::Struct => CatppuccinMocha::OVERLAY0, | |
| 753 | + NodeKind::Enum => CatppuccinMocha::MAUVE, | |
| 754 | + }; | |
| 755 | + let badge_font = egui::FontId::proportional(9.0 * self.zoom); | |
| 756 | + painter.text( | |
| 757 | + pos2( | |
| 758 | + header_rect.left() + 10.0 * self.zoom, | |
| 759 | + header_rect.top() + 8.0 * self.zoom, | |
| 760 | + ), | |
| 761 | + egui::Align2::LEFT_TOP, | |
| 762 | + badge_text, | |
| 763 | + badge_font, | |
| 764 | + badge_color, | |
| 765 | + ); | |
| 766 | + | |
| 767 | + // Node name | |
| 768 | + let name_font = egui::FontId::proportional(13.0 * self.zoom); | |
| 769 | + let name_color = match node.kind { | |
| 770 | + NodeKind::Struct if node.is_shared => CatppuccinMocha::BLUE, | |
| 771 | + NodeKind::Struct => CatppuccinMocha::TEXT, | |
| 772 | + NodeKind::Enum => CatppuccinMocha::MAUVE, | |
| 773 | + }; | |
| 774 | + painter.text( | |
| 775 | + pos2( | |
| 776 | + header_rect.center().x, | |
| 777 | + header_rect.top() + 16.0 * self.zoom, | |
| 778 | + ), | |
| 779 | + egui::Align2::CENTER_CENTER, | |
| 780 | + &node.name, | |
| 781 | + name_font, | |
| 782 | + name_color, | |
| 783 | + ); | |
| 784 | + | |
| 785 | + // Divider line under header | |
| 786 | + painter.line_segment( | |
| 787 | + [ | |
| 788 | + pos2(header_rect.left() + 4.0 * self.zoom, header_rect.bottom()), | |
| 789 | + pos2(header_rect.right() - 4.0 * self.zoom, header_rect.bottom()), | |
| 790 | + ], | |
| 791 | + egui::Stroke::new(1.0 * self.zoom, CatppuccinMocha::SURFACE2), | |
| 792 | + ); | |
| 793 | + | |
| 794 | + // Fields | |
| 795 | + let field_start_y = header_rect.bottom() + 6.0 * self.zoom; | |
| 796 | + let field_height = 22.0 * self.zoom; | |
| 797 | + let field_font = egui::FontId::proportional(11.0 * self.zoom); | |
| 798 | + let type_font = egui::FontId::proportional(10.0 * self.zoom); | |
| 799 | + | |
| 800 | + for (i, (name, typ, color)) in node.fields.iter().enumerate() { | |
| 801 | + let y = field_start_y + i as f32 * field_height; | |
| 802 | + | |
| 803 | + if node.kind == NodeKind::Enum { | |
| 804 | + // Enum variant — just the name | |
| 805 | + painter.text( | |
| 806 | + pos2(screen_pos.x + 12.0 * self.zoom, y), | |
| 807 | + egui::Align2::LEFT_TOP, | |
| 808 | + name, | |
| 809 | + field_font.clone(), | |
| 810 | + *color, | |
| 811 | + ); | |
| 812 | + } else { | |
| 813 | + // Struct field — name : type | |
| 814 | + painter.text( | |
| 815 | + pos2(screen_pos.x + 12.0 * self.zoom, y), | |
| 816 | + egui::Align2::LEFT_TOP, | |
| 817 | + name, | |
| 818 | + field_font.clone(), | |
| 819 | + CatppuccinMocha::SUBTEXT0, | |
| 820 | + ); | |
| 821 | + | |
| 822 | + if !typ.is_empty() { | |
| 823 | + painter.text( | |
| 824 | + pos2(screen_pos.x + screen_size.x - 12.0 * self.zoom, y), | |
| 825 | + egui::Align2::RIGHT_TOP, | |
| 826 | + typ, | |
| 827 | + type_font.clone(), | |
| 828 | + *color, | |
| 829 | + ); | |
| 830 | + } | |
| 831 | + } | |
| 832 | + } | |
| 833 | + } | |
| 834 | +} | |
| 835 | + | |
| 836 | +/// Find the point on the edge of a rectangle closest to a target point | |
| 837 | +fn edge_point(rect_pos: Pos2, rect_size: Vec2, target: Pos2) -> Pos2 { | |
| 838 | + let center = rect_pos + rect_size / 2.0; | |
| 839 | + let dir = target - center; | |
| 840 | + | |
| 841 | + if dir.x.abs() < 0.001 && dir.y.abs() < 0.001 { | |
| 842 | + return center; | |
| 843 | + } | |
| 844 | + | |
| 845 | + let half_w = rect_size.x / 2.0; | |
| 846 | + let half_h = rect_size.y / 2.0; | |
| 847 | + | |
| 848 | + // Scale to find intersection with rectangle edge | |
| 849 | + let scale_x = if dir.x.abs() > 0.001 { | |
| 850 | + half_w / dir.x.abs() | |
| 851 | + } else { | |
| 852 | + f32::MAX | |
| 853 | + }; | |
| 854 | + let scale_y = if dir.y.abs() > 0.001 { | |
| 855 | + half_h / dir.y.abs() | |
| 856 | + } else { | |
| 857 | + f32::MAX | |
| 858 | + }; | |
| 859 | + | |
| 860 | + let scale = scale_x.min(scale_y); | |
| 861 | + center + dir * scale | |
| 862 | +} | |
| 863 | + | |
| 864 | +fn to_pascal_case(s: &str) -> String { | |
| 865 | + let mut result = String::new(); | |
| 866 | + let mut capitalize_next = true; | |
| 867 | + for ch in s.chars() { | |
| 868 | + if ch == '_' || ch == '-' || ch == ' ' { | |
| 869 | + capitalize_next = true; | |
| 870 | + } else if capitalize_next { | |
| 871 | + result.push(ch.to_ascii_uppercase()); | |
| 872 | + capitalize_next = false; | |
| 873 | + } else { | |
| 874 | + result.push(ch); | |
| 875 | + } | |
| 876 | + } | |
| 877 | + result | |
| 878 | +} | |
| 879 | + | |
| 880 | +/// Extract the inner type name from wrapper types like Vec<X>, Option<X>, [X] | |
| 881 | +fn extract_inner_type(type_str: &str) -> String { | |
| 882 | + let s = type_str.trim(); | |
| 883 | + if let Some(inner) = s.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { | |
| 884 | + return extract_inner_type(inner); | |
| 885 | + } | |
| 886 | + if let Some(inner) = s.strip_suffix('?') { | |
| 887 | + return extract_inner_type(inner); | |
| 888 | + } | |
| 889 | + if let Some(rest) = s.strip_prefix("Vec<") { | |
| 890 | + if let Some(inner) = rest.strip_suffix('>') { | |
| 891 | + return extract_inner_type(inner); | |
| 892 | + } | |
| 893 | + } | |
| 894 | + if let Some(rest) = s.strip_prefix("Option<") { | |
| 895 | + if let Some(inner) = rest.strip_suffix('>') { | |
| 896 | + return extract_inner_type(inner); | |
| 897 | + } | |
| 898 | + } | |
| 899 | + s.to_string() | |
| 900 | +} |
+545-0src/views/table.rs
| @@ -0,0 +1,545 @@ | ||
| 1 | +use egui::{self, RichText, Ui}; | |
| 2 | +use std::collections::BTreeSet; | |
| 3 | + | |
| 4 | +use crate::temporal::{detect_temporal, detect_unix_timestamp}; | |
| 5 | +use crate::theme::{type_color, CatppuccinMocha}; | |
| 6 | +use crate::types::infer_type; | |
| 7 | + | |
| 8 | +#[derive(Debug, Clone, Copy, PartialEq)] | |
| 9 | +pub enum SchemaSortMode { | |
| 10 | + Name, | |
| 11 | + Fields, | |
| 12 | + Occurrences, | |
| 13 | + Files, | |
| 14 | +} | |
| 15 | + | |
| 16 | +enum SchemaRow { | |
| 17 | + Header { struct_idx: usize, expanded: bool }, | |
| 18 | + GridHeader { struct_idx: usize }, | |
| 19 | + Field { struct_idx: usize, field_idx: usize }, | |
| 20 | + Spacer, | |
| 21 | +} | |
| 22 | + | |
| 23 | +struct TableRow { | |
| 24 | + field: String, | |
| 25 | + field_color: egui::Color32, | |
| 26 | + type_name: String, | |
| 27 | + type_color: egui::Color32, | |
| 28 | + value_text: String, | |
| 29 | + value_color: egui::Color32, | |
| 30 | + toggle_path: Option<String>, | |
| 31 | + /// Tooltip info for temporal values | |
| 32 | + temporal_tip: Option<String>, | |
| 33 | + depth: usize, | |
| 34 | +} | |
| 35 | + | |
| 36 | +pub struct TableView { | |
| 37 | + expanded: BTreeSet<String>, | |
| 38 | + rows: Vec<TableRow>, | |
| 39 | + dirty: bool, | |
| 40 | + schema_sort: SchemaSortMode, | |
| 41 | + schema_sort_ascending: bool, | |
| 42 | +} | |
| 43 | + | |
| 44 | +impl TableView { | |
| 45 | + pub fn new() -> Self { | |
| 46 | + Self { | |
| 47 | + expanded: BTreeSet::new(), | |
| 48 | + rows: Vec::new(), | |
| 49 | + dirty: true, | |
| 50 | + schema_sort: SchemaSortMode::Occurrences, | |
| 51 | + schema_sort_ascending: false, | |
| 52 | + } | |
| 53 | + } | |
| 54 | + | |
| 55 | + pub fn mark_dirty(&mut self) { | |
| 56 | + self.dirty = true; | |
| 57 | + } | |
| 58 | + | |
| 59 | + pub fn show_schema_matrix( | |
| 60 | + &mut self, | |
| 61 | + ui: &mut Ui, | |
| 62 | + structs: &[crate::schema::SharedStruct], | |
| 63 | + filenames: &[String], | |
| 64 | + ) { | |
| 65 | + use egui_phosphor::regular; | |
| 66 | + | |
| 67 | + // Sort struct indices | |
| 68 | + let mut sorted_indices: Vec<usize> = (0..structs.len()).collect(); | |
| 69 | + let asc = self.schema_sort_ascending; | |
| 70 | + sorted_indices.sort_by(|&a, &b| { | |
| 71 | + let cmp = match self.schema_sort { | |
| 72 | + SchemaSortMode::Name => structs[a].name.to_lowercase().cmp(&structs[b].name.to_lowercase()), | |
| 73 | + SchemaSortMode::Fields => structs[a].fields.len().cmp(&structs[b].fields.len()), | |
| 74 | + SchemaSortMode::Occurrences => structs[a].occurrence_count.cmp(&structs[b].occurrence_count), | |
| 75 | + SchemaSortMode::Files => structs[a].source_files.len().cmp(&structs[b].source_files.len()), | |
| 76 | + }; | |
| 77 | + if asc { cmp } else { cmp.reverse() } | |
| 78 | + }); | |
| 79 | + | |
| 80 | + // Sortable column header | |
| 81 | + let sort_arrow = |mode: SchemaSortMode, current: SchemaSortMode, ascending: bool| -> &str { | |
| 82 | + if mode == current { | |
| 83 | + if ascending { regular::CARET_UP } else { regular::CARET_DOWN } | |
| 84 | + } else { | |
| 85 | + "" | |
| 86 | + } | |
| 87 | + }; | |
| 88 | + | |
| 89 | + ui.horizontal(|ui| { | |
| 90 | + let col_data: [(SchemaSortMode, &str, f32); 4] = [ | |
| 91 | + (SchemaSortMode::Name, "Struct", 220.0), | |
| 92 | + (SchemaSortMode::Fields, "Fields", 70.0), | |
| 93 | + (SchemaSortMode::Occurrences, "Occurrences", 100.0), | |
| 94 | + (SchemaSortMode::Files, "Files", 80.0), | |
| 95 | + ]; | |
| 96 | + for (mode, label, width) in col_data { | |
| 97 | + let arrow = sort_arrow(mode, self.schema_sort, self.schema_sort_ascending); | |
| 98 | + let text = if arrow.is_empty() { | |
| 99 | + label.to_string() | |
| 100 | + } else { | |
| 101 | + format!("{} {}", label, arrow) | |
| 102 | + }; | |
| 103 | + let r = ui.add_sized( | |
| 104 | + [width, 20.0], | |
| 105 | + egui::Label::new( | |
| 106 | + RichText::new(text) | |
| 107 | + .color(CatppuccinMocha::SUBTEXT0) | |
| 108 | + .strong() | |
| 109 | + .size(12.0), | |
| 110 | + ).sense(egui::Sense::click()), | |
| 111 | + ); | |
| 112 | + if r.clicked() { | |
| 113 | + if self.schema_sort == mode { | |
| 114 | + self.schema_sort_ascending = !self.schema_sort_ascending; | |
| 115 | + } else { | |
| 116 | + self.schema_sort = mode; | |
| 117 | + self.schema_sort_ascending = mode == SchemaSortMode::Name; | |
| 118 | + } | |
| 119 | + } | |
| 120 | + } | |
| 121 | + }); | |
| 122 | + | |
| 123 | + // Subtle divider | |
| 124 | + let sep_rect = ui.available_rect_before_wrap(); | |
| 125 | + ui.painter().line_segment( | |
| 126 | + [ | |
| 127 | + egui::pos2(sep_rect.left(), sep_rect.top()), | |
| 128 | + egui::pos2(sep_rect.right(), sep_rect.top()), | |
| 129 | + ], | |
| 130 | + egui::Stroke::new(1.0, CatppuccinMocha::SURFACE0), | |
| 131 | + ); | |
| 132 | + ui.add_space(4.0); | |
| 133 | + | |
| 134 | + // Pre-compute visible rows using sorted order | |
| 135 | + let mut rows: Vec<SchemaRow> = Vec::new(); | |
| 136 | + for &si in &sorted_indices { | |
| 137 | + let shared = &structs[si]; | |
| 138 | + let is_expanded = self.expanded.contains(&shared.name); | |
| 139 | + rows.push(SchemaRow::Header { struct_idx: si, expanded: is_expanded }); | |
| 140 | + if is_expanded { | |
| 141 | + rows.push(SchemaRow::GridHeader { struct_idx: si }); | |
| 142 | + for field_idx in 0..shared.fields.len() { | |
| 143 | + rows.push(SchemaRow::Field { struct_idx: si, field_idx }); | |
| 144 | + } | |
| 145 | + rows.push(SchemaRow::Spacer); | |
| 146 | + } | |
| 147 | + } | |
| 148 | + | |
| 149 | + let row_height = 20.0; | |
| 150 | + let num_rows = rows.len(); | |
| 151 | + | |
| 152 | + egui::ScrollArea::both() | |
| 153 | + .id_salt("schema_matrix_scroll") | |
| 154 | + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) | |
| 155 | + .auto_shrink(false) | |
| 156 | + .show_rows(ui, row_height, num_rows, |ui, row_range| { | |
| 157 | + let mut toggle = None; | |
| 158 | + for idx in row_range { | |
| 159 | + match &rows[idx] { | |
| 160 | + SchemaRow::Header { struct_idx, expanded } => { | |
| 161 | + let shared = &structs[*struct_idx]; | |
| 162 | + let arrow = if *expanded { | |
| 163 | + regular::CARET_DOWN | |
| 164 | + } else { | |
| 165 | + regular::CARET_RIGHT | |
| 166 | + }; | |
| 167 | + let stripe = if idx % 2 == 0 { | |
| 168 | + CatppuccinMocha::BASE | |
| 169 | + } else { | |
| 170 | + CatppuccinMocha::MANTLE | |
| 171 | + }; | |
| 172 | + egui::Frame::new().fill(stripe).show(ui, |ui| { | |
| 173 | + ui.horizontal(|ui| { | |
| 174 | + // Name column — left-aligned in fixed width | |
| 175 | + let (rect, _) = ui.allocate_exact_size( | |
| 176 | + egui::vec2(220.0, row_height), | |
| 177 | + egui::Sense::click(), | |
| 178 | + ); | |
| 179 | + let name_text = format!("{} {}", arrow, shared.name); | |
| 180 | + let galley = ui.painter().layout_no_wrap( | |
| 181 | + name_text, | |
| 182 | + egui::FontId::proportional(14.0), | |
| 183 | + CatppuccinMocha::LAVENDER, | |
| 184 | + ); | |
| 185 | + ui.painter().galley( | |
| 186 | + egui::pos2(rect.left(), rect.center().y - galley.size().y / 2.0), | |
| 187 | + galley, | |
| 188 | + CatppuccinMocha::LAVENDER, | |
| 189 | + ); | |
| 190 | + if ui.rect_contains_pointer(rect) && ui.input(|i| i.pointer.any_pressed()) { | |
| 191 | + toggle = Some(shared.name.clone()); | |
| 192 | + } | |
| 193 | + | |
| 194 | + // Fields column | |
| 195 | + ui.add_sized( | |
| 196 | + [70.0, row_height], | |
| 197 | + egui::Label::new( | |
| 198 | + RichText::new(shared.fields.len().to_string()) | |
| 199 | + .color(CatppuccinMocha::TEXT), | |
| 200 | + ), | |
| 201 | + ); | |
| 202 | + | |
| 203 | + // Occurrences column | |
| 204 | + ui.add_sized( | |
| 205 | + [100.0, row_height], | |
| 206 | + egui::Label::new( | |
| 207 | + RichText::new(shared.occurrence_count.to_string()) | |
| 208 | + .color(CatppuccinMocha::TEXT), | |
| 209 | + ), | |
| 210 | + ); | |
| 211 | + | |
| 212 | + // Files column | |
| 213 | + let files_text = format!("{}/{}", shared.source_files.len(), filenames.len()); | |
| 214 | + ui.add_sized( | |
| 215 | + [80.0, row_height], | |
| 216 | + egui::Label::new( | |
| 217 | + RichText::new(&files_text) | |
| 218 | + .color(CatppuccinMocha::TEXT), | |
| 219 | + ), | |
| 220 | + ); | |
| 221 | + }); | |
| 222 | + }); | |
| 223 | + } | |
| 224 | + SchemaRow::GridHeader { struct_idx } => { | |
| 225 | + let shared = &structs[*struct_idx]; | |
| 226 | + ui.horizontal(|ui| { | |
| 227 | + ui.add_space(16.0); | |
| 228 | + ui.add_sized([220.0, row_height], egui::Label::new( | |
| 229 | + RichText::new("Field") | |
| 230 | + .color(CatppuccinMocha::BLUE) | |
| 231 | + .strong(), | |
| 232 | + )); | |
| 233 | + for file in &shared.source_files { | |
| 234 | + ui.add_sized([120.0, row_height], egui::Label::new( | |
| 235 | + RichText::new(file) | |
| 236 | + .color(CatppuccinMocha::YELLOW) | |
| 237 | + .strong() | |
| 238 | + .size(11.0), | |
| 239 | + )); | |
| 240 | + } | |
| 241 | + }); | |
| 242 | + } | |
| 243 | + SchemaRow::Field { struct_idx, field_idx } => { | |
| 244 | + let shared = &structs[*struct_idx]; | |
| 245 | + let fields: Vec<_> = shared.fields.iter().collect(); | |
| 246 | + if let Some((field_name, field_type)) = fields.get(*field_idx) { | |
| 247 | + let stripe = if field_idx % 2 == 0 { | |
| 248 | + CatppuccinMocha::SURFACE0 | |
| 249 | + } else { | |
| 250 | + CatppuccinMocha::BASE | |
| 251 | + }; | |
| 252 | + egui::Frame::new().fill(stripe).show(ui, |ui| { | |
| 253 | + ui.horizontal(|ui| { | |
| 254 | + ui.add_space(16.0); | |
| 255 | + ui.add_sized([220.0, row_height], egui::Label::new( | |
| 256 | + RichText::new(*field_name) | |
| 257 | + .color(CatppuccinMocha::BLUE), | |
| 258 | + )); | |
| 259 | + for _file in &shared.source_files { | |
| 260 | + let type_str = field_type.short_name(structs); | |
| 261 | + let color = type_color(&type_str); | |
| 262 | + let r = ui.add_sized([120.0, row_height], egui::Label::new( | |
| 263 | + RichText::new(&type_str).color(color), | |
| 264 | + )); | |
| 265 | + if let Some(tip) = field_type.tooltip(structs) { | |
| 266 | + r.on_hover_text(tip); | |
| 267 | + } | |
| 268 | + } | |
| 269 | + }); | |
| 270 | + }); | |
| 271 | + } | |
| 272 | + } | |
| 273 | + SchemaRow::Spacer => { | |
| 274 | + ui.add_space(8.0); | |
| 275 | + } | |
| 276 | + } | |
| 277 | + } | |
| 278 | + if let Some(name) = toggle { | |
| 279 | + if self.expanded.contains(&name) { | |
| 280 | + self.expanded.remove(&name); | |
| 281 | + } else { | |
| 282 | + self.expanded.insert(name); | |
| 283 | + } | |
| 284 | + } | |
| 285 | + }); | |
| 286 | + } | |
| 287 | + | |
| 288 | + pub fn show(&mut self, ui: &mut Ui, value: &serde_json::Value, filename: &str) { | |
| 289 | + ui.horizontal(|ui| { | |
| 290 | + if ui.button("Expand All").clicked() { | |
| 291 | + self.expand_all(value, ""); | |
| 292 | + self.dirty = true; | |
| 293 | + } | |
| 294 | + if ui.button("Collapse All").clicked() { | |
| 295 | + self.expanded.clear(); | |
| 296 | + self.dirty = true; | |
| 297 | + } | |
| 298 | + ui.label( | |
| 299 | + RichText::new(format!("{} rows", self.rows.len())) | |
| 300 | + .color(CatppuccinMocha::OVERLAY0) | |
| 301 | + .small(), | |
| 302 | + ); | |
| 303 | + }); | |
| 304 | + | |
| 305 | + ui.add_space(8.0); | |
| 306 | + | |
| 307 | + if self.dirty { | |
| 308 | + self.rows.clear(); | |
| 309 | + flatten_table(value, "", 0, &self.expanded, &mut self.rows); | |
| 310 | + self.dirty = false; | |
| 311 | + } | |
| 312 | + | |
| 313 | + let row_height = ui.text_style_height(&egui::TextStyle::Body) + 4.0; | |
| 314 | + let num_rows = self.rows.len(); | |
| 315 | + let mut toggle: Option<String> = None; | |
| 316 | + | |
| 317 | + // Header | |
| 318 | + ui.horizontal(|ui| { | |
| 319 | + ui.set_min_width(ui.available_width()); | |
| 320 | + ui.columns(3, |cols| { | |
| 321 | + cols[0].label(RichText::new("Field").color(CatppuccinMocha::SUBTEXT0).strong()); | |
| 322 | + cols[1].label(RichText::new("Type").color(CatppuccinMocha::SUBTEXT0).strong()); | |
| 323 | + cols[2].label(RichText::new("Value").color(CatppuccinMocha::SUBTEXT0).strong()); | |
| 324 | + }); | |
| 325 | + }); | |
| 326 | + | |
| 327 | + egui::ScrollArea::vertical() | |
| 328 | + .id_salt(format!("table_scroll_{}", filename)) | |
| 329 | + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) | |
| 330 | + .auto_shrink(false) | |
| 331 | + .show_rows(ui, row_height, num_rows, |ui, row_range| { | |
| 332 | + for idx in row_range { | |
| 333 | + let row = &self.rows[idx]; | |
| 334 | + let stripe = if idx % 2 == 0 { | |
| 335 | + CatppuccinMocha::BASE | |
| 336 | + } else { | |
| 337 | + CatppuccinMocha::MANTLE | |
| 338 | + }; | |
| 339 | + | |
| 340 | + egui::Frame::new() | |
| 341 | + .fill(stripe) | |
| 342 | + .show(ui, |ui| { | |
| 343 | + ui.columns(3, |cols| { | |
| 344 | + // Field column with indentation | |
| 345 | + cols[0].horizontal(|ui| { | |
| 346 | + if row.depth > 0 { | |
| 347 | + ui.add_space(row.depth as f32 * 16.0); | |
| 348 | + } | |
| 349 | + if row.toggle_path.is_some() { | |
| 350 | + let r = ui.add( | |
| 351 | + egui::Label::new( | |
| 352 | + RichText::new(&row.field).color(row.field_color), | |
| 353 | + ) | |
| 354 | + .sense(egui::Sense::click()), | |
| 355 | + ); | |
| 356 | + if r.clicked() { | |
| 357 | + toggle = row.toggle_path.clone(); | |
| 358 | + } | |
| 359 | + } else { | |
| 360 | + ui.label( | |
| 361 | + RichText::new(&row.field).color(row.field_color), | |
| 362 | + ); | |
| 363 | + } | |
| 364 | + }); | |
| 365 | + | |
| 366 | + // Type column | |
| 367 | + cols[1].label( | |
| 368 | + RichText::new(&row.type_name).color(row.type_color), | |
| 369 | + ); | |
| 370 | + | |
| 371 | + // Value column | |
| 372 | + let r = cols[2].label( | |
| 373 | + RichText::new(&row.value_text).color(row.value_color), | |
| 374 | + ); | |
| 375 | + if let Some(tip) = &row.temporal_tip { | |
| 376 | + r.on_hover_text(tip); | |
| 377 | + } | |
| 378 | + }); | |
| 379 | + }); | |
| 380 | + } | |
| 381 | + }); | |
| 382 | + | |
| 383 | + if let Some(path) = toggle { | |
| 384 | + if self.expanded.contains(&path) { | |
| 385 | + self.expanded.remove(&path); | |
| 386 | + } else { | |
| 387 | + self.expanded.insert(path); | |
| 388 | + } | |
| 389 | + self.dirty = true; | |
| 390 | + } | |
| 391 | + } | |
| 392 | + | |
| 393 | + fn expand_all(&mut self, value: &serde_json::Value, path: &str) { | |
| 394 | + match value { | |
| 395 | + serde_json::Value::Object(map) => { | |
| 396 | + if !path.is_empty() { | |
| 397 | + self.expanded.insert(path.to_string()); | |
| 398 | + } | |
| 399 | + for (key, val) in map { | |
| 400 | + let child_path = if path.is_empty() { | |
| 401 | + key.clone() | |
| 402 | + } else { | |
| 403 | + format!("{}.{}", path, key) | |
| 404 | + }; | |
| 405 | + self.expand_all(val, &child_path); | |
| 406 | + } | |
| 407 | + } | |
| 408 | + serde_json::Value::Array(arr) => { | |
| 409 | + if !path.is_empty() { | |
| 410 | + self.expanded.insert(path.to_string()); | |
| 411 | + } | |
| 412 | + for (i, val) in arr.iter().enumerate() { | |
| 413 | + self.expand_all(val, &format!("{}[{}]", path, i)); | |
| 414 | + } | |
| 415 | + } | |
| 416 | + _ => {} | |
| 417 | + } | |
| 418 | + } | |
| 419 | +} | |
| 420 | + | |
| 421 | +fn flatten_table( | |
| 422 | + value: &serde_json::Value, | |
| 423 | + path: &str, | |
| 424 | + depth: usize, | |
| 425 | + expanded: &BTreeSet<String>, | |
| 426 | + out: &mut Vec<TableRow>, | |
| 427 | +) { | |
| 428 | + match value { | |
| 429 | + serde_json::Value::Object(map) => { | |
| 430 | + for (key, val) in map { | |
| 431 | + let child_path = if path.is_empty() { | |
| 432 | + key.clone() | |
| 433 | + } else { | |
| 434 | + format!("{}.{}", path, key) | |
| 435 | + }; | |
| 436 | + flatten_field(key, val, &child_path, depth, expanded, out); | |
| 437 | + } | |
| 438 | + } | |
| 439 | + serde_json::Value::Array(arr) => { | |
| 440 | + for (i, val) in arr.iter().enumerate() { | |
| 441 | + let child_path = format!("{}[{}]", path, i); | |
| 442 | + let key = format!("[{}]", i); | |
| 443 | + flatten_field(&key, val, &child_path, depth, expanded, out); | |
| 444 | + } | |
| 445 | + } | |
| 446 | + _ => {} | |
| 447 | + } | |
| 448 | +} | |
| 449 | + | |
| 450 | +fn flatten_field( | |
| 451 | + key: &str, | |
| 452 | + value: &serde_json::Value, | |
| 453 | + path: &str, | |
| 454 | + depth: usize, | |
| 455 | + expanded: &BTreeSet<String>, | |
| 456 | + out: &mut Vec<TableRow>, | |
| 457 | +) { | |
| 458 | + let is_expandable = value.is_object() || value.is_array(); | |
| 459 | + let is_expanded = expanded.contains(path); | |
| 460 | + let inferred = infer_type(value); | |
| 461 | + | |
| 462 | + let field = if is_expandable { | |
| 463 | + let arrow = if is_expanded { | |
| 464 | + egui_phosphor::regular::CARET_DOWN | |
| 465 | + } else { | |
| 466 | + egui_phosphor::regular::CARET_RIGHT | |
| 467 | + }; | |
| 468 | + let count = match value { | |
| 469 | + serde_json::Value::Object(m) => format!(" ({})", m.len()), | |
| 470 | + serde_json::Value::Array(a) => format!(" [{}]", a.len()), | |
| 471 | + _ => String::new(), | |
| 472 | + }; | |
| 473 | + format!("{} {}{}", arrow, key, count) | |
| 474 | + } else { | |
| 475 | + key.to_string() | |
| 476 | + }; | |
| 477 | + | |
| 478 | + let tn = inferred.display_name(); | |
| 479 | + let tc = type_color(&tn); | |
| 480 | + | |
| 481 | + let (value_text, value_color, temporal_tip) = match value { | |
| 482 | + serde_json::Value::Null => ("null".to_string(), CatppuccinMocha::OVERLAY0, None), | |
| 483 | + serde_json::Value::Bool(b) => (b.to_string(), CatppuccinMocha::MAUVE, None), | |
| 484 | + serde_json::Value::Number(n) => { | |
| 485 | + let tip = n.as_i64().and_then(|i| { | |
| 486 | + detect_unix_timestamp(i).map(|t| { | |
| 487 | + let rel = t.relative_time(); | |
| 488 | + if rel.is_empty() { | |
| 489 | + format!("🕐 {}", t.display()) | |
| 490 | + } else { | |
| 491 | + format!("🕐 {} ({})", t.display(), rel) | |
| 492 | + } | |
| 493 | + }) | |
| 494 | + }); | |
| 495 | + (n.to_string(), CatppuccinMocha::PEACH, tip) | |
| 496 | + } | |
| 497 | + serde_json::Value::String(s) => { | |
| 498 | + if let Some(t) = detect_temporal(s) { | |
| 499 | + let label = match &t { | |
| 500 | + crate::temporal::TemporalValue::NaiveDate(_) => "d", | |
| 501 | + crate::temporal::TemporalValue::NaiveTime(_) => "t", | |
| 502 | + _ => "dt", | |
| 503 | + }; | |
| 504 | + let mut tip_parts = vec![format!("🕐 {}", t.display())]; | |
| 505 | + if let Some(tz) = t.timezone_info() { | |
| 506 | + tip_parts.push(format!("Timezone: {}", tz)); | |
| 507 | + } | |
| 508 | + let rel = t.relative_time(); | |
| 509 | + if !rel.is_empty() { | |
| 510 | + tip_parts.push(rel); | |
| 511 | + } | |
| 512 | + (format!("{} {}", label, t.display()), CatppuccinMocha::BLUE, Some(tip_parts.join("\n"))) | |
| 513 | + } else { | |
| 514 | + let display = if s.len() > 80 { | |
| 515 | + format!("\"{}...\"", &s[..77]) | |
| 516 | + } else { | |
| 517 | + format!("\"{}\"", s) | |
| 518 | + }; | |
| 519 | + (display, CatppuccinMocha::GREEN, None) | |
| 520 | + } | |
| 521 | + } | |
| 522 | + serde_json::Value::Object(m) => { | |
| 523 | + (format!("{{ {} fields }}", m.len()), CatppuccinMocha::SUBTEXT0, None) | |
| 524 | + } | |
| 525 | + serde_json::Value::Array(a) => { | |
| 526 | + (format!("[ {} items ]", a.len()), CatppuccinMocha::SUBTEXT0, None) | |
| 527 | + } | |
| 528 | + }; | |
| 529 | + | |
| 530 | + out.push(TableRow { | |
| 531 | + field, | |
| 532 | + field_color: CatppuccinMocha::BLUE, | |
| 533 | + type_name: tn, | |
| 534 | + type_color: tc, | |
| 535 | + value_text, | |
| 536 | + value_color, | |
| 537 | + toggle_path: if is_expandable { Some(path.to_string()) } else { None }, | |
| 538 | + temporal_tip, | |
| 539 | + depth, | |
| 540 | + }); | |
| 541 | + | |
| 542 | + if is_expandable && is_expanded { | |
| 543 | + flatten_table(value, path, depth + 1, expanded, out); | |
| 544 | + } | |
| 545 | +} |
+14-0watch9.sh
| @@ -0,0 +1,14 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | + | |
| 3 | +command=$(cat << 'BASH' | |
| 4 | +cargo run --color always 2>&1 | while IFS= read -r line; do | |
| 5 | + printf '%s\n' "$line" | |
| 6 | + if [[ "$line" == *"UI loaded"* ]]; then | |
| 7 | + sleep 0.5 | |
| 8 | + hyprctl dispatch movetoworkspace 9,pid:$(pgrep -n jv) | |
| 9 | + fi | |
| 10 | +done | |
| 11 | +BASH | |
| 12 | +) | |
| 13 | + | |
| 14 | +watchexec -r -e rs -- "$command" |
+977-0world.svg
| @@ -0,0 +1,977 @@ | ||
| 1 | +<?xml version="1.0"?> | |
| 2 | +<!-- | |
| 3 | +*************Map created by Simplemaps.com******************** | |
| 4 | +*************Attribution is highly appreciated!*************** | |
| 5 | +*************http://simplemaps.com**************************** | |
| 6 | + | |
| 7 | +The MIT License (MIT) | |
| 8 | + | |
| 9 | +Copyright (c) 2020 Pareto Softare, LLC DBA Simplemaps.com | |
| 10 | + | |
| 11 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 12 | +of this software and associated documentation files (the "Software"), to deal | |
| 13 | +in the Software without restriction, including without limitation the rights | |
| 14 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 15 | +copies of the Software, and to permit persons to whom the Software is | |
| 16 | +furnished to do so, subject to the following conditions: | |
| 17 | + | |
| 18 | +The above copyright notice and this permission notice shall be included in | |
| 19 | +all copies or substantial portions of the Software. | |
| 20 | + | |
| 21 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 22 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 23 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 24 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 25 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 26 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 27 | +THE SOFTWARE. | |
| 28 | + | |
| 29 | +--> | |
| 30 | +<svg baseprofile="tiny" fill="#ececec" height="857" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".2" version="1.2" viewbox="0 0 2000 857" width="2000" xmlns="http://www.w3.org/2000/svg"> | |
| 31 | + <path d="M1383 261.6l1.5 1.8-2.9 0.8-2.4 1.1-5.9 0.8-5.3 1.3-2.4 2.8 1.9 2.7 1.4 3.2-2 2.7 0.8 2.5-0.9 2.3-5.2-0.2 3.1 4.2-3.1 1.7-1.4 3.8 1.1 3.9-1.8 1.8-2.1-0.6-4 0.9-0.2 1.7-4.1 0-2.3 3.7 0.8 5.4-6.6 2.7-3.9-0.6-0.9 1.4-3.4-0.8-5.3 1-9.6-3.3 3.9-5.8-1.1-4.1-4.3-1.1-1.2-4.1-2.7-5.1 1.6-3.5-2.5-1 0.5-4.7 0.6-8 5.9 2.5 3.9-0.9 0.4-2.9 4-0.9 2.6-2-0.2-5.1 4.2-1.3 0.3-2.2 2.9 1.7 1.6 0.2 3 0 4.3 1.4 1.8 0.7 3.4-2 2.1 1.2 0.9-2.9 3.2 0.1 0.6-0.9-0.2-2.6 1.7-2.2 3.3 1.4-0.1 2 1.7 0.3 0.9 5.4 2.7 2.1 1.5-1.4 2.2-0.6 2.5-2.9 3.8 0.5 5.4 0z" id="AF" name="Afghanistan"> | |
| 32 | + </path> | |
| 33 | + <path class="Angola" d="M 1121.2 572 1121.8 574 1121.1 577.1 1122 580.1 1121.1 582.5 1121.5 584.7 1109.8 584.6 1109 605.1 1112.6 610.3 1116.2 614.3 1105.8 616.9 1092.3 616 1088.5 613 1065.8 613.2 1065 613.7 1061.7 610.8 1058.1 610.6 1054.7 611.7 1052 612.9 1051.5 608.9 1052.4 603.2 1054.4 597.3 1054.7 594.6 1056.6 588.8 1058 586.2 1061.3 582 1063.2 579.1 1063.8 574.4 1063.5 570.7 1061.9 568.4 1060.4 564.5 1059.1 560.7 1059.4 559.3 1061.1 556.8 1059.5 550.6 1058.3 546.3 1055.5 542.2 1056.1 541 1058.4 540.1 1060.1 540.2 1062.1 539.5 1078.8 539.6 1080.1 544.3 1081.7 548.2 1083 550.3 1085.1 553.6 1088.9 553.1 1090.7 552.2 1093.8 553.1 1094.7 551.5 1096.2 547.8 1099.7 547.5 1100 546.4 1102.9 546.4 1102.4 548.7 1109.2 548.6 1109.3 552.7 1110.4 555.1 1109.5 559 1109.9 563 1111.7 565.4 1111.3 573 1112.7 572.4 1115.1 572.6 1118.6 571.6 1121.2 572 Z"> | |
| 34 | + </path> | |
| 35 | + <path class="Angola" d="M 1055.3 539 1053.8 534.2 1056.1 531.4 1057.8 530.3 1059.9 532.5 1057.9 533.9 1056.9 535.5 1056.7 538.3 1055.3 539 Z"> | |
| 36 | + </path> | |
| 37 | + <path d="M1088 228l0.4 1.2 1.4-0.6 1.2 1.7 1.3 0.7 0.6 2.3-0.5 2.2 1 2.7 2.3 1.5 0.1 1.7-1.7 0.9-0.1 2.1-2.2 3.1-0.9-0.4-0.2-1.4-3.1-2.2-0.7-3 0.1-4.4 0.5-1.9-0.9-1-0.5-2.1 1.9-3.1z" id="AL" name="Albania"> | |
| 38 | + </path> | |
| 39 | + <path d="M1296.2 336.7l1.3 5.1-2.8 0 0 4.2 1.1 0.9-2.4 1.3 0.2 2.6-1.3 2.6 0 2.6-1 1.4-16.9-3.2-2.7-6.6-0.3-1.4 0.9-0.4 0.4 1.8 4.2-1 4.6 0.2 3.4 0.2 3.3-4.4 3.7-4.1 3-4 1.3 2.2z" id="AE" name="United Arab Emirates"> | |
| 40 | + </path> | |
| 41 | + <path class="Argentina" d="M 669.1 851.7 666.1 851.5 661.1 851.5 655.1 837.9 658.2 840.7 662.5 845.3 670.3 849 677.6 850.5 676.8 853.5 672.4 853.8 669.1 851.7 Z"> | |
| 42 | + </path> | |
| 43 | + <path class="Argentina" d="M 638.6 644.7 649.9 655.1 654.5 656.1 661.8 660.9 667.7 663.4 668.8 666.2 664.6 676 670.4 677.7 676.7 678.7 680.9 677.7 685.2 672.7 685.5 667.1 688.1 665.8 691.3 669.6 691.7 674.7 687.5 678.2 684.2 680.8 678.9 687.1 672.9 695.8 672.4 701 672 707.6 673.2 714 672.3 715.4 672.7 719.5 673 722.9 680.8 728.4 681 732.8 684.9 735.6 685.2 738.7 681.9 746.9 674.9 750.4 664.7 751.7 658.7 751 660.8 754.9 660.9 759.6 662.7 762.8 660.2 765.1 655.1 766 649.5 763.6 648 765.3 650.5 771.6 654.5 773.5 656.8 771.5 659.3 774.8 655.1 776.8 652.2 780.8 653.4 787.1 653.3 790.5 648.5 790.5 645.5 793.7 645.6 798.5 652.1 803.1 657.3 804.3 657.5 810 652.9 813.5 652.3 820.8 648.8 823.2 647.9 826.1 652.1 832.6 656.7 836.1 654.6 835.8 649.7 834.8 637.6 834 634.1 830.4 632.2 825.8 629.1 826.2 626.5 823.9 623.4 817.4 626.1 814.6 626.2 810.7 624.4 807.5 625.1 802.1 624 793.8 622.2 790.1 624 788.9 622.6 786.5 619.8 785.3 620.6 782.6 617.5 780.2 613.8 772.9 615.5 771.6 612.2 763.8 611.4 757.3 611.2 751.6 613.7 749.3 610.4 743 608.8 737.2 611.8 733 610.4 727.6 612 721.4 610.6 715.5 609 714.3 604.1 703.2 606.2 696.6 604.5 690.4 605.4 684.5 608 678.5 611.3 674.5 609.3 672 610.1 669.9 608.5 659.2 614.1 656.1 615.3 649.4 614.4 647.8 618.4 642 625.9 643.6 629.6 648.2 631.2 643 637.6 643.3 638.6 644.7 Z"> | |
| 44 | + </path> | |
| 45 | + <path d="M1230.8 253l-1.8 0.2-2.8-3.7-0.2-1-2.3 0-1.9-1.7-1 0.1-2.4-1.8-4.2-1.6-0.1-3.1-1.3-2.2 7-1 1.4 1.6 2.2 1.1-0.7 1.6 3.2 2.2-1.1 2.1 2.6 1.7 2.5 1 0.9 4.5z" id="AM" name="Armenia"> | |
| 46 | + </path> | |
| 47 | + <path class="Australia" d="M 1743 763.6 1746.7 765.8 1750 764.9 1754.9 763.7 1757.7 764.1 1753.2 771.7 1749.9 773.8 1745.9 779 1745.3 777.2 1738.7 781.6 1737.9 781.3 1734.9 781.1 1735.4 775.7 1737.4 771.5 1738 765.9 1740 763 1743 763.6 Z"> | |
| 48 | + </path> | |
| 49 | + <path class="Australia" d="M 1793.5 590.2 1794.7 595.2 1798.7 592.8 1800.1 595.5 1802.4 598 1801.3 600.9 1801.5 606.4 1801.7 609.6 1803 610.4 1803.4 615.9 1802.2 619.2 1803 623.5 1808.4 626.9 1811.6 629.9 1814.8 632.7 1813.7 634.3 1816 638.3 1816.5 645.3 1819.1 643.9 1820.6 646.6 1822.2 645.7 1821.5 652.5 1824.4 656.4 1826.3 658.8 1829.1 664 1829.1 669.2 1828.1 672.9 1826.3 676.8 1827 682.3 1824.5 688 1822.4 691 1818.6 696.7 1817.1 700.4 1814 705 1809 710.8 1803.5 714 1799.1 718.9 1795.8 722.1 1791.4 727.6 1787.7 730.8 1783.8 735.6 1780.7 740 1779.9 742.1 1775.6 744.3 1769.5 744.5 1763.2 747.2 1759.4 749.6 1754.6 752.4 1751.9 749.5 1749.3 748.4 1751.9 745.1 1748.4 746.3 1741.2 750.9 1737.6 749.2 1735.2 748.2 1732.4 747.7 1728.3 745.9 1727 741.9 1728.5 737.1 1728.9 733.8 1727.5 731.2 1722.8 730.5 1726 727.3 1726.9 722.6 1722.2 727 1716.9 728.2 1721.4 724.7 1723.9 721 1727.4 717.8 1729 713.1 1722.2 718.5 1717.9 720.7 1713.6 725.8 1710.6 723.2 1712.3 719.8 1710.9 715.1 1709.1 712.7 1710.7 711.2 1705.4 707.3 1701.6 707.2 1697.6 704 1687.7 704.6 1679.8 706.9 1672.9 709.1 1667.9 708.7 1660.9 712 1655.6 713.4 1653.3 716.8 1650.3 719.4 1645.6 719.6 1642 720.1 1637.8 719 1633.6 719.7 1629.8 720 1625.3 723.4 1623.8 723.1 1620.4 724.9 1617 726.9 1613.2 726.7 1609.7 726.7 1605.6 722.6 1603.2 721.4 1604.7 717.7 1607.6 716.8 1609.1 715.4 1609.7 713.1 1612 708.6 1612.7 704.8 1612 698.3 1612.2 694.6 1613.6 691 1612.7 686.8 1613 684.9 1611.3 682.3 1612 677.3 1610.1 672.2 1610 669.5 1611.8 672.3 1611.3 666.3 1613.6 668.2 1614.7 670.7 1615.3 667.4 1613.7 662.3 1613.6 660.3 1612.8 658.4 1614.1 654.7 1615.6 653.1 1616.9 649.9 1617 646.1 1620.1 641.5 1619.7 646.4 1622.8 642 1627.7 639.8 1630.9 637.1 1635.6 634.7 1638.2 634.2 1639.6 635 1644.4 632.6 1647.9 631.9 1649 630.5 1650.5 629.9 1653.6 630.1 1659.8 628.2 1663.3 625.3 1665.3 621.9 1669.2 618.7 1669.9 616.1 1670.6 612.6 1675.5 607.1 1676.9 612.7 1679.5 611.4 1678 608.4 1680.3 605.3 1682.5 606.7 1684 601.8 1687.5 598.6 1689.3 596.1 1692.2 595 1692.6 593.2 1694.9 593.9 1695.3 592.3 1697.9 591.4 1700.7 590.5 1704.4 593.5 1707 597.3 1710.5 597.3 1714 597.9 1713.3 594.4 1716.8 589.3 1719.5 587.6 1718.9 586 1721.8 582.3 1725.5 580 1728.2 580.8 1733.1 579.6 1733.4 576.3 1729.5 574.2 1732.6 573.3 1736.2 574.9 1738.9 577.5 1743.4 579.1 1745.1 578.5 1748.4 580.5 1751.9 578.6 1753.9 579.2 1755.4 577.9 1757.6 581.1 1755.6 584.6 1753.1 587.2 1751.2 587.4 1751.5 589.9 1749.3 593.1 1746.8 596.3 1747 598.1 1750.8 601.7 1754.8 603.7 1757.3 605.9 1760.6 609.7 1762.2 609.7 1764.8 611.4 1765.3 613.4 1770.2 615.5 1774.3 613.3 1776.1 609.9 1777.8 607 1779.1 603.5 1781.7 598.4 1781.5 595.3 1782.2 593.4 1782.1 589.8 1783.5 584.9 1784.8 583.6 1784.2 581.5 1786 578.1 1787.5 574.6 1787.9 572.7 1790.2 570.3 1791.5 573.5 1791.4 577.5 1792.7 578.3 1792.6 581 1794.2 584.2 1794.1 587.9 1793.5 590.2 Z"> | |
| 50 | + </path> | |
| 51 | + <path d="M1070.6 190.8l-0.3 0.8 0.7 2.1-0.2 2.6-2.8 0 1.1 1.4-1.3 4-0.9 1.1-4.4 0.1-2.4 1.5-4.2-0.5-7.3-1.7-1.3-2.1-4.9 1.1-0.5 1.2-3.1-0.9-2.6-0.2-2.3-1.2 0.7-1.5-0.2-1.1 1.4-0.3 2.7 1.7 0.6-1.7 4.4 0.3 3.5-1.1 2.4 0.2 1.7 1.3 0.4-1.1-1-4.1 1.7-0.8 1.6-2.9 3.8 2.1 2.6-2.6 1.7-0.5 4 1.9 2.3-0.3 2.4 1.2z" id="AT" name="Austria"> | |
| 52 | + </path> | |
| 53 | + <path class="Azerbaijan" d="M 1229 253.2 1225.2 252.3 1222 249.4 1220.8 246.9 1221.8 246.8 1223.7 248.5 1226 248.5 1226.2 249.5 1229 253.2 Z"> | |
| 54 | + </path> | |
| 55 | + <path class="Azerbaijan" d="M 1235.3 236.2 1237.8 233.6 1241.3 236.9 1244.9 241.5 1247.4 241.8 1249.3 243.5 1245.1 244 1245.2 249 1244.8 251.2 1243.1 252.7 1243.9 255.8 1242.6 256.2 1238.7 252.8 1239.9 249.7 1238 247.8 1236.1 248.3 1230.8 253 1229.9 248.5 1227.4 247.5 1224.8 245.8 1225.9 243.7 1222.7 241.5 1223.4 239.9 1221.2 238.8 1219.8 237.2 1220.9 236.1 1225.1 238 1228 238.3 1228.6 237.6 1225.3 234.1 1226.5 233.3 1228 233.5 1232.3 237.3 1234.7 237.8 1235.3 236.2 Z"> | |
| 56 | + </path> | |
| 57 | + <path d="M1154.9 530.4l-0.6 0.1 0-0.3-2-6.1-0.01-0.06-0.09-1.04-1.4-2.9 3.5 0.5 1.7-3.7 3.1 0.4 0.3 2.5 1.2 1.5 0 2.1-1.4 1.3-2.3 3.4-2 2.3z" id="BI" name="Burundi"> | |
| 58 | + </path> | |
| 59 | + <path d="M1016.5 177.1l-0.4 4.2-1.3 0.2-0.4 3.5-4.4-2.9-2.5 0.5-3.5-2.9-2.4-2.5-2.2-0.1-0.8-2.2 3.9-1.2 3.6 0.5 4.5-1.3 3.1 2.7 2.8 1.5z" id="BE" name="Belgium"> | |
| 60 | + </path> | |
| 61 | + <path d="M1006.7 427l-0.2 2.1 1.3 3.8-1.1 2.6 0.6 1.7-2.8 4-1.7 2-1.1 4 0.2 4.1-0.3 10.3-4.7 0.8-1.4-4.4 0.3-14.8-1.2-1.3-0.2-3.2-2-2.2-1.7-1.9 0.7-3.4 2-0.7 1.1-2.8 2.8-0.6 1.2-1.9 1.9-1.9 2 0 4.3 3.7z" id="BJ" name="Benin"> | |
| 62 | + </path> | |
| 63 | + <path d="M988.5 406l-0.5 3.1 0.8 2.9 3.1 4.2 0.2 3.1 6.5 1.5-0.1 4.4-1.2 1.9-2.8 0.6-1.1 2.8-2 0.7-4.9-0.1-2.6-0.5-1.8 1-2.5-0.5-9.8 0.3-0.2 3.7 0.8 4.8-3.9-1.6-2.6 0.2-2 1.6-2.5-1.3-1-2.2-2.5-1.4-0.4-3.7 1.6-2.7-0.2-2.2 4.5-5.3 0.9-4.4 1.5-1.6 2.7 0.9 2.4-1.3 0.8-1.7 4.3-2.8 1.1-2 5.3-2.7 3.1-0.9 1.4 1.2 3.6 0z" id="BF" name="Burkina Faso"> | |
| 64 | + </path> | |
| 65 | + <path d="M1500.6 360.3l0.6 4.6-2.1-1 1.1 5.2-2.1-3.3-0.8-3.3-1.5-3.1-2.8-3.7-5.2-0.3 0.9 2.7-1.2 3.5-2.6-1.3-0.6 1.2-1.7-0.7-2.2-0.6-1.6-5.3-2.6-4.8 0.3-3.9-3.7-1.7 0.9-2.3 3-2.4-4.6-3.4 1.2-4.4 4.9 2.8 2.7 0.3 1.2 4.5 5.4 0.9 5.1-0.1 3.4 1.1-1.6 5.4-2.4 0.4-1.2 3.6 3.6 3.4 0.3-4.2 1.5 0 4.4 10.2z" id="BD" name="Bangladesh"> | |
| 66 | + </path> | |
| 67 | + <path d="M1132.6 221.6l-2.3 2.6-1.3 4.5 2.1 3.6-4.6-0.8-5 2 0.3 3.2-4.6 0.6-3.9-2.3-4 1.8-3.8-0.2-0.8-4.2-2.8-2.1 0.7-0.8-0.6-0.8 0.6-2 1.8-2-2.8-2.7-0.7-2.4 1.1-1.4 1.8 2.6 1.9-0.4 4 0.9 7.6 0.4 2.3-1.6 5.9-1.5 4 2.3 3.1 0.7z" id="BG" name="Bulgaria"> | |
| 68 | + </path> | |
| 69 | + <path d="M1083 214.3l1.9-0.1-1.1 2.8 2.7 2.5-0.5 2.9-1.1 0.3-0.9 0.6-1.6 1.5-0.4 3.5-4.8-2.4-2.1-2.7-2.1-1.4-2.5-2.4-1.3-1.9-2.7-3 0.8-2.6 2 1.5 1-1.4 2.3-0.1 4.5 1.1 3.5-0.1 2.4 1.4z" id="BA" name="Bosnia and Herzegovina"> | |
| 70 | + </path> | |
| 71 | + <path d="M1141.6 162.7l-3.9-0.2-0.8 0.6 1.5 2 2 4-4.1 0.3-1.3 1.4 0.3 3.1-2.1-0.6-4.3 0.3-1.5-1.5-1.7 1.1-1.9-0.9-3.9-0.1-5.7-1.5-4.9-0.5-3.8 0.2-2.4 1.6-2.3 0.3-0.5-2.8-1.9-2.8 2.8-1.3-0.4-2.4-1.7-2.3-0.6-2.7 4.7 0 4.8-2.3 0.5-3.4 3.6-2-1-2.7 2.7-1 4.6-2.3 5.3 1.5 0.9 1.5 2.4-0.7 4.8 1.4 1.1 2.9-0.7 1.6 3.8 4 2.1 1.1 0 1.1 3.4 1.1 1.7 1.6-1.6 1.3z" id="BY" name="Belarus"> | |
| 72 | + </path> | |
| 73 | + <path d="M487.8 399.8l-1.7 0 1.3-7.2 0.7-5.1 0.1-1 0.7-0.3 0.9 0.8 2.5-3.9 1.1-0.1-0.1 1 1 0-0.3 1.8-1.3 2.7 0.4 1-0.9 2.3 0.3 0.6-1 3.3-1.3 1.7-1.1 0.2-1.3 2.2z" id="BZ" name="Belize"> | |
| 74 | + </path> | |
| 75 | + <path d="M662.5 631.4l-0.3-2-5.4-3.3-5.2-0.1-9.6 1.9-2.1 5.6 0.2 3.5-1.5 7.7-1-1.4-6.4-0.3-1.6 5.2-3.7-4.6-7.5-1.6-4 5.8-3.9 0.9-3.1-8.9-3.7-7.2 1.1-6.2-3.2-2.7-1.2-4.6-3.2-4.4 2.9-6.9-2.9-5.4 1.1-2.2-1.2-2.4 1.9-3.2-0.3-5.4 0-4.6 1.1-2.1-5.5-10.4 4.2 0.6 2.9-0.2 1.1-1.9 4.8-2.6 2.9-2.4 7.3-1.1-0.4 4.8 0.9 2.5-0.3 4.3 6.5 5.7 6.4 1.1 2.3 2.4 3.9 1.3 2.5 1.8 3.5 0 3.4 1.9 0.5 3.7 1.2 1.9 0.3 2.7-1.7 0.1 2.8 7.5 10.7 0.3-0.5 3.7 0.8 2.5 3.2 1.8 1.7 4-0.6 5.1-1.3 2.8 0.8 3.6-1.6 1.4z" id="BO" name="Bolivia"> | |
| 76 | + </path> | |
| 77 | + <path d="M665.8 489.6l3.1 0.6 0.6-1.4-1-1.2 0.6-1.9 2.3 0.6 2.7-0.7 3.2 1.4 2.5 1.3 1.7-1.7 1.3 0.2 0.8 1.8 2.7-0.4 2.2-2.5 1.8-4.7 3.4-5.9 2-0.3 1.3 3.6 3 11.2 3.1 1.1 0.1 4.4-4.3 5.3 1.7 1.9 10.1 1 0.2 6.5 4.3-4.2 7.1 2.3 9.5 3.9 2.8 3.7-0.9 3.6 6.6-2 11 3.4 8.5-0.2 8.4 5.3 7.4 7.2 4.4 1.8 4.8 0.3 2.1 2 2 8.2 1.1 3.9-2.1 10.6-2.7 4.2-7.7 8.9-3.4 7.3-4 5.5-1.4 0.2-1.3 4.7 0.9 12-1.1 9.9-0.3 4.2-1.6 2.6-0.5 8.6-5.2 8.3-0.5 6.7-4.3 2.7-1.1 3.9-6 0-8.5 2.4-3.7 2.9-6 1.9-6.1 5.1-4.1 6.4-0.3 4.8 1.3 3.5-0.3 6.5-0.8 3.1-3.4 3.6-4.5 11.3-4 5-3.2 3.1-1.5 6.1-2.9 3.6-2.1-3.6 1.8-3.1-3.8-4.3-4.8-3.6-6.3-4.1-1.9 0.2-6.3-5-3.4 0.7 6-8.7 5.3-6.3 3.3-2.6 4.2-3.5-0.4-5.1-3.2-3.8-2.6 1.3 0.7-3.7 0.3-3.8-0.3-3.6-2.1-1.1-2 1-2.1-0.3-0.8-2.4-1.1-5.9-1.2-1.9-3.9-1.8-2.2 1.3-5.9-1.3-0.4-8.7-2-3.5 1.6-1.4-0.8-3.6 1.3-2.8 0.6-5.1-1.7-4-3.2-1.8-0.8-2.5 0.5-3.7-10.7-0.3-2.8-7.5 1.7-0.1-0.3-2.7-1.2-1.9-0.5-3.7-3.4-1.9-3.5 0-2.5-1.8-3.9-1.3-2.3-2.4-6.4-1.1-6.5-5.7 0.3-4.3-0.9-2.5 0.4-4.8-7.3 1.1-2.9 2.4-4.8 2.6-1.1 1.9-2.9 0.2-4.2-0.6-3.2 1.1-2.6-0.7-0.1-9.7-4.4 3.7-5-0.1-2.3-3.5-3.8-0.3 1-2.8-3.3-3.9-2.6-5.8 1.5-1.1-0.2-2.8 3.4-1.8-0.7-3.5 1.4-2.2 0.3-3 6.3-4.4 4.6-1.2 0.8-1 5.1 0.3 2.2-17.6 0.1-2.8-0.9-3.6-2.6-2.4 0.1-4.7 3.2-1 1.1 0.7 0.2-2.5-3.3-0.7 0-4 11 0.2 1.9-2.3 1.6 2.1 1 3.8 1.1-0.8 3.1 3.4 4.4-0.4 1.1-2 4.2-1.5 2.4-1.1 0.7-2.7 4.1-1.8-0.3-1.4-4.8-0.5-0.7-4.1 0.3-4.3-2.5-1.6 1.1-0.6 4.1 0.8 4.5 1.6 1.7-1.5 4.1-1 6.4-2.4 2.1-2.5-0.7-1.8 3-0.2 1.2 1.4-0.8 2.9 2 0.9 1.2 3-1.6 2.3-1 5.4 1.4 3.3 0.3 3 3.5 3 2.8 0.3 0.6-1.3 1.8-0.3 2.6-1.1 1.8-1.7 3.2 0.6 1.3-0.3z" id="BR" name="Brazil"> | |
| 78 | + </path> | |
| 79 | + <path d="M1633.1 472.8l2.2-2.4 4.6-3.6-0.1 3.2-0.1 4.1-2.7-0.2-1.1 2.2-2.8-3.3z" id="BN" name="Brunei Darussalam"> | |
| 80 | + </path> | |
| 81 | + <path d="M1488.8 323.5l2.6 2.1 0.5 3.9-4.5 0.2-4.7-0.4-3.2 1-5.5-2.5-0.4-1.2 2.6-4.8 2.6-1.6 4.3 1.4 2.9 0.2 2.8 1.7z" id="BT" name="Bhutan"> | |
| 82 | + </path> | |
| 83 | + <path d="M1127.6 615.7l1.9 5.1 1.1 1.2 1.6 3.7 6.1 7 2.3 0.7-0.1 2.3 1.5 4.1 4.3 1 3.4 2.9-8.1 4.7-5.2 4.8-2 4.3-1.8 2.4-3 0.5-1.2 3.1-0.6 2-3.6 1.4-4.5-0.3-2.5-1.8-2.3-0.7-2.8 1.4-1.5 3.1-2.7 1.9-2.8 2.9-4 0.7-1.1-2.3 0.6-3.9-3-6.1-1.4-1 0.6-18.7 5.5-0.2 0.8-22.9 4.2-0.2 8.7-2.3 2 2.7 3.7-2.5 1.7 0 3.2-1.5 1 0.5z" id="BW" name="Botswana"> | |
| 84 | + </path> | |
| 85 | + <path d="M1121.3 446.5l3.9 2.5 3.1 2.6 0.1 2.1 3.9 3.3 2.4 2.8 1.4 3.8 4.3 2.6 0.9 2-1.8 0.7-3.7-0.1-4.2-0.7-2.1 0.5-0.9 1.6-1.8 0.2-2.2-1.4-6.3 3.2-2.6-0.6-0.8 0.5-1.6 3.9-4.3-1.3-4.1-0.6-3.6-2.4-4.7-2.2-3 2.1-2.2 3.2-0.5 4.5-3.6-0.3-3.9-1.1-3.3 3.4-3 6-0.6-1.9-0.3-2.9-2.6-2.1-2.1-3.3-0.5-2.3-2.7-3.4 0.5-1.9-0.6-2.7 0.4-5 1.4-1.1 2.8-6.5 4.6-0.5 1-1.7 1 0.2 1.4 1.4 7.1-2.4 2.4-2.5 2.9-2.3-0.6-2.2 1.6-0.6 5.5 0.4 5.2-3 4-7 2.8-2.6 3.6-1.1 0.7 2.7 3.3 4 0 2.7-0.8 2.6 0.4 2 1.9 1.9 0.5 0.3z" id="CF" name="Central African Republic"> | |
| 86 | + </path> | |
| 87 | + <path class="Canada" d="M 665.9 203.6 669.3 204.5 674 204.3 670.7 206.9 668.7 207.3 663.2 204.6 662.6 202.5 665.1 200.6 665.9 203.6 Z"> | |
| 88 | + </path> | |
| 89 | + <path class="Canada" d="M 680.3 187.6 677.9 187.7 672.1 185.8 668.6 182.8 670.5 182.3 676.4 183.9 680.6 186.5 680.3 187.6 Z"> | |
| 90 | + </path> | |
| 91 | + <path class="Canada" d="M 372.4 191.3 369.3 192.2 363 189.4 363 187.2 360.1 185 360.4 183.2 356.1 182.1 356.7 178.7 358.2 177.3 362.3 178.6 364.7 179.6 368.8 180.2 369 182.4 369.4 185.3 372.6 187.9 372.4 191.3 Z"> | |
| 92 | + </path> | |
| 93 | + <path class="Canada" d="M 711.5 177.8 706.5 183.2 710.4 181.1 713.3 182.5 710.9 184.6 714.7 186.3 717.5 184.8 721.8 186.7 719 191.3 722.8 190.2 722.5 193.5 723 197.4 719.4 203 717 203.2 714.1 202 716.6 196.9 715.4 196.1 708.1 201.5 705.1 201.3 709.5 198.3 705.1 196.8 699.6 197.2 690 197 689.8 195.2 693.6 192.9 691.9 191.3 697.3 187.5 705.5 177.6 709.7 174.1 714.7 172 716.8 172.2 715.3 173.9 711.5 177.8 Z"> | |
| 94 | + </path> | |
| 95 | + <path class="Canada" d="M 351.5 156.4 353 157.2 358 156.7 350.8 163.6 351.1 168.6 349.2 168.6 348.5 165.8 349 162.9 348.2 161 349.5 158.3 351.5 156.4 Z"> | |
| 96 | + </path> | |
| 97 | + <path class="Canada" d="M 634.9 108.9 631.3 111.9 629.6 111.4 629.5 109.7 629.9 109.3 632.7 107.6 634.4 107.7 634.9 108.9 Z"> | |
| 98 | + </path> | |
| 99 | + <path class="Canada" d="M 625.2 105.7 618 108.9 615 108.7 615 107.2 620 104.5 626 104.6 625.2 105.7 Z"> | |
| 100 | + </path> | |
| 101 | + <path class="Canada" d="M 622.1 88.9 621.2 91.4 623.9 90.5 625.4 92 628.9 94 632.7 95.7 631.3 98.4 634.8 98 636.7 99.9 631.7 101.7 625.8 100.3 625 97.7 618.7 100.8 610.5 103.7 611.2 100.4 604.9 100.9 610.6 98.1 614.2 93.6 619.3 88.5 622.1 88.9 Z"> | |
| 102 | + </path> | |
| 103 | + <path class="Canada" d="M 667 80.6 662.1 80.9 662.8 78.2 666.6 75.1 670.9 74.4 673.2 75.9 671.7 78.2 670.8 79 667 80.6 Z"> | |
| 104 | + </path> | |
| 105 | + <path class="Canada" d="M 592.5 69.9 588.4 71.8 584.2 70.2 580.3 70.7 576.9 68.3 581.9 66.6 586.8 64.3 589.8 65.8 591.4 66.8 591.8 67.8 592.5 69.9 Z"> | |
| 106 | + </path> | |
| 107 | + <path class="Canada" d="M 645.5 212.5 643.3 208.9 646.2 200.4 644.6 198.6 640.9 199.6 639.8 198 634.3 202.7 631.1 207.6 628.3 210.5 625.8 211.5 624.1 211.8 623 213.3 613.7 213.3 605.9 213.4 603.2 214.5 596.4 218.9 596.4 218.8 595.5 218.4 593.5 219.3 591.6 220.6 589.8 219.5 585.1 220.3 581.2 221.2 579.3 222 577 224.1 578.8 224.8 580.5 224.4 580.8 224.4 580.5 226.3 575.7 227 572.9 227.8 571.2 228.8 568.6 228.2 567 228.5 564.1 230.3 559.5 232.3 556.8 231.9 558.8 229.7 562.5 226.2 566.6 224.1 567.7 222.3 568.6 219.3 572.4 215.8 573.3 211.8 574.4 215.7 578.2 216.6 580.6 214.5 579.2 209.7 578.3 207.7 574.3 206.5 570.5 205.8 566.6 205.8 563.2 205 562.8 203.6 561.4 204.5 560.2 204.3 562.1 202.2 560.3 201.4 562.2 199 561 197.2 562.7 195.4 557.5 194.5 557.4 190.9 556.6 190.1 553.3 189.9 549.2 188.7 547.7 189.5 545.9 191 542.6 192 539.5 194.5 534.1 192.8 529.7 193.6 525.8 191.7 521.2 190.7 517.9 190.3 516.9 189.3 517.8 185.9 516.1 185.9 514.8 188.3 504.6 188.3 487.6 188.3 470.8 188.3 455.9 188.3 441.1 188.3 426.4 188.3 411.3 188.3 406.5 188.3 391.8 188.3 377.7 188.3 377 188.3 371.6 182.2 370 179.5 363 176.9 364.3 171.4 367.9 167.7 363.8 165 366.9 160.1 364.8 155.7 367.3 152.5 372.4 149.6 375.6 145.8 371 142 372.4 135.1 373.5 130.9 371.9 128.2 371.1 125.8 371.7 122.7 365.2 124.6 357.6 127.9 357.3 124.1 356.8 121.5 354 119.9 349.8 119.7 385.4 87 410 66.6 416 67.9 419.3 70.5 423 71 429.3 68.8 436.3 67.1 441.6 67.7 450.5 65.4 458.7 64.1 458.9 66.3 463.4 65 467.3 62.5 469.4 63.1 470.8 67.9 480.3 64.2 476.4 68.3 482.4 67.4 485.6 65.9 490.2 66.2 494.1 68.4 501.6 70.4 506.3 71.3 510.7 71 513.6 73.8 505.1 76.5 511.5 77.6 523.4 77 527.8 76 529.2 79.3 536.3 76.6 534.2 74.2 538.7 72.4 543.9 72.1 547.8 71.6 549.9 72.9 551.4 75.8 556.4 75.4 561.7 77.9 568.9 77 574.9 77.1 577.3 73.7 581.8 72.8 586.7 74.6 582.4 79.8 588.6 75.4 591.8 75.6 598.2 70.1 596.6 66.8 593.7 64.6 599.2 58.7 607.4 54.9 611.9 55.8 613.9 58.1 614.3 64.1 608.5 66.7 615.2 67.8 610.8 73.3 619.7 69.1 621.9 72.6 617.6 76.6 618.9 80.3 626.2 76.4 632.7 71.6 637.4 65.7 642.9 66.1 648.3 66.9 651.9 69.6 650.2 72.3 645.1 75.2 646 78.1 643.6 80.8 632.7 84.7 626.2 85.6 623 83.9 619.7 86.7 612.3 91.4 609.3 93.9 601.6 97.7 595.1 98.1 590 100.5 587.1 104.3 581.4 105 572.7 109.7 563.3 116.2 558.3 120.8 553.4 127.7 559.4 128.7 557.9 134.2 557.1 138.8 564.4 137.6 571.4 140.2 574.7 142.5 576.4 145.3 581.3 147 584.9 149.5 592.5 149.9 597.3 150.5 593.7 155.7 592 161.8 592.1 168.7 596.5 174.6 601.2 172.6 606.8 166.2 609.1 156.6 607.4 153.4 616.4 150.5 623.9 146.3 628.7 142.1 630.4 138.1 630 133 626.8 128.5 635.7 122.3 636.7 117 640.6 108 644.4 106.6 651.1 108.2 655.3 108.8 659.8 107.2 662.9 109.2 666.5 112.6 666.7 114.8 674.4 115.3 671.8 120.2 669.5 127.6 673.3 128.6 674.9 132.1 683.1 128.8 690.6 122.2 694.8 119.5 695.9 124.8 698.5 132.3 700.5 139.5 697.1 143.3 701.9 146.7 704.8 150.1 711.7 151.7 714.1 153.6 714.1 158.8 717.5 159.6 718.6 161.9 716.6 168.8 712.3 171.1 708.1 173.3 699.3 175.5 691.4 180.5 682.8 181.6 672.7 180.2 665.4 180.2 660.1 180.6 654.4 185.1 647 187.9 636.9 196.1 629 201.9 633.7 200.9 644.6 192.6 656.9 187.4 664.5 186.8 667.8 189.9 661.7 194.1 661.1 200.8 661.2 205.6 666.8 208.7 675.4 207.8 682.6 200.7 681.6 205.3 684.1 207.6 676.7 211.7 664.3 215.5 658.5 218 651.3 222.6 647.6 222.1 649.1 216.8 659.5 211.5 651.4 211.7 645.5 212.5 Z"> | |
| 108 | + </path> | |
| 109 | + <path class="Canada" d="M 539 48.7 534.3 51.1 544.8 49.6 547.4 52.2 554.7 49.5 556.5 51.2 554.3 56.3 558.6 54.2 561.4 48.9 565.7 48.1 568.7 48.9 570.9 51 568.2 56.1 565.8 59.8 570 62.4 575 65 572.4 67.3 565.3 67.8 566.1 69.8 562.9 71.8 556.2 71 550.5 69.5 545.4 69.8 535.9 71.7 524.6 72.5 516.7 73 517 70.4 512.9 69 508.6 69.6 508 65.3 511.3 64.7 518.6 63.8 524.1 64.1 530.3 63.1 523.8 61.9 514.7 62.3 509 62.2 509 60.3 520.7 58.1 514.4 58.2 509 56.8 516.8 52.9 522 50.8 536.2 47.7 539 48.7 Z"> | |
| 110 | + </path> | |
| 111 | + <path class="Canada" d="M 578.5 47.2 571.5 50.6 569.2 47 571.3 46.2 576.8 46 578.5 47.2 Z"> | |
| 112 | + </path> | |
| 113 | + <path class="Canada" d="M 687.1 48.8 686.4 50.2 682.3 50.1 678.2 50 673.3 50.7 672.4 50.3 670.3 47.6 672 45.8 674.2 45.4 682.6 46 687.1 48.8 Z"> | |
| 114 | + </path> | |
| 115 | + <path class="Canada" d="M 647.3 48.5 647.6 51.7 654.8 47.6 666.5 45.5 668.8 50.8 665.6 54.2 674.6 52.7 679.9 50.6 686.7 53.2 690.4 55.7 689.3 58 697.5 56.8 699.4 60.2 707.8 62.3 710 64.4 710.9 69.5 701.8 72 709.5 75.6 715.4 76.8 718.7 81.8 725.2 82.2 721.9 86.1 711 92.6 707 90.2 703.2 84.8 697.3 85.5 695 88.8 697.7 92 702.2 94.6 703.2 96.1 703.1 101.8 699.5 105.9 694.8 104.3 686.2 99.8 689.8 104.7 692.6 108.2 692.3 110.2 681.4 107.9 673.8 104.5 670 101.8 672.4 100.1 667.6 97.2 662.9 94.5 662 96.1 648.9 97 646.5 95.1 651.9 90.9 660 90.8 669.3 90.1 669.1 88.1 672.2 85.3 681 79.9 681.4 77.4 680.9 75.6 676.2 72.9 669.1 71.1 672.6 69.7 670.6 66.3 667.2 66 665.3 64.2 662 65.8 654.1 66.5 640.1 65.3 632.7 63.7 626.8 62.9 624.9 61 631.2 58.6 625.5 58.6 628.8 53.3 635.9 48.7 641.9 46.6 653.1 45.2 647.3 48.5 Z"> | |
| 116 | + </path> | |
| 117 | + <path class="Canada" d="M 596.9 45 600.5 46.1 608.1 45.4 607.6 46.9 601.5 49.4 605.3 51.6 600.1 56.3 591.7 58.3 588.3 57.9 587.4 55.9 581.5 51.9 583.2 50.2 590.6 50.8 589.7 47.5 596.9 45 Z"> | |
| 118 | + </path> | |
| 119 | + <path class="Canada" d="M 619.5 50.5 611.7 54.4 607.3 54.2 609.1 49.6 611.7 47.1 615.9 44.9 621.1 43.5 629 43.7 635.2 44.9 625.1 49.5 619.5 50.5 Z"> | |
| 120 | + </path> | |
| 121 | + <path class="Canada" d="M 503.7 57.7 490.2 60.3 490.8 58 484.8 55.2 489.2 53 496.7 49.2 504.3 45.8 504.7 42.7 518.7 41.9 522.9 43 532.4 43.3 534.3 44.7 535.9 46.9 529.4 48.2 515.5 51.8 506.3 55.5 503.7 57.7 Z"> | |
| 122 | + </path> | |
| 123 | + <path class="Canada" d="M 628.9 39.3 624.8 41.2 619.6 40.8 616.4 39.5 620.9 37.3 627.8 35.9 629.2 37.7 628.9 39.3 Z"> | |
| 124 | + </path> | |
| 125 | + <path class="Canada" d="M 620.1 30.6 620.3 32.9 617.5 35.4 611.7 39.2 604.8 39.7 601.6 38.9 605 36 598.4 36.3 602.7 32.5 606.5 32.7 614.2 31 619.1 31.3 620.1 30.6 Z"> | |
| 126 | + </path> | |
| 127 | + <path class="Canada" d="M 580.7 33.2 580 34.9 584.4 34.1 588 34.3 585.7 36.8 580.6 39.1 566.7 39.9 554.4 42.1 548.4 42.2 549.9 40.6 560.6 38.3 542.7 38.9 538.5 38 550.2 33.2 555.6 31.8 563.9 33.5 566.8 36.4 572.9 36.8 573.6 32.1 579.4 30.3 582.5 30.8 580.7 33.2 Z"> | |
| 128 | + </path> | |
| 129 | + <path class="Canada" d="M 636.4 28.8 638.7 30.4 646.1 30.4 647.5 32 644.7 33.8 647.8 34.9 649 36.1 653.9 36.3 659 36.7 666.1 35.7 674.2 35.2 680 35.6 682.3 37.5 681.3 39.5 677.7 40.9 671 42 666.7 41.3 654.9 42.1 646.9 42.2 641.3 41.6 632.7 40 634.3 37.2 636.4 34.8 635 32.6 628 32 625.4 30.5 629.2 28.5 636.4 28.8 Z"> | |
| 130 | + </path> | |
| 131 | + <path class="Canada" d="M 560.7 26.2 554.9 29.9 549.6 31.6 545.8 31.8 535.9 33.9 528.8 34.7 525.2 33.6 536.9 29.9 549.1 26.8 554.6 26.9 560.7 26.2 Z"> | |
| 132 | + </path> | |
| 133 | + <path class="Canada" d="M 641.9 26.8 640.1 26.9 633.5 26.6 634.2 25.3 641.4 25.4 643 26.2 641.9 26.8 Z"> | |
| 134 | + </path> | |
| 135 | + <path class="Canada" d="M 583.1 25.9 574.3 27.3 571 25.8 576.1 24.3 582.1 23.8 586.2 24.5 583.1 25.9 Z"> | |
| 136 | + </path> | |
| 137 | + <path class="Canada" d="M 590.9 21.7 585.2 22.6 579.2 22.6 580.2 21.9 585.9 20.6 587.6 20.8 590.9 21.7 Z"> | |
| 138 | + </path> | |
| 139 | + <path class="Canada" d="M 637.7 24.2 631.1 25.2 629.5 24.1 630.1 22.4 632.3 20.5 636.7 20.7 638.4 21 640.7 22.6 637.7 24.2 Z"> | |
| 140 | + </path> | |
| 141 | + <path class="Canada" d="M 623.9 23 622.8 24.9 617.5 24.4 613.5 22.9 605.7 22.7 611 21.4 608.2 20.3 610.4 18.5 616.5 19.1 624 20.8 623.9 23 Z"> | |
| 142 | + </path> | |
| 143 | + <path class="Canada" d="M 678 16.9 680.7 18.4 674.2 19.7 663.6 23.2 656.8 23.5 650 22.9 648.4 21 650.5 19.4 654.8 18.2 648.2 18.2 646.2 16.7 646.6 14.8 651.5 12.9 655.6 11.6 659.6 11.3 659.3 10.3 667.6 10.1 669.2 12.3 673.9 13.2 678.7 14.1 678 16.9 Z"> | |
| 144 | + </path> | |
| 145 | + <path class="Canada" d="M 757.2 2.9 765.9 3.2 772.6 3.7 777.8 4.7 776.7 5.7 766.9 7.4 757.9 8.2 753.9 9.1 761.4 9.1 750.9 11.6 744.1 12.8 734.8 16.3 726.7 17 723.6 18 712.3 18.4 716.7 19 713.4 19.8 714.1 22.1 709.1 23.7 702.2 25 698.7 26.8 692.1 28.3 691.6 29.4 698.1 29.2 697 30.4 684.3 33.3 675.8 31.9 664.1 32.7 659.1 32.1 652.3 31.8 654.4 29.5 662.4 28.4 664.4 25 667 24.7 674.5 26.7 672.8 23.7 667.9 22.8 672.9 21 680.5 19.9 683.3 18.3 680.3 16.6 681.4 14.4 690.8 14.5 693 15 700.3 13.4 693 12.9 680.4 13.2 676 11.8 675.2 10.1 672.7 8.9 673.8 7.6 679.8 6.8 684 6.7 691.4 6.1 698.2 4.6 702.1 4.8 704.5 5.9 709.5 3.9 714.7 3.3 721.2 2.9 731.6 2.7 732.9 3.1 743.2 2.5 750.2 2.7 757.2 2.9 Z"> | |
| 146 | + </path> | |
| 147 | + <path d="M1034.4 197.5l0.2 1.1-0.7 1.5 2.3 1.2 2.6 0.2-0.3 2.5-2.1 1.1-3.8-0.8-1 2.5-2.4 0.2-0.9-1-2.7 2.2-2.5 0.3-2.2-1.4-1.8-2.7-2.4 1 0-2.9 3.6-3.5-0.2-1.6 2.3 0.6 1.3-1.1 4.2 0 1-1.3 5.5 1.9z" id="CH" name="Switzerland"> | |
| 148 | + </path> | |
| 149 | + <path class="China" d="M 1602.2 381.9 1597.9 385 1593 383 1592 377.5 1594.2 374.6 1600 372.8 1603.3 372.9 1604.9 375.4 1602.9 378.2 1602.2 381.9 Z"> | |
| 150 | + </path> | |
| 151 | + <path class="China" d="M 1625.6 185.5 1634.6 190 1640.6 195.8 1648.2 195.8 1650.8 193.4 1657.7 191.5 1659 197.2 1658.7 199.5 1661.5 206.3 1662.1 212.5 1655.2 211.4 1652.3 213.6 1657 219 1660.9 226.5 1658.4 226.6 1660.3 229.9 1654.8 226.1 1654.8 229.7 1648.4 232.4 1651.2 235.8 1646.6 235.5 1643 233.5 1641.9 238.1 1638 241.5 1635.9 245.6 1629.6 247.4 1627.2 250.4 1622.4 252.2 1623.7 249.2 1621.4 246.7 1623.4 242.4 1618.9 239.1 1615.5 241.3 1611.9 245.8 1610.6 249.9 1605.6 250.2 1604.3 253.2 1609.1 257.5 1613.9 258.6 1615.3 261.4 1620.4 263.3 1624.2 258.7 1630.1 261.2 1633.6 261.4 1635.9 264.7 1629.2 266.5 1628.2 270 1624.4 273.2 1623.5 277.7 1630.6 281.2 1635.2 287.5 1640.7 293.4 1646.2 298.3 1647.8 303.1 1645 304.9 1647.4 308.3 1651.3 310.3 1652.1 315.5 1652.2 320.6 1649.4 321.2 1647.3 328.1 1645 336.6 1641.6 344.2 1635.2 350.1 1628.6 355.6 1622.5 356.3 1619.6 359.1 1617.3 357.1 1614.8 360.2 1607.6 363.5 1601.8 364.4 1601.1 371.2 1598 371.6 1595.8 366.9 1596.7 364.5 1588.9 362.4 1586.5 363.5 1580.6 361.8 1577.5 359.2 1577.8 355.5 1572.5 354.3 1569.4 351.9 1565.3 355.3 1560 356.1 1555.6 356 1552.9 357.6 1550.2 358.5 1552.2 365.9 1549.2 365.7 1548.4 364.2 1547.9 361.5 1544.1 363.4 1541.5 362.2 1537 359.8 1537.8 354.5 1534.1 353.2 1531.7 347.3 1526.1 348.4 1525.4 340.8 1529.6 335.4 1528.7 330.1 1527.4 325.2 1524.7 323.7 1522 319.9 1518.9 320.4 1512.8 319.4 1514 316.7 1510.4 312.7 1507.2 315.4 1502.3 313.9 1496.9 317.9 1493 322.7 1488.8 323.5 1486 321.8 1483.1 321.6 1478.8 320.2 1476.2 321.8 1473.6 326.6 1472.1 321.5 1469 322.9 1462.5 322.2 1456 320.8 1451 317.9 1446.5 316.7 1444 313.6 1440.7 312.7 1434.3 308.5 1429.5 306.5 1427.6 308 1419 303.5 1412.8 299.5 1409.6 292.4 1413.7 293.3 1413.1 290 1410.1 286.7 1409.3 281.5 1401.6 273.9 1392.1 271.4 1389.2 266.4 1384.5 263.4 1383 261.6 1381.2 258 1380.7 255.5 1377 254 1375.5 254.7 1372.4 248.7 1373.5 247.3 1372.3 245.8 1376.4 242.8 1379.6 241.5 1385.4 242.4 1386 238.3 1392.4 237.6 1393.4 235 1400.3 231.6 1400.5 230.2 1398.8 226.5 1401.7 224.9 1392.9 213.9 1402 211.4 1404 210 1403 198.7 1413.8 200.8 1415.4 197.9 1412.9 191.7 1416.7 191.1 1418.6 186.9 1420.3 186.4 1423.6 190.8 1429.3 194.1 1437.5 196.4 1443.3 201.5 1444.7 208.8 1447.7 211.6 1454.2 212.7 1461.4 213.5 1469.4 217.5 1472.8 218.2 1477.8 224 1482.5 227.8 1488.1 227.6 1499.4 229.1 1505.8 228.2 1511.4 229.1 1520.8 233 1527 233 1530.3 234.9 1534.7 231.5 1541.9 229.3 1549.5 229.1 1554.4 226.9 1556.4 223.5 1558.8 221.3 1556.9 219.2 1554 216.8 1554.5 212.7 1557.7 213.3 1563.6 214.6 1566.8 211.2 1573.2 208.8 1574.5 204.6 1577 202.8 1583.8 202 1588.2 202.7 1587.4 200.5 1580.2 196.1 1575 194.1 1572.5 196.4 1567 195.4 1564.7 196.2 1561.9 193.7 1561.6 187.4 1561 182.7 1568.4 185.1 1572.8 181.2 1570.9 178.4 1570.7 171.9 1572 169.9 1569.5 166.5 1565.8 165.1 1567.5 162 1572.6 160.9 1578.8 160.7 1587.4 162.6 1593.4 164.8 1601.1 171 1604.9 173.7 1609.4 177.5 1615.6 183.5 1625.6 185.5 Z"> | |
| 152 | + </path> | |
| 153 | + <path d="M955.9 435.2l2.5 1.4 1 2.2 2.5 1.3 2-1.6 2.6-0.2 3.9 1.6 1.5 9.2-2.4 5.3-1.5 7.3 2.4 5.5-0.2 2.6-2.6 0-3.9-1.2-3.7 0-6.7 1.2-3.9 1.8-5.6 2.4-1.1-0.2 0.4-5.3 0.6-0.8-0.2-2.5-2.4-2.7-1.8-0.4-1.6-1.8 1.2-2.9-0.5-3.1 0.2-1.8 0.9 0 0.4-2.8-0.4-1.3 0.5-0.9 2.1-0.7-1.4-5.2-1.3-2.6 0.5-2.2 1.1-0.5 0.8-0.6 1.5 1 4.4 0 1-1.8 1 0.1 1.6-0.7 0.9 2.7 1.3-0.8 2.4-1z" id="CI" name="Côte d'Ivoire"> | |
| 154 | + </path> | |
| 155 | + <path d="M1072.8 454.2l-2.8 6.5-1.4 1.1-0.4 5 0.6 2.7-0.5 1.9 2.7 3.4 0.5 2.3 2.1 3.3 2.6 2.1 0.3 2.9 0.6 1.9-0.4 3.4-4.5-1.5-4.6-1.7-7.1-0.2-0.7-0.4-3.4 0.8-3.4-0.8-2.7 0.4-9.3-0.1 0.9-5.1-2.3-4.3-2.6-1-1.1-2.9-1.5-0.9 0.1-1.8 1.4-4.6 2.7-6.2 1.6 0 3.4-3.8 2.1-0.1 3.2 2.7 3.9-2.2 0.5-2.7 1.3-2.6 0.8-3.2 3-2.6 1.1-4.5 1.2-1.5 0.8-3.3 1.4-4.1 4.7-5 0.3-2.1 0.6-1.2-2.3-2.5 0.2-2.1 1.5-0.3 2.3 4.1 0.5 4.2-0.2 4.3 3.2 5.8-3.2 0-1.6 0.4-2.6-0.6-1.2 3 3.4 3.8 2.5 1.1 0.8 2.6 1.8 4.4-0.8 1.8z" id="CM" name="Cameroon"> | |
| 156 | + </path> | |
| 157 | + <path d="M1141.3 468.2l3.5 5.3 2.6 0.8 1.5-1.1 2.6 0.4 3.1-1.3 1.4 2.7 5.1 4.3-0.3 7.5 2.3 0.9-1.9 2.2-2.1 1.8-2.2 3.3-1.2 3-0.3 5.1-1.3 2.5-0.1 4.8-1.6 1.8-0.2 3.8-0.8 0.5-0.6 3.6 1.4 2.9 0.1 1-1.2 10.3 1.5 3.6-1 2.7 1.8 4.6 3.4 3.5 0.7 3.5 1.6 1.7-0.3 1.1-0.9-0.3-7.7 1.1-1.5 0.8-1.7 4.1 1.2 2.8-1.1 7.6-0.9 6.4 1.5 1.2 3.9 2.5 1.6-1.2 0.2 6.9-4.3 0-2.2-3.5-2-2.8-4.3-0.9-1.2-3.3-3.5 2-4.4-0.9-1.9-2.9-3.5-0.6-2.7 0.1-0.3-2-1.9-0.1-2.6-0.4-3.5 1-2.4-0.2-1.4 0.6 0.4-7.6-1.8-2.4-0.4-4 0.9-3.9-1.1-2.4-0.1-4.1-6.8 0.1 0.5-2.3-2.9 0-0.3 1.1-3.5 0.3-1.5 3.7-0.9 1.6-3.1-0.9-1.8 0.9-3.8 0.5-2.1-3.3-1.3-2.1-1.6-3.9-1.3-4.7-16.7-0.1-2 0.7-1.7-0.1-2.3 0.9-0.8-2 1.4-0.7 0.2-2.8 1-1.6 2-1.4 1.5 0.7 2-2.5 3.1 0.1 0.3 1.8 2.1 1.1 3.4-4 3.3-3.1 1.4-2.1-0.1-5.3 2.5-6.2 2.6-3.3 3.7-3.1 0.7-2 0.1-2.4 0.9-2.2-0.3-3.7 0.7-5.7 1.1-4 1.7-3.4 0.3-3.9 0.5-4.5 2.2-3.2 3-2.1 4.7 2.2 3.6 2.4 4.1 0.6 4.3 1.3 1.6-3.9 0.8-0.5 2.6 0.6 6.3-3.2 2.2 1.4 1.8-0.2 0.9-1.6 2.1-0.5 4.2 0.7 3.7 0.1 1.8-0.7z" id="CD" name="Democratic Republic of the Congo"> | |
| 158 | + </path> | |
| 159 | + <path d="M1090.9 479.3l-0.3 3.9-1.7 3.4-1.1 4-0.7 5.7 0.3 3.7-0.9 2.2-0.1 2.4-0.7 2-3.7 3.1-2.6 3.3-2.5 6.2 0.1 5.3-1.4 2.1-3.3 3.1-3.4 4-2.1-1.1-0.3-1.8-3.1-0.1-2 2.5-1.5-0.7-2.1-2.2-1.7 1.1-2.3 2.8-4.6-6.8 4.3-3.6-2.1-4.2 2-1.6 3.8-0.8 0.4-2.9 3.1 3.1 5 0.3 1.7-3 0.7-4.3-0.6-5-2.7-3.8 2.5-7.5-1.4-1.2-4.2 0.5-1.6-3.3 0.4-2.8 7.1 0.2 4.6 1.7 4.5 1.5 0.4-3.4 3-6 3.3-3.4 3.9 1.1 3.6 0.3z" id="CG" name="Republic of Congo"> | |
| 160 | + </path> | |
| 161 | + <path d="M584.4 426.2l-3.7 1.1-1.6 3.2-2.3 1.8-1.8 2.4-0.9 4.6-1.8 3.8 2.9 0.4 0.6 2.9 1.2 1.5 0.3 2.5-0.7 2.4 0.1 1.4 1.4 0.5 1.2 2.2 7.3-0.6 3.3 0.8 3.8 5.6 2.3-0.7 4.1 0.3 3.2-0.7 2 1.1-1.2 3.4-1.3 2.2-0.6 4.6 1.1 4.3 1.5 1.9 0.2 1.4-2.9 3.2 2 1.4 1.5 2.3 1.6 6.4-1.1 0.8-1-3.8-1.6-2.1-1.9 2.3-11-0.2 0 4 3.3 0.7-0.2 2.5-1.1-0.7-3.2 1-0.1 4.7 2.6 2.4 0.9 3.6-0.1 2.8-2.2 17.6-2.9-3.4-1.7-0.1 3.5-6.6-4.4-3-3.4 0.6-2.1-1.1-3.1 1.7-4.2-0.9-3.5-6.7-2.6-1.6-1.8-3.1-3.8-3-1.5 0.6-2.4-1.5-2.8-2.1-1.6 1-4.8-0.9-1.4-2.8-1.1 0.1-5.6-3.6-0.7-2 2.1-0.5-0.2-3.2 1.4-2.4 2.8-0.4 2.5-4 2.2-3.4-2-1.5 1.2-3.7-1.1-5.9 1.3-1.7-0.7-5.4-2.2-3.5 0.9-3.1 1.8 0.5 1.1-1.9-1.1-3.8 0.7-0.9 2.9 0.2 4.5-4.5 2.4-0.7 0.1-2.1 1.4-5.5 3.4-2.9 3.5-0.2 0.6-1.3 4.4 0.5 4.6-3.2 2.3-1.4 2.9-3.1 2 0.4 1.3 1.7-1.2 2.1z" id="CO" name="Colombia"> | |
| 162 | + </path> | |
| 163 | + <path d="M514.6 431.6l1.2 3.5 2 2.6 2.5 2.7-2.2 0.6-0.1 2.6 1.1 0.9-0.9 0.8 0.2 1.1-0.6 1.3-0.3 1.3-3-1.4-1.1-1.4 0.7-1.1-0.1-1.4-1.5-1.5-2.2-1.3-1.8-0.8-0.3-1.9-1.4-1.1 0.2 1.8-1.2 1.6-1.2-1.8-1.7-0.7-0.7-1.2 0.1-2 0.9-2-1.5-0.9 1.4-1.3 0.9-0.8 3.6 1.7 1.3-0.8 1.8 0.5 0.8 1.3 1.7 0.5 1.4-1.4z" id="CR" name="Costa Rica"> | |
| 164 | + </path> | |
| 165 | + <path d="M544.8 355.7l1.9 2.3 5.2-0.7 1.8 1.5 4.2 4 3.2 2.9 1.8-0.1 3.2 1.3-0.6 1.8 4 0.3 3.9 2.6-0.8 1.5-3.8 0.8-3.8 0.3-3.7-0.5-8.1 0.6 4.2-3.5-2.1-1.7-3.6-0.4-1.7-1.9-0.8-3.6-3.2 0.2-5-1.7-1.5-1.4-7.1-1-1.8-1.2 2.3-1.6-5.4-0.3-4.4 3.3-2.3 0.1-1 1.6-2.8 0.7-2.3-0.7 3.2-1.9 1.5-2.4 2.7-1.4 3-1.2 4.3-0.6 1.4-0.8 4.7 0.5 4.4 0.1 4.9 2.2z" id="CU" name="Cuba"> | |
| 166 | + </path> | |
| 167 | + <path d="M1059.7 175.2l2.5 2 3.7 0.5-0.2 1.7 2.8 1.3 0.6-1.6 3.4 0.7 0.7 2 3.7 0.3 2.6 3.1-1.5 0-0.7 1.1-1.1 0.3-0.2 1.4-0.9 0.3-0.1 0.6-1.6 0.6-2.2-0.1-0.6 1.4-2.4-1.2-2.3 0.3-4-1.9-1.7 0.5-2.6 2.6-3.8-2.1-3-2.6-2.6-1.5-0.7-2.7-1-1.8 3.4-1.3 1.7-1.6 3.5-1.2 1.1-1.2 1.3 0.7 2.2-0.6z" id="CZ" name="Czech Republic"> | |
| 168 | + </path> | |
| 169 | + <path d="M1053.9 158.9l1.4 3.1-1.2 1.7 1.9 2.1 1.5 3.3-0.2 2.2 2.4 3.9-2.2 0.6-1.3-0.7-1.1 1.2-3.5 1.2-1.7 1.6-3.4 1.3 1 1.8 0.7 2.7 2.6 1.5 3 2.6-1.6 2.9-1.7 0.8 1 4.1-0.4 1.1-1.7-1.3-2.4-0.2-3.5 1.1-4.4-0.3-0.6 1.7-2.7-1.7-1.4 0.3-5.5-1.9-1 1.3-4.2 0 0.4-4.5 2.4-4.2-7.2-1.2-2.4-1.6 0.2-2.7-1-1.4 0.4-4.2-1.1-6.5 2.9 0 1.2-2.3 0.9-5.6-0.9-2.1 0.8-1.3 4-0.3 1 1.3 3.1-3-1.3-2.3-0.4-3.4 3.7 0.8 2.9-0.9 0.3 2.3 4.9 1.4 0.1 2.2 4.7-1.2 2.6-1.6 5.6 2.4 2.4 1.9z" id="DE" name="Germany"> | |
| 170 | + </path> | |
| 171 | + <path d="M1229.5 428.2l-1.9 3.5-1.3-1.2-1.3 0.5-3.2-0.1-0.2-2-0.5-1.8 1.8-3 1.9-2.8 2.4 0.6 1.7-1.6 1.4 2-0.1 2.6-3.1 1.6 2.4 1.7z" id="DJ" name="Djibouti"> | |
| 172 | + </path> | |
| 173 | + <path class="Denmark" d="M 1046.1 147.7 1043.7 152.6 1038.5 149.1 1037.6 146.6 1044.4 144.6 1046.1 147.7 Z"> | |
| 174 | + </path> | |
| 175 | + <path class="Denmark" d="M 1033.3 151.5 1030.4 152.4 1026.7 151.6 1024.6 148.2 1024.2 142.1 1024.8 140.4 1026.1 138.6 1030.1 138.3 1031.7 136.6 1035.3 134.9 1035.3 138 1034.1 140 1034.8 141.6 1037.4 142.5 1036.4 144.8 1035 144.2 1031.9 148.5 1033.3 151.5 Z"> | |
| 176 | + </path> | |
| 177 | + <path d="M585.7 386l0.3-1.8-1.3-1.9 1.5-1.1 0.7-2.5-0.1-3.4 0.8-1.1 4.3 0 3.2 1.6 1.5-0.1 0.7 2.3 3.1-0.2-0.4 1.9 2.5 0.3 2.5 2.3-2.3 2.6-2.6-1.4-2.6 0.3-1.8-0.3-1.1 1.2-2.2 0.4-0.7-1.6-1.9 0.9-2.7 4.4-1.3-1-0.1-1.8z" id="DO" name="Dominican Republic"> | |
| 178 | + </path> | |
| 179 | + <path d="M1031 264.6l-1 3.3 1 6.1-1.1 5.3-3.2 3.6 0.6 4.8 4.5 3.9 0.1 1.5 3.4 2.6 2.6 11.5 1.9 5.7 0.4 3-0.8 5.2 0.4 3-0.6 3.5 0.6 4-2.2 2.7 3.4 4.7 0.2 2.7 2.1 3.6 2.5-1.2 4.5 3 2.5 4-18.8 12.3-16 12.6-7.8 2.8-6.2 0.7-0.1-4.1-2.6-1.1-3.5-1.8-1.3-3-18.7-14-18.6-14-20.5-15.6 0.1-1.2 0.1-0.4 0.1-7.6 8.9-4.8 5.4-1 4.5-1.7 2.1-3.2 6.4-2.5 0.3-4.8 3.1-0.6 2.5-2.3 7.1-1.1 1-2.5-1.4-1.4-1.9-6.8-0.3-3.9-1.9-4.1 5.1-3.5 5.8-1.1 3.3-2.6 5.1-2 9-1.1 8.8-0.5 2.7 0.9 4.9-2.5 5.7-0.1 2.2 1.5 3.6-0.4z" id="DZ" name="Algeria"> | |
| 180 | + </path> | |
| 181 | + <path d="M559 502.8l0.8 4.9-1.7 4.1-6.1 6.8-6.7 2.5-3.4 5.6-0.9 4.3-3.1 2.7-2.5-3.3-2.3-0.7-2.3 0.5-0.3-2.3 1.6-1.5-0.7-2.7 2.9-4.8-1.3-2.8-2.1 3-3.5-2.9 1.1-1.8-1-5.8 2-1 1-4 2.1-4.1-0.3-2.6 3.1-1.4 3.9-2.5 5.6 3.6 1.1-0.1 1.4 2.8 4.8 0.9 1.6-1 2.8 2.1 2.4 1.5z" id="EC" name="Ecuador"> | |
| 182 | + </path> | |
| 183 | + <path d="M1172.1 301.4l3.9 9.4 0.7 1.6-1.3 2.6-0.7 4.8-1.2 3.4-1.2 1.1-2-2.1-2.7-2.8-4.7-9.2-0.5 0.6 2.8 6.7 3.9 6.5 4.9 10 2.3 3.5 2 3.6 5.4 7.1-1 1.1 0.4 4.2 6.8 5.8 1.1 1.3-22.1 0-21.5 0-22.3 0-1-23.7-1.3-22.8-2-5.2 1.1-3.9-1-2.8 1.7-3.1 7.2-0.1 5.4 1.7 5.5 1.9 2.6 1 4-2 2.1-1.8 4.7-0.6 3.9 0.8 1.8 3.2 1.1-2.1 4.4 1.5 4.3 0.4 2.5-1.6z" id="EG" name="Egypt"> | |
| 184 | + </path> | |
| 185 | + <path d="M1228.9 420.3l-1.7 1.6-2.4-0.6-2-2.1-2.5-3.7-2.6-2.1-1.5-2.2-5-2.6-3.9-0.1-1.4-1.3-3.2 1.5-3.6-2.9-1.5 4.8-6.6-1.4-0.7-2.5 2-9.5 0.3-4.2 1.7-2 4-1.1 2.7-3.6 3.6 7.4 1.9 5.9 3.2 3.1 8 6.1 3.3 3.6 3.2 3.8 1.8 2.2 2.9 1.9z" id="ER" name="Eritrea"> | |
| 186 | + </path> | |
| 187 | + <path d="M1113.7 124.6l0.9 1-2.6 3.4 2.4 5.6-1.6 1.9-3.8-0.1-4.4-2.2-2.1-0.7-3.8 1-0.1-3.5-1.5 0.8-3.3-2.1-1-3.4 5.5-1.7 5.6-0.8 5.1 0.9 4.7-0.1z" id="EE" name="Estonia"> | |
| 188 | + </path> | |
| 189 | + <path d="M1207.3 408.5l3.9 0.1 5 2.6 1.5 2.2 2.6 2.1 2.5 3.7 2 2.1-1.9 2.8-1.8 3 0.5 1.8 0.2 2 3.2 0.1 1.3-0.5 1.3 1.2-1.2 2.2 2.2 3.6 2.2 3.1 2.2 2.3 18.7 7.6 4.8-0.1-15.6 19.3-7.3 0.3-5 4.5-3.6 0.1-1.5 2.1-3.9 0-2.3-2.2-5.2 2.7-1.6 2.7-3.8-0.6-1.3-0.7-1.3 0.2-1.8-0.1-7.2-5.4-4 0-1.9-2.1-0.1-3.6-2.9-1.1-3.5-7-2.6-1.5-1-2.6-3-3.1-3.5-0.5 1.9-3.6 3-0.2 0.8-1.9-0.2-5 0-0.8 1.5-6.7 2.6-1.8 0.5-2.6 2.3-5 3.3-3.1 2-6.4 0.7-5.5 6.6 1.4 1.5-4.8 3.6 2.9 3.2-1.5 1.4 1.3z" id="ET" name="Ethiopia"> | |
| 190 | + </path> | |
| 191 | + <path d="M1104.1 70.1l0.4 3.8 7.3 3.7-2.9 4.2 6.5 6.3-1.7 4.8 4.9 4.2-0.9 3.8 7.4 3.9-0.9 2.9-3.4 3.4-8 7.4-8 0.5-7.6 2.1-7.1 1.3-3.2-3.2-4.7-1.9 0.1-5.8-3-5.2 1.6-3.4 3.3-3.5 8.8-6.2 2.6-1.2-0.9-2.4-6.5-2.6-1.8-2.2-1.8-8.5-7.2-3.7-6-2.7 2.2-1.4 5.1 2.8 5.3-0.2 4.7 1.3 3.4-2.4 1.1-4 5.9-1.8 5.8 2.1-0.8 3.8z" id="FI" name="Finland"> | |
| 192 | + </path> | |
| 193 | + <path d="M1060.5 487.3l-0.4 2.8 1.6 3.3 4.2-0.5 1.4 1.2-2.5 7.5 2.7 3.8 0.6 5-0.7 4.3-1.7 3-5-0.3-3.1-3.1-0.4 2.9-3.8 0.8-2 1.6 2.1 4.2-4.3 3.6-5.8-6.5-3.7-5.3-3.5-6.6 0.2-2.2 1.3-2 1.3-4.7 1.2-4.8 1.9-0.3 8.2 0 0-7.7 2.7-0.4 3.4 0.8 3.4-0.8 0.7 0.4z" id="GA" name="Gabon"> | |
| 194 | + </path> | |
| 195 | + <path class="United Kingdom" d="M 956.7 158.2 953.2 157 950.2 157.1 951.4 153.8 950.5 150.6 954.5 150.3 959.4 154.1 956.7 158.2 Z"> | |
| 196 | + </path> | |
| 197 | + <path class="United Kingdom" d="M 972.6 129.5 967.5 136 972.2 135.2 977.3 135.2 976 140.1 971.7 145.5 976.6 145.8 976.9 146.5 981.1 153.6 984.3 154.6 987.2 161.6 988.6 164 994.5 165.1 993.9 169.1 991.5 170.9 993.4 174.1 989 177.3 982.5 177.2 974.1 179 971.9 177.7 968.6 180.6 964.1 179.9 960.5 182.3 958 181.1 965.3 174.6 969.7 173.2 962.1 172.2 960.8 169.7 965.9 167.8 963.4 164.5 964.4 160.5 971.5 161.1 972.3 157.5 969.2 153.8 969.1 153.7 963.4 152.6 962.3 151 964.1 148.3 962.6 146.6 960 149.5 959.9 143.6 957.7 140.6 959.6 134.4 963.4 129.6 967 130 972.6 129.5 Z"> | |
| 198 | + </path> | |
| 199 | + <path d="M1215.7 227.9l5.1 1.3 2.1 2.6 3.6 1.5-1.2 0.8 3.3 3.5-0.6 0.7-2.9-0.3-4.2-1.9-1.1 1.1-7 1-5.6-3.2-5.5 0.3 0.3-2.7-2.1-4.3-3.4-2.4-3-0.7-2.2-1.9 0.4-0.8 4.6 1.1 7.7 1 7.6 3.1 1.2 1.2 2.9-1z" id="GE" name="Georgia"> | |
| 200 | + </path> | |
| 201 | + <path d="M986.5 431.1l-0.4 2 2.3 3.3 0 4.7 0.6 5 1.4 2.4-1.3 5.7 0.5 3.2 1.5 4.1 1.3 2.3-8.9 3.7-3.2 2.2-5.1 1.9-5-1.8 0.2-2.6-2.4-5.5 1.5-7.3 2.4-5.3-1.5-9.2-0.8-4.8 0.2-3.7 9.8-0.3 2.5 0.5 1.8-1 2.6 0.5z" id="GH" name="Ghana"> | |
| 202 | + </path> | |
| 203 | + <path d="M921.5 421.9l0.3 2.4 0.9 0 1.5-0.9 0.9 0.2 1.6 1.7 2.4 0.5 1.5-1.4 1.9-0.9 1.3-0.9 1.1 0.2 1.3 1.4 0.6 1.8 2.3 2.7-1.1 1.6-0.3 2.1 1.2-0.6 0.7 0.7-0.3 1.9 1.7 1.9-1.1 0.5-0.5 2.2 1.3 2.6 1.4 5.2-2.1 0.7-0.5 0.9 0.4 1.3-0.4 2.8-0.9 0-1.6-0.2-1.1 2.6-1.6 0-1.1-1.4 0.4-2.6-2.4-3.9-1.4 0.7-1.3 0.2-1.5 0.3 0.1-2.3-0.9-1.7 0.2-1.9-1.2-2.7-1.6-2.3-4.5 0-1.3 1.2-1.6 0.2-1 1.4-0.6 1.7-3.1 2.9-2.4-3.8-2.2-2.5-1.4-0.9-1.4-1.3-0.6-2.8-0.8-1.4-1.7-1.1 2.6-3.1 1.7 0.1 1.5-1 1.2-0.1 0.9-0.8-0.4-2.1 0.6-0.7 0.1-2.2 2.7 0.1 4.1 1.5 1.2-0.1 0.4-0.7 3.1 0.5 0.8-0.4z" id="GN" name="Guinea"> | |
| 204 | + </path> | |
| 205 | + <path d="M891.6 417.4l0.8-2.9 6.1-0.1 1.3-1.6 1.8-0.1 2.2 1.6 1.7 0 1.9-1 1.1 1.8-2.5 1.5-2.4-0.2-2.4-1.3-2.1 1.5-1 0-1.4 0.9-5.1-0.1z" id="GM" name="The Gambia"> | |
| 206 | + </path> | |
| 207 | + <path d="M909.2 421l-0.1 2.2-0.6 0.7 0.4 2.1-0.9 0.8-1.2 0.1-1.5 1-1.7-0.1-2.6 3.1-2.9-2.6-2.4-0.5-1.3-1.8 0.1-1-1.7-1.3-0.4-1.4 3-1 1.9 0.2 1.5-0.8 10.4 0.3z" id="GW" name="Guinea-Bissau"> | |
| 208 | + </path> | |
| 209 | + <path d="M1050.3 487.3l0 7.7-8.2 0-1.9 0.3-1.1-0.9 1.9-7.2 9.3 0.1z" id="GQ" name="Equatorial Guinea"> | |
| 210 | + </path> | |
| 211 | + <path class="Greece" d="M 1112.7 272.6 1115.8 274.8 1119.9 274.4 1123.9 274.8 1123.9 276 1126.7 275.2 1126.2 277.1 1118.6 277.6 1118.5 276.6 1111.9 275.3 1112.7 272.6 Z"> | |
| 212 | + </path> | |
| 213 | + <path class="Greece" d="M 1121.9 239.9 1118.7 239.7 1116 239.1 1109.8 240.7 1113.8 244.3 1111.3 245.4 1108.4 245.4 1105.3 242.1 1104.4 243.5 1106 247.3 1108.9 250.3 1107 251.7 1110.2 254.6 1113 256.5 1113.4 260.1 1108.4 258.4 1110.2 261.7 1106.9 262.3 1109.4 268 1105.9 268.1 1101.3 265.3 1098.9 260.2 1097.6 255.9 1095.3 253 1092.3 249.3 1091.8 247.5 1094 244.4 1094.1 242.3 1095.8 241.4 1095.7 239.7 1099.1 239.2 1100.9 237.8 1103.7 237.9 1104.5 236.8 1105.5 236.6 1109.3 236.8 1113.3 235 1117.2 237.3 1121.8 236.7 1121.5 233.5 1124.2 235.2 1123.1 239.2 1121.9 239.9 Z"> | |
| 214 | + </path> | |
| 215 | + <path d="M896.3 1.4l19.9 3-6.7 1.4-13 0.2-18.5 0.4 1.4 0.7 12.3-0.5 9.7 1.4 7-1.2 2.4 1.4-4.5 2.4 9.2-1.6 17.1-1.5 10 0.8 1.7 1.7-14.8 2.9-2.2 1-11.4 0.8 8.1 0.2-4.9 3.2-3.6 2.9-1.2 5.2 3.7 3.2-5.9 0.1-6.5 1.6 6.3 2.6-0.1 4.2-4.2 0.5 4.1 4.3-8.7 0.4 4 2-1.6 1.8-5.7 0.8-5.5 0.1 4.2 3.4-0.5 2.4-7.3-2.2-2.4 1.4 5 1.3 4.6 3.2 0.6 4.3-7.4 1-2.7-2.1-4.2-3 0.5 3.6-5.4 2.8 10.7 0.2 5.5 0.3-11.9 4.7-12.2 4.3-12.7 1.8-4.6 0.1-4.9 2.1-7.5 5.8-10.2 3.9-3 0.3-6.1 1.3-6.6 1.4-4.8 3.4-1.4 4-3.4 3.8-8.6 4.6 0.3 4.5-3.6 4.8-4.1 5.7-6.5 0.4-5-4.8-9-0.1-3.2-3.2-0.8-5.6-4.8-7.2-0.7-3.7 1.5-5.1-3.7-5.1 3.3-4.1-1.9-2 7-6.4 7.2-2.1 2.6-2.2 2.8-4.2-5.5 1.9-2.6 0.8-4.1 0.7-4.2-1.7 1.5-3.7 3-2.8 3.8-0.1 7.6 1.5-5.3-3.4-2.7-1.8-4.4 0.7-2.6-1.3 7-4.8-1.3-2-1.1-3.5-1.4-5.4-3.6-1.9 1.4-2.1-8.1-2.9-7.7-0.4-10.1 0.2-9.5 0.4-3.1-1.6-3.8-3.1 11-1.5 7.6-0.2-14.6-1.3-6.5-1.9 2.2-1.8 15.7-2.2 15-2.2 2.8-1.6-8.1-1.6 4.6-1.7 14.7-2.9 5.4-0.4 0.2-1.8 9-1.1 11-0.6 10.4 0 2.8 1.2 10.6-2.2 7.1 1.5 4.6 0.3 6.2 1.3-6.7-2.1 1.6-1.7 12.7-2.2 11.6 0.2 5.1-1.4 11.9-0.3 26.3 0.4z" id="GL" name="Greenland"> | |
| 216 | + </path> | |
| 217 | + <path d="M488.1 387.5l-0.7 5.1-1.3 7.2 1.7 0 1.7 1.2 0.6-1 1.5 0.8-2.8 2.5-2.9 1.8-0.5 1.2 0.3 1.3-1.3 1.6-1.4 0.4 0.3 0.8-1.2 0.7-2 1.6-0.3 0.9-2.8-1.1-3.5-0.1-2.4-1.3-2.8-2.6 0.4-1.9 0.8-1.5-0.7-1.2 3.3-5.2 7.2 0 0.4-2.2-0.8-0.4-0.5-1.4-1.9-1.5-1.8-2.1 2.5-0.1 0.5-3.6 5.2 0 5.2 0.1z" id="GT" name="Guatemala"> | |
| 218 | + </path> | |
| 219 | + <path d="M662.9 463.5l-1 5.8-3.5 1.6 0.3 1.5-1.1 3.4 2.4 4.6 1.8 0 0.7 3.6 3.3 5.6-1.3 0.3-3.2-0.6-1.8 1.7-2.6 1.1-1.8 0.3-0.6 1.3-2.8-0.3-3.5-3-0.3-3-1.4-3.3 1-5.4 1.6-2.3-1.2-3-2-0.9 0.8-2.9-1.2-1.4-3 0.2-3.7-4.8 1.6-1.8 0-3 3.5-1 1.4-1.2-1.8-2.4 0.5-2.3 4.7-3.8 3.6 2.4 3.3 4.1 0.1 3.4 2.1 0.1 3 3.1 2.1 2.3z" id="GY" name="Guyana"> | |
| 220 | + </path> | |
| 221 | + <path d="M519.6 405.5l-1.9-0.1-0.9 0.9-2 0.8-1.4 0-1.3 0.8-1.1-0.2-0.9-1-0.6 0.2-0.9 1.5-0.5-0.1-0.2 1.4-2.1 1.7-1.2 0.8-0.6 0.8-1.5-1.3-1.4 1.7-1.2 0-1.3 0.1-0.2 3.2-0.8 0-0.8 1.5-1.8 0.3-0.8-2-1.7-0.6 0.7-2.6-0.7-0.7-1.2-0.4-2.5 0.7-0.1-0.8-1.6-1.1-1.1-1.2-1.6-0.6 1.3-1.6-0.3-1.3 0.5-1.2 2.9-1.8 2.8-2.5 0.6 0.3 1.3-1.1 1.6-0.1 0.5 0.5 0.9-0.3 2.6 0.6 2.6-0.2 1.8-0.7 0.8-0.7 1.7 0.3 1.3 0.4 1.5-0.1 1.2-0.6 2.5 0.9 0.8 0.2 1.6 1.2 1.5 1.4 1.9 1 1.3 1.7z" id="HN" name="Honduras"> | |
| 222 | + </path> | |
| 223 | + <path d="M1081.5 207.6l1.5 2.5 1.7 1.8-1.7 2.4-2.4-1.4-3.5 0.1-4.5-1.1-2.3 0.1-1 1.4-2-1.5-0.8 2.6 2.7 3 1.3 1.9 2.5 2.4 2.1 1.4 2.1 2.7 4.8 2.4-0.5 1-5-2.3-3.2-2.3-4.8-1.9-4.7-4.6 1-0.5-2.5-2.7-0.3-2.1-3.3-1-1.4 2.7-1.6-2.1 0-2.2 0.1-0.1 3.6 0.2 0.8-1 1.8 1 2 0.1-0.1-1.7 1.7-0.7 0.3-2.5 3.9-1.7 1.6 0.8 4 2.7 4.3 1.2 1.8-1z" id="HR" name="Croatia"> | |
| 224 | + </path> | |
| 225 | + <path d="M586.8 375.3l0.1 3.4-0.7 2.5-1.5 1.1 1.3 1.9-0.3 1.8-3.6-1.1-2.7 0.4-3.4-0.4-2.7 1.2-2.8-2 0.7-2.1 5.1 0.9 4.1 0.5 2.2-1.4-2.3-2.8 0.4-2.5-3.5-1 1.5-1.7 3.4 0.2 4.7 1.1z" id="HT" name="Haiti"> | |
| 226 | + </path> | |
| 227 | + <path d="M1096.2 191.9l3 1.7 0.5 1.7-2.9 1.3-1.9 4.2-2.6 4.3-3.9 1.2-3.2-0.3-3.7 1.6-1.8 1-4.3-1.2-4-2.7-1.6-0.8-1.2-2.1-0.8-0.1 1.3-4-1.1-1.4 2.8 0 0.2-2.6 2.7 1.7 1.9 0.6 4.1-0.7 0.3-1.3 1.9-0.2 2.3-0.9 0.6 0.4 2.3-0.8 1-1.5 1.6-0.4 5.5 1.9 1-0.6z" id="HU" name="Hungary"> | |
| 228 | + </path> | |
| 229 | + <path class="Indonesia" d="M 1667.5 567.6 1665.1 567.7 1658 563.2 1663.4 561.9 1666.2 563.9 1668 565.8 1667.5 567.6 Z"> | |
| 230 | + </path> | |
| 231 | + <path class="Indonesia" d="M 1692.3 558.9 1692.8 560.2 1692.7 562.1 1688.6 566.9 1683.6 568.3 1683 567.6 1683.7 565.4 1686.5 561.5 1692.3 558.9 Z"> | |
| 232 | + </path> | |
| 233 | + <path class="Indonesia" d="M 1652.7 553.8 1654.6 555.5 1658.2 555 1659.4 557.7 1652.7 559 1648.8 559.9 1645.7 559.8 1647.9 556.1 1651.1 556.1 1652.7 553.8 Z"> | |
| 234 | + </path> | |
| 235 | + <path class="Indonesia" d="M 1681 553.8 1679.9 557.4 1671.3 559.2 1663.8 558.4 1664 556 1668.6 554.7 1672 556.6 1675.8 556.1 1681 553.8 Z"> | |
| 236 | + </path> | |
| 237 | + <path class="Indonesia" d="M 1600.8 545.3 1611.6 546 1613 543.3 1623.3 546.4 1625.1 550.6 1633.5 551.8 1640.2 555.6 1633.6 558 1627.5 555.4 1622.4 555.6 1616.6 555.1 1611.4 554 1605 551.5 1600.9 550.9 1598.5 551.7 1588.3 549 1587.5 546.3 1582.4 545.8 1586.6 539.7 1593.4 540.1 1597.8 542.6 1600.2 543.1 1600.8 545.3 Z"> | |
| 238 | + </path> | |
| 239 | + <path class="Indonesia" d="M 1748.7 541.7 1745.5 546.1 1745.3 541.3 1746.4 539 1747.7 536.8 1748.9 538.7 1748.7 541.7 Z"> | |
| 240 | + </path> | |
| 241 | + <path class="Indonesia" d="M 1707.3 524 1705.1 526.2 1701.3 525 1700.3 522.2 1706 521.9 1707.3 524 Z"> | |
| 242 | + </path> | |
| 243 | + <path class="Indonesia" d="M 1725.7 521.7 1727.5 526.6 1722.9 523.9 1718.2 523.4 1714.9 523.8 1711 523.6 1712.5 520.1 1719.5 519.8 1725.7 521.7 Z"> | |
| 244 | + </path> | |
| 245 | + <path class="Indonesia" d="M 1785.5 518.5 1784.5 539.4 1782.9 560.4 1778.3 555.1 1772.7 553.8 1771.2 555.6 1764 555.8 1766.8 550.6 1770.5 548.8 1769.5 541.8 1767.2 536.5 1756.5 531 1751.9 530.5 1743.6 524.5 1741.8 527.7 1739.6 528.2 1738.5 525.9 1738.6 523.1 1734.4 519.9 1740.6 517.6 1744.6 517.7 1744.2 516 1735.9 516 1733.7 512.2 1728.7 511 1726.4 507.8 1734 506.3 1736.9 504.2 1746 506.8 1746.9 509.2 1748.2 519.6 1753.9 523.4 1758.9 516.6 1765.5 512.8 1770.5 512.8 1775.3 515 1779.4 517.3 1785.5 518.5 Z"> | |
| 246 | + </path> | |
| 247 | + <path class="Indonesia" d="M 1696.4 492.7 1691.9 499.1 1687.6 500.3 1682.2 499.1 1672.7 499.4 1667.8 500.3 1667 505.2 1672 510.9 1675.1 508 1685.7 505.8 1685.2 508.7 1682.7 507.8 1680.2 511.6 1675.1 514.1 1680.2 522.3 1679.1 524.5 1683.9 531.9 1683.6 536.1 1680.5 538 1678.4 535.7 1681.4 530.5 1675.7 533 1674.4 531.2 1675.2 528.7 1671.3 524.9 1672 518.7 1668.1 520.6 1668.3 528.1 1668.1 537.3 1664.4 538.2 1662.1 536.4 1664 530.5 1663.4 524.3 1661 524.2 1659.4 519.8 1661.9 515.6 1662.8 510.5 1665.8 500.8 1666.9 498.2 1671.7 493.4 1676.2 495.3 1683.3 496.2 1689.8 495.9 1695.4 491.3 1696.4 492.7 Z"> | |
| 248 | + </path> | |
| 249 | + <path class="Indonesia" d="M 1716 494.6 1715.7 500.2 1712.8 499.5 1711.9 503.4 1714.2 506.8 1712.6 507.6 1710.4 503.5 1708.7 495.3 1709.7 490.2 1711.5 487.9 1712 491.4 1715.4 491.9 1716 494.6 Z"> | |
| 250 | + </path> | |
| 251 | + <path class="Indonesia" d="M 1608 488.9 1609 493.2 1612.9 496.9 1616.6 495.6 1620.2 496 1623.5 492.8 1626.2 492.2 1631.6 494 1636.2 492.6 1638.8 483.7 1640.9 481.5 1642.6 474.2 1649.1 474.2 1654.1 475.3 1651.1 481.1 1655.5 487.1 1654.6 490.1 1661 496 1654.3 496.8 1652.5 501.2 1652.7 507 1647.2 511.4 1646.8 517.8 1644.3 527.6 1643.6 525.3 1637 528.2 1634.9 524.3 1630.9 523.9 1628.1 521.8 1621.3 524.2 1619.4 521 1615.6 521.4 1611 520.7 1610.4 512.1 1607.6 510.3 1604.9 504.8 1604.1 499.2 1604.7 493.2 1608 488.9 Z"> | |
| 252 | + </path> | |
| 253 | + <path class="Indonesia" d="M 1585.2 539.4 1579 539.5 1574.5 534.2 1567.4 528.9 1565.1 525 1561 519.8 1558.3 515 1554.1 506 1549.2 500.6 1547.5 495.1 1545.3 490.1 1540.1 486.1 1537 480.6 1532.6 477 1526.4 469.9 1525.8 466.6 1529.4 466.9 1538.3 468.1 1543.5 474.4 1548.1 478.8 1551.3 481.4 1556.8 488.3 1562.6 488.4 1567.4 492.8 1570.8 498.2 1575.1 501.2 1572.8 506.4 1576.1 508.6 1578.1 508.8 1579 513.3 1580.9 516.8 1585 517.4 1587.6 521.5 1585.9 529.5 1585.2 539.4 Z"> | |
| 254 | + </path> | |
| 255 | + <path d="M1427.6 308l-2.8 3-0.9 6 5.8 2.4 5.8 3.1 7.8 3.6 7.7 0.9 3.8 3.2 4.3 0.6 6.9 1.5 4.6-0.1 0.1-2.5-1.5-4.1-0.2-2.7 3.1-1.4 1.5 5.1 0.4 1.2 5.5 2.5 3.2-1 4.7 0.4 4.5-0.2-0.5-3.9-2.6-2.1 4.2-0.8 3.9-4.8 5.4-4 4.9 1.5 3.2-2.7 3.6 4-1.2 2.7 6.1 1 1 2.4-1.7 1.2 1.4 3.9-4.2-1.1-6.2 4.4 0.9 3.7-2 5.4 0.3 3.1-1.6 5.3-4.6-1.5 0.9 6.7-1 2.2 1 2.7-2.5 1.5-4.4-10.2-1.5 0-0.3 4.2-3.6-3.4 1.2-3.6 2.4-0.4 1.6-5.4-3.4-1.1-5.1 0.1-5.4-0.9-1.2-4.5-2.7-0.3-4.9-2.8-1.2 4.4 4.6 3.4-3 2.4-0.9 2.3 3.7 1.7-0.3 3.9 2.6 4.8 1.6 5.3-0.5 2.4-3.8-0.1-6.6 1.3 0.9 4.8-2.4 3.8-7.5 4.4-5.3 7.5-3.8 4.1-5 4.2 0.3 2.9-2.6 1.6-4.8 2.3-2.6 0.3-1.2 4.9 1.9 8.4 0.7 5.3-1.9 6.1 0.7 10.9-2.9 0.3-2.3 4.9 1.9 2.2-5.1 1.8-1.7 4.3-2.2 1.9-5.6-6-3.1-9-2.5-6.5-2.2-3-3.4-6.2-2-8-1.4-4-5.9-8.8-3.5-12.5-2.6-8.2-0.8-7.8-1.7-6-7.7 3.9-4-0.8-8.1-7.8 2.4-2.3-1.9-2.5-7.1-5.5 3.2-4.3 12.1 0-1.8-5.5-3.5-3.2-1.4-5-4-2.8 4.9-6.8 6.5 0.5 4.5-6.7 2.2-6.5 3.9-6.5-1-4.6 3.8-3.7-5.1-3.1-2.9-4.4-3.3-5.6 2-2.8 8.5 1.6 5.7-1 3.8-5.4 7.7 7.6 0.8 5.2 3 3.3 0.6 3.3-4.1-0.9 3.2 7.1 6.2 4 8.6 4.5z" id="IN" name="India"> | |
| 256 | + </path> | |
| 257 | + <path d="M956.7 158.2l0.7 4.4-3.9 5.5-8.8 3.6-6.8-0.9 4.3-6.4-2.1-6.2 6.7-4.8 3.7-2.8 0.9 3.2-1.2 3.3 3-0.1 3.5 1.2z" id="IE" name="Ireland"> | |
| 258 | + </path> | |
| 259 | + <path d="M1229 253.2l1.8-0.2 5.3-4.7 1.9-0.5 1.9 1.9-1.2 3.1 3.9 3.4 1.3-0.4 2.5 4.8 5.3 1.3 4.3 3.2 7.7 1.1 8-1.7 0.2-1.5 4.4-1.2 3-3.7 3.6 0.2 2-1.2 3.9 0.6 6.6 3.3 4.3 0.7 7.3 5.6 4 0.3 1.7 5.3-0.6 8-0.5 4.7 2.5 1-1.6 3.5 2.7 5.1 1.2 4.1 4.3 1.1 1.1 4.1-3.9 5.8 3.2 3.4 2.8 3.9 5.7 2.8 1 5.6 2.7 1.1 0.9 2.9-7.5 3.4-1.1 7.4-10.6-1.9-6.2-1.5-6.3-0.8-3.3-7.9-2.8-1.1-4.1 1.1-5.1 3.1-7-2.1-6.1-5-5.5-1.8-4.4-6.1-5.2-8.5-2.8 1-3.7-2.1-1.7 2.5-3.5-3.4-0.5-3.4-1.7 0 0.2-4.7-3.5-4.8-7.1-3.6-4.6-6.1 0.5-5 2.3-2.2-0.9-3.7-3.8-2-4.7-7.6-3.8-5.1 0.7-2-2.9-7.3 3.3-1.9 1.2 2.5 3.2 2.9 3.8 0.9z" id="IR" name="Iran"> | |
| 260 | + </path> | |
| 261 | + <path d="M1223.5 263.2l4.7 7.6 3.8 2 0.9 3.7-2.3 2.2-0.5 5 4.6 6.1 7.1 3.6 3.5 4.8-0.2 4.7 1.7 0 0.5 3.4 3.5 3.4-3.3-0.3-3.7-0.6-3.3 6.2-10.2-0.5-16.8-12.9-8.6-4.5-6.8-1.8-3.1-7.8 11-6.7 1-7.7-1.2-4.7 2.7-1.6 2.1-4 2.1-1 6.3 0.9 2.1 1.6 2.4-1.1z" id="IQ" name="Iraq"> | |
| 262 | + </path> | |
| 263 | + <path d="M924.8 84.5l-1.4 3.6 4.4 3.8-6.1 4.3-13.1 3.9-3.9 1.1-5.6-0.9-11.9-1.8 4.8-2.5-9-2.7 7.9-1.1 0.1-1.7-8.8-1.3 3.6-3.7 6.6-0.8 6 3.8 7-3 5.1 1.5 7.3-2.9 7 0.4z" id="IS" name="Iceland"> | |
| 264 | + </path> | |
| 265 | + <path d="M1179.1 288.2l0.4 2.6-0.6 1 0.1 0-0.7 2-2.1-0.8-0.7 4.2 1.5 0.7-1.3 0.9-0.1 1.7 2.5-0.8 0.4 2.5-1.8 10.2-0.7-1.6-3.9-9.4 1.4-2.1-0.4-0.4 1.1-3 0.6-4.8 0.6-1.7 0.1 0 1.8 0 0.4-1.1 1.4-0.1z" id="IL" name="Israel"> | |
| 266 | + </path> | |
| 267 | + <path class="Italy" d="M 1068.2 256.4 1066.5 261.5 1067.4 263.4 1066.5 266.7 1062.3 264.3 1059.6 263.6 1052.1 260.4 1052.6 257.1 1058.8 257.7 1064.2 257 1068.2 256.4 Z"> | |
| 268 | + </path> | |
| 269 | + <path class="Italy" d="M 1034.2 237.4 1037.5 241.9 1037.1 250.4 1034.7 250 1032.6 252.1 1030.6 250.4 1030.1 242.7 1028.8 239.1 1031.7 239.4 1034.2 237.4 Z"> | |
| 270 | + </path> | |
| 271 | + <path class="Italy" d="M 1055.9 203.9 1055.5 207 1056.9 209.7 1052.8 208.7 1048.9 211 1049.3 214.1 1048.8 215.9 1050.7 219.1 1055.7 222.3 1058.6 227.6 1064.7 232.7 1068.7 232.6 1070.1 234 1068.7 235.3 1073.5 237.6 1077.5 239.5 1082.2 242.9 1082.8 244 1082 246.3 1078.9 243.3 1074.3 242.3 1072.4 246.4 1076.3 248.8 1075.9 252.1 1073.8 252.5 1071.3 258 1069.1 258.5 1069 256.5 1069.9 253.1 1071 251.7 1068.7 248 1066.9 244.8 1064.7 244 1062.9 241.3 1059.5 240.1 1057.1 237.5 1053.3 237.1 1049 234.3 1044.1 230.1 1040.4 226.5 1038.5 220.2 1035.9 219.5 1031.7 217.4 1029.4 218.2 1026.5 221.2 1024.4 221.6 1024.9 218.9 1022.1 218.1 1020.6 213.2 1022.3 211.3 1020.8 208.9 1020.9 207.1 1023.1 208.5 1025.6 208.2 1028.3 206 1029.2 207 1031.6 206.8 1032.6 204.3 1036.4 205.1 1038.5 204 1038.8 201.5 1041.9 202.4 1042.4 201.2 1047.3 200.1 1048.6 202.2 1055.9 203.9 Z"> | |
| 272 | + </path> | |
| 273 | + <path d="M556.5 387.1l-1.8 1.1-3-1.1-2.9-2.3 0.8-1.5 2.4-0.4 1.3 0.2 3.7 0.6 2.7 1.5 0.8 1.8-4 0.1z" id="JM" name="Jamaica"> | |
| 274 | + </path> | |
| 275 | + <path d="M1198.1 295.3l-0.9 1-10.4 3.2 6 6.5-1.6 1-0.7 2.2-4.1 0.9-1.1 2.3-2.1 2-6.2-1.1-0.3-0.9 1.8-10.2-0.4-2.5 0.6-1.9-0.4-4 0.7-2 6.3 2.6 9.7-6.9 3.1 7.8z" id="JO" name="Jordan"> | |
| 276 | + </path> | |
| 277 | + <path class="Japan" d="M 1708.5 282.6 1710.1 284.8 1708.8 288.7 1705.7 286.6 1703.6 288.1 1703.7 291.8 1699.4 290 1698.2 287 1699.5 283.1 1702.9 283.9 1703.9 281.2 1708.5 282.6 Z"> | |
| 278 | + </path> | |
| 279 | + <path class="Japan" d="M 1733.1 263.4 1733.7 268.5 1736.2 271.7 1735.6 276.2 1730.2 279.2 1721 279.6 1716.6 287 1711.9 284.5 1709.5 279.7 1700.9 281.1 1695.8 284.1 1689.6 284.3 1697 289 1697.8 299.9 1695.3 302.6 1691.7 300.1 1690.8 294.3 1686.7 292.5 1682.7 288.1 1687 286.1 1688.1 282 1692 278.7 1694 274.3 1703.7 272.4 1710 273.7 1710 262.3 1715.1 265.4 1719.6 259 1721.3 256.5 1720.3 248.7 1715.2 241.5 1715 237.5 1719.8 236.3 1728 245.2 1730.8 250.3 1729.5 256.8 1733.1 263.4 Z"> | |
| 280 | + </path> | |
| 281 | + <path class="Japan" d="M 1721.2 218.6 1725.7 219.9 1727.5 217.3 1733.5 224.4 1727.1 226.1 1726.7 232.4 1715.8 228.1 1717.4 235 1711.7 235.1 1707 228.8 1706.4 223.9 1711.6 223.6 1707.2 214.8 1705.4 209.9 1715.9 216.5 1721.2 218.6 Z"> | |
| 282 | + </path> | |
| 283 | + <path d="M1338.3 160.5l4.4-0.3 9.2-5.8-0.8 2 8.4 4.7 18.3 15.6 1.1-3.2 8.4 3.5 6.2-1.6 3.3 1.1 4.1 3.6 4 1.2 3.3 2.7 6-0.9 4.4 3.8-1.9 4.2-3.8 0.6 2.5 6.2-1.6 2.9-10.8-2.1 1 11.3-2 1.4-9.1 2.5 8.8 11-2.9 1.6 1.7 3.7-3.5-1-3.4-2.3-7.9-0.6-8.6-0.2-1.6 0.7-8.2-2.7-2.5 1.4 0.5 3.7-9.2-2.2-3.1 0.9-0.3 2.8-2.6 1.2-5.4 4.4-0.9 4.6-2 0-2.3-3-6.7-0.2-2.5-5.2-2.6-0.1-1.5-6.4-7.6-4.6-8.6 0.5-5.7 0.9-6.6-5.7-4.8-2.4-9.2-4.5-1.1-0.5-12 3.7 6.2 23.4-2.6 0.3-4.8-5-3.9-1.8-5.6 1.3-1.8 2.2-0.6-1.6 0.6-2.6-1.5-2.2-6.5-2.2-3.7-5.7-3.2-1.6-0.6-2.1 5.1 0.6-1-4.6 4.1-1 4.7 0.9-0.7-6.1-1.9-3.9-5 0.3-4.7-1.5-5.1 2.7-4.4 1.4-2.8-1.1-0.2-3.2-4.3-4.2-3.6 0.2-5.3-4.2 1.7-4.8-1.8-1.2 2.2-6.9 6 3.6-0.6-4.5 8.1-6.7 7.6-0.2 12 4.3 6.6 2.5 4.4-2.6 7.7-0.1 7.3 3.2 0.8-1.9 7 0.3 0.2-2.9-9.4-4.3 3.5-3-1.5-1.6 4-1.6-5.1-4.2 1.4-2.1 17-2.1 1.7-1.5 10.9-2.3 3.1-2.5 9.1 1.3 4.4 6.3 4.3-1.5 7.1 2.1 1.1 3.3z" id="KZ" name="Kazakhstan"> | |
| 284 | + </path> | |
| 285 | + <path d="M1223.5 476.7l-4.9 7.2 0.2 23.4 3.3 5.3-4 2.6-1.4 2.7-2.2 0.4-0.8 4.6-1.9 2.6-1.1 4.2-2.3 2.1-8.1-6.4-0.3-3.7-20.5-13.1 0.4-4.7-1.4-2.5 0-0.3 1.6-2.6 2.8-4.2 2.1-4.7-2.6-7.4-0.7-3.2-2.7-4.5 3.4-3.8 3.8-4.2 2.9 1.1 0.1 3.6 1.9 2.1 4 0 7.2 5.4 1.8 0.1 1.3-0.2 1.3 0.7 3.8 0.6 1.6-2.7 5.2-2.7 2.3 2.2 3.9 0z" id="KE" name="Kenya"> | |
| 286 | + </path> | |
| 287 | + <path d="M1400.5 230.2l-0.2 1.4-6.9 3.4-1 2.6-6.4 0.7-0.6 4.1-5.8-0.9-3.2 1.3-4.1 3 1.2 1.5-1.1 1.4-9.6 1-7.1-2.1-5.5 0.5-0.6-3.6 6 1 1.4-1.9 4.1 0.6 5.3-4.6-7.2-3.4-3.2 1.6-4.6-2.4 3-4.1-1.7-0.6 0.3-2.8 3.1-0.9 9.2 2.2-0.5-3.7 2.5-1.4 8.2 2.7 1.6-0.7 8.6 0.2 7.9 0.6 3.4 2.3 3.5 1z" id="KG" name="Kyrgyzstan"> | |
| 288 | + </path> | |
| 289 | + <path d="M1589.8 410.6l1.8 4.3 0.1 7.7-9 5 2.8 3.8-5.9 0.5-4.6 2.6-4.8-0.9-2.6-3.4-3.5-6.6-2.1-7.8 3.1-5.3 7.1-1.2 5.3 0.9 5 2.5 2-4.4 5.3 2.3z" id="KH" name="Cambodia"> | |
| 290 | + </path> | |
| 291 | + <path d="M1652.9 259.5l0-0.6 2.5 0.2 0.6-2.8 3.6-0.4 2-0.4 0-1.5 8.3 7.5 3.3 4.2 3.4 7.4-0.5 3.5-4.3 1.2-3.1 2.7-4.6 0.5-2.1-3.5-1.1-4.8-5.3-6.6 3.4-1.1-6.1-5.5z" id="KR" name="Republic of Korea"> | |
| 292 | + </path> | |
| 293 | + <path d="M1247.5 309.4l1.5 2.8-0.3 1.5 2.4 4.8-3.9 0.2-1.7-3.1-5-0.6 3.3-6.2 3.7 0.6z" id="KW" name="Kuwait"> | |
| 294 | + </path> | |
| 295 | + <path d="M1589.8 410.6l-5.3-2.3-2 4.4-5-2.5 1.5-2.9-0.4-5.4-5.3-5.6-1.3-6.4-5-5.2-4.3-0.4-0.8 2.2-3.2 0.2-1.9-1.1-5.3 3.8-1-5.8 0.4-6.7-3.8-0.3-0.9-3.9-2.7-2 0.8-2.3 4.1-4.2 0.8 1.5 3 0.2-2-7.4 2.7-0.9 4 5.1 3.5 5.8 6.8 0 3 5.6-3.3 1.7-1.2 2.3 7.3 3.9 5.7 7.6 4.4 5.6 4.9 4.5 2 4.5-0.2 6.4z" id="LA" name="Lao PDR"> | |
| 296 | + </path> | |
| 297 | + <path d="M1179.1 288.2l-1.4 0.1-0.4 1.1-1.8 0 1.3-5.3 2.2-4.5 0-0.2 2.5 0.3 1.2 2.5-2.7 2.5-0.9 3.5z" id="LB" name="Lebanon"> | |
| 298 | + </path> | |
| 299 | + <path d="M938.6 452.5l-0.2 1.8 0.5 3.1-1.2 2.9 1.6 1.8 1.8 0.4 2.4 2.7 0.2 2.5-0.6 0.8-0.4 5.3-1.5 0.1-5.8-3.1-5.2-4.9-4.8-3.5-3.8-4.1 1.4-2.1 0.3-1.9 2.6-3.4 2.6-3 1.3-0.2 1.4-0.7 2.4 3.9-0.4 2.6 1.1 1.4 1.6 0 1.1-2.6 1.6 0.2z" id="LR" name="Liberia"> | |
| 300 | + </path> | |
| 301 | + <path d="M1122.6 299.1l-1.7 3.1 1 2.8-1.1 3.9 2 5.2 1.3 22.8 1 23.7 0.5 12.8-6.4 0 0 2.7-22.6-12.3-22.5-12.3-5.5 3.5-3.8 2.4-3.2-3.5-8.8-2.8-2.5-4-4.5-3-2.5 1.2-2.1-3.6-0.2-2.7-3.4-4.7 2.2-2.7-0.6-4 0.6-3.5-0.4-3 0.8-5.2-0.4-3-1.9-5.7 2.6-1.4 0.4-2.8-0.6-2.6 3.6-2.5 1.6-2.1 2.6-1.8 0.1-4.9 6.4 2.2 2.3-0.6 4.5 1.1 7.3 2.9 2.8 5.7 4.9 1.2 7.8 2.7 6 3.2 2.5-1.7 2.5-2.9-1.6-4.9 1.5-3.2 3.7-3 3.7-0.8 7.4 1.3 2 2.8 2 0.1 1.8 1.1 5.4 0.7 1.5 2.1z" id="LY" name="Libya"> | |
| 302 | + </path> | |
| 303 | + <path d="M1445.9 462l-4.8 1.5-2.9-5.1-1.4-9.2 2-10.4 4.1 3.5 2.8 4.5 3.1 6.7-0.6 6.7-2.3 1.8z" id="LK" name="Sri Lanka"> | |
| 304 | + </path> | |
| 305 | + <path d="M1139.1 697.9l-2 0.7-3.7-5 3.2-4 3.1-2.5 2.7-1.4 2.2 2 1.7 2-1.9 3.1-1.1 2.1-3.1 1-1.1 2z" id="LS" name="Lesotho"> | |
| 306 | + </path> | |
| 307 | + <path d="M1111.1 147.6l1 2.7-3.6 2-0.5 3.4-4.8 2.3-4.7 0-1.4-1.9-2.5-0.7-0.6-1.5 0.2-1.7-2.2-0.9-5.1-1.1-1.7-5.1 5.1-1.8 7.9 0.4 4.5-0.6 0.9 1.2 2.5 0.4 5 2.9z" id="LT" name="Lithuania"> | |
| 308 | + </path> | |
| 309 | + <path d="M1016.9 185.4l-1.4 0.1-1.1-0.5 0.4-3.5 1.3-0.2 1 1.4-0.2 2.7z" id="LU" name="Luxembourg"> | |
| 310 | + </path> | |
| 311 | + <path d="M1112.8 136.5l2.5 1.3 1 2.9 2.1 3.6-4.6 2.3-2.7 1-5-2.9-2.5-0.4-0.9-1.2-4.5 0.6-7.9-0.4-5.1 1.8-0.5-4.5 1.7-3.8 4.1-2 4.4 4.5 3.7-0.2 0.1-4.6 3.8-1 2.1 0.7 4.4 2.2 3.8 0.1z" id="LV" name="Latvia"> | |
| 312 | + </path> | |
| 313 | + <path d="M974.8 276l1.9 4.1 0.3 3.9 1.9 6.8 1.4 1.4-1 2.5-7.1 1.1-2.5 2.3-3.1 0.6-0.3 4.8-6.4 2.5-2.1 3.2-4.5 1.7-5.4 1-8.9 4.8-0.1 7.6-0.9 0 0.1 3.4-3.4 0.2-1.8 1.5-2.5 0-2-0.9-4.6 0.7-1.9 5-1.8 0.5-2.7 8.1-7.9 6.9-2 8.9-2.4 2.9-0.7 2.3-12.5 0.5-0.1 0 0.3-3 2.2-1.7 1.9-3.4-0.3-2.2 2-4.5 3.2-4.1 1.9-1 1.6-3.7 0.2-3.5 2.1-3.9 3.8-2.4 3.6-6.5 0.1-0.1 2.9-2.5 5.1-0.7 4.4-4.4 2.8-1.7 4.7-5.4-1.2-7.9 2.2-5.6 0.9-3.4 3.6-4.3 5.4-2.9 4.1-2.7 3.7-6.6 1.8-4 3.9 0.1 3.1 2.7 5.1-0.4 5.5 1.4 2.4 0z" id="MA" name="Morocco"> | |
| 314 | + </path> | |
| 315 | + <path d="M1129.4 210.3l-1.3-2.9 0.2-2.7-0.6-2.7-3.4-3.8-2-2.6-1.8-1.8-1.6-0.7 1.1-0.9 3.2-0.6 4 1.9 2 0.3 2.6 1.7-0.1 2.1 2 1 1.1 2.6 2 1.6-0.2 1 1 0.6-1.3 0.5-3-0.2-0.6-0.9-1 0.5 0.6 1.1-1.1 2.1-0.6 2.1-1.2 0.7z" id="MD" name="Moldova"> | |
| 316 | + </path> | |
| 317 | + <path d="M1267.9 588.9l0.4 7.7 1.3 3-0.7 3.1-1.2 1.8-1.6-3.7-1.2 1.9 0.8 4.7-0.7 2.8-1.7 1.4-0.7 5.5-2.7 7.5-3.4 8.8-4.3 12.2-2.9 8.9-3.1 7.5-4.6 1.5-5.1 2.7-3-1.6-4.2-2.3-1.2-3.4 0-5.7-1.5-5.1-0.2-4.7 1.3-4.6 2.6-1.1 0.2-2.1 2.9-4.9 0.8-4.1-1.1-3-0.8-4.1-0.1-5.9 2.2-3.6 1-4.1 2.8-0.2 3.2-1.3 2.2-1.2 2.4-0.1 3.4-3.6 4.9-4 1.8-3.2-0.6-2.8 2.4 0.8 3.3-4.4 0.3-3.9 2-2.9 1.8 2.8 1.4 2.7 1.2 4.3z" id="MG" name="Madagascar"> | |
| 318 | + </path> | |
| 319 | + <path d="M449.3 335.9l2.2-0.2-3.2 5.7-1.8 4.6-1.8 8.6-1.1 3.1 0.4 3.5 1.3 3.2 0.4 4.9 3 4.8 0.8 3.7 1.7 3.1 5.7 1.7 1.9 2.7 5.2-1.8 4.3-0.6 4.4-1.2 3.6-1.1 3.9-2.6 1.8-3.7 1.2-5.4 1.2-1.9 4-1.7 6.1-1.5 4.9 0.3 3.4-0.6 1.2 1.4-0.6 3.1-3.5 3.8-1.8 3.9 0.9 1.1-1.2 2.8-2.1 5-1.2-1.7-1.1 0.1-1.1 0.1-2.5 3.9-0.9-0.8-0.7 0.3-0.1 1-5.2-0.1-5.2 0-0.5 3.6-2.5 0.1 1.8 2.1 1.9 1.5 0.5 1.4 0.8 0.4-0.4 2.2-7.2 0-3.3 5.2 0.7 1.2-0.8 1.5-0.4 1.9-5.6-6.9-2.6-2.1-4.4-1.7-3.2 0.5-4.8 2.4-2.9 0.6-3.7-1.7-4.1-1.2-4.8-2.9-4.1-0.9-5.9-3-4.3-3.1-1.1-1.7-3.1-0.4-5.4-2-1.9-2.9-5.4-3.7-2.2-4-0.8-3.2 1.9-0.6-0.3-1.8 1.6-1.7 0.4-2.2-1.5-2.9 0-2.5-1.3-3.3-3.8-6.4-4.6-5-1.9-4-4.1-2.6-0.7-1.6 1.7-3.9-2.4-1.5-2.5-3.2-0.2-4.4-2.8-0.6-2.3-3.3-1.7-3.2 0.3-2-1.5-4.8-0.3-4.9 0.8-2.5-3.1-2.6-1.9 0.3-2.4-1.7-1.8 2.6-0.1 3-1 4.9 1 2.6 2.8 4.4 0.4 1.6 0.7 0.4 0.1 2.2 1-0.1 0 4.2 1.3 1.6 0.5 2.3 2.7 3.2 0.4 6 1 2.8 0.9 3-0.3 3.4 2.6 0.2 1.6 2.9 1.5 2.9-0.3 1.2-2.8 2.3-1 0-0.7-3.9-2.9-3.7-3.4-3.1-2.5-1.6 1.2-4.7-0.1-3.5-2.1-2-3.1-2.8-0.9 0.8-1-1.7-3-1.5-2.2-3.8 0.5-0.4 2.1 0.3 2.7-2.4 1-2.9-2.9-4.6-2.6-1.7-0.8-4-0.6-4.3-0.8-5.1-0.2-5.8 6.3-0.5 7.1-0.7-0.9 1.3 7 3.1 10.9 4.5 10.8 0 4.3 0 0.8-2.7 9.4 0 1.3 2.3 2.1 2.1 2.4 2.8 0.8 3.3 0.4 3.6 2.3 1.9 4 1.9 4.8-5 4.5-0.2 3.2 2.6 1.6 4.4 0.9 3.8 2.4 3.6 0.2 4.5 0.9 3 3.9 2 3.6 1.4z" id="MX" name="Mexico"> | |
| 320 | + </path> | |
| 321 | + <path d="M1105.5 236.6l-1 0.2-0.8 1.1-2.8-0.1-1.8 1.4-3.4 0.5-2.3-1.5-1-2.7 0.5-2.2 0.7 0.1 0.1-1.3 2.9-1 1.2-0.3 1.7-0.3 2.4-0.2 2.8 2.1 0.8 4.2z" id="MK" name="Macedonia"> | |
| 322 | + </path> | |
| 323 | + <path d="M1010.2 378.8l0.1 14.8-3.1 4.3-0.4 4-5 1-7.7 0.5-2 2.3-3.6 0.3-3.6 0-1.4-1.2-3.1 0.9-5.3 2.7-1.1 2-4.3 2.8-0.8 1.7-2.4 1.3-2.7-0.9-1.5 1.6-0.9 4.4-4.5 5.3 0.2 2.2-1.6 2.7 0.4 3.7-2.4 1-1.3 0.8-0.9-2.7-1.6 0.7-1-0.1-1 1.8-4.4 0-1.5-1-0.8 0.6-1.7-1.9 0.3-1.9-0.7-0.7-1.2 0.6 0.3-2.1 1.1-1.6-2.3-2.7-0.6-1.8-1.3-1.4-1.1-0.2-1.3 0.9-1.9 0.9-1.5 1.4-2.4-0.5-1.6-1.7-0.9-0.2-1.5 0.9-0.9 0-0.3-2.4 0.3-2-0.5-2.4-2-1.8-1.1-3.7-0.2-4 1.9-1.2 1-3.8 1.8-0.1 3.9 1.8 3.2-1.3 2.1 0.4 0.9-1.4 22.5-0.1 1.3-4.5-1-0.8-2.5-27.7-2.4-27.7 8.5-0.1 18.6 14 18.7 14 1.3 3 3.5 1.8 2.6 1.1 0.1 4.1 6.2-0.7z" id="ML" name="Mali"> | |
| 324 | + </path> | |
| 325 | + <path d="M1548.4 364.2l-4.1 4.2-0.8 2.3-3 1.5-2.8 2.8-3.9 0.3-1.5 6.9-2.2 1.2 3.5 5.6 4.1 4.7 2.9 4.3-1.4 5.5-1.8 1.2 1.8 3.2 4.3 5.1 1 3.6 0.2 3 2.7 5.9-2.6 6-2.2 6.6-0.9-4.8 1.3-4.9-2.2-3.8-0.2-7-2.6-3.4-2.7-7.6-2-8.1-3.1-5.4-3.2 3.3-5.8 4.5-3.3-0.5-3.6-1.5 0.9-8-2-6-5.3-7.4 0.3-2.3-3.4-0.9-4.6-5.2-1.1-5.2 2.1 1-0.6-4.6 2.5-1.5-1-2.7 1-2.2-0.9-6.7 4.6 1.5 1.6-5.3-0.3-3.1 2-5.4-0.9-3.7 6.2-4.4 4.2 1.1-1.4-3.9 1.7-1.2-1-2.4 3.1-0.5 2.7 3.8 2.7 1.5 1.3 4.9 0.9 5.3-4.2 5.4 0.7 7.6 5.6-1.1 2.4 5.9 3.7 1.3-0.8 5.3 4.5 2.4 2.6 1.2 3.8-1.9 0.5 2.7z" id="MM" name="Myanmar"> | |
| 326 | + </path> | |
| 327 | + <path d="M1090.6 227.2l-0.8 1.4-1.4 0.6-0.4-1.2-1.9 3.1 0.5 2.1-1.1-0.5-1.7-2.1-2.3-1.3 0.5-1 0.4-3.5 1.6-1.5 0.9-0.6 1.4 1.1 0.9 0.9 1.7 0.7 2.1 1.3-0.4 0.5z" id="ME" name="Montenegro"> | |
| 328 | + </path> | |
| 329 | + <path d="M1496.2 181.5l4-1.2 5.7-0.8 5.4 0.9 6.6 2.9 4.9 3.2 4.6 0 6.8 1 3.6-1.6 5.9-1 4.4-4.4 3.4 0.7 3.9 2.1 5.6-0.6 0.6 4.7 0.3 6.3 2.8 2.5 2.3-0.8 5.5 1 2.5-2.3 5.2 2 7.2 4.4 0.8 2.2-4.4-0.7-6.8 0.8-2.5 1.8-1.3 4.2-6.4 2.4-3.2 3.4-5.9-1.3-3.2-0.6-0.5 4.1 2.9 2.4 1.9 2.1-2.4 2.2-2 3.4-4.9 2.2-7.6 0.2-7.2 2.2-4.4 3.4-3.3-1.9-6.2 0-9.4-3.9-5.6-0.9-6.4 0.9-11.3-1.5-5.6 0.2-4.7-3.8-5-5.8-3.4-0.7-8-4-7.2-0.8-6.5-1.1-3-2.8-1.4-7.3-5.8-5.1-8.2-2.3-5.7-3.3-3.3-4.4 4.7-1.1 6.7-5.3 5.9-2.9 5.3 1.9 5.2 0.1 4.8 2.9 5 0.2 8 1.6 2.4-4.4-4-3.6 1.3-6.4 7 2.5 4.8 0.8 6.7 1.6 3.6 4.6 8.5 2.6z" id="MN" name="Mongolia"> | |
| 330 | + </path> | |
| 331 | + <path d="M1166.7 673.5l-4.1 0-0.3-2.9-0.6-2.9-0.4-2.3 1.4-7.1-1.1-4.6-2.2-9 6.2-7.3 1.7-4.6 0.8-0.6 0.9-3.8-0.8-1.9 0.4-4.8 1.3-4.4 0.4-8.2-2.8-2-2.7-0.5-1.1-1.6-2.6-1.3-4.7 0.1-0.2-2.4-0.4-4.6 17.2-5.3 3.2 3.1 1.5-0.6 2.2 1.6 0.2 2.6-1.3 3 0.2 4.5 3.5 4 1.9-4.5 2.5-1.3-0.1-8.3-2.2-4.6-1.9-2.1-0.4 0-0.6-7.3 1.5-6.1 2.2-0.2 6.7 1.8 1.5-0.8 3.9-0.2 2.1-1.9 3.4 0.1 6.2-2.5 4.6-3.7 0.9 2.8-0.5 6.4 0.5 5.7-0.2 10 0.8 3.1-1.9 4.6-2.4 4.5-3.7 4-5.3 2.4-6.5 3.1-6.6 6.9-2.2 1.2-4.2 4.6-2.3 1.4-0.8 4.6 2.4 4.9 0.9 3.7 0 2 1-0.4-0.5 6.3-1.1 3 1.2 1.1-1 2.7-2.4 2.3-4.7 2.1-6.9 3.5-2.5 2.4 0.3 2.7 1.3 0.4-0.7 3.4z" id="MZ" name="Mozambique"> | |
| 332 | + </path> | |
| 333 | + <path d="M959.2 341.5l-8.5 0.1 2.4 27.7 2.5 27.7 1 0.8-1.3 4.5-22.5 0.1-0.9 1.4-2.1-0.4-3.2 1.3-3.9-1.8-1.8 0.1-1 3.8-1.9 1.2-3.6-4.4-3.4-4.8-3.6-1.7-2.7-1.8-3.1 0-2.8 1.4-2.7-0.5-2 2-0.4-3.4 1.6-3.2 0.8-6-0.4-6.4-0.6-3.2 0.6-3.2-1.4-3-2.8-2.8 1.3-2.1 21.7 0-0.9-9.3 1.5-3.3 5.2-0.5 0.2-16.5 18 0.4 0.2-9.8 20.5 15.6z" id="MR" name="Mauritania"> | |
| 334 | + </path> | |
| 335 | + <path d="M1182.3 588.9l0.4 0 1.9 2.1 2.2 4.6 0.1 8.3-2.5 1.3-1.9 4.5-3.5-4-0.2-4.5 1.3-3-0.2-2.6-2.2-1.6-1.5 0.6-3.2-3.1-2.9-1.6 2-6 1.8-2.2-0.9-5.4 1.3-5.2 1-1.7-1.3-5.4-2.6-2.9 5.5 1.2 1 1.7-0.1 0.8 1.8 4.1 0.2 7.7-1.8 3.6 1.6 4.7-0.2 2.8 1.2 1.9-0.1 2.4 0.9 1.4 1-1.6 1.9 2.5 0.2-0.8-1-3.4-1.1-0.3-0.1-0.9z" id="MW" name="Malawi"> | |
| 336 | + </path> | |
| 337 | + <path class="Malaysia" d="M 1564.3 461.9 1565.7 462.5 1569.2 466.4 1571.7 470.7 1572.3 475 1571.8 477.9 1572.4 480.1 1572.9 483.9 1575 485.7 1577.3 491.4 1577.3 493.5 1573.3 494 1567.8 489.2 1561 484.1 1560.2 480.8 1556.8 476.5 1555.8 471.2 1553.6 467.7 1554 463 1552.6 460.3 1553.5 459.2 1558.3 462 1558.9 465.3 1562.6 464.5 1564.3 461.9 Z"> | |
| 338 | + </path> | |
| 339 | + <path class="Malaysia" d="M 1654.1 475.3 1649.1 474.2 1642.6 474.2 1640.9 481.5 1638.8 483.7 1636.2 492.6 1631.6 494 1626.2 492.2 1623.5 492.8 1620.2 496 1616.6 495.6 1612.9 496.9 1609 493.2 1608 488.9 1612.2 491.1 1616.5 489.9 1617.5 484.5 1619.9 483.3 1626.7 481.9 1630.5 476.8 1633.1 472.8 1635.9 476.1 1637 473.9 1639.7 474.1 1639.8 470 1639.9 466.8 1644 462.4 1646.6 457.4 1648.9 457.3 1652 460.6 1652.4 463.4 1656.2 465.2 1661 467.1 1660.8 469.6 1657 469.9 1658.1 473.1 1654.1 475.3 Z"> | |
| 340 | + </path> | |
| 341 | + <path d="M1116.2 614.3l4.6-1.4 3.6 0.3 2.2 1.5 0 0.5-3.2 1.5-1.7 0-3.7 2.5-2-2.7-8.7 2.3-4.2 0.2-0.8 22.9-5.5 0.2-0.6 18.7-1.1 23.7-5 3.3-2.9 0.5-3.4-1.2-2.5-0.5-0.8-2.7-2-1.8-2.8 3.2-3.9-4.9-2-4.6-1-6.3-1.2-4.6-1.6-9.9 0.1-7.7-0.6-3.5-2.1-2.7-2.8-5.3-2.8-7.7-1.1-4-4.4-6.3-0.3-4.9 2.7-1.2 3.4-1.1 3.6 0.2 3.3 2.9 0.8-0.5 22.7-0.2 3.8 3 13.5 0.9 10.4-2.6z" id="NA" name="Namibia"> | |
| 342 | + </path> | |
| 343 | + <path d="M1068.6 355l1.6 10 2.2 1.7 0.1 2 2.4 2.2-1.2 2.8-1.8 13-0.2 8.4-7 6-2.3 8.5 2.4 2.4 0 4.1 3.7 0.1-0.6 3.1-1.5 0.3-0.2 2.1-1 0.1-3.9-7-1.4-0.3-4.3 3.6-4.4-1.9-3-0.3-1.6 0.9-3.3-0.2-3.3 2.7-2.9 0.2-6.8-3.3-2.7 1.5-2.9-0.1-2.1-2.4-5.6-2.4-6.1 0.8-1.4 1.3-0.8 3.7-1.6 2.6-0.4 5.8-4.3-3.7-2 0-1.9 1.9 0.1-4.4-6.5-1.5-0.2-3.1-3.1-4.2-0.8-2.9 0.5-3.1 3.6-0.3 2-2.3 7.7-0.5 5-1 0.4-4 3.1-4.3-0.1-14.8 7.8-2.8 16-12.6 18.8-12.3 8.8 2.8 3.2 3.5 3.8-2.4z" id="NE" name="Niger"> | |
| 344 | + </path> | |
| 345 | + <path d="M1066.2 421.7l2.3 2.5-0.6 1.2-0.3 2.1-4.7 5-1.4 4.1-0.8 3.3-1.2 1.5-1.1 4.5-3 2.6-0.8 3.2-1.3 2.6-0.5 2.7-3.9 2.2-3.2-2.7-2.1 0.1-3.4 3.8-1.6 0-2.7 6.2-1.4 4.6-5.9 2.3-2.1-0.3-2.2 1.4-4.5-0.1-3.1-4.1-1.9-4.6-4-4.2-4.2 0-5 0 0.3-10.3-0.2-4.1 1.1-4 1.7-2 2.8-4-0.6-1.7 1.1-2.6-1.3-3.8 0.2-2.1 0.4-5.8 1.6-2.6 0.8-3.7 1.4-1.3 6.1-0.8 5.6 2.4 2.1 2.4 2.9 0.1 2.7-1.5 6.8 3.3 2.9-0.2 3.3-2.7 3.3 0.2 1.6-0.9 3 0.3 4.4 1.9 4.3-3.6 1.4 0.3 3.9 7 1-0.1z" id="NG" name="Nigeria"> | |
| 346 | + </path> | |
| 347 | + <path d="M519.6 405.5l-0.5 0.7-0.5 1.4 0.4 2.3-1.5 2.2-0.8 2.6-0.5 2.8 0.2 1.7-0.1 2.9-0.9 0.6-0.7 2.8 0.2 1.7-1.2 1.6 0.1 1.7 0.8 1.1-1.4 1.4-1.7-0.5-0.8-1.3-1.8-0.5-1.3 0.8-3.6-1.7-0.9 0.8-1.8-2-2.5-2.6-1.1-2.1-2.2-2.1-2.5-2.9 0.7-1 0.8 1 0.5-0.4 1.8-0.3 0.8-1.5 0.8 0 0.2-3.2 1.3-0.1 1.2 0 1.4-1.7 1.5 1.3 0.6-0.8 1.2-0.8 2.1-1.7 0.2-1.4 0.5 0.1 0.9-1.5 0.6-0.2 0.9 1 1.1 0.2 1.3-0.8 1.4 0 2-0.8 0.9-0.9 1.9 0.1z" id="NI" name="Nicaragua"> | |
| 348 | + </path> | |
| 349 | + <path class="Norway" d="M 1113.7 67.5 1107.3 69.6 1104.1 70.1 1104.9 66.3 1099.1 64.2 1093.2 66 1092.1 70 1088.7 72.4 1084 71.1 1078.7 71.3 1073.6 68.5 1071.4 69.9 1068.8 70.1 1068.9 73.7 1060.9 72.8 1060.3 75.9 1056.3 75.9 1054 79.8 1050.6 85.9 1044.9 93.8 1046.7 95.8 1045.4 98 1041.1 97.9 1038.7 103.3 1039.7 111 1042.8 113.9 1042 120.8 1038.6 124.8 1036.8 128.2 1033.5 124.6 1024.9 131.4 1018.8 132.8 1012.3 129.8 1010.5 123.5 1008.5 110 1012.5 106.3 1023.8 101.4 1031.9 95.5 1039.1 87.7 1048 77 1054.4 72.9 1064.7 66.1 1073.2 63.7 1079.9 64 1085.1 59.6 1092.5 59.8 1099.5 58.8 1113.2 62.7 1108.3 64.1 1113.7 67.5 Z"> | |
| 350 | + </path> | |
| 351 | + <path class="Norway" d="M 1076.6 25.2 1069 27.1 1062.2 26 1064.4 24.8 1061.8 23.3 1069.1 22.4 1071 24.1 1076.6 25.2 Z"> | |
| 352 | + </path> | |
| 353 | + <path class="Norway" d="M 1051 16.7 1063.6 20.1 1055 21.9 1053.8 25.3 1050.8 26.2 1049.9 30.2 1045.5 30.4 1037 27.5 1040 25.8 1034.3 24.4 1026.6 20.5 1023.4 17 1032.7 15.4 1035 16.9 1040 16.9 1041 15.4 1046.2 15.2 1051 16.7 Z"> | |
| 354 | + </path> | |
| 355 | + <path class="Norway" d="M 1075.4 13.7 1082.8 15.2 1078.4 17.6 1068.3 18.1 1057.6 17.3 1056.6 16.1 1051.5 16.1 1047.2 14.1 1057.7 12.9 1063.1 13.9 1066.2 12.6 1075.4 13.7 Z"> | |
| 356 | + </path> | |
| 357 | + <path d="M1469 322.9l0.2 2.7 1.5 4.1-0.1 2.5-4.6 0.1-6.9-1.5-4.3-0.6-3.8-3.2-7.7-0.9-7.8-3.6-5.8-3.1-5.8-2.4 0.9-6 2.8-3 1.9-1.5 4.8 2 6.4 4.2 3.3 0.9 2.5 3.1 4.5 1.2 5 2.9 6.5 1.4 6.5 0.7z" id="NP" name="Nepal"> | |
| 358 | + </path> | |
| 359 | + <path class="Oman" d="M 1283.8 394.9 1281.6 390.4 1276.4 379.8 1292.7 373.4 1295.3 360.6 1292.3 356 1292.3 353.4 1293.6 350.8 1293.4 348.2 1295.8 346.9 1294.7 346 1294.7 341.8 1297.5 341.8 1300.5 346.2 1303.8 348.5 1307.9 349.4 1311.3 350.5 1314.2 354.2 1315.9 356.3 1317.9 357.2 1318.1 358.6 1316.4 362.4 1315.7 364.2 1313.5 366.3 1311.8 370.7 1309.3 370.3 1308.3 371.9 1307.6 375.1 1308.7 379.4 1308.2 380.2 1305.7 380.2 1302.4 382.6 1302.1 385.7 1300.9 387.1 1297.4 387 1295.4 388.6 1295.6 391.2 1293 393 1289.9 392.4 1286.3 394.6 1283.8 394.9 Z"> | |
| 360 | + </path> | |
| 361 | + <path class="Oman" d="M 1296.2 336.7 1294.9 334.5 1296.3 332.4 1297 332.9 1296.8 335.6 1296.2 336.7 Z"> | |
| 362 | + </path> | |
| 363 | + <path d="M1401.6 273.9l-3.8 5.4-5.7 1-8.5-1.6-2 2.8 3.3 5.6 2.9 4.4 5.1 3.1-3.8 3.7 1 4.6-3.9 6.5-2.2 6.5-4.5 6.7-6.5-0.5-4.9 6.8 4 2.8 1.4 5 3.5 3.2 1.8 5.5-12.1 0-3.2 4.3-4.2-1.6-2.2-4.6-4.9-4.9-10 1.2-9 0.1-7.6 0.9 1.1-7.4 7.5-3.4-0.9-2.9-2.7-1.1-1-5.6-5.7-2.8-2.8-3.9-3.2-3.4 9.6 3.3 5.3-1 3.4 0.8 0.9-1.4 3.9 0.6 6.6-2.7-0.8-5.4 2.3-3.7 4.1 0 0.2-1.7 4-0.9 2.1 0.6 1.8-1.8-1.1-3.9 1.4-3.8 3.1-1.7-3.1-4.2 5.2 0.2 0.9-2.3-0.8-2.5 2-2.7-1.4-3.2-1.9-2.7 2.4-2.8 5.3-1.3 5.9-0.8 2.4-1.1 2.9-0.8 4.7 3 2.9 5 9.5 2.5z" id="PK" name="Pakistan"> | |
| 364 | + </path> | |
| 365 | + <path d="M549.3 446.2l-0.7 0.9 1.1 3.8-1.1 1.9-1.8-0.5-0.9 3.1-1.8-1.8-1-3.5 1.4-1.7-1.4-0.4-0.9-2.1-2.8-1.8-2.4 0.4-1.3 2.2-2.4 1.6-1.2 0.2-0.6 1.4 2.5 3.5-1.6 0.8-0.8 0.9-2.7 0.4-0.8-3.9-0.8 1.1-1.8-0.4-1-2.5-2.3-0.5-1.5-0.7-2.4 0-0.2 1.4-0.6-1 0.3-1.3 0.6-1.3-0.2-1.1 0.9-0.8-1.1-0.9 0.1-2.6 2.2-0.6 1.9 2.3-0.2 1.4 2.2 0.3 0.6-0.6 1.5 1.6 2.8-0.5 2.5-1.6 3.5-1.3 2-1.9 3.1 0.4-0.2 0.6 3.1 0.2 2.4 1.2 1.8 1.9 2 1.8z" id="PA" name="Panama"> | |
| 366 | + </path> | |
| 367 | + <path d="M590.5 529.4l-5.1-0.3-0.8 1-4.6 1.2-6.3 4.4-0.3 3-1.4 2.2 0.7 3.5-3.4 1.8 0.2 2.8-1.5 1.1 2.6 5.8 3.3 3.9-1 2.8 3.8 0.3 2.3 3.5 5 0.1 4.4-3.7 0.1 9.7 2.6 0.7 3.2-1.1 5.5 10.4-1.1 2.1 0 4.6 0.3 5.4-1.9 3.2 1.2 2.4-1.1 2.2 2.9 5.4-2.9 6.9-1.1 3.3-2.8 1.6-5.9-3.7-0.8-2.6-11.7-6.4-10.7-7.1-4.7-3.9-2.8-5.3 0.8-1.9-5.4-8.4-6.4-11.8-6-12.8-2.4-3-2-4.7-4.6-4.2-4.1-2.6 1.7-2.8-3-6.2 1.7-4.5 4.4-4 0.7 2.7-1.6 1.5 0.3 2.3 2.3-0.5 2.3 0.7 2.5 3.3 3.1-2.7 0.9-4.3 3.4-5.6 6.7-2.5 6.1-6.8 1.7-4.1-0.8-4.9 1.5-0.6 3.8 3 1.8 3.1 2.6 1.6 3.5 6.7 4.2 0.9 3.1-1.7 2.1 1.1 3.4-0.6 4.4 3-3.5 6.6 1.7 0.1 2.9 3.4z" id="PE" name="Peru"> | |
| 368 | + </path> | |
| 369 | + <path class="Philippines" d="M 1700.5 447.8 1701.4 452.1 1702 455.7 1700.4 461.5 1697.9 455 1695.5 458.3 1697.6 463 1696.2 466 1689.3 462.3 1687.4 457.6 1688.9 454.6 1685.1 451.5 1683.5 454.2 1680.9 453.9 1676.9 457.5 1675.9 455.6 1677.7 450.2 1681.1 448.4 1684 446 1686.2 448.9 1690.4 447.2 1691.1 444.3 1695.1 444.1 1694.3 439.2 1699.2 442.2 1699.9 445.4 1700.5 447.8 Z"> | |
| 370 | + </path> | |
| 371 | + <path class="Philippines" d="M 1685.9 435.8 1684.1 437.9 1682.7 442 1681.1 443.9 1677.2 439.5 1678.2 437.7 1679.5 435.9 1679.7 432 1682.8 431.6 1682.3 435.9 1685.9 429.7 1685.9 435.8 Z"> | |
| 372 | + </path> | |
| 373 | + <path class="Philippines" d="M 1655.6 442 1648.5 448.1 1650.9 443.6 1654.7 439.6 1657.7 435.2 1660 428.8 1661.6 434.1 1658.1 437.6 1655.6 442 Z"> | |
| 374 | + </path> | |
| 375 | + <path class="Philippines" d="M 1673.1 425.5 1676.7 427.5 1680.2 427.5 1680.4 430.1 1678.1 432.9 1674.7 434.8 1674.2 431.8 1674.3 428.5 1673.1 425.5 Z"> | |
| 376 | + </path> | |
| 377 | + <path class="Philippines" d="M 1693.2 423.7 1695.6 430.9 1691.1 429.2 1691.4 431.4 1693.2 435.3 1690.6 436.8 1690 432.2 1688.2 431.9 1687 428 1690.3 428.5 1690 426.1 1686 421.2 1691.4 421.3 1693.2 423.7 Z"> | |
| 378 | + </path> | |
| 379 | + <path class="Philippines" d="M 1670.2 417.9 1669.3 423.5 1666.6 420.3 1663.1 415.4 1667.9 415.6 1670.2 417.9 Z"> | |
| 380 | + </path> | |
| 381 | + <path class="Philippines" d="M 1663.7 383 1667.5 384.9 1668.9 383.2 1669.7 384.8 1669.3 387.5 1671.9 392.1 1671.3 397.4 1668.4 399.5 1668.3 404.7 1670.3 409.8 1673.3 410.5 1675.7 409.8 1683.2 413.3 1683.1 416.8 1685.2 418.4 1684.9 421.3 1680.2 418.2 1677.7 414.8 1676.5 417.2 1672.5 413.3 1667.5 414.3 1664.5 412.9 1664.4 410.2 1666 408.6 1664.1 407.1 1663.7 409.4 1660.4 405.7 1659.1 402.9 1658 396.8 1660.6 398.9 1659.5 388.8 1660.3 383 1663.7 383 Z"> | |
| 382 | + </path> | |
| 383 | + <path class="Papua New Guinea" d="M 1868.1 545.6 1866.5 546.3 1864.2 543.8 1862 539.7 1861.2 534.8 1862 534.2 1862.5 536.1 1864.1 537.6 1866.5 541.6 1869 543.8 1868.1 545.6 Z"> | |
| 384 | + </path> | |
| 385 | + <path class="Papua New Guinea" d="M 1846.7 537 1843.7 537.5 1842.7 539.3 1839.5 540.9 1836.5 542.4 1833.4 542.4 1828.9 540.5 1825.8 538.7 1826.4 536.7 1831.4 537.7 1834.6 537.2 1835.6 534.1 1836.5 533.9 1836.8 537.3 1840 536.9 1841.8 534.7 1845.1 532.4 1844.7 528.6 1848.1 528.5 1849.2 529.5 1848.8 533.1 1846.7 537 Z"> | |
| 386 | + </path> | |
| 387 | + <path class="Papua New Guinea" d="M 1782.9 560.4 1784.5 539.4 1785.5 518.5 1795.1 522.9 1805.4 526.6 1809.1 529.9 1812.1 533.1 1812.7 536.9 1821.9 540.9 1823 544.3 1817.8 545 1818.7 549.3 1823.4 553.5 1826.4 560.3 1829.7 560.1 1829.1 562.9 1833.4 564 1831.6 565.2 1837.3 567.9 1836.4 569.8 1832.6 570.2 1831.4 568.6 1826.7 567.8 1821.1 566.9 1817.1 562.8 1814.3 559.2 1811.8 553.6 1804.7 550.8 1799.8 552.6 1796.1 554.8 1796.4 559.5 1791.8 561.7 1788.7 560.6 1782.9 560.4 Z"> | |
| 388 | + </path> | |
| 389 | + <path class="Papua New Guinea" d="M 1853.6 530.7 1851.8 532.4 1850.9 528.6 1849.8 526.2 1847.3 524.1 1844.2 521.3 1840.2 519.4 1841.8 517.9 1844.8 519.7 1846.7 521.1 1849 522.6 1851.1 525.3 1853.2 527.4 1853.6 530.7 Z"> | |
| 390 | + </path> | |
| 391 | + <path d="M1079.9 154.8l5.9 0.7 8.8-0.1 2.5 0.7 1.4 1.9 0.6 2.7 1.7 2.3 0.4 2.4-2.8 1.3 1.9 2.8 0.5 2.8 3.2 5.4-0.3 1.7-2.3 0.7-3.8 5.2 1.6 2.8-1.1-0.4-5-2.4-3.5 0.9-2.4-0.6-2.8 1.3-2.7-2.2-1.9 0.9-0.3-0.4-2.6-3.1-3.7-0.3-0.7-2-3.4-0.7-0.6 1.6-2.8-1.3 0.2-1.7-3.7-0.5-2.5-2-2.4-3.9 0.2-2.2-1.5-3.3-1.9-2.1 1.2-1.7-1.4-3.1 3.1-1.8 7.1-2.8 5.8-2 4.8 1 0.6 1.5 4.6 0z" id="PL" name="Poland"> | |
| 392 | + </path> | |
| 393 | + <path d="M1660.3 229.9l1.4 1.1-2.2-0.4-1 2.2-0.3 2.1 2.8 4.6-1.9 1.4-0.3 1.1-0.9 1.9-2.9 1.1-1.4 1.7 1.3 2.7-0.3 0.7 2.6 1.1 4.4 2.8 0 1.5-2 0.4-3.6 0.4-0.6 2.8-2.5-0.2 0 0.6-3.2-1.2-0.1 1.2-1.3 0.5-0.7-1.2-1.7-0.6-1.9-1 0.1-2.8 0.9-0.8-1-1.1-0.4-3.5-0.9-1-3.4-0.7-3.4-1.7 2.1-4.1 3.9-3.4 1.1-4.6 3.6 2 4.6 0.3-2.8-3.4 6.4-2.7 0-3.6 5.5 3.8z" id="KP" name="Dem. Rep. Korea"> | |
| 394 | + </path> | |
| 395 | + <path d="M662.5 631.4l2 3.5 0.4 8.7 5.9 1.3 2.2-1.3 3.9 1.8 1.2 1.9 1.1 5.9 0.8 2.4 2.1 0.3 2-1 2.1 1.1 0.3 3.6-0.3 3.8-0.7 3.7-0.3 5.6-4.3 5-4.2 1-6.3-1-5.8-1.7 4.2-9.8-1.1-2.8-5.9-2.5-7.3-4.8-4.6-1-11.3-10.4 1.5-7.7-0.2-3.5 2.1-5.6 9.6-1.9 5.2 0.1 5.4 3.3 0.3 2z" id="PY" name="Paraguay"> | |
| 396 | + </path> | |
| 397 | + <path d="M1178.3 293.8l0.4 4-0.6 1.9-2.5 0.8 0.1-1.7 1.3-0.9-1.5-0.7 0.7-4.2 2.1 0.8z" id="PS" name="Palestine"> | |
| 398 | + </path> | |
| 399 | + <path d="M1270.1 343.7l-1.5 0.5-1.8-1.3-0.8-4.7 1.1-3.3 1.5-0.7 1.8 2 0.5 3.7-0.8 3.8z" id="QA" name="Qatar"> | |
| 400 | + </path> | |
| 401 | + <path d="M1118.9 193.1l1.6 0.7 1.8 1.8 2 2.6 3.4 3.8 0.6 2.7-0.2 2.7 1.3 2.9 2.4 1.2 2.3-1.1 2.4 1.1 0.4 1.7-2.3 1.3-1.6-0.6-0.4 7.7-3.1-0.7-4-2.3-5.9 1.5-2.3 1.6-7.6-0.4-4-0.9-1.9 0.4-1.8-2.6-1-1.1 1-1.1-1.3-0.7-1.5 1.4-3.1-1.9-0.7-2.6-3.2-1.4-0.8-2.1-3-2.4 3.9-1.2 2.6-4.3 1.9-4.2 2.9-1.3 2-1.4 3.2 0.7 3.2 0 2.5 1.6 1.6-1 3.6-0.6 1-1.5 2.1 0z" id="RO" name="Romania"> | |
| 402 | + </path> | |
| 403 | + <path d="M1158.8 509.1l2.2 3.6-0.3 3.8-1.6 0.8-3.1-0.4-1.7 3.7-3.5-0.5 0.6-3.6 0.8-0.5 0.2-3.8 1.6-1.8 1.4 0.7 3.4-2z" id="RW" name="Rwanda"> | |
| 404 | + </path> | |
| 405 | + <path d="M938.9 324.3l-0.1 0.4-0.1 1.2-0.2 9.8-18-0.4-0.2 16.5-5.2 0.5-1.5 3.3 0.9 9.3-21.7 0-1.3 2.1 0.3-2.7 0.1 0 12.5-0.5 0.7-2.3 2.4-2.9 2-8.9 7.9-6.9 2.7-8.1 1.8-0.5 1.9-5 4.6-0.7 2 0.9 2.5 0 1.8-1.5 3.4-0.2-0.1-3.4 0.9 0z" id="EH" name="Western Sahara"> | |
| 406 | + </path> | |
| 407 | + <path d="M1240.5 315l5 0.6 1.7 3.1 3.9-0.2 2.7 5.6 2.9 1.4 1.2 2.3 4 2.7 0.7 2.6-0.4 2.2 0.9 2.1 1.8 1.8 0.9 2.1 1 1.6 1.8 1.3 1.5-0.5 1.3 2.5 0.3 1.4 2.7 6.6 16.9 3.2 1-1.4 3 4.6-2.6 12.8-16.3 6.4-15.9 2.5-5 2.9-3.5 6.7-2.6 1.1-1.5-2.1-2.1 0.3-5.5-0.7-1.1-0.6-6.4 0.1-1.5 0.6-2.4-1.6-1.3 3.1 0.8 2.7-2.4 2.1-0.9-2.8-1.8-1.9-0.5-2.6-3.1-2.3-3.3-5.4-1.9-5.2-4.1-4.4-2.5-1.1-4.1-6.1-0.9-4.4 0-3.8-3.6-7.2-2.8-2.5-3-1.3-2.1-3.7 0.2-1.4-1.8-3.4-1.7-1.4-2.5-4.8-3.8-5.1-3.1-4.4-2.7 0 0.5-3.5 0.1-2.3 0.4-2.6 6.2 1.1 2.1-2 1.1-2.3 4.1-0.9 0.7-2.2 1.6-1-6-6.5 10.4-3.2 0.9-1 6.8 1.8 8.6 4.5 16.8 12.9 10.2 0.5z" id="SA" name="Saudi Arabia"> | |
| 408 | + </path> | |
| 409 | + <path d="M1191 409.2l-0.7 5.5-2 6.4-3.3 3.1-2.3 5-0.5 2.6-2.6 1.8-1.5 6.7 0 0.8-0.8-0.2 0.1-3.2-0.8-2.2-2.9-2.5-0.9-4.6 0.6-4.8-2.6-0.4-0.4 1.4-3.4 0.4 1.5 1.8 0.5 3.9-3 3.5-2.7 4.6-2.9 0.7-4.8-3.7-2.1 1.3-0.5 1.8-2.9 1.3-0.2 1.3-5.6 0-0.8-1.3-4.1-0.3-2 1.1-1.6-0.5-2.9-3.8-1-1.7-4.1 0.9-1.5 2.9-1.3 5.8-2 1.2-1.7 0.7-0.5-0.3-1.9-1.9-0.4-2 0.8-2.6 0-2.7-3.3-4-0.7-2.7 0-1.6-2.1-1.9-0.1-3.7-1.3-2.5-1.9 0.4 0.5-2.4 1.4-2.6-0.7-2.7 1.8-2-1.2-1.5 1.3-3.9 2.5-4.8 4.8 0.5-1.1-25.5 0-2.7 6.4 0-0.5-12.8 22.3 0 21.5 0 22.1 0 2.1 6.3-1.2 1.1 1.2 6.7 2.5 7.6 2.2 1.6 3.2 2.4-2.7 3.6-4 1.1-1.7 2-0.3 4.2-2 9.5 0.7 2.5z" id="SD" name="Sudan"> | |
| 410 | + </path> | |
| 411 | + <path d="M1178.1 441.1l0.2 5-0.8 1.9-3 0.2-1.9 3.6 3.5 0.5 3 3.1 1 2.6 2.6 1.5 3.5 7-3.8 4.2-3.4 3.8-3.5 3-4 0-4.5 1.5-3.6-1.5-2.3 1.8-5.1-4.3-1.4-2.7-3.1 1.3-2.6-0.4-1.5 1.1-2.6-0.8-3.5-5.3-0.9-2-4.3-2.6-1.4-3.8-2.4-2.8-3.9-3.3-0.1-2.1-3.1-2.6-3.9-2.5 1.7-0.7 2-1.2 1.3-5.8 1.5-2.9 4.1-0.9 1 1.7 2.9 3.8 1.6 0.5 2-1.1 4.1 0.3 0.8 1.3 5.6 0 0.2-1.3 2.9-1.3 0.5-1.8 2.1-1.3 4.8 3.7 2.9-0.7 2.7-4.6 3-3.5-0.5-3.9-1.5-1.8 3.4-0.4 0.4-1.4 2.6 0.4-0.6 4.8 0.9 4.6 2.9 2.5 0.8 2.2-0.1 3.2 0.8 0.2z" id="SS" name="South Sudan"> | |
| 412 | + </path> | |
| 413 | + <path d="M918 408l0.2 4 1.1 3.7 2 1.8 0.5 2.4-0.3 2-0.8 0.4-3.1-0.5-0.4 0.7-1.2 0.1-4.1-1.5-2.7-0.1-10.4-0.3-1.5 0.8-1.9-0.2-3 1-0.8-4.9 5.1 0.1 1.4-0.9 1 0 2.1-1.5 2.4 1.3 2.4 0.2 2.5-1.5-1.1-1.8-1.9 1-1.7 0-2.2-1.6-1.8 0.1-1.3 1.6-6.1 0.1-2.3-5-2.7-2.2 2.5-1.3 2.8-4.5 1.4-3.3 2-2 2.7 0.5 2.8-1.4 3.1 0 2.7 1.8 3.6 1.7 3.4 4.8 3.6 4.4z" id="SN" name="Senegal"> | |
| 414 | + </path> | |
| 415 | + <path d="M928.5 447.9l-2.6 3-2.6 3.4-0.3 1.9-1.4 2.1-1.5-0.5-4-2.6-3-3.4-0.9-2.4-0.7-4.7 3.1-2.9 0.6-1.7 1-1.4 1.6-0.2 1.3-1.2 4.5 0 1.6 2.3 1.2 2.7-0.2 1.9 0.9 1.7-0.1 2.3 1.5-0.3z" id="SL" name="Sierra Leone"> | |
| 416 | + </path> | |
| 417 | + <path d="M492.5 415.9l-0.7 1.5-3.3-0.1-2-0.6-2.2-1.3-3-0.4-1.5-1.4 0.3-0.9 2-1.6 1.2-0.7-0.3-0.8 1.4-0.4 1.6 0.6 1.1 1.2 1.6 1.1 0.1 0.8 2.5-0.7 1.2 0.4 0.7 0.7-0.7 2.6z" id="SV" name="El Salvador"> | |
| 418 | + </path> | |
| 419 | + <path d="M1102 218.2l-1.1 1.4 0.7 2.4 2.8 2.7-1.8 2-0.6 2 0.6 0.8-0.7 0.8-2.4 0.2-1.7 0.3-0.3-0.5 0.6-0.7 0.4-1.6-0.7 0.1-1.1-1.2-0.9-0.3-0.8-1-1-0.4-0.8-0.9-0.9 0.4-0.5 2.1-1.2 0.4 0.4-0.5-2.1-1.3-1.7-0.7-0.9-0.9-1.4-1.1 1.1-0.3 0.5-2.9-2.7-2.5 1.1-2.8-1.9 0.1 1.7-2.4-1.7-1.8-1.5-2.5 3.7-1.6 3.2 0.3 3 2.4 0.8 2.1 3.2 1.4 0.7 2.6 3.1 1.9 1.5-1.4 1.3 0.7-1 1.1 1 1.1z" id="RS" name="Serbia"> | |
| 420 | + </path> | |
| 421 | + <path d="M681 464.9l-3.1 5.5 0.3 4.4 2.2 3.8-1.1 2.7-0.5 3-1.5 2.7-3.2-1.4-2.7 0.7-2.3-0.6-0.6 1.9 1 1.2-0.6 1.4-3.1-0.6-3.3-5.6-0.7-3.6-1.8 0-2.4-4.6 1.1-3.4-0.3-1.5 3.5-1.6 1-5.8 6.8 1.3 0.6-1.2 4.6-0.5 6.1 1.8z" id="SR" name="Suriname"> | |
| 422 | + </path> | |
| 423 | + <path d="M1098.1 187.7l-1.2 1.7-0.7 2.5-1 0.6-5.5-1.9-1.6 0.4-1 1.5-2.3 0.8-0.6-0.4-2.3 0.9-1.9 0.2-0.3 1.3-4.1 0.7-1.9-0.6-2.7-1.7-0.7-2.1 0.3-0.8 0.6-1.4 2.2 0.1 1.6-0.6 0.1-0.6 0.9-0.3 0.2-1.4 1.1-0.3 0.7-1.1 1.5 0 0.3 0.4 1.9-0.9 2.7 2.2 2.8-1.3 2.4 0.6 3.5-0.9 5 2.4z" id="SK" name="Slovakia"> | |
| 424 | + </path> | |
| 425 | + <path d="M1069.8 203.9l-3.9 1.7-0.3 2.5-1.7 0.7 0.1 1.7-2-0.1-1.8-1-0.8 1-3.6-0.2 1.1-0.5-1.4-2.7 0.4-3.1 4.2 0.5 2.4-1.5 4.4-0.1 0.9-1.1 0.8 0.1 1.2 2.1z" id="SI" name="Slovenia"> | |
| 426 | + </path> | |
| 427 | + <path d="M1088.2 87l-7 1.6-3.5 3.9 1.3 3.5-6.2 4.5-7.8 5-2.1 8.1 3.7 4.1 4.8 3.3-3.3 6.6-4.6 1.4-0.6 10-2.1 5.7-5.7-0.6-2.2 4.8-5.5 0.3-1.9-5.7-4.5-6.9-4.2-8.4 1.8-3.4 3.4-4 0.8-6.9-3.1-2.9-1-7.7 2.4-5.4 4.3 0.1 1.3-2.2-1.8-2 5.7-7.9 3.4-6.1 2.3-3.9 4 0 0.6-3.1 8 0.9-0.1-3.6 2.6-0.2 6 2.7 7.2 3.7 1.8 8.5 1.8 2.2z" id="SE" name="Sweden"> | |
| 428 | + </path> | |
| 429 | + <path d="M1161.7 667.7l0.6 2.9 0.3 2.9-1.4 2.8-3.2 0.7-3.1-3.5 0.1-2.2 1.7-2.4 0.6-1.9 1.7-0.4 2.7 1.1z" id="SZ" name="Swaziland"> | |
| 430 | + </path> | |
| 431 | + <path d="M1195 287.5l-9.7 6.9-6.3-2.6-0.1 0 0.6-1-0.4-2.6 0.9-3.5 2.7-2.5-1.2-2.5-2.5-0.3-1.1-4.9 1-2.7 1.3-1.4 1.2-1.4-0.2-3.5 1.9 1.2 5.6-1.8 3 1.2 4.4 0 5.7-2.4 2.9 0.1 5.9-1-2.1 4-2.7 1.6 1.2 4.7-1 7.7-11 6.7z" id="SY" name="Syria"> | |
| 432 | + </path> | |
| 433 | + <path d="M1119.2 376.1l1.1 25.5-4.8-0.5-2.5 4.8-1.3 3.9 1.2 1.5-1.8 2 0.7 2.7-1.4 2.6-0.5 2.4 1.9-0.4 1.3 2.5 0.1 3.7 2.1 1.9 0 1.6-3.6 1.1-2.8 2.6-4 7-5.2 3-5.5-0.4-1.6 0.6 0.6 2.2-2.9 2.3-2.4 2.5-7.1 2.4-1.4-1.4-1-0.2-1 1.7-4.6 0.5 0.8-1.8-1.8-4.4-0.8-2.6-2.5-1.1-3.4-3.8 1.2-3 2.6 0.6 1.6-0.4 3.2 0-3.2-5.8 0.2-4.3-0.5-4.2-2.3-4.1 0.6-3.1-3.7-0.1 0-4.1-2.4-2.4 2.3-8.5 7-6 0.2-8.4 1.8-13 1.2-2.8-2.4-2.2-0.1-2-2.2-1.7-1.6-10 5.5-3.5 22.5 12.3 22.6 12.3z" id="TD" name="Chad"> | |
| 434 | + </path> | |
| 435 | + <path d="M991.4 431.2l-0.7 3.4 1.7 1.9 2 2.2 0.2 3.2 1.2 1.3-0.3 14.8 1.4 4.4-4.5 1.4-1.3-2.3-1.5-4.1-0.5-3.2 1.3-5.7-1.4-2.4-0.6-5 0-4.7-2.3-3.3 0.4-2 4.9 0.1z" id="TG" name="Togo"> | |
| 436 | + </path> | |
| 437 | + <path d="M1577.5 410.2l-5.3-0.9-7.1 1.2-3.1 5.3 2.1 7.8-5.3-3-4.8 0.2 0.3-5.1-4.9 0 0.2 7.1-2.2 9.4-1.4 5.7 0.7 4.6 3.7 0.2 2.7 5.9 1.3 5.5 3.4 3.7 3.4 0.7 3.1 3.4-1.7 2.6-3.7 0.8-0.6-3.3-4.8-2.8-0.9 1.1-2.3-2.4-1.2-3.2-3.2-3.6-2.9-3.1-0.7 3.8-1.3-3.6 0.4-4 1.2-6.1 2.2-6.6 2.6-6-2.7-5.9-0.2-3-1-3.6-4.3-5.1-1.8-3.2 1.8-1.2 1.4-5.5-2.9-4.3-4.1-4.7-3.5-5.6 2.2-1.2 1.5-6.9 3.9-0.3 2.8-2.8 3-1.5 2.7 2 0.9 3.9 3.8 0.3-0.4 6.7 1 5.8 5.3-3.8 1.9 1.1 3.2-0.2 0.8-2.2 4.3 0.4 5 5.2 1.3 6.4 5.3 5.6 0.4 5.4-1.5 2.9z" id="TH" name="Thailand"> | |
| 438 | + </path> | |
| 439 | + <path d="M1357 243.6l-1.4 1.9-6-1 0.6 3.6 5.5-0.5 7.1 2.1 9.6-1 3.1 6 1.5-0.7 3.7 1.5 0.5 2.5 1.8 3.6-5.4 0-3.8-0.5-2.5 2.9-2.2 0.6-1.5 1.4-2.7-2.1-0.9-5.4-1.7-0.3 0.1-2-3.3-1.4-1.7 2.2 0.2 2.6-0.6 0.9-3.2-0.1-0.9 2.9-2.1-1.2-3.4 2-1.8-0.7 1.3-6.5-2.4-4.8-4.2-1.5 0.6-2.8 4.4 0.3 1.5-3.5 0.5-4.1 6.5-1.5-0.2 3 1.3 1.7 2.1-0.1z" id="TJ" name="Tajikistan"> | |
| 440 | + </path> | |
| 441 | + <path d="M1338.3 262l-1.6-0.2-2.9-1.7-0.3 2.2-4.2 1.3 0.2 5.1-2.6 2-4 0.9-0.4 2.9-3.9 0.9-5.9-2.5-1.7-5.3-4-0.3-7.3-5.6-4.3-0.7-6.6-3.3-3.9-0.6-2 1.2-3.6-0.2-3 3.7-4.4 1.2-1.9-4.5-0.6-6.7-4.6-2.2 0.4-4.3-3.5-0.4-0.1-5.4 5.3 1.6 4.1-2-4.7-3.9-2.4-3.6-3.8 1.6 0.6 4.7-2.6-4.1 1.8-2.2 5.6-1.3 3.9 1.8 4.8 5 2.6-0.3 5.9-0.1-1.7-3.2 3.8-2.2 3.4-3.7 7.9 3.4 1.9 5 2.3 1.3 5.5-0.3 2.1 1.2 4.3 6.6 7.1 4.4 4.2 3 6.3 3.1 7.7 2.8 0.8 3.9z" id="TM" name="Turkmenistan"> | |
| 442 | + </path> | |
| 443 | + <path d="M1692.7 562.1l0.1-1.9-0.5-1.3 0.8-1.5 4.9-1.4 4-0.3 1.8-0.8 2.1 0.8-2.2 1.8-6.1 2.8-4.9 1.8z" id="TL" name="Timor-Leste"> | |
| 444 | + </path> | |
| 445 | + <path d="M1048.2 289.1l-0.1 4.9-2.6 1.8-1.6 2.1-3.6 2.5 0.6 2.6-0.4 2.8-2.6 1.4-2.6-11.5-3.4-2.6-0.1-1.5-4.5-3.9-0.6-4.8 3.2-3.6 1.1-5.3-1-6.1 1-3.3 5.7-2.5 3.7 0.7 0 3.3 4.4-2.4 0.4 1.2-2.5 3.2 0.1 2.9 1.9 1.6-0.5 5.6-3.5 3.2 1.2 3.5 2.8 0.1 1.4 3.1 2.1 1z" id="TN" name="Tunisia"> | |
| 446 | + </path> | |
| 447 | + <path class="Turkey" d="M 1201.7 235.3 1207.2 235 1212.8 238.2 1214.1 240.4 1214.2 243.5 1218.4 245.1 1220.8 246.9 1217.5 248.8 1220.4 256.1 1219.7 258.1 1223.5 263.2 1221.1 264.3 1219 262.7 1212.7 261.8 1210.6 262.8 1204.7 263.8 1201.8 263.7 1196.1 266.1 1191.7 266.1 1188.7 264.9 1183.1 266.7 1181.2 265.5 1181.4 269 1180.2 270.4 1178.9 271.8 1176.6 268.9 1178.3 266.5 1175.1 267.1 1170.5 265.6 1167.3 269.3 1159.3 270 1154.6 266.6 1148.9 266.4 1147.9 269 1144.3 269.8 1138.9 266.4 1133.1 266.5 1129.3 260.1 1125.1 256.6 1127.1 251.6 1123.5 248.5 1128.6 242.4 1136.6 242.2 1138.2 237.3 1148.2 238.2 1153.8 234.1 1159.6 232.3 1168.1 232.1 1177.9 236.6 1185.8 239.1 1191.6 238.1 1196.2 238.7 1201.7 235.3 Z"> | |
| 448 | + </path> | |
| 449 | + <path class="Turkey" d="M 1121.9 239.9 1123.1 239.2 1124.2 235.2 1121.5 233.5 1126.5 231.5 1131.1 232.3 1132 234.8 1136.8 236.8 1136.1 238.4 1129.9 238.7 1127.9 240.7 1124 244.1 1122 241.2 1121.9 239.9 Z"> | |
| 450 | + </path> | |
| 451 | + <path d="M1657.9 355.5l-1.4 5.3-4-5.5-1.5-4.7 1.9-6.3 3.3-4.9 3 1.9-0.1 3.9-1.2 10.3z" id="TW" name="Taiwan"> | |
| 452 | + </path> | |
| 453 | + <path d="M1167 508.4l-0.2 3.9-1.1 4.5 1.6 2.5 2.5-1.5 3.3-0.4 0.7 0.8 3.3-1.6-2.3-2.2 1.9-2.9 2.8-2.9 20.5 13.1 0.3 3.7 8.1 6.4-2.8 8 0.3 3.6 3.5 2.3 0.2 1.7-1.7 3.9 0.3 1.9-0.4 3.1 1.8 4 2.2 6.4 2 1.4-4.6 3.7-6.2 2.5-3.4-0.1-2.1 1.9-3.9 0.2-1.5 0.8-6.7-1.8-2.2 0.2 0.1-0.1-1.8-2.4-0.3-6.8-2.9-3.4-0.4 1.2-1-1.7-5.5-1.2-3.2-1.9-3.6-1.1-2.2-1.1-0.3-0.2-2.7-6.6-0.4-3.9-4.5-4.4 1.4-2.4-1.1-2.6 0.2-2.7-1-0.9 0.3-2.8 0.6-0.1 2-2.3 2.3-3.4 1.4-1.3 0-2.1-1.2-1.5-0.3-2.5 1.6-0.8 0.3-3.8-2.2-3.6 2-0.8 6.2 0.1z" id="TZ" name="Tanzania"> | |
| 454 | + </path> | |
| 455 | + <path d="M1179 474.5l2.7 4.5 0.7 3.2 2.6 7.4-2.1 4.7-2.8 4.2-1.6 2.6 0 0.3-0.2-0.4-3-1.3-2.4 1.6-3.6 0.9-2.6 3.7 0.3 2.5-6.2-0.1-2 0.8-3.4 2-1.4-0.7 0.1-4.8 1.3-2.5 0.3-5.1 1.2-3 2.2-3.3 2.1-1.8 1.9-2.2-2.3-0.9 0.3-7.5 2.3-1.8 3.6 1.5 4.5-1.5 4 0 3.5-3z" id="UG" name="Uganda"> | |
| 456 | + </path> | |
| 457 | + <path d="M1157.2 174.6l2.3 2.7 0.1 1.2 6.7 2.2 3.6-1 3.6 2.9 2.9-0.1 7.7 2 0.4 1.9-1.3 3.2 1.8 3.5-0.3 2.1-4.8 0.4-2.2 1.8 0.4 2.7-3.9 0.5-3 2.1-4.6 0.3-4 2.4 1 3.9 2.8 1.5 5.1-0.4-0.6 2.3-5.4 1.1-6.3 3.6-3.1-1.3 0.7-2.9-5.9-1.9 0.7-1.2 4.6-2.1-1.7-1.4-8.1-1.6-0.8-2.4-4.5 0.8-1.3 3.5-3.3 4.6-2.4-1.1-2.3 1.1-2.4-1.2 1.2-0.7 0.6-2.1 1.1-2.1-0.6-1.1 1-0.5 0.6 0.9 3 0.2 1.3-0.5-1-0.6 0.2-1-2-1.6-1.1-2.6-2-1 0.1-2.1-2.6-1.7-2-0.3-4-1.9-3.2 0.6-1.1 0.9-2.1 0-1 1.5-3.6 0.6-1.6 1-2.5-1.6-3.2 0-3.2-0.7-2 1.4-0.5-1.7-3-1.7 0.7-2.5 1.2-1.7 1.1 0.4-1.6-2.8 3.8-5.2 2.3-0.7 0.3-1.7-3.2-5.4 2.3-0.3 2.4-1.6 3.8-0.2 4.9 0.5 5.7 1.5 3.9 0.1 1.9 0.9 1.7-1.1 1.5 1.5 4.3-0.3 2.1 0.6-0.3-3.1 1.3-1.4 4.1-0.3 1.8 0.2 1-1.4 1.5 0.3 4.9-0.6 3.8 3.5-0.9 1.3 0.8 1.9 3.9 0.3z" id="UA" name="Ukraine"> | |
| 458 | + </path> | |
| 459 | + <path d="M699.7 718.6l-1.6 4.1-5.4 3.5-4.2-1.3-2.8 0.7-5.5-2.7-3.6 0.2-3.9-3.6-0.4-4.1 0.9-1.4-1.2-6.4 0.4-6.6 0.5-5.2 3.4-0.7 6.3 5 1.9-0.2 6.3 4.1 4.8 3.6 3.8 4.3-1.8 3.1 2.1 3.6z" id="UY" name="Uruguay"> | |
| 460 | + </path> | |
| 461 | + <path d="M1352.7 230.7l1.7 0.6-3 4.1 4.6 2.4 3.2-1.6 7.2 3.4-5.3 4.6-4.1-0.6-2.1 0.1-1.3-1.7 0.2-3-6.5 1.5-0.5 4.1-1.5 3.5-4.4-0.3-0.6 2.8 4.2 1.5 2.4 4.8-1.3 6.5-4.3-1.4-3 0-0.8-3.9-7.7-2.8-6.3-3.1-4.2-3-7.1-4.4-4.3-6.6-2.1-1.2-5.5 0.3-2.3-1.3-1.9-5-7.9-3.4-3.4 3.7-3.8 2.2 1.7 3.2-5.9 0.1-6.2-23.4 12-3.7 1.1 0.5 9.2 4.5 4.8 2.4 6.6 5.7 5.7-0.9 8.6-0.5 7.6 4.6 1.5 6.4 2.6 0.1 2.5 5.2 6.7 0.2 2.3 3 2 0 0.9-4.6 5.4-4.4 2.6-1.2z" id="UZ" name="Uzbekistan"> | |
| 462 | + </path> | |
| 463 | + <path d="M648.7 448.1l-4.7 3.8-0.5 2.3 1.8 2.4-1.4 1.2-3.5 1 0 3-1.6 1.8 3.7 4.8 0.7 1.8-2.1 2.5-6.4 2.4-4.1 1-1.7 1.5-4.5-1.6-4.1-0.8-1.1 0.6 2.5 1.6-0.3 4.3 0.7 4.1 4.8 0.5 0.3 1.4-4.1 1.8-0.7 2.7-2.4 1.1-4.2 1.5-1.1 2-4.4 0.4-3.1-3.4-1.6-6.4-1.5-2.3-2-1.4 2.9-3.2-0.2-1.4-1.5-1.9-1.1-4.3 0.6-4.6 1.3-2.2 1.2-3.4-2-1.1-3.2 0.7-4.1-0.3-2.3 0.7-3.8-5.6-3.3-0.8-7.3 0.6-1.2-2.2-1.4-0.5-0.1-1.4 0.7-2.4-0.3-2.5-1.2-1.5-0.6-2.9-2.9-0.4 1.8-3.8 0.9-4.6 1.8-2.4 2.3-1.8 1.6-3.2 3.7-1.1-0.2 1.5-3.4 0.8 1.7 2.9-0.3 3.4-2.7 3.7 1.9 5.1 2.5-0.4 1.5-4.7-1.7-2.2 0-4.9 7.2-2.6-0.6-3 2.1-2.1 1.7 4.6 4 0.1 3.4 3.5 0.2 2.2 5 0 6.1-0.6 3.1 2.8 4.2 0.8 3.3-2 0.1-1.6 7.1-0.4 6.7-0.1-4.9 1.9 1.8 3.1 4.5 0.4 4.2 3.2 0.7 5.1 2.9-0.1 2.2 1.5z" id="VE" name="Venezuela"> | |
| 464 | + </path> | |
| 465 | + <path d="M1586.5 363.5l-6.5 5.4-3.7 6.1-0.6 4.5 5.3 6.7 6.5 8.4 5.7 4 4.1 5.1 4 11.9 0.4 11.3-4.3 4.2-6.1 4.2-4.2 5.3-6.6 6-2.3-4.1 1.2-4.4-4.4-3.6 4.6-2.6 5.9-0.5-2.8-3.8 9-5-0.1-7.7-1.8-4.3 0.2-6.4-2-4.5-4.9-4.5-4.4-5.6-5.7-7.6-7.3-3.9 1.2-2.3 3.3-1.7-3-5.6-6.8 0-3.5-5.8-4-5.1 2.7-1.6 4.4 0.1 5.3-0.8 4.1-3.4 3.1 2.4 5.3 1.2-0.3 3.7 3.1 2.6 5.9 1.7z" id="VN" name="Vietnam"> | |
| 466 | + </path> | |
| 467 | + <path d="M1283.8 394.9l-4 1.7-0.9 2.9 0 2.2-5.4 2.7-8.8 3-4.7 4.5-2.5 0.4-1.7-0.4-3.2 2.7-3.5 1.2-4.7 0.3-1.4 0.4-1.1 1.7-1.5 0.5-0.8 1.6-2.8-0.2-1.7 0.9-4-0.3-1.6-3.8 0-3.5-1-1.9-1.3-4.7-1.8-2.6 1.1-0.4-0.7-2.9 0.6-1.2-0.4-2.8 2.4-2.1-0.8-2.7 1.3-3.1 2.4 1.6 1.5-0.6 6.4-0.1 1.1 0.6 5.5 0.7 2.1-0.3 1.5 2.1 2.6-1.1 3.5-6.7 5-2.9 15.9-2.5 5.2 10.6 2.2 4.5z" id="YE" name="Yemen"> | |
| 468 | + </path> | |
| 469 | + <path d="M1162.1 556.8l0.3 0.2 2.2 1.1 3.6 1.1 3.2 1.9 2.6 2.9 1.3 5.4-1 1.7-1.3 5.2 0.9 5.4-1.8 2.2-2 6 2.9 1.6-17.2 5.3 0.4 4.6-4.3 0.9-3.3 2.5-0.8 2.2-2 0.6-5.1 5.2-3.2 4.2-1.9 0.1-1.8-0.7-6.2-0.7-1-0.5 0-0.5-2.2-1.5-3.6-0.3-4.6 1.4-3.6-4-3.6-5.2 0.8-20.5 11.7 0.1-0.4-2.2 0.9-2.4-0.9-3 0.7-3.1-0.6-2 1.9 0.1 0.3 2 2.7-0.1 3.5 0.6 1.9 2.9 4.4 0.9 3.5-2 1.2 3.3 4.3 0.9 2 2.8 2.2 3.5 4.3 0-0.2-6.9-1.6 1.2-3.9-2.5-1.5-1.2 0.9-6.4 1.1-7.6-1.2-2.8 1.7-4.1 1.5-0.8 7.7-1.1 0.9 0.3-0.3 1.4 1.9 0.5 1.2 1.3 1-0.3-0.5-1.1z" id="ZM" name="Zambia"> | |
| 470 | + </path> | |
| 471 | + <path d="M1159.4 644.7l-2.9-0.7-1.9 0.8-2.7-1.1-2.2 0-3.4-2.9-4.3-1-1.5-4.1 0.1-2.3-2.3-0.7-6.1-7-1.6-3.7-1.1-1.2-1.9-5.1 6.2 0.7 1.8 0.7 1.9-0.1 3.2-4.2 5.1-5.2 2-0.6 0.8-2.2 3.3-2.5 4.3-0.9 0.2 2.4 4.7-0.1 2.6 1.3 1.1 1.6 2.7 0.5 2.8 2-0.4 8.2-1.3 4.4-0.4 4.8 0.8 1.9-0.9 3.8-0.8 0.6-1.7 4.6-6.2 7.3z" id="ZW" name="Zimbabwe"> | |
| 472 | + </path> | |
| 473 | + <path d="M1222.1 512.6l-3.3-5.3-0.2-23.4 4.9-7.2 1.5-2.1 3.6-0.1 5-4.5 7.3-0.3 15.6-19.3-4.8 0.1-18.7-7.6-2.2-2.3-2.2-3.1-2.2-3.6 1.2-2.2 1.9-3.5 1.9 1.2 1.2 2.7 2.7 2.7 2.8 0 5.2-1.7 6.1-0.7 4.9-2 2.8-0.4 2-1.2 3.2-0.2 1.8-0.2 2.5-0.9 3-0.7 2.5-2.2 2.2 0 0.2 1.8-0.4 3.7 0.2 3.4-1.1 2.3-1.4 7-2.4 7.1-3.3 8.2-4.6 9.4-4.7 7.2-6.6 8.8-5.6 5.2-8.4 6.4-5.3 4.8-6.2 7.8-1.3 3.4-1.3 1.5z" id="SO" name="Somalia"> | |
| 474 | + </path> | |
| 475 | + <path d="M1097.8 230.8l-1.2 0.3-2.9 1-0.1 1.3-0.7-0.1-0.6-2.3-1.3-0.7-1.2-1.7 0.8-1.4 1.2-0.4 0.5-2.1 0.9-0.4 0.8 0.9 1 0.4 0.8 1 0.9 0.3 1.1 1.2 0.7-0.1-0.4 1.6-0.6 0.7 0.3 0.5z" id="XK" name="Kosovo"> | |
| 476 | + </path> | |
| 477 | + <path d="M1159.4 644.7l2.2 9 1.1 4.6-1.4 7.1 0.4 2.3-2.7-1.1-1.7 0.4-0.6 1.9-1.7 2.4-0.1 2.2 3.1 3.5 3.2-0.7 1.4-2.8 4.1 0-1.7 4.7-1 5.3-1.7 2.9-4 3.3-1.1 0.9-2.6 3.3-1.8 3.3-3.5 4.6-6.7 6.6-4.1 3.8-4.3 3-5.9 2.4-2.7 0.4-0.9 1.8-3.2-1-2.7 1.2-5.7-1.2-3.3 0.8-2.2-0.4-5.8 2.6-4.6 1-3.5 2.4-2.4 0.2-2.1-2.3-1.8-0.1-2.2-2.9-0.3 0.9-0.6-1.7 0.3-3.8-1.5-4.3 1.8-1.2 0.1-4.9-3.3-6-2.4-5.4 0-0.1-3.6-8.3 2.8-3.2 2 1.8 0.8 2.7 2.5 0.5 3.4 1.2 2.9-0.5 5-3.3 1.1-23.7 1.4 1 3 6.1-0.6 3.9 1.1 2.3 4-0.7 2.8-2.9 2.7-1.9 1.5-3.1 2.8-1.4 2.3 0.7 2.5 1.8 4.5 0.3 3.6-1.4 0.6-2 1.2-3.1 3-0.5 1.8-2.4 2-4.3 5.2-4.8 8.1-4.7 2.2 0 2.7 1.1 1.9-0.8 2.9 0.7z m-20.3 53.2l1.1-2 3.1-1 1.1-2.1 1.9-3.1-1.7-2-2.2-2-2.7 1.4-3.1 2.5-3.2 4 3.7 5 2-0.7z" id="ZA" name="South Africa"> | |
| 478 | + </path> | |
| 479 | + <path class="New Zealand" d="M 1886.2 764.4 1885.4 767 1891 764.4 1890.5 767.1 1888.4 769.8 1884.2 772.7 1877.1 777.4 1872.4 780 1871.8 783 1867.8 783.1 1861.5 785.5 1856.8 789.6 1848.6 796 1842.3 798.8 1838.3 800.6 1833.7 800.5 1832.2 798.4 1827.1 798 1828.1 795.6 1834.6 791 1846 784.7 1850.3 783.5 1855.9 781.1 1862.9 777.8 1868.6 774.5 1874.6 769.8 1877.7 768.2 1881.2 764.6 1887 761.7 1886.2 764.4 Z"> | |
| 480 | + </path> | |
| 481 | + <path class="New Zealand" d="M 1915.2 733.9 1914.8 740.7 1917.7 736.3 1919 738.1 1916.6 742.9 1919.5 745 1922.7 745.5 1927.4 743.1 1929.6 743.8 1924.4 749.5 1920.2 753.2 1916.3 753.1 1913.5 755 1912 757.8 1910.4 758.9 1905.8 762.4 1899.9 766.7 1893.9 769.3 1894.4 767.6 1892.9 766.7 1899.8 761.5 1900.7 757.9 1896.9 755.4 1898.7 753.1 1904 750.9 1908.2 745.9 1910.7 741.8 1911.3 737.5 1912.1 736.4 1911.2 733.7 1910.5 728.1 1910.9 723.5 1913.1 723 1914 726.6 1917.2 728.2 1915.2 733.9 Z"> | |
| 482 | + </path> | |
| 483 | + <path class="Chile" d="M 655.1 837.9 661.1 851.5 666.1 851.5 669.1 851.7 668.6 854.1 665.4 856 663 855.8 659.9 855.3 655.7 853.5 650.3 852.6 642.8 849.2 636.4 846 626.6 839.1 631.2 840.4 639.8 844.5 647.1 846.7 648.3 843.9 648 839.7 651.4 837.1 655.1 837.9 Z"> | |
| 484 | + </path> | |
| 485 | + <path class="Chile" d="M 614.4 647.8 615.3 649.4 614.1 656.1 608.5 659.2 610.1 669.9 609.3 672 611.3 674.5 608 678.5 605.4 684.5 604.5 690.4 606.2 696.6 604.1 703.2 609 714.3 610.6 715.5 612 721.4 610.4 727.6 611.8 733 608.8 737.2 610.4 743 613.7 749.3 611.2 751.6 611.4 757.3 612.2 763.8 615.5 771.6 613.8 772.9 617.5 780.2 620.6 782.6 619.8 785.3 622.6 786.5 624 788.9 622.2 790.1 624 793.8 625.1 802.1 624.4 807.5 626.2 810.7 626.1 814.6 623.4 817.4 626.5 823.9 629.1 826.2 632.2 825.8 634.1 830.4 637.6 834 649.7 834.8 654.6 835.8 650.3 835.8 648.6 837.3 645.2 839.5 646.9 845.2 645 845.3 638.7 843.4 631.2 839.1 623.6 835.6 620.4 831.7 620.1 828.1 615.9 823.9 610.9 813.3 610.7 807.3 614.1 802.5 605.8 800.6 608.5 795.1 606.5 784.6 613 786.8 611.2 773.7 607.2 772 608.2 779.9 604.7 779 603.2 770 601.2 758.2 602.3 753.8 599 747.6 596.6 740.4 598.8 740.1 599.4 729.8 600.6 719.5 600.7 710 597.4 700.3 598 695 595.9 687.1 597.7 679.3 596.8 666.9 596.8 653.5 596.9 639.2 595.4 628.7 593.3 619.6 596.1 618 597.2 614.7 600.4 619.1 601.6 623.7 604.8 626.4 603.7 632.6 607.4 639.8 610.5 648.7 614.4 647.8 Z"> | |
| 486 | + </path> | |
| 487 | + <path d="M1016.5 177.1l-2.8-1.5-3.1-2.7-4.5 1.3-3.6-0.5 2.5-1.7 4-9 6.5-2.6 4 0.2 0.9 2.1-0.9 5.6-1.2 2.3-2.9 0 1.1 6.5z" id="NL" name="Netherlands"> | |
| 488 | + </path> | |
| 489 | + <path d="M946.9 263.7l-2.2 1.6-2.8-0.9-2.7 0.7 0.9-5-0.3-3.9-2.4-0.6-1.1-2.4 0.5-4.2 2.2-2.3 0.5-2.6 1.2-3.8 0-2.7-0.9-2.3-0.2-2.2 1.9-1.6 2.2-0.9 1.2 3.1 3 0 0.9-0.8 3.1 0.2 1.3 3.2-2.4 1.7-0.3 5-0.8 0.9-0.3 3.1-2.3 0.5 2 3.8-1.6 4.2 1.8 1.9-0.8 1.7-2 2.4 0.4 2.2z" id="PT" name="Portugal"> | |
| 490 | + </path> | |
| 491 | + <path class="Russian Federation" d="M 1689.5 177.4 1703.2 188.4 1694.3 186.4 1698 195.4 1707.6 201.8 1710.6 206.2 1704.1 202.4 1704.2 207.3 1699.5 202 1695.7 195.9 1690.1 189.2 1687.7 184.4 1681.3 176.2 1673.3 170.1 1666.5 161.7 1668.4 158.9 1664 156.1 1665.3 155.2 1670.2 159.2 1677.1 165.1 1682.3 171.2 1689.5 177.4 Z"> | |
| 492 | + </path> | |
| 493 | + <path class="Russian Federation" d="M 1094.6 155.4 1085.8 155.5 1079.9 154.8 1080.6 152.2 1086.9 150.2 1092 151.3 1094.2 152.2 1094 153.9 1094.6 155.4 Z"> | |
| 494 | + </path> | |
| 495 | + <path class="Russian Federation" d="M 1548.4 48.2 1542.5 48.3 1533.6 47.7 1532.6 47.4 1533.3 45.4 1537.5 44.9 1545.9 46.9 1548.4 48.2 Z"> | |
| 496 | + </path> | |
| 497 | + <path class="Russian Federation" d="M 1561 38.7 1559.9 40.7 1553 40.3 1542.7 38.3 1540.9 36.7 1549.2 37.4 1561 38.7 Z"> | |
| 498 | + </path> | |
| 499 | + <path class="Russian Federation" d="M 1535.5 36.3 1538.6 40.1 1524.3 39.9 1519.7 41.1 1507.2 37.8 1503.8 34.4 1507.3 33.5 1517.5 33.7 1535.5 36.3 Z"> | |
| 500 | + </path> | |
| 501 | + <path class="Russian Federation" d="M 1218.8 61.3 1216.6 61.7 1203.1 61 1200.8 58.7 1192.9 57.3 1190.9 54.4 1194.4 53.3 1192.8 50.5 1198.2 46.1 1194.2 45.5 1200.9 41 1198.4 38.7 1205.2 36.1 1215.5 32.9 1227.2 32 1232.2 30.2 1238.9 29.6 1243.1 31.5 1241.8 33 1230.4 35.5 1220.5 37.8 1211.8 42.6 1209 47.6 1205.7 52.6 1208.8 56.9 1218.8 61.3 Z"> | |
| 502 | + </path> | |
| 503 | + <path class="Russian Federation" d="M 1661.7 231 1660.3 229.9 1658.4 226.6 1660.9 226.5 1657 219 1652.3 213.6 1655.2 211.4 1662.1 212.5 1661.5 206.3 1658.7 199.5 1659 197.2 1657.7 191.5 1650.8 193.4 1648.2 195.8 1640.6 195.8 1634.6 190 1625.6 185.5 1615.6 183.5 1609.4 177.5 1604.9 173.7 1601.1 171 1593.4 164.8 1587.4 162.6 1578.8 160.7 1572.6 160.9 1567.5 162 1565.8 165.1 1569.5 166.5 1572 169.9 1570.7 171.9 1570.9 178.4 1572.8 181.2 1568.4 185.1 1561 182.7 1555.4 183.3 1551.5 181.2 1548.1 180.5 1543.7 184.9 1537.8 185.9 1534.2 187.5 1527.4 186.5 1522.8 186.5 1517.9 183.3 1511.3 180.4 1505.9 179.5 1500.2 180.3 1496.2 181.5 1487.7 178.9 1484.1 174.3 1477.4 172.7 1472.6 171.9 1465.6 169.4 1464.3 175.8 1468.3 179.4 1465.9 183.8 1457.9 182.2 1452.9 182 1448.1 179.1 1442.9 179 1437.6 177.1 1431.7 180 1425 185.3 1420.3 186.4 1418.6 186.9 1414.2 183.1 1408.2 184 1404.9 181.3 1400.9 180.1 1396.8 176.5 1393.5 175.4 1387.3 177 1378.9 173.5 1377.8 176.7 1359.5 161.1 1351.1 156.4 1351.9 154.4 1342.7 160.2 1338.3 160.5 1337.2 157.2 1330.1 155.1 1325.8 156.6 1321.4 150.3 1312.3 149 1309.2 151.5 1298.3 153.8 1296.6 155.3 1279.6 157.4 1278.2 159.5 1283.3 163.7 1279.3 165.3 1280.8 166.9 1277.3 169.9 1286.7 174.2 1286.5 177.1 1279.5 176.8 1278.7 178.7 1271.4 175.5 1263.7 175.6 1259.3 178.2 1252.7 175.7 1240.7 171.4 1233.1 171.6 1225 178.3 1225.6 182.8 1219.6 179.2 1217.4 186.1 1219.2 187.3 1217.5 192.1 1222.8 196.3 1226.4 196.1 1230.7 200.3 1230.9 203.5 1233.7 204.6 1232.3 208.3 1227.7 209.3 1224.1 215.8 1230.1 221.9 1230.5 226.1 1237.8 233.6 1235.3 236.2 1234.7 237.8 1232.3 237.3 1228 233.5 1226.5 233.3 1222.9 231.8 1220.8 229.2 1215.7 227.9 1212.8 228.9 1211.6 227.7 1204 224.6 1196.3 223.6 1191.7 222.5 1191.3 223.3 1183.7 217.9 1177.5 215.5 1172.4 211.8 1176 210.8 1179.2 205.6 1175.9 203.1 1182.8 200.5 1182.4 199.1 1178.1 200.1 1177.7 197.4 1179.9 195.6 1184.7 195.2 1185 193.1 1183.2 189.6 1184.5 186.4 1184.1 184.5 1176.4 182.5 1173.5 182.6 1169.9 179.7 1166.3 180.7 1159.6 178.5 1159.5 177.3 1157.2 174.6 1153.3 174.3 1152.5 172.4 1153.4 171.1 1149.6 167.6 1144.7 168.2 1143.2 167.9 1142.2 169.3 1140.4 169.1 1138.4 165.1 1136.9 163.1 1137.7 162.5 1141.6 162.7 1143.2 161.4 1141.5 159.8 1138.1 158.7 1138.1 157.6 1136 156.5 1132.2 152.5 1132.9 150.9 1131.8 148 1127 146.6 1124.6 147.3 1123.7 145.8 1118.4 144.3 1116.3 140.7 1115.3 137.8 1112.8 136.5 1114.4 134.6 1112 129 1114.6 125.6 1113.7 124.6 1118.2 121.3 1112.8 118.5 1120.8 111.1 1124.2 107.7 1125.1 104.8 1117.7 100.9 1118.6 97.1 1113.7 92.9 1115.4 88.1 1108.9 81.8 1111.8 77.6 1104.5 73.9 1104.1 70.1 1107.3 69.6 1113.7 67.5 1117.4 65.6 1125.1 68.8 1136.8 70.1 1154.5 76.3 1158.6 78.9 1160.1 82.6 1156.4 85.5 1149.9 87 1129.7 82.8 1126.8 83.5 1134.9 87.6 1135.9 90.2 1137.8 96 1143.9 97.7 1147.7 99.2 1147.5 96.4 1144.1 94 1146.2 91.8 1157.8 95.4 1161 94 1156.7 89.8 1164.8 84.4 1168.9 84.7 1173.5 86.6 1174.6 82.8 1169.9 79.5 1170.7 76.2 1166.4 72.8 1178.7 74.6 1182.3 77.6 1177.2 78.3 1178.4 81.4 1182.5 83.3 1188.5 82.1 1188.1 78.6 1195.8 75.9 1208.2 71.3 1211.4 71.5 1208.9 74.8 1214.3 75.4 1216.4 73.5 1224.1 73.4 1229.1 71.1 1235.5 74.4 1238.3 70.8 1232.3 67.7 1233.5 65.9 1246.5 67.5 1253.1 69.2 1271.7 75.4 1272.8 72.6 1266.9 69.7 1266.1 68.6 1260.8 68 1260.6 65.5 1255.9 61.3 1254.8 59.6 1259.1 54.9 1258.5 50.1 1260.7 49.1 1272.1 50.5 1275 53.4 1274.1 57.6 1277.8 59.3 1281.6 63 1285.4 70.4 1292.1 73.7 1292.6 77.4 1289.1 85.2 1294.4 86 1294.9 84 1298.7 82.6 1298.2 79.9 1300.1 77.2 1295.7 74.1 1295.3 70.5 1290.5 70.1 1287.6 67.1 1287.2 61.7 1279 57.4 1283.7 53.9 1280 50.2 1281.9 50.1 1286.1 52.9 1288.2 58 1293.2 59 1288.6 55.2 1293.7 53.1 1301.8 52.8 1311.4 55.8 1304.5 51.5 1299.6 46 1305.5 45 1315 45.2 1322.8 44.5 1317.2 41.9 1318.5 38.6 1322.7 38.4 1327.6 36 1336.9 35.3 1336.7 33.9 1346 33.5 1350.3 34.6 1355.7 32 1362.7 32.1 1361.2 29.9 1362.1 27.9 1368.3 25.9 1376.6 27.5 1373.1 28.7 1382.5 29.4 1386.6 31.8 1388.5 30.6 1399.4 30.7 1411 33.1 1416.5 34.9 1419 37.5 1416.6 39 1410 41.8 1408.9 43.3 1414.5 44 1421.7 45.3 1424.1 44.3 1429.9 47.6 1430.1 46.3 1435.3 45.5 1448.9 46.3 1452.7 48.7 1470.1 49.5 1465.3 45.6 1474.8 46.5 1480.9 46.4 1490.8 49.1 1496.8 52.4 1497.2 54.6 1507.3 58.8 1516.3 60.9 1513.3 55.4 1522.7 57.7 1527.7 56.3 1537.5 57.9 1538.5 56.5 1546.1 57.2 1536.7 52.3 1538.8 50.1 1579.2 53.5 1587.1 56.6 1603.4 60.6 1618.2 59.6 1627.5 60.5 1634.1 62.7 1639.2 66.6 1646.5 68.1 1650.4 67 1657.4 66.9 1666.7 67.9 1673.6 67.3 1687.9 72.1 1690.4 70.4 1681.9 67 1680.1 64.6 1695.4 66.1 1703.4 65.8 1719.2 68.3 1728.7 70.7 1761.9 92.8 1759.9 95.3 1753.7 94.9 1761.9 97.9 1771 102.6 1775.2 104.1 1779 106.5 1780 108 1770 106.8 1763.4 111.1 1760.4 111.8 1758.8 115.9 1756.8 119.5 1758.5 122.2 1747 118.1 1740.9 122.7 1736.2 120.5 1734.8 123.1 1727.8 122.2 1730.9 126.1 1732 131.9 1734.9 134.3 1741.7 135.6 1750.8 144.3 1746.7 144.6 1750.1 149.6 1754.8 152.2 1749.8 155.3 1755.2 162.3 1749.5 163.8 1754 170 1752.3 175.8 1746.6 171.5 1736.1 162.6 1719.9 149 1713.4 140.7 1713.5 137.1 1710.7 134.3 1716.4 133 1716.2 125.5 1716.9 119.5 1719.4 114.8 1712.8 106.6 1708.1 107.1 1711.3 111.9 1707.7 118.3 1695.4 111.1 1686.3 113.1 1686.3 122.9 1694 126.5 1685.5 128.1 1679.1 128.7 1674.8 124.4 1666.8 123.5 1664.3 126.4 1649.2 125.4 1636 127.1 1632.5 138.8 1627.5 153 1635.7 153.8 1641.4 157.6 1647.2 158.9 1647.6 155.9 1653.2 156.3 1666 163 1670.7 168.2 1672.1 174.4 1677.6 181.8 1682.9 191.7 1681.9 200.8 1683.2 205.1 1681.2 212.5 1679.1 219.8 1678.2 223.5 1673.6 227.2 1670.5 227.3 1665.3 224.2 1661.2 228.9 1661.7 231 Z"> | |
| 504 | + </path> | |
| 505 | + <path class="Russian Federation" d="M 1367.1 23.1 1349 24.9 1347.7 18.8 1349.9 18.3 1353 18.6 1365.6 21.2 1367.1 23.1 Z"> | |
| 506 | + </path> | |
| 507 | + <path class="Russian Federation" d="M 1164.8 13.1 1160.5 13.6 1157.6 14 1157.6 14.7 1154 15.4 1149.5 14.4 1150.6 13 1142.8 12.9 1149.1 12.1 1154.3 12.1 1155.8 13.2 1157.1 12.2 1159.9 11.5 1165.7 12.4 1164.8 13.1 Z"> | |
| 508 | + </path> | |
| 509 | + <path class="Russian Federation" d="M 1345.1 20.4 1338 21 1326.3 19.7 1318.2 18 1311.6 14.8 1305.7 14 1311.1 11.1 1317.3 10.2 1326.9 12.2 1340.6 16.4 1345.1 20.4 Z"> | |
| 510 | + </path> | |
| 511 | + <path d="M976.6 223.4l2 2.4 9.5 2.9 1.9-1.4 5.8 2.9 5.9-0.8 0.4 3.7-4.9 4.2-6.6 1.4-0.5 2.1-3.2 3.5-2 5.2 2 3.7-3 2.8-1.2 4.2-4 1.3-3.7 4.9-6.8 0.1-5-0.1-3.4 2.2-2.1 2.4-2.6-0.5-1.9-2.2-1.4-3.6-4.9-1-0.4-2.2 2-2.4 0.8-1.7-1.8-1.9 1.6-4.2-2-3.8 2.3-0.5 0.3-3.1 0.8-0.9 0.3-5 2.4-1.7-1.3-3.2-3.1-0.2-0.9 0.8-3 0-1.2-3.1-2.2 0.9-1.9 1.6 0.5-4.5-2-2.7 7.4-4.6 6.2 1.1 6.9 0 5.4 1.1 4.3-0.4 8.3 0.3z" id="ES" name="Spain"> | |
| 512 | + </path> | |
| 513 | + <path class="France" d="M 1035.7 231.4 1034.2 236.3 1031.8 235 1030.5 230.8 1031.4 228.4 1034.6 226 1035.7 231.4 Z"> | |
| 514 | + </path> | |
| 515 | + <path class="France" d="M 1014.4 185 1015.5 185.5 1016.9 185.4 1019.3 187 1026.5 188.2 1024.1 192.4 1023.7 196.9 1022.4 198 1020.1 197.4 1020.3 199 1016.7 202.5 1016.7 205.4 1019.1 204.4 1020.9 207.1 1020.8 208.9 1022.3 211.3 1020.6 213.2 1022.1 218.1 1024.9 218.9 1024.4 221.6 1019.9 225.2 1009.7 223.5 1002.3 225.6 1001.7 229.4 995.8 230.2 990 227.3 988.1 228.7 978.6 225.8 976.6 223.4 979.3 219.6 980.3 207 975.2 200.4 971.5 197.2 963.9 194.8 963.5 190.2 970 188.9 978.3 190.5 976.8 183.4 981.5 186.1 992.9 181.3 994.4 176.2 998.6 174.9 999.4 177.1 1001.6 177.2 1004 179.7 1007.5 182.6 1010 182.1 1014.4 185 Z"> | |
| 516 | + </path> | |
| 517 | + <path class="United States" d="M 118.8 379.3 117.7 380.4 116.5 379.5 117.1 377.7 116.7 375.3 117.2 374.6 118.4 373.6 118.3 372.3 118.7 371.7 119.1 371.8 121 372.9 121.9 373.5 122.6 374.3 123.5 376.6 123.3 376.9 120.8 378.3 118.8 379.3 Z"> | |
| 518 | + </path> | |
| 519 | + <path class="United States" d="M 118.1 369.3 116.1 369.7 115.5 368.4 115 367.9 115 367.5 115.7 366.9 117.5 367.5 118.7 368.5 118.1 369.3 Z"> | |
| 520 | + </path> | |
| 521 | + <path class="United States" d="M 115.1 365.9 114.8 366.6 111.8 366.4 112.4 365.6 115.1 365.9 Z"> | |
| 522 | + </path> | |
| 523 | + <path class="United States" d="M 110.4 364.9 110 365.3 109.6 365.2 107.7 365 107.4 363.5 107.2 363.3 108.9 362.4 109.3 362.8 110.4 364.9 Z"> | |
| 524 | + </path> | |
| 525 | + <path class="United States" d="M 102 360.7 101.2 361.3 99.6 360.2 100 359.7 101 359.1 102.3 359.2 102 360.7 Z"> | |
| 526 | + </path> | |
| 527 | + <path class="United States" d="M 539.5 194.5 533.4 196.5 528.7 199 524.1 201.7 523.6 202.6 529.3 201.3 531.4 203.4 536 201.9 540.9 199.8 546.3 197.7 543.2 201 545.7 201.8 548.2 204.2 553.3 202.8 558.4 202.3 558.7 204.1 560.2 204.3 561.4 204.5 562.9 207 558.2 207.6 558.1 207.6 554.4 206.9 549.9 208.1 546.2 208.7 541.5 212.8 538.5 215.1 538.9 215.8 544.4 211.7 545.1 211.7 540.4 216.6 537.5 221 535 224.6 534.4 227.7 533.6 229.2 533 230.9 533.1 234.2 533.4 234.7 535.2 234.6 536.8 233.9 538.2 233.1 541.5 230 543.3 225.8 543.2 221.9 544.6 219.2 547.2 216.1 549.3 213.9 552 212.4 551.6 214.5 553.8 211.4 555.1 210.8 556.8 208.4 560.6 209.7 563.4 212.1 562.6 215 561 217.9 557.2 220.4 556.8 222 557.8 222 562.1 219.3 563.7 219.9 563.2 223.6 562.5 226.2 558.8 229.7 556.8 231.9 554.1 234.3 556.8 235.6 559.3 236 563.3 235.1 567 233.4 570 232.5 574.6 230.7 580.4 226.9 580.5 226.3 580.8 224.4 583.5 223.6 587.4 223.9 591.4 224.4 596 222.3 596.6 219.8 596.4 218.9 603.2 214.5 605.9 213.4 613.7 213.3 623 213.3 624.1 211.8 625.8 211.5 628.3 210.5 631.1 207.6 634.3 202.7 639.8 198 640.9 199.6 644.6 198.6 646.2 200.4 643.3 208.9 645.5 212.5 645.7 214.6 639.3 217.6 633.3 219.8 627.3 221.7 623.3 225.5 622 226.9 620.8 230.3 621.5 233.6 623.6 233.8 623.8 231.5 624.9 232.9 623.9 234.7 620.1 235.7 617.6 235.6 613.4 236.7 611.1 237 608 237.3 603 239.2 611.1 238 612.2 239.2 604.3 241.1 601 241.1 601.4 240.3 599.3 242.1 600.7 242.4 598.2 247 592.9 251.9 593 250.2 591.9 249.9 590.7 248.3 590.7 251.8 591.7 252.9 591.1 255.3 588.7 257.8 584.2 262.9 583.8 262.7 586.7 258.3 584.7 255.9 585.7 250.5 583.8 253.3 583.8 257.4 580.6 256.4 583.6 258.4 582.1 264.5 583.5 265 583.5 267.2 582.5 273.6 577.9 278.3 571.8 280.2 567.4 284 564.6 284.4 561.2 286.8 559.9 288.9 553 293.1 549.2 296.2 545.7 300 543.8 304.5 543.8 309 544.4 314.5 545.9 319 545.4 321.8 546.7 329.2 545.7 333.6 545.1 336.1 543.1 340 541.3 340.8 538.7 340 538.3 337.2 536.5 335.7 534.5 330.2 532.9 325.3 532.5 322.8 534.5 318.5 533.7 315 530.6 309.6 528.7 308.6 522.6 311.6 521.7 311.2 519.7 308.2 516.7 306.6 510.3 307.5 505.7 306.7 501.4 307.2 498.9 308.2 499.5 309.9 498.8 312.5 499.6 313.8 498.4 314.6 496.6 313.7 494.3 314.9 490.4 314.7 487.1 311.3 482.2 312.1 478.6 310.6 475.1 311.1 470.1 312.6 464 317.3 457.9 320.1 454.2 323.1 452.3 326 451.3 330.5 450.9 333.5 451.5 335.7 449.3 335.9 445.7 334.5 441.8 332.5 440.9 329.5 440.7 325 438.3 321.4 437.4 317.6 435.8 313.2 432.6 310.6 428.1 310.8 423.3 315.8 419.3 313.9 417 312 416.6 308.4 415.8 305.1 413.4 302.3 411.3 300.2 410 297.9 400.6 297.9 399.8 300.6 395.5 300.6 384.7 300.6 373.8 296.1 366.8 293 367.7 291.7 360.6 292.4 354.3 292.9 354.6 289.7 352.5 286 350.3 285.2 350.4 283.4 347.5 283 346.3 281.3 341.5 280.7 340.6 279.6 341.4 276.1 338.9 269.7 338.4 260.8 339.3 259.3 338 257.2 336.5 251.8 338.3 246.6 337.4 243.1 341.3 237.8 344.1 232.4 345.2 227.5 350.7 221.5 354.7 215.8 358.7 210.1 363 201.6 364.8 196.3 365.2 193.4 366.6 192.1 372.4 194.3 371.4 200.2 373.6 198.5 376.1 193.4 377.7 188.3 391.8 188.3 406.5 188.3 411.3 188.3 426.4 188.3 441.1 188.3 455.9 188.3 470.8 188.3 487.6 188.3 504.6 188.3 514.8 188.3 516.1 185.9 517.8 185.9 516.9 189.3 517.9 190.3 521.2 190.7 525.8 191.7 529.7 193.6 534.1 192.8 539.5 194.5 Z"> | |
| 528 | + </path> | |
| 529 | + <path class="United States" d="M 275 138.6 268 140.9 267.2 139.3 269.5 136.5 275.9 134.4 279.4 133.5 282 133.9 282 135.8 275 138.6 Z"> | |
| 530 | + </path> | |
| 531 | + <path class="United States" d="M 236 122 232.1 122.9 230.4 121.8 229.6 120.2 235.3 119.2 238.3 119.8 236 122 Z"> | |
| 532 | + </path> | |
| 533 | + <path class="United States" d="M 237.2 99.6 238.4 100.6 241.9 100.1 243.5 101.6 246.8 102.3 245.6 103 240.7 104.2 239 102.9 238.7 101.9 234.4 102.2 234.1 101.7 237.2 99.6 Z"> | |
| 534 | + </path> | |
| 535 | + <path class="United States" d="M 410 66.6 385.4 87 349.8 119.7 354 119.9 356.8 121.5 357.3 124.1 357.6 127.9 365.2 124.6 371.7 122.7 371.1 125.8 371.9 128.2 373.5 130.9 372.4 135.1 371 142 375.6 145.8 372.4 149.6 367.3 152.5 366.7 150.3 364.2 148.3 367.5 143.1 365.9 138.2 368.6 132.6 364.5 132.2 357.4 132.1 353.6 130.3 350.3 124.2 347 123.1 341.3 121 334.5 121.5 328.5 118.8 325.8 116.3 319.5 117.5 316 121.6 313.1 122 306.5 123.2 300.3 125.2 293.9 126.5 297.1 123 305.5 117.2 312.3 115.4 312.7 114 303.3 117.2 295.9 121.1 284.7 125.3 284.9 128.2 275.9 132.4 268.2 134.9 261.6 136.8 257.6 139.4 247 142.5 242.5 145.3 234.3 147.9 231.6 147.5 225.4 149.1 218.4 151.2 212.3 153.2 202.3 155 202.7 153.9 210.9 151.1 217.5 149.2 226.1 145.9 232.6 145.3 237.6 142.8 248 139.2 250.3 138 256 135.9 261.8 131.4 268 127.9 260.7 129.7 260.4 128.6 255.5 130.8 255.9 127.8 252.3 129.9 253.9 127 246.6 129.3 243.8 129.3 247.5 125.8 250.8 123.6 250.4 121.5 243.2 122.7 242.6 119.9 241.3 118.5 245.3 115.2 244.9 112.7 250.8 109.4 258.5 106.1 263.8 103.2 267.9 102.8 269.7 103.7 276.8 100.9 279.3 101.4 284.9 99.6 287.4 97 286.3 96 292.3 93.8 289.5 93.9 283.3 95.1 280.4 96.4 278.6 95.1 271.7 95.8 267.1 94.4 268.3 92.1 267.3 88.9 276.5 86.5 289.7 83.8 293.2 83.8 288.9 86.6 298.1 86.4 299.3 82.9 297 80.8 297.8 78 297.1 75.7 293.8 74 300.3 71.1 307.8 70.9 316.6 68.5 321.4 65.9 329.3 63.3 334.1 62.7 345.3 60.3 348.4 60.7 358.8 57.9 363.2 59 362.7 61.4 366 60.4 372.3 60.7 370.4 61.9 375.3 62.8 380.2 62.3 386.4 63.9 393.6 64.5 395.8 65.1 402.4 64.3 406.5 65.9 410 66.6 Z"> | |
| 536 | + </path> | |
| 537 | + <path d="M677.3 487l1.5-2.8 0.5-2.9 1-2.7-2.1-3.8-0.3-4.4 3.1-5.5 1.9 0.7 4.1 1.5 5.9 5.4 0.8 2.6-3.4 5.9-1.8 4.7-2.2 2.5-2.7 0.4-0.8-1.8-1.3-0.2-1.7 1.7-2.5-1.3z" id="GF" name="French Guiana"> | |
| 538 | + </path> | |
| 539 | + <path d="M592.9 422l-0.5-0.2-0.5-0.5 0.1-0.6 0.2 0.3 0.4 0.4 0.3 0.5 0 0.1z" id="AW" name="Aruba"> | |
| 540 | + </path> | |
| 541 | + <path d="M634.2 384.9l-0.2 0 0.3-0.4 0.3 0-0.2 0.3-0.2 0.1z" id="AI" name="Anguilla"> | |
| 542 | + </path> | |
| 543 | + <path class="American Samoa" d="M 27.7 593.3 27.4 593.7 27.2 594 27 594.1 26.6 593.8 26.8 593.6 27.1 593.6 27.2 593.4 27.7 593.3 Z"> | |
| 544 | + </path> | |
| 545 | + <path class="American Samoa" d="M 34.3 593.3 33.8 593.4 33.7 593.2 34.1 593.1 34.3 593.3 Z"> | |
| 546 | + </path> | |
| 547 | + <path class="Antigua and Barbuda" d="M 640.7 391.9 641.3 392.2 641.1 392.5 640.9 392.5 640.2 392.6 640 392.5 640 392 640.3 391.9 640.4 391.6 640.6 391.6 640.7 391.9 Z"> | |
| 548 | + </path> | |
| 549 | + <path class="Antigua and Barbuda" d="M 640.7 388.2 640.9 388.2 641.1 388.4 641.2 388.8 641.1 389.1 640.9 389.2 640.8 389 640.5 388.8 640.5 388.3 640.7 388.2 Z"> | |
| 550 | + </path> | |
| 551 | + <path d="M1264.1 333.3l0.3 0.1 0.2-0.1 0.4 0.7-0.1 0.2 0.1 0.9 0 0.7-0.2 0.4-0.1-0.4-0.6-0.8 0.1-0.4-0.2-0.7 0-0.4 0.1-0.2z" id="BH" name="Bahrain"> | |
| 552 | + </path> | |
| 553 | + <path class="Bahamas" d="M 580.4 366 580 366.5 579.8 367 579.2 367.3 578.7 367.3 578.6 367.2 578.2 367.4 577.7 367.5 577.1 367.3 576.7 367.4 576.6 366.9 576.9 366.7 577.1 366.4 577.5 366.2 577.8 365.8 578.2 365.8 578.5 365.6 578.7 365.9 579.2 366.1 579.7 365.9 580.4 364.9 580.7 364.9 580.7 365.1 580.4 366 Z"> | |
| 554 | + </path> | |
| 555 | + <path class="Bahamas" d="M 580.6 364.3 580.4 363.7 581 363.4 581.3 363.7 581.3 363.9 581 364 580.6 364.3 Z"> | |
| 556 | + </path> | |
| 557 | + <path class="Bahamas" d="M 581 357.8 581.2 357.7 581.7 358 582.1 358 582.5 358.1 582.8 358.3 582.8 358.6 582.6 358.7 582.1 358.3 581.7 358.3 581.6 358.2 581.1 358.4 580.6 358.2 580.6 358.1 580.8 357.6 581 357.8 Z"> | |
| 558 | + </path> | |
| 559 | + <path class="Bahamas" d="M 577.1 356 576.9 356.4 576.9 357.1 576.8 357.5 576.4 357.8 576.2 358.2 575.8 358.5 575.1 358.8 575 359 574.8 358.8 574.9 358.6 575.3 358.5 575.5 358.3 575.5 358.1 575.9 358 576.1 357.6 576.5 357.5 576.8 357.1 576.6 356.8 576.2 356.8 576 356.6 576.4 356.1 576.5 356 577.1 356 Z"> | |
| 560 | + </path> | |
| 561 | + <path class="Bahamas" d="M 575.2 355.7 575.3 355.8 575.7 355.8 576.1 356 575.7 356.4 575.6 356.2 575.4 356.2 575 356.1 574.6 355.9 574.3 355.3 574.6 355.2 575 355.3 575.2 355.7 Z"> | |
| 562 | + </path> | |
| 563 | + <path class="Bahamas" d="M 566 349.8 566.6 350.5 566.9 350.7 567.2 351.2 566.9 351.1 566.8 350.9 566.5 350.8 566.1 350.4 565.9 350.3 565.7 349.9 566 349.8 Z"> | |
| 564 | + </path> | |
| 565 | + <path class="Bahamas" d="M 569.7 349.8 570 350.4 570.2 350.9 570.2 351.3 570.4 351.5 570.6 352.2 570.6 353.1 570.8 353.4 571.3 353.6 571.7 354.2 571.7 355 571.3 354.3 571.3 354.1 571 353.7 570.4 353.5 570.6 353.3 570.3 353 570.3 352.7 570.5 352.4 570.4 351.9 570.1 351.5 570.2 351.4 569.9 351 569.8 350.4 569.6 350.5 569.7 349.8 Z"> | |
| 566 | + </path> | |
| 567 | + <path class="Bahamas" d="M 572.6 349.8 572.3 350 572 349.9 571.9 349.7 572.5 349.5 572.6 349.8 Z"> | |
| 568 | + </path> | |
| 569 | + <path class="Bahamas" d="M 564.4 347.9 564.3 347.8 564 347.1 564.2 347.1 564.4 347.9 Z"> | |
| 570 | + </path> | |
| 571 | + <path class="Bahamas" d="M 574.9 347.4 574.5 348 574.1 348 574.3 347.7 574.4 347.1 574.8 346.8 574.9 347.2 574.9 347.4 Z"> | |
| 572 | + </path> | |
| 573 | + <path class="Bahamas" d="M 556.4 346.2 556.1 346.3 555.9 346.6 555.6 346.5 556.2 346.1 556.4 346.2 Z"> | |
| 574 | + </path> | |
| 575 | + <path class="Bahamas" d="M 556.6 346.9 557 347.1 557.2 346.9 557.3 346.4 557.5 346.3 557.7 346.9 557.7 347.4 557.6 347.7 557.7 348.3 557.7 348.6 557.3 349.4 557.1 349.5 556.7 349.3 556.9 349 557.3 348.8 557.2 348.7 556.6 349.1 556.4 348.9 556.8 348.6 556.4 348.6 556.2 348.7 556 348.5 556 347.8 556.1 347.6 555.7 347.1 555.7 346.9 556.1 346.7 556.2 346.4 556.5 346.3 556.6 346.1 557.2 345.8 557.3 346.2 556.8 346.6 556.6 346.9 Z"> | |
| 576 | + </path> | |
| 577 | + <path class="Bahamas" d="M 570.1 346.3 570.2 346.8 570 346.7 569.5 346.8 569.1 346.9 569 346.7 569.3 346.5 569.7 346.2 569.7 346 569.3 345.8 569.1 345.2 569 344.6 568.6 344.2 568.6 344 568.1 343.5 568.4 343.2 568.8 343.6 568.8 344.1 569 344.4 569.1 344.9 569.4 345.4 569.8 345.7 569.9 346 570.1 346.3 Z"> | |
| 578 | + </path> | |
| 579 | + <path class="Bahamas" d="M 559.8 340.8 560.1 341 559.5 341.3 559.2 341.2 559 341.4 558.5 341.3 558.7 341 559.1 340.8 559.8 340.8 Z"> | |
| 580 | + </path> | |
| 581 | + <path class="Bahamas" d="M 555.6 340.3 556 340.1 556.3 340.4 556.2 340.6 556.3 340.8 556.1 341.2 556.4 341.5 556.6 342.4 556.8 342.7 557.2 343.2 557 343.9 557.1 344.5 556.9 344.8 556.6 344.9 556.2 345.3 555.9 345.5 555.6 345.5 555.3 345.7 555 345.7 555 345.3 554.7 345.1 554.8 344.5 554.5 344.8 554.3 344.7 553.7 344.3 553.4 344 553.2 343.7 553.9 343.4 554.4 344.1 554.5 343.5 554.2 343.4 554.1 343.1 554.2 342.8 554.8 342.2 555 341.8 554.9 341.7 555.2 341.3 555.2 340.8 555 340.1 555.1 340 555.4 340 555.6 340.3 Z"> | |
| 582 | + </path> | |
| 583 | + <path class="Bahamas" d="M 563.7 337.7 563.8 337.9 564.1 338.3 564.9 339 565.4 339.1 565.5 339.2 565.7 339.6 566 340 566.5 340.4 566.5 340.6 566.3 341.4 566.2 341.6 566.2 341.9 566.1 342.4 565.9 342.7 565.8 343.4 565.7 343.5 565.5 342.9 565.1 342.6 565.2 342.4 565.7 342.5 565.9 342.3 565.8 342.1 565.9 341.7 566.2 341.3 566.3 340.8 566.3 340.5 565.8 340.1 565.4 339.4 565.1 339.2 564.5 338.9 564.2 338.6 563.6 338.4 563.2 338.6 563.6 337.7 563.7 337.7 Z"> | |
| 584 | + </path> | |
| 585 | + <path class="Bahamas" d="M 554.9 330.2 555.4 330.4 555.5 330.4 556.2 330.5 556.4 330.3 556.6 330.4 557.1 330.4 557.5 330.2 557.9 330.2 558.2 330 558.3 330.5 558.2 330.7 557.9 330.7 557.5 330.7 556.4 330.9 556.1 330.9 554.6 331.3 553.9 331.7 553.5 331.7 553.3 331.6 553 331.3 552.5 330.7 553.1 331 553.2 331.2 553.7 331.2 554.2 331 554.6 330.4 554.3 330.3 554.5 330 554.6 329.7 554.9 330.2 Z"> | |
| 586 | + </path> | |
| 587 | + <path class="Bahamas" d="M 559.2 328.9 560 329.1 560.2 329 560.6 329.3 561.5 330.3 561.7 330.9 562.1 331 562.8 331.5 562.8 331.7 562.6 332.2 562.8 332.9 562.4 332.9 561.9 333.3 561.8 333.5 561.5 334.5 561.4 335.4 561.2 335.6 560.9 335.4 560.8 335 560.4 334.9 560.7 334.4 560.9 334.4 561.4 334 561.3 333.6 561.5 333.2 561.5 332.9 561.7 332.6 561.7 332 561.9 331.9 562.1 331.6 562.2 331.3 561.9 331.2 561.4 331.1 561.3 330.8 561.3 330.6 560.9 330.4 560.9 330.1 560.6 329.4 560.4 329.3 560.1 329.4 559.6 329.3 559.3 329.3 558.7 328.9 559.2 328.9 Z"> | |
| 588 | + </path> | |
| 589 | + <path d="M635.2 387l-0.1-0.3 0.3 0.1-0.2 0.2z" id="BL" name="Saint-Barthélemy"> | |
| 590 | + </path> | |
| 591 | + <path d="M637.3 294l-0.3 0-0.1 0.1 0.1 0.2-0.4 0.2-0.1-0.1 0.2-0.1 0.1 0 0.1-0.3 0.3-0.1 0.1 0.1z" id="BM" name="Bermuda"> | |
| 592 | + </path> | |
| 593 | + <path d="M651.5 418l-0.6-0.2-0.1-0.5 0-0.8 0.2-0.4 0.2 0.2 0.2 0.6 0.5 0.3 0.1 0.4-0.5 0.4z" id="BB" name="Barbados"> | |
| 594 | + </path> | |
| 595 | + <path class="Comoros" d="M 1233.2 581.3 1232.3 581.2 1232.1 580.8 1232.1 580.5 1232.6 580.6 1233.3 581.1 1233.2 581.3 Z"> | |
| 596 | + </path> | |
| 597 | + <path class="Comoros" d="M 1236.9 579.4 1237.1 580.4 1237.1 581.1 1237 581.3 1236.8 581 1236.4 580.7 1236.3 580.5 1236 580.4 1235.4 580 1235.5 579.9 1236 580.1 1236.3 580 1236.5 579.6 1236.5 579.4 1236.8 579.3 1236.9 579.4 Z"> | |
| 598 | + </path> | |
| 599 | + <path class="Comoros" d="M 1231.2 578.4 1230.7 578 1230.4 577.9 1230.1 577.7 1229.9 577.1 1230 576.8 1230 576.6 1230.2 575.4 1230.1 575.3 1230.3 574.9 1230.8 574.8 1231 575.1 1230.8 576.3 1230.9 576.6 1231.1 577 1231.2 577.5 1231.4 578.1 1231.2 578.4 Z"> | |
| 600 | + </path> | |
| 601 | + <path class="Cape Verde" d="M 847.8 406.5 847.6 406.8 847.4 406.6 847.4 406.3 847.7 406.2 847.8 406.5 Z"> | |
| 602 | + </path> | |
| 603 | + <path class="Cape Verde" d="M 849.7 406.6 849.5 406.7 849.1 406.6 848.8 406.3 848.7 406 848.9 405.6 849.4 405.3 849.8 405.3 850 406 849.9 406.4 849.7 406.6 Z"> | |
| 604 | + </path> | |
| 605 | + <path class="Cape Verde" d="M 853.2 403.5 853.4 403.6 853.4 403.8 853.6 404 854 404.5 854.2 404.5 854.4 404.8 854.6 405.3 854.8 405.5 854.5 406 854.2 406.1 853.7 406 853.4 405.9 853.1 405.6 853.1 405.4 852.9 405.3 852.8 404.9 852.9 404.7 852.9 404.4 853.1 404 852.9 403.5 853.2 403.5 Z"> | |
| 606 | + </path> | |
| 607 | + <path class="Cape Verde" d="M 856.3 403.5 856.6 403.6 856.7 404.2 856.6 404.6 856.2 404.8 855.8 404.4 856 404 855.9 403.8 856.3 403.5 Z"> | |
| 608 | + </path> | |
| 609 | + <path class="Cape Verde" d="M 858.3 397.8 858.6 397.6 859.1 397.8 859.2 398 859.2 398.7 858.8 399 858.5 399.2 858.2 399.2 857.7 399 857.7 398.5 857.9 398.3 857.9 397.7 858 397.6 858.3 397.8 Z"> | |
| 610 | + </path> | |
| 611 | + <path class="Cape Verde" d="M 850.3 395 850.7 394.9 851 395.1 851.4 395.1 851.7 395.3 851.8 395.5 851.3 395.5 850.6 395.2 850.4 395.3 850.1 395.9 849.8 395.3 849.7 395.2 849.6 394.8 849.9 394.7 850.3 395 Z"> | |
| 612 | + </path> | |
| 613 | + <path class="Cape Verde" d="M 858.1 394.5 858.2 394.6 858.1 395.2 857.9 395.3 857.9 394.8 857.7 394.5 857.7 394.2 857.6 393.9 858 393.6 858.2 393.8 858.1 394.4 858.1 394.5 Z"> | |
| 614 | + </path> | |
| 615 | + <path class="Cape Verde" d="M 846.8 393.2 847 393.5 847.1 393.8 846.4 394.1 846.1 393.9 845.9 393.7 846.3 393.3 846.8 393.2 Z"> | |
| 616 | + </path> | |
| 617 | + <path class="Cape Verde" d="M 846 391.4 846.3 391.5 846.3 391.7 846.6 392 846.4 392.4 845.8 392.7 845.4 393.1 844.7 393.2 844.7 393.1 844.7 392.6 844.5 392.4 844.5 392.2 844.7 392 844.9 392 845.1 391.8 845.8 391.4 846 391.4 Z"> | |
| 618 | + </path> | |
| 619 | + <path d="M599 424.5l-0.3 0-0.4-0.3-0.3-0.1-0.3-0.3-0.1-0.2-0.3-0.1-0.2-0.4-0.3-0.3 0.1-0.5 0.5 0.3 0.1 0.5 0.4 0.4 0.7 0.2 0.2 0.3 0.3 0.4-0.1 0.1z" id="CW" name="Curaçao"> | |
| 620 | + </path> | |
| 621 | + <path class="Cayman Islands" d="M 532.7 377.8 533 377.9 533.2 377.5 533.6 377.6 534.1 377.6 534.2 377.8 534 378 533.8 377.9 533.4 378 533.2 378.1 532.5 378.1 532.7 377.8 Z"> | |
| 622 | + </path> | |
| 623 | + <path class="Cayman Islands" d="M 540.8 375.3 540.1 375.6 540.3 375.3 540.8 375.3 Z"> | |
| 624 | + </path> | |
| 625 | + <path class="Cayman Islands" d="M 541.4 375.4 541.3 375.3 542.1 375 541.9 375.3 541.4 375.4 Z"> | |
| 626 | + </path> | |
| 627 | + <path class="Cyprus" d="M 1168.2 276.7 1168.3 276.9 1168.6 277.3 1168.2 277.2 1167.9 277.2 1167.5 277.4 1167.2 277.1 1167.2 276.8 1167.5 276.7 1167.7 276.8 1168.2 276.7 Z"> | |
| 628 | + </path> | |
| 629 | + <path class="Cyprus" d="M 1166.5 277.2 1166.5 277.3 1166.3 277.5 1166.2 278.1 1166.1 278.3 1165.8 278.3 1165.6 278.5 1165.1 278.7 1164.9 278.8 1164.3 279.1 1163.9 279 1163.4 279.1 1163.1 279.5 1162.9 279.2 1162.6 279.3 1162.4 279.3 1162.1 279 1161.7 279.3 1161.2 279.3 1160.7 279.1 1160.2 279 1159.8 278.7 1159.5 278.1 1159.2 277.8 1159 277.1 1158.8 276.8 1158.9 276.5 1159.4 276.8 1159.8 276.7 1160 276.4 1160.1 276.1 1160.4 276 1160.6 276.1 1160.8 275.9 1161 275.9 1161.1 276.3 1161.6 276.5 1161.7 276.7 1162.3 276.7 1162.9 276.2 1163.3 276.3 1163.6 276.1 1163.9 276.2 1164.3 276.4 1164.5 276.1 1164.8 276.1 1165.1 276.5 1165.1 277.1 1165.4 276.9 1165.7 277.1 1165.8 277 1166.5 277.2 Z"> | |
| 630 | + </path> | |
| 631 | + <path d="M642 401.7l0.5 0.1 0.2 0.5 0 0.6-0.1 0.8-0.1 0.2-0.2 0.1-0.4 0.2 0.1-0.4-0.1-0.2-0.1-0.7-0.3-0.5 0-0.3 0.1-0.3 0-0.3 0.2 0 0.2 0.2z" id="DM" name="Dominica"> | |
| 632 | + </path> | |
| 633 | + <path class="Falkland Islands" d="M 697.4 836.2 697.8 836.1 697.8 836.5 697 836.2 697.3 835.9 697.5 835.8 697.4 836.2 Z"> | |
| 634 | + </path> | |
| 635 | + <path class="Falkland Islands" d="M 696.7 834.8 696.9 835 697.3 835.1 697.4 835.5 697.1 835.6 697 835.2 696.8 835 696.7 834.8 Z"> | |
| 636 | + </path> | |
| 637 | + <path class="Falkland Islands" d="M 702.9 834 703.1 834.1 703 834.5 702.7 834.4 702.5 834.1 702.8 833.9 702.9 834 Z"> | |
| 638 | + </path> | |
| 639 | + <path class="Falkland Islands" d="M 690.8 833.1 690.8 833.3 690.9 833.6 690.5 833.7 690.2 833.6 690 833.4 689.7 833.3 689.6 832.9 689.8 832.9 689.9 832.7 690.2 832.6 690.4 832.7 690.1 833 690.4 833.1 690.7 832.8 690.8 833.1 Z"> | |
| 640 | + </path> | |
| 641 | + <path class="Falkland Islands" d="M 693.7 831.8 693.3 831.6 692.8 831.3 692 831 692.1 830.6 691.4 830.5 691.1 830.4 690.8 830 691 829.9 691.8 830.2 692.6 830.6 693.1 830.7 693.5 830.8 693.8 830.5 693.9 830.2 694.1 830.3 694.5 830.1 694.6 830.3 695.1 830.3 695.1 830.6 695.4 830.6 696.3 830.4 696.4 830.6 696.7 830.6 697.1 830.5 696.8 830.3 696.7 830 697.1 830 697.7 830.2 697.8 830.6 697.4 831.1 697.5 831.4 697.3 831.6 696.9 831.6 696.6 832.4 696.3 832.7 696 833.3 695.9 833.6 695.4 833.9 695.3 833.7 694.9 833.7 694.9 834 694.1 833.9 694 834 694.3 834.4 694.2 834.5 694 834.9 693.6 834.9 693.3 835.3 693 835.4 692.8 835.4 692.6 835.1 692.2 834.8 692.2 834.6 691.7 834.6 692.3 835 691.9 835 691.4 834.7 691.2 834.6 690.8 834.4 690.5 834.2 690.5 834 691.2 834.2 691.8 834 691.3 833.8 691.2 833.7 692.1 833.7 692.5 833.8 692.8 834 692.8 833.8 693.2 833.7 693.3 833.4 692.9 833.2 692.7 832.7 692.8 832.5 693.4 832.5 693.6 832.6 694 832.5 693.9 832.2 693.1 832.2 693.1 832.3 692.7 832.5 692.1 832.5 691.6 832.2 691.6 831.9 692.3 832.1 692.9 831.9 693.2 831.9 693.7 831.8 694.1 832 694.1 831.9 693.7 831.8 Z"> | |
| 642 | + </path> | |
| 643 | + <path class="Falkland Islands" d="M 695.3 829.9 695.6 829.9 695.9 830.2 695.6 830.3 695.3 830.3 695.3 829.9 Z"> | |
| 644 | + </path> | |
| 645 | + <path class="Falkland Islands" d="M 694.3 829.9 694 829.9 693.9 829.6 694.3 829.7 694.3 829.9 Z"> | |
| 646 | + </path> | |
| 647 | + <path class="Falkland Islands" d="M 692.9 829.7 693.5 829.6 693.7 829.9 693.4 830.3 693.2 830.2 692.9 830.3 692.6 830.1 692.9 829.9 693.2 830.1 693.3 829.9 692.8 829.8 692.9 829.7 Z"> | |
| 648 | + </path> | |
| 649 | + <path class="Falkland Islands" d="M 700.1 829.8 700.7 829.8 700.8 829.7 701.3 829.6 701.7 829.8 702.1 830.1 702.2 830.4 702.2 830.5 701.7 830.6 701.5 830.3 701.3 830.2 701.2 830.5 701.4 830.6 701.5 830.9 701.8 830.7 702 830.8 702.1 831.2 702.4 831.3 702.5 831.2 703 831.5 703.1 831.1 702.3 830.9 702.4 830.4 702.7 830.2 704.2 830.1 704.9 830.7 705.1 830.8 705.1 831.2 704.5 831.1 704 830.9 703.4 831.1 703.7 831.3 704.1 831.4 705 831.5 705.3 831.6 705.4 831.8 705.2 832.3 704.7 832.3 704.6 832.4 703.9 832.5 703.4 832.7 703.7 832.9 702.8 833.3 702.4 833.2 701.9 833.3 701.2 833.3 700.6 833.1 700 832.7 700 833 700.3 833.2 700.9 833.4 701.2 833.7 701.4 833.6 701.8 833.7 702.2 834 702 834.1 702.2 834.6 702 834.6 701.7 834.3 701.3 834.2 700.8 834.6 700.6 834.3 700.3 834.1 700 834.1 699.5 833.9 699.3 834 699.5 834.4 699.8 834.4 700 834.6 699.8 834.7 700.5 835.3 700.3 835.4 699.9 835.2 699.6 835.2 699.3 834.9 699 834.8 698.7 834.7 698.6 835.3 699.1 835.4 699.1 835.7 699 835.9 698.6 835.8 698.5 835.6 698 835.4 697.9 835.2 697.7 835.2 697.3 834.7 697.6 834.3 697.6 834.1 697.2 833.6 697.2 833.4 697.7 833.2 697.6 833 698.1 832.9 698 832.6 698.6 832.2 699.2 832 699.6 832.4 699.6 832.8 699.9 832.7 699.4 832.1 699.5 831.8 698.9 831.6 698.5 831.3 698.6 831.1 699 831.3 699.1 830.8 698.4 830.6 698.5 830.3 698.7 830.4 699 830.2 699.3 830.2 699.5 829.9 699.2 829.6 699.6 829.6 700.1 829.8 Z"> | |
| 650 | + </path> | |
| 651 | + <path class="Falkland Islands" d="M 695.1 829.4 695.8 829.4 696 829.6 696.2 829.4 696.4 829.5 696.6 829.8 696.4 829.9 696 829.8 695.6 829.6 694.8 829.5 694.9 829.3 695.1 829.4 Z"> | |
| 652 | + </path> | |
| 653 | + <path class="Falkland Islands" d="M 688.3 828.3 687.8 828.2 687.9 828 688.3 828.3 Z"> | |
| 654 | + </path> | |
| 655 | + <path class="Faeroe Islands" d="M 955.6 112 956 112.2 956.3 112.2 956.4 112.4 956.4 112.8 956.6 113.1 956.5 113.3 955.9 112.9 955.7 112.5 955.5 112.3 955.3 112 955.6 112 Z"> | |
| 656 | + </path> | |
| 657 | + <path class="Faeroe Islands" d="M 955.9 110.4 956.7 110.6 956.9 110.8 956.8 111.2 956.7 111.3 956.3 110.8 955.8 110.7 955.6 110.3 955.9 110.4 Z"> | |
| 658 | + </path> | |
| 659 | + <path class="Faeroe Islands" d="M 954.62 109.03999999999999 954.7 109.2 955 109.2 955 109.5 954.6 109.5 954.4 109.7 953.8 109.5 953.6 109.4 953.4 109.1 953.9 109 954 108.9 954.5571428571428 108.99285714285715 954.3 108.8 954.5 108 955 108.1 955.3 108.6 955.3 108.7 955.9 108.9 956.2 109.4 956.5 109.6 956.4 110.2 955.9 109.8 955.7 109.6 955.5 109.5 955.4 109.3 955.2 109.1 954.7 109.1 954.62 109.03999999999999 Z"> | |
| 660 | + </path> | |
| 661 | + <path class="Faeroe Islands" d="M 958.3 108.4 958.1 108.2 958.4 108 958.3 108.4 Z"> | |
| 662 | + </path> | |
| 663 | + <path class="Faeroe Islands" d="M 955.9 108.9 955.5 108.7 955.2 108.4 955.1 108.1 955.2 108 955.6 107.9 955.9 107.9 956.5 108.3 956.4 108.5 956.5 108.7 957.1 108.9 957.1 109.1 956.7 109.4 955.9 108.9 Z"> | |
| 664 | + </path> | |
| 665 | + <path class="Faeroe Islands" d="M 958.1 108.3 957.8 108.7 957.5 108.5 957.2 108.5 957.4 108.2 957.3 107.8 957.4 107.7 957.6 108.1 958.1 108.3 Z"> | |
| 666 | + </path> | |
| 667 | + <path class="Faeroe Islands" d="M 957.2 108.3 957 108.3 956.8 107.9 956.9 107.6 957.1 108 957.2 108.3 Z"> | |
| 668 | + </path> | |
| 669 | + <path class="Federated States of Micronesia" d="M 1909.4 467.7 1909.2 468 1908.7 467.9 1908.7 467.7 1909.2 467.3 1909.4 467.7 Z"> | |
| 670 | + </path> | |
| 671 | + <path class="Federated States of Micronesia" d="M 1881.2 457.1 1881.6 457.1 1881.9 457.3 1882 457.7 1881.8 457.8 1881.9 458 1881.8 458.2 1881.2 458.2 1881 458 1881 457.8 1880.8 457.5 1880.8 457.2 1881.2 457.1 Z"> | |
| 672 | + </path> | |
| 673 | + <path class="Federated States of Micronesia" d="M 1766.5 440.5 1766.6 440.8 1766.3 440.8 1766 441.3 1765.9 441.2 1766.2 440.4 1766.6 440.3 1766.5 440.5 Z"> | |
| 674 | + </path> | |
| 675 | + <path d="M639.2 424.5l-0.4 0.3-0.2-0.1-0.1-0.5 0.2-0.5 0.3-0.4 0.3 0 0.1 0.3-0.1 0.7-0.1 0.2z" id="GD" name="Grenada"> | |
| 676 | + </path> | |
| 677 | + <path d="M1800.8 415.5l-0.1 0.4 0 0.6-0.1 0.3-0.3 0-0.2-0.3-0.1-0.5 0-0.5 0.6-0.5 0.1-0.2 0.1-0.5 0.2-0.1 0.1 0.2 0.4 0.1-0.2 0.5-0.5 0.5z" id="GU" name="Guam"> | |
| 678 | + </path> | |
| 679 | + <path class="Saint Kitts and Nevis" d="M 636.3 392 636 392 635.9 391.8 636.1 391.4 636.3 391.4 636.4 391.6 636.3 392 Z"> | |
| 680 | + </path> | |
| 681 | + <path class="Saint Kitts and Nevis" d="M 635.6 390.5 635.5 390.8 635.3 390.8 634.9 390.6 634.7 390.4 635 390 635.5 390.4 635.6 390.5 Z"> | |
| 682 | + </path> | |
| 683 | + <path d="M643.7 413.8l-0.2-0.2-0.4-0.2-0.1-0.2 0-0.6 0.1-0.2 0.7-1.1 0.3 0.2 0 0.7-0.1 0.8-0.1 0.4-0.2 0.4z" id="LC" name="Saint Lucia"> | |
| 684 | + </path> | |
| 685 | + <path d="M634.2 386l-0.4-0.1 0.2-0.3 0.3-0.1-0.1 0.5z" id="MF" name="Saint-Martin"> | |
| 686 | + </path> | |
| 687 | + <path d="M1402.9 474.8l0 0.2-0.2-0.1 0.2-0.2 0 0.1z" id="MV" name="Maldives"> | |
| 688 | + </path> | |
| 689 | + <path d="M1953.8 456l1 0.4-0.2 0-0.8-0.4z" id="MH" name="Marshall Islands"> | |
| 690 | + </path> | |
| 691 | + <path class="Malta" d="M 1063.9 271.7 1063.7 272 1063.2 271.8 1062.7 271.5 1062.7 271 1062.6 270.9 1063.2 270.9 1063.6 271.2 1063.8 271.4 1063.9 271.7 Z"> | |
| 692 | + </path> | |
| 693 | + <path class="Malta" d="M 1062.3 270.6 1061.8 270.5 1061.8 270.3 1062.2 270.2 1062.6 270.5 1062.3 270.6 Z"> | |
| 694 | + </path> | |
| 695 | + <path class="Northern Mariana Islands" d="M 1802.5 411.1 1802.4 411.2 1802.2 411.1 1802.1 410.9 1802.6 410.7 1802.8 410.9 1802.5 411.1 Z"> | |
| 696 | + </path> | |
| 697 | + <path class="Northern Mariana Islands" d="M 1804 406 1803.8 406 1803.5 405.4 1803.6 405.1 1803.9 405.1 1803.9 405.4 1804.1 405.6 1804 406 Z"> | |
| 698 | + </path> | |
| 699 | + <path class="Northern Mariana Islands" d="M 1804.5 404.4 1804.3 404.6 1804.4 404.9 1804 404.9 1804 404.2 1804.1 404 1804.5 403.8 1804.4 404.3 1804.5 404.4 Z"> | |
| 700 | + </path> | |
| 701 | + <path class="Northern Mariana Islands" d="M 1802.6 396.9 1802.3 396.9 1802.2 396.7 1802.6 396.7 1802.6 396.9 Z"> | |
| 702 | + </path> | |
| 703 | + <path class="Northern Mariana Islands" d="M 1800.7 385.9 1800.6 385.7 1800.6 385.2 1800.9 385.1 1801.1 385.4 1800.7 385.9 Z"> | |
| 704 | + </path> | |
| 705 | + <path class="Northern Mariana Islands" d="M 1799.5 381.6 1799.1 381.5 1799.1 381.2 1799.3 381.1 1799.5 381.3 1799.5 381.6 Z"> | |
| 706 | + </path> | |
| 707 | + <path d="M638.2 393.9l0.2 0.4 0 0.3-0.2 0.2-0.2-0.1-0.1-0.3 0.3-0.5z" id="MS" name="Montserrat"> | |
| 708 | + </path> | |
| 709 | + <path class="Mauritius" d="M 1307.7 630.8 1308.1 631.7 1307.9 632.3 1307.5 632.7 1307.6 633 1307.3 633.3 1306.8 633.5 1306.3 633.5 1305.7 633.4 1305.6 633.5 1305.3 633.2 1305.5 633 1305.6 632.6 1305.7 631.9 1305.9 631.5 1306.4 631.1 1306.5 630.9 1306.7 630.4 1307.1 630.1 1307.5 630.2 1307.7 630.8 Z"> | |
| 710 | + </path> | |
| 711 | + <path class="Mauritius" d="M 1339.5 628.7 1339.2 628.4 1339.6 628.2 1339.9 628.1 1340.1 628.2 1340 628.4 1339.5 628.7 Z"> | |
| 712 | + </path> | |
| 713 | + <path class="New Caledonia" d="M 1915.2 646.9 1915 647 1914.8 647.4 1914.4 647.2 1914.6 646.7 1914.7 646.5 1915 646.6 1915.2 646.9 Z"> | |
| 714 | + </path> | |
| 715 | + <path class="New Caledonia" d="M 1919.6 639.1 1919.5 639.2 1919.7 639.6 1920 639.6 1920.4 639.4 1920.3 640 1920.1 640.2 1920 640.6 1919.5 640.9 1919.1 640.7 1919.1 640.6 1918.8 640.3 1918.9 640.1 1918.7 639.9 1918.8 639.5 1918.7 639.1 1918.8 639.1 1919.2 639.2 1919.6 639.1 Z"> | |
| 716 | + </path> | |
| 717 | + <path class="New Caledonia" d="M 1916.9 634.8 1916.7 635.3 1916.7 635.7 1916.5 636 1916.8 636.2 1917 636.3 1917.1 636.5 1917 636.7 1917.3 637.1 1917.1 637.1 1916.9 637.6 1916.7 637.8 1916.4 637.7 1916.2 637.4 1916.1 637.2 1915.5 637.1 1915.2 636.6 1915.2 636.4 1915.1 636.1 1915.4 636.1 1915.7 635.9 1916.1 635.6 1916.2 635.3 1915.8 635.1 1915.4 635.2 1915.6 634.8 1915.9 634.7 1916.1 634.8 1916.5 634.5 1916.9 634.8 Z"> | |
| 718 | + </path> | |
| 719 | + <path class="New Caledonia" d="M 1913.5 633.2 1913.2 633.8 1913.1 634.1 1913.4 634.1 1912.9 634.6 1912.7 634.8 1912.6 634.6 1912.9 634.2 1913.1 633.7 1913.5 633.2 Z"> | |
| 720 | + </path> | |
| 721 | + <path class="New Caledonia" d="M 1913.5 633.2 1913.3 632.8 1913.7 632.8 1913.8 633.2 1913.5 633.2 Z"> | |
| 722 | + </path> | |
| 723 | + <path class="New Caledonia" d="M 1899.6 630.8 1899.8 630.9 1900.3 631.8 1900.6 632.1 1900.9 632 1901.1 631.7 1902 632.2 1902.3 632.7 1903 633.3 1903.3 633.7 1903.4 634 1903.8 634.4 1903.9 634.6 1904.1 634.6 1904.4 634.9 1905.3 635.1 1905.4 635.4 1905.4 635.8 1905.7 636.2 1906.1 636.3 1905.8 636.8 1905.9 637 1906.3 637.5 1906.4 637.5 1906.8 638 1906.7 638.4 1907.4 638.6 1907.8 639.1 1908.1 639.1 1908.4 639.3 1908.9 639.7 1908.8 640 1909.1 640 1909.5 640.5 1910.1 640.9 1910.1 641.1 1910.4 641.2 1910.7 641.5 1910.7 641.7 1911 642.3 1911.4 642.7 1911.5 642.6 1911.9 643.2 1912.4 643.3 1912.7 643.6 1912.7 644 1912.8 644.4 1912.9 644.5 1912.7 645.1 1912 645.5 1911.9 645.2 1911.6 645.3 1911.3 645.6 1910.7 644.8 1910.3 644.8 1910.2 644.6 1910 644.6 1909.8 644.9 1908.9 644 1908.7 644 1908.3 643.5 1908.4 642.7 1908.1 642.7 1907.7 642.7 1907.4 642.5 1907.3 642.3 1907.1 642.2 1906.8 642.2 1906.8 641.9 1906.6 641.4 1906.2 641.5 1905.4 641 1905.3 640.8 1905.3 640.5 1905 640.7 1904.4 640.4 1904.3 640 1903.9 639.7 1903.7 639.2 1903.2 639.1 1903.4 638.9 1903.1 638.6 1902.8 638.5 1902.5 637.8 1902.5 637.4 1902.3 637.1 1902 637.1 1901.8 636.5 1901.9 636.3 1901.2 635.9 1900.9 635.7 1900.5 635.1 1900.8 634.9 1900.6 634.7 1900.4 634.4 1900.4 633.9 1899.9 633.6 1899.9 633 1899.7 632.7 1900 632.7 1900.1 632.4 1900 632.2 1899.4 632.2 1899.2 631.9 1899.7 631.2 1899.4 630.9 1899.6 630.8 Z"> | |
| 724 | + </path> | |
| 725 | + <path class="New Caledonia" d="M 1900.7 631.1 1900.4 631 1900.5 630.6 1900.7 631.1 Z"> | |
| 726 | + </path> | |
| 727 | + <path class="New Caledonia" d="M 1898.4 628.7 1898.2 628.9 1898.1 628.5 1898.3 628.3 1898.4 628.7 Z"> | |
| 728 | + </path> | |
| 729 | + <path d="M1933 505.3l-0.2 0-0.1-0.3 0.3 0 0 0.3z" id="NR" name="Nauru"> | |
| 730 | + </path> | |
| 731 | + <path d="M1747.7 453.1l-0.2 0.4 0.1 0.1-0.2 0.6 0.1 0.2-0.5 0.2-0.2-0.7 0.3-0.2-0.2-0.2 0.3-0.6 0.3-0.1 0.2 0.3z" id="PW" name="Palau"> | |
| 732 | + </path> | |
| 733 | + <path class="Puerto Rico" d="M 607.1 385.9 606.8 385.7 606.9 385.5 607.2 385.5 607.3 385.6 607.1 385.9 Z"> | |
| 734 | + </path> | |
| 735 | + <path class="Puerto Rico" d="M 621.7 385.4 621.5 385.4 620.6 385.7 620 385.7 620.2 385.5 620.6 385.3 621 385.2 621.6 385.3 621.7 385.4 Z"> | |
| 736 | + </path> | |
| 737 | + <path class="Puerto Rico" d="M 612.2 383 612.8 383.1 613.4 383.1 613.7 383.2 614.3 383.1 614.6 383.2 615 383.1 615.2 383.2 615.7 383.1 616.1 383.2 616.4 383.2 617.2 383.4 617.2 383.2 617.8 383.4 617.9 383.3 618.7 383.4 618.9 383.6 619.1 383.6 619.7 383.9 619.9 383.8 619.8 384.5 620 384.8 619.1 385.1 618.8 385.5 618.5 386.1 618.2 386.3 617.9 386.4 617.5 386.4 616.9 386.5 616.6 386.7 615.9 386.6 615.7 386.4 615.2 386.6 614.9 386.3 614.2 386.5 613.3 386.4 613 386.5 612.4 386.6 612.2 386.7 612 386.5 611.6 386.4 611.3 386.6 610.7 386.5 611 385.8 611 385.4 611.2 385 611 384.4 610.8 384.3 610.7 383.9 611.1 383.7 611.3 383.6 611.3 383.1 611.7 382.9 612.2 383 Z"> | |
| 738 | + </path> | |
| 739 | + <path class="French Polynesia" d="M 195.3 679.3 195.2 679.2 195 678.9 195.1 678.7 195.4 678.9 195.3 679.3 Z"> | |
| 740 | + </path> | |
| 741 | + <path class="French Polynesia" d="M 205.6 641.2 205.5 641.3 205.2 641.1 205.4 640.9 205.6 641.2 Z"> | |
| 742 | + </path> | |
| 743 | + <path class="French Polynesia" d="M 199.4 616.5 199.2 616.4 199.4 615.8 199.9 615.4 200.2 615.4 200.2 615.6 200 616 199.4 616.5 Z"> | |
| 744 | + </path> | |
| 745 | + <path class="French Polynesia" d="M 151.7 615.5 152.4 615.7 152.7 616.1 152.7 616.4 152.5 616.5 152 616.4 151.6 615.8 151.5 615.6 150.9 615.8 150.7 615.8 150.1 615.7 149.9 615.3 149.6 614.8 149.6 614.5 149.7 614.3 150.2 614.2 150.7 614.2 151.2 614.4 151.4 614.8 151.5 615.4 151.7 615.5 Z"> | |
| 746 | + </path> | |
| 747 | + <path class="French Polynesia" d="M 148.7 614 148.5 614.6 148 614.4 148.1 614.1 148.6 613.9 148.7 614 Z"> | |
| 748 | + </path> | |
| 749 | + <path class="French Polynesia" d="M 141.1 609.7 140.9 609.8 140.7 609.5 141 609.4 141.1 609.7 Z"> | |
| 750 | + </path> | |
| 751 | + <path class="French Polynesia" d="M 139 610.1 138.7 610.3 138.4 610.3 138.2 609.5 138.4 609.3 139.1 610.1 139 610.1 Z"> | |
| 752 | + </path> | |
| 753 | + <path class="French Polynesia" d="M 138 608.3 138.2 608.4 138.2 608.9 137.8 608.7 137.7 608.4 138 608.3 Z"> | |
| 754 | + </path> | |
| 755 | + <path class="French Polynesia" d="M 166.3 605.4 166 605.5 166.1 605.2 166.3 605.4 Z"> | |
| 756 | + </path> | |
| 757 | + <path class="French Polynesia" d="M 204.3 569.5 204 569.5 204 569.1 203.8 568.8 204.2 568.9 204.3 569.3 204.3 569.5 Z"> | |
| 758 | + </path> | |
| 759 | + <path class="French Polynesia" d="M 201.2 566 201 566 201 565.6 201.2 565.3 201.5 565.5 201.3 565.7 201.2 566 Z"> | |
| 760 | + </path> | |
| 761 | + <path class="French Polynesia" d="M 202.4 564.4 202.4 564.7 202 564.8 201.4 564.8 201.4 565.1 201 565 200.8 564.8 200.7 564.5 201.4 564.1 201.7 564.1 201.9 564.3 202.4 564.4 Z"> | |
| 762 | + </path> | |
| 763 | + <path class="French Polynesia" d="M 195.5 561.8 195.5 562.2 195.5 562.4 195.2 562.4 194.9 562 195.2 561.7 195.5 561.8 Z"> | |
| 764 | + </path> | |
| 765 | + <path class="French Polynesia" d="M 197.9 558.8 198.2 558.9 198.3 559 197.7 559.2 197.6 558.9 197.9 558.8 Z"> | |
| 766 | + </path> | |
| 767 | + <path class="French Polynesia" d="M 194.2 558.2 195.1 558.3 195.4 558.6 195.4 558.8 195 559.1 194.2 559.1 194.1 558.6 194 558.2 194.2 558.2 Z"> | |
| 768 | + </path> | |
| 769 | + <path class="Solomon Islands" d="M 1926.8 576.2 1927.1 576.4 1927 576.8 1927.3 576.8 1927.3 577 1926.5 577 1926.2 576.5 1926.4 576.2 1926.8 576.2 Z"> | |
| 770 | + </path> | |
| 771 | + <path class="Solomon Islands" d="M 1888.5 575.6 1888.7 575.6 1889.7 576.4 1890.2 576.7 1891.1 577.4 1891 577.7 1890.8 577.7 1890.6 577.9 1890.4 577.6 1890.3 577.6 1890.1 577.4 1890 577 1889.6 576.9 1889.6 576.6 1889.3 576.5 1889.2 576.6 1889 576.6 1888.1 576.2 1887.9 575.8 1888.2 575.4 1888.5 575.6 Z"> | |
| 772 | + </path> | |
| 773 | + <path class="Solomon Islands" d="M 1925.4 574.1 1925.2 574.4 1924.9 574.2 1924.9 574 1925.2 573.8 1925.4 574.1 Z"> | |
| 774 | + </path> | |
| 775 | + <path class="Solomon Islands" d="M 1923.1 570.2 1923.2 570.3 1923.5 570.3 1923.4 570.7 1922.9 570.9 1922.6 570.9 1922.2 571 1921.8 571.6 1921.7 571.3 1921.4 571.4 1921.2 571.2 1921.2 570.9 1921.7 570.8 1921.7 570.6 1922 570.2 1922.7 570.3 1923.1 570.2 Z"> | |
| 776 | + </path> | |
| 777 | + <path class="Solomon Islands" d="M 1896.8 567.4 1897.1 567.3 1897.7 567.6 1898.2 568.1 1898.5 568.4 1898.7 568.4 1898.9 568.6 1899.1 568.6 1899.4 568.9 1899.7 568.8 1900.1 568.9 1900.3 569.1 1900.7 569.1 1901 569 1901.2 569.3 1901.4 570.1 1901.7 570.4 1901.7 570.9 1901.5 571.1 1901.6 571.4 1901.2 571.2 1900.9 571.4 1900.6 571.3 1900.1 571 1899.9 571 1899.3 570.8 1898.9 570.7 1898.7 570.4 1898.4 570.2 1898.1 570.1 1897.9 569.8 1897.5 569.6 1897.7 569.2 1897.6 568.8 1897.6 568.4 1897.4 568.3 1896.8 568.1 1896.5 568.2 1896.4 568 1896.5 567.7 1896.6 567.4 1896.8 567.4 Z"> | |
| 778 | + </path> | |
| 779 | + <path class="Solomon Islands" d="M 1900.8 564 1900.7 564.2 1900.7 564.9 1900.5 564.7 1900.5 564.2 1900.8 564 Z"> | |
| 780 | + </path> | |
| 781 | + <path class="Solomon Islands" d="M 1897.9 562.2 1898.2 563.1 1898.4 563.5 1898.4 563.9 1898.5 564.4 1898.3 564.7 1897.9 564 1897.8 564.3 1897.5 563.8 1897.6 562.8 1897.7 562.6 1897.7 562.3 1897.9 562.2 Z"> | |
| 782 | + </path> | |
| 783 | + <path class="Solomon Islands" d="M 1889.5 562.4 1889.8 562.4 1890.1 562.3 1890.5 562.4 1890.6 562.3 1891.1 562.3 1891.9 562.3 1892.1 562.6 1892.4 562.7 1892.6 563 1892.8 563 1893 563.1 1893.4 563.5 1893.4 564.1 1893.6 564.1 1894 564.3 1894.1 564.6 1894.1 565.2 1894 565.3 1893.3 565.5 1893.1 565.6 1892.7 565.5 1892.3 565.3 1891.9 565.3 1891.8 565.1 1891.3 564.8 1890.7 564.8 1890.2 564.9 1889.7 564.9 1889.2 564.8 1888.9 564.6 1888.6 564.7 1888.3 564.3 1888 564.1 1887.7 563.3 1887.5 562.9 1887.7 562.7 1887.6 562.5 1887.6 561.8 1887.7 561.5 1888.3 561.2 1888.7 561.4 1889.1 561.9 1889.2 562.2 1889.5 562.4 Z"> | |
| 784 | + </path> | |
| 785 | + <path class="Solomon Islands" d="M 1891.8 560 1892.3 560.5 1892 560.8 1891.7 560.6 1891.7 560.5 1891.8 560 Z"> | |
| 786 | + </path> | |
| 787 | + <path class="Solomon Islands" d="M 1891.8 560 1891.6 560.4 1891.5 560.4 1891.1 560.1 1890.8 560.2 1890.6 560.1 1890.5 559.9 1890.8 559.8 1891 559.6 1891.5 559.7 1891.8 560 Z"> | |
| 788 | + </path> | |
| 789 | + <path class="Solomon Islands" d="M 1885.2 559.5 1885.3 559.7 1885.5 559.8 1885.3 560.2 1884.9 560.4 1884.6 560 1885.2 559.5 Z"> | |
| 790 | + </path> | |
| 791 | + <path class="Solomon Islands" d="M 1876.7 557.7 1876.9 558 1876.8 558.3 1876 557.9 1876.5 557.6 1876.7 557.7 Z"> | |
| 792 | + </path> | |
| 793 | + <path class="Solomon Islands" d="M 1880.1 558.5 1879.8 558.3 1879.9 558 1880.2 557.7 1880.2 558.2 1880.3 558.4 1880.1 558.5 Z"> | |
| 794 | + </path> | |
| 795 | + <path class="Solomon Islands" d="M 1879.2 556.7 1879.4 556.6 1879.8 556.6 1879.8 556.8 1879.4 557.1 1879.6 557.3 1879.6 557.6 1879.2 558 1879 558.1 1878.7 558 1878.3 557.6 1878.4 557 1878.7 556.9 1878.9 556.5 1879.2 556.4 1879.2 556.7 Z"> | |
| 796 | + </path> | |
| 797 | + <path class="Solomon Islands" d="M 1875.6 556.8 1875.4 556.9 1875.2 557.4 1875.6 557.9 1875.1 557.7 1875.1 557.5 1875 557.2 1874.6 556.8 1874.6 556.6 1875 556.4 1875.1 556.1 1875.5 555.8 1875.8 556 1875.8 556.4 1875.6 556.8 Z"> | |
| 798 | + </path> | |
| 799 | + <path class="Solomon Islands" d="M 1888.5 556.3 1888.7 556.5 1888.6 556.7 1888.4 556.6 1887.8 556.1 1888 555.7 1888.3 555.6 1888.5 555.8 1888.4 556.1 1888.5 556.3 Z"> | |
| 800 | + </path> | |
| 801 | + <path class="Solomon Islands" d="M 1895 555.5 1895.4 556.3 1896 557.1 1896 557.4 1895.6 557.6 1895.6 558.1 1895.6 558.4 1895.7 558.6 1895.9 558.6 1896.1 558.9 1896.6 559.3 1896.7 559.8 1896.8 560.1 1897 560.3 1896.8 560.5 1897.1 560.8 1897 560.9 1897.1 561.4 1896.9 561.6 1897.2 561.9 1897.3 562.4 1897.5 562.9 1897.5 563.5 1897.4 563.6 1897.1 563.3 1896.8 562.8 1896.8 562.5 1896.5 562.1 1896.1 561.8 1895.8 561.5 1895.3 560.9 1894.9 560.6 1894.6 559.7 1894.4 558.7 1894.2 558 1894.2 557.5 1894.1 557.1 1894.5 556.7 1894.4 556.4 1893.9 555.7 1893.8 555.4 1893.9 555.3 1894.3 555.4 1894.5 555.5 1894.8 555.2 1895 555.5 Z"> | |
| 802 | + </path> | |
| 803 | + <path class="Solomon Islands" d="M 1874.3 555.2 1874.1 554.8 1873.8 554.5 1874.2 554.5 1874.3 555.2 Z"> | |
| 804 | + </path> | |
| 805 | + <path class="Solomon Islands" d="M 1874.2 554.2 1874.6 554.3 1874.7 554.4 1874.8 554.9 1874.6 554.9 1874.4 554.5 1874.2 554.2 Z"> | |
| 806 | + </path> | |
| 807 | + <path class="Solomon Islands" d="M 1877.3 554.4 1877.2 554.7 1877.4 554.7 1877.9 554.7 1878.3 555.4 1878.3 555.6 1878.5 555.9 1878.6 556.4 1878.4 556.6 1878.4 556.9 1877.8 556.7 1877.6 556.5 1877.3 556.5 1876.9 556.2 1876.8 556 1876.7 555.7 1877.1 555.8 1876.8 555 1876.7 554.8 1876.2 554.9 1876 555 1875.6 555 1875.3 555.4 1874.9 555.2 1874.9 554.6 1875.1 554.4 1875.5 554.4 1875.5 554 1875.9 553.4 1876.3 553.1 1876.6 552.9 1877 553.2 1877.2 553.6 1877.1 554.3 1877.3 554.4 Z"> | |
| 808 | + </path> | |
| 809 | + <path class="Solomon Islands" d="M 1871.4 554.4 1871.3 554.4 1871.1 553.6 1871.2 553.3 1871.2 552.9 1871.4 553 1871.5 553.4 1871.5 554.1 1871.4 554.4 Z"> | |
| 810 | + </path> | |
| 811 | + <path class="Solomon Islands" d="M 1874.8 553.9 1874.5 554.1 1874.4 554 1874 554 1873.7 553.7 1873.6 553.4 1873.5 553.1 1873.6 552.8 1873.9 552.4 1874.4 552.2 1874.6 552.4 1874.9 552.9 1874.9 553.5 1874.8 553.9 Z"> | |
| 812 | + </path> | |
| 813 | + <path class="Solomon Islands" d="M 1871.8 550.4 1872.2 550.9 1872.2 551 1872.9 551.4 1872.8 551.7 1872.2 552.4 1872.2 552.7 1871.9 552.8 1871.9 552.4 1871.7 552.4 1871.7 552 1871.5 551.6 1871.2 551.4 1871.1 551.1 1871.2 550.8 1871.4 550.6 1871.6 550.4 1871.8 550.4 Z"> | |
| 814 | + </path> | |
| 815 | + <path class="Solomon Islands" d="M 1882.1 550.5 1882.4 550.7 1882.2 550.8 1881.7 550.9 1881.6 550.6 1881.8 550.4 1882.1 550.5 Z"> | |
| 816 | + </path> | |
| 817 | + <path class="Solomon Islands" d="M 1883 550.5 1883.2 550.4 1883.7 550.7 1884 550.8 1884.2 551 1884.5 551.5 1884.8 551.6 1885.1 551.9 1885.1 552.2 1885.6 552.3 1885.6 552.6 1885.9 552.7 1886.2 552.6 1886.3 552.7 1886.7 552.9 1886.8 553.1 1887.1 553 1887.4 553.1 1887.5 553.4 1887.9 553.7 1888.2 554 1888.6 554.3 1889.6 555.3 1889.3 555.7 1889.6 556.2 1889.7 556.8 1889.4 556.7 1889.1 556.3 1888.4 555.5 1888 555.4 1887.5 555 1886.9 554.9 1886.8 554.6 1886.2 554.4 1885.7 553.9 1885.5 553.9 1884.7 553.3 1884.2 553 1884.2 552.8 1883.8 552.6 1883.6 552.4 1883.4 552 1883 551.7 1882.9 551.5 1882.9 551.1 1882.5 550.6 1882.3 550.5 1882.3 550.2 1883 550.5 Z"> | |
| 818 | + </path> | |
| 819 | + <path class="Solomon Islands" d="M 1881.5 549.9 1881.7 550.1 1881.9 550 1882.2 550.2 1882.1 550.4 1881.8 550.3 1881.5 549.9 Z"> | |
| 820 | + </path> | |
| 821 | + <path class="Solomon Islands" d="M 1877 549.3 1877.5 549.3 1877.3 549.5 1877 549.4 1877 549.3 Z"> | |
| 822 | + </path> | |
| 823 | + <path class="Solomon Islands" d="M 1881.3 549.3 1881.1 549.8 1881 549.6 1881.3 549.3 Z"> | |
| 824 | + </path> | |
| 825 | + <path class="Solomon Islands" d="M 1878.6 549.7 1878.1 549.8 1877.9 549.6 1878.2 549.2 1878.5 549.3 1878.6 549.7 Z"> | |
| 826 | + </path> | |
| 827 | + <path class="Solomon Islands" d="M 1866.1 548.8 1866.3 549.1 1866.1 549.3 1865.8 549.2 1865.7 549.1 1866.1 548.8 Z"> | |
| 828 | + </path> | |
| 829 | + <path class="Solomon Islands" d="M 1867.6 546.9 1867.9 547.1 1867.8 547.4 1867.1 547.6 1866.8 547.3 1867.1 546.7 1867.3 546.6 1867.6 546.9 Z"> | |
| 830 | + </path> | |
| 831 | + <path class="Solomon Islands" d="M 1869.3 546 1869.3 546.5 1869.1 546.3 1869.1 546.1 1869.3 546 Z"> | |
| 832 | + </path> | |
| 833 | + <path class="Solomon Islands" d="M 1872.2 544.5 1872.5 544.6 1872.9 545.1 1873.2 545.3 1873.5 545.4 1873.8 545.7 1874.6 546.1 1875 546.6 1875 547 1875.1 547.6 1875.3 547.8 1875.6 548.1 1875.8 548.1 1875.9 548.5 1876.6 548.8 1877 548.7 1877.1 548.8 1877.1 549.1 1876.8 549.2 1876.6 549.5 1876.1 549.3 1875.3 548.9 1874.8 548.9 1874.5 548.5 1873.8 548.1 1873.2 547.1 1872.6 546.1 1872.1 545.8 1871.4 545.1 1871.4 544.6 1871.4 544.4 1871.7 544.2 1872 544.3 1872.2 544.5 Z"> | |
| 834 | + </path> | |
| 835 | + <path class="São Tomé and Principe" d="M 1023.9 501.3 1023.7 501.3 1023.5 501.6 1023.3 501.6 1023.2 501.2 1023 500.4 1023 500.1 1023.4 499.6 1023.7 499.5 1024 499.2 1024.4 499.3 1024.6 499.7 1024.7 500 1024.6 500.4 1024.4 500.7 1024.1 501.1 1023.9 501.3 Z"> | |
| 836 | + </path> | |
| 837 | + <path class="São Tomé and Principe" d="M 1028.5 491.5 1028.4 491.8 1028.2 491.9 1027.9 491.8 1027.9 491.5 1028.1 491.4 1028.1 491.1 1028.3 490.9 1028.6 491 1028.5 491.5 Z"> | |
| 838 | + </path> | |
| 839 | + <path d="M634.2 386l0 0.1-0.4-0.1 0-0.1 0.4 0.1z" id="SX" name="Sint Maarten"> | |
| 840 | + </path> | |
| 841 | + <path class="Seychelles" d="M 1249 562.1 1248.7 562.3 1248.2 562.3 1247.8 562.5 1247.4 562.5 1247.4 562.3 1247.9 562.3 1248.3 562.2 1248.7 561.9 1248.9 561.9 1249 562.1 Z"> | |
| 842 | + </path> | |
| 843 | + <path class="Seychelles" d="M 1248.5 561.8 1248.3 561.9 1247.9 561.9 1247.9 561.7 1248.5 561.8 Z"> | |
| 844 | + </path> | |
| 845 | + <path class="Seychelles" d="M 1300.4 531.5 1300.8 531.9 1300.6 532.2 1300.4 531.9 1300.1 531.7 1300.3 531.2 1300.4 531.5 Z"> | |
| 846 | + </path> | |
| 847 | + <path class="Turks and Caicos Islands" d="M 587.7 361.6 588.4 361.6 588.7 362 588.4 362 588.1 361.9 587.6 362 587.5 361.7 587.7 361.6 Z"> | |
| 848 | + </path> | |
| 849 | + <path class="Turks and Caicos Islands" d="M 585 361.5 585.3 361.9 585.9 361.8 585.7 362 585.1 362 584.7 361.8 585 361.5 Z"> | |
| 850 | + </path> | |
| 851 | + <path class="Turks and Caicos Islands" d="M 587.2 360.9 587.2 361.4 586.7 361.2 586.6 360.9 586.7 360.8 587.2 360.9 Z"> | |
| 852 | + </path> | |
| 853 | + <path class="Tonga" d="M 14.7 639.5 14.2 639.2 14.2 639 14.5 638.8 14.7 639.5 Z"> | |
| 854 | + </path> | |
| 855 | + <path class="Tonga" d="M 11.9 637.4 12.2 637.4 12.6 637.7 12.9 637.8 13.1 637.4 13.4 637.7 13.1 638 13.2 638.2 13.1 638.4 12.9 638.3 12.6 638 11.9 637.8 11.9 637.4 Z"> | |
| 856 | + </path> | |
| 857 | + <path class="Tonga" d="M 11.1 628.8 10.9 628.9 10.8 628.4 11 628.5 11.1 628.8 Z"> | |
| 858 | + </path> | |
| 859 | + <path class="Tonga" d="M 15.5 621.3 15 621.6 14.8 621.6 14.7 621.5 15 621 15.3 621.1 15.5 621.3 Z"> | |
| 860 | + </path> | |
| 861 | + <path class="Tonga" d="M 1.4 602 1.3 602.1 1 602 1.1 601.8 1.4 601.8 1.4 602 Z"> | |
| 862 | + </path> | |
| 863 | + <path class="Trinidad and Tobago" d="M 642.8 432.2 642.6 432.7 642.3 433.1 642.1 433.2 642 433.6 642.2 434.2 642 434.3 642 435 642.2 435.3 642.4 435.5 642.2 435.7 642.1 436 642.1 436.7 641.9 436.8 641.5 437 641 437.1 640.7 437.1 640.2 437.2 639.7 437.1 639.3 437.2 639 437.1 638.5 437.3 638.1 437.1 637.5 437.1 637.2 437.3 636.9 437.2 637.4 437 637.7 436.7 638.1 436.6 638.5 436.3 638.7 436 639.3 436.1 639.6 435.7 639.4 434.9 639.6 434.5 639.6 434 639.6 433.8 639.3 433.4 639 433.3 638.6 433.2 638.5 433.1 638.9 432.8 640 432.7 640.2 432.5 641.3 432.5 641.4 432.4 642.6 432.2 642.8 432.2 Z"> | |
| 864 | + </path> | |
| 865 | + <path class="Trinidad and Tobago" d="M 645.2 429 645.1 429.5 644.8 429.6 644.4 429.9 644.2 429.9 643.5 430.3 643.3 430.1 643.6 429.8 644.3 429.3 645.2 429 Z"> | |
| 866 | + </path> | |
| 867 | + <path d="M1998.9 556.6l0-0.2 0.1 0 0 0.1-0.1 0.1z" id="TV" name="Tuvalu"> | |
| 868 | + </path> | |
| 869 | + <path d="M642.2 417.4l-0.1 0.1-0.5-0.3 0-0.4 0.2-0.3 0.2-0.5 0.4 0 0.1 0.4-0.1 0.8-0.2 0.2z" id="VC" name="Saint Vincent and the Grenadines"> | |
| 870 | + </path> | |
| 871 | + <path d="M626.1 383.4l-0.4 0.2-0.1 0-0.3 0.2-0.1-0.1 0.1-0.3 0.6-0.1 0.2 0.1z" id="VG" name="British Virgin Islands"> | |
| 872 | + </path> | |
| 873 | + <path class="United States Virgin Islands" d="M 624.5 387.8 624.8 388.1 623.7 388.3 623.6 387.8 624.3 387.6 624.5 387.8 Z"> | |
| 874 | + </path> | |
| 875 | + <path class="United States Virgin Islands" d="M 625.2 384 625 384.2 624.5 384.2 624.5 384 624.8 383.9 625.2 384 Z"> | |
| 876 | + </path> | |
| 877 | + <path class="United States Virgin Islands" d="M 623.8 383.8 624.2 384.1 624 384.3 623.7 384.1 623.2 384.1 623.4 383.9 623.8 383.8 Z"> | |
| 878 | + </path> | |
| 879 | + <path class="Vanuatu" d="M 1931.8 631.8 1931.3 631.6 1931.5 631.2 1932 631.1 1932.2 631.5 1932.1 631.7 1931.8 631.8 Z"> | |
| 880 | + </path> | |
| 881 | + <path class="Vanuatu" d="M 1930.6 628 1930.4 628 1930.1 627.9 1929.7 627.4 1929.6 627 1929.6 626.6 1929.8 626.4 1929.9 626 1930.2 625.9 1930.6 625.9 1930.4 626.2 1930.4 626.7 1931.1 627.2 1930.8 627.5 1930.6 628 Z"> | |
| 882 | + </path> | |
| 883 | + <path class="Vanuatu" d="M 1930.6 622.1 1930.9 622.1 1931 622.3 1930.5 622.4 1930.6 622.7 1931 622.9 1931.1 623.2 1931 623.6 1930.6 623.7 1930.2 623.4 1929.8 623.4 1929.6 623.2 1929.3 623 1929.4 622.6 1929.4 622.3 1929.6 621.8 1929.9 621.4 1930.4 621.4 1930.6 621.5 1930.5 621.9 1930.6 622.1 Z"> | |
| 884 | + </path> | |
| 885 | + <path class="Vanuatu" d="M 1927.6 614.5 1928.2 614.4 1928.3 614.5 1928.5 615.1 1928.7 615.4 1928.8 615.5 1928.4 616.1 1927.6 616.3 1927.3 616 1927.2 615.5 1926.9 615.4 1926.5 615.7 1926.4 615.6 1926.6 615.2 1927 614.9 1927.4 614.4 1927.6 614.5 Z"> | |
| 886 | + </path> | |
| 887 | + <path class="Vanuatu" d="M 1929.9 610.6 1929.8 610.3 1930.1 610.3 1929.9 610.6 Z"> | |
| 888 | + </path> | |
| 889 | + <path class="Vanuatu" d="M 1928.3 608.2 1928.4 608.7 1928.4 608.9 1928.8 609 1929 609.5 1929.5 609.5 1929.5 609.9 1929.4 610 1929.1 609.7 1928.8 609.6 1928.2 609.8 1927.9 609.7 1927.8 609.5 1927.7 609.1 1927.9 608.6 1928.1 608.3 1928.3 608.2 Z"> | |
| 890 | + </path> | |
| 891 | + <path class="Vanuatu" d="M 1929.2 608 1929.1 608 1929 607.7 1929.2 607.7 1929.2 608 Z"> | |
| 892 | + </path> | |
| 893 | + <path class="Vanuatu" d="M 1929.4 606.6 1929.2 606.8 1928.6 606.8 1928.3 606.9 1928.1 606.7 1927.7 606.6 1927.2 606.2 1927.3 606 1927.8 605.8 1928.2 605.7 1928.6 605.4 1928.7 605.1 1928.9 605.1 1929 605.4 1929 606 1929.5 606.4 1929.4 606.6 Z"> | |
| 894 | + </path> | |
| 895 | + <path class="Vanuatu" d="M 1924.7 605.3 1924.9 605.6 1925.5 606 1926.2 606.7 1926.3 606.8 1926.3 607.2 1926.5 607.5 1926.2 607.7 1925.8 608.1 1925.8 607.9 1925.5 607.8 1925.1 607.8 1924.9 607.9 1924.9 608.1 1924.5 608.3 1924.2 608.2 1924.2 607.5 1924.1 606.9 1924.2 606.3 1924.3 605.8 1924.1 605.3 1923.5 605.6 1923.3 605.4 1923.2 605.1 1923.2 604.8 1923.5 604.5 1923.5 604.1 1923.6 603.8 1923.9 603.7 1924.5 604 1924.4 604.2 1924.6 604.7 1924.5 604.9 1924.7 605.3 Z"> | |
| 896 | + </path> | |
| 897 | + <path class="Vanuatu" d="M 1924.1 602.9 1923.8 602.9 1923.3 602.6 1923.3 602.5 1923.5 602.2 1923.7 602.1 1924.2 602.2 1924.1 602.6 1924.1 602.9 Z"> | |
| 898 | + </path> | |
| 899 | + <path class="Vanuatu" d="M 1923.6 601.9 1924 601.6 1924.1 601.7 1924.2 602 1923.7 602 1923.6 601.9 Z"> | |
| 900 | + </path> | |
| 901 | + <path class="Vanuatu" d="M 1929.3 604.5 1929.1 604.4 1929.2 603.2 1929.1 602.9 1929.2 602.4 1929.5 601.6 1929.6 601.1 1929.8 601.7 1929.7 602 1929.7 602.6 1929.7 603.6 1929.6 604.2 1929.3 604.5 Z"> | |
| 902 | + </path> | |
| 903 | + <path class="Vanuatu" d="M 1928 601.2 1927.3 601.2 1927 601.1 1927 600.9 1927.9 600.2 1928.4 600 1928.9 599.9 1928.9 600.1 1928.6 600.5 1928.4 600.8 1928 601.2 Z"> | |
| 904 | + </path> | |
| 905 | + <path class="Vanuatu" d="M 1929.9 600.5 1929.9 600.7 1929.6 600.7 1929.7 600.4 1929.9 599.1 1929.9 599 1930 597.8 1930.2 597.9 1930.1 599.2 1930.2 599.5 1930.1 600 1929.9 600.5 Z"> | |
| 906 | + </path> | |
| 907 | + <path class="Vanuatu" d="M 1923.6 601.9 1922.7 601.8 1922.1 602.1 1922 602.3 1921.8 602.3 1921.7 601.7 1921.5 601.6 1921.1 600.6 1921.5 599.8 1921.5 599.4 1921.4 599.2 1921.4 597.8 1921.3 597.5 1921.3 597 1921.5 596.7 1921.5 596.4 1921.7 596.1 1921.7 595.9 1922 595.8 1922.1 596.3 1922.3 596.7 1922.5 597 1922.6 598.3 1922.5 599.1 1922.5 599.2 1923.1 599.1 1923.4 598.8 1923.6 598 1923.6 597.8 1923.9 597.6 1924.2 597.7 1924.1 598.1 1924.1 599 1924.4 599.1 1924.4 599.6 1924.2 599.7 1924.5 600.1 1924.3 600.3 1924.3 600.8 1924.5 601.3 1924.4 601.5 1924.1 601.5 1923.6 601.9 Z"> | |
| 908 | + </path> | |
| 909 | + <path class="Vanuatu" d="M 1927.8 592.6 1928.2 592.9 1928.1 593.3 1927.8 593.6 1927.6 593.8 1927 593.8 1927 593.4 1927.1 593 1927.3 592.7 1927.8 592.6 Z"> | |
| 910 | + </path> | |
| 911 | + <path class="Vanuatu" d="M 1928.4 590.7 1928.1 590.9 1927.9 591 1927.9 591.2 1927.7 591.3 1927.7 591 1927.5 590.8 1927.4 590.5 1927.5 590.1 1927.7 590 1928.1 589.8 1928.2 590 1928.3 590.4 1928.3 590.5 1928.4 590.7 Z"> | |
| 912 | + </path> | |
| 913 | + <path class="Vanuatu" d="M 1923.4 586.5 1923.3 586.4 1923.4 585.9 1923.6 586.1 1923.4 586.5 Z"> | |
| 914 | + </path> | |
| 915 | + <path class="Samoa" d="M 21.9 591.3 22.2 591.4 22.5 591.5 22.8 591.7 22.7 592 21.7 592 21.4 591.9 21.1 592 20.9 591.9 20.5 591.7 20.2 591.8 19.9 591.6 19.7 591.4 19.1 591.1 19.2 590.7 19.6 590.5 20 590.5 20.5 590.5 21.4 590.9 21.9 591 21.9 591.3 Z"> | |
| 916 | + </path> | |
| 917 | + <path class="Samoa" d="M 17.4 588.5 18 589.1 18.2 589.8 18.1 590.1 18.2 590.4 17.9 590.4 17.6 590.2 17.3 590.4 16.4 590.5 16.1 590.2 15.9 589.8 15.7 589.7 15.4 589.4 14.9 589 14.7 588.8 14.7 588.6 15.1 588.7 15.4 588.6 16 588.4 16.4 588.4 16.9 588.3 17.2 588.3 17.4 588.5 Z"> | |
| 918 | + </path> | |
| 919 | + <path d="M602 424.6l-0.2 0-0.1-0.5 0.1-0.3-0.1-0.3-0.4-0.1-0.3-0.3 0.1-0.3 1.2 0.6-0.1 0.2 0 0.4-0.2 0.2 0 0.4z" id="BQBO" name="Netherlands"> | |
| 920 | + </path> | |
| 921 | + <path d="M634.3 389.6l-0.2 0.1 0-0.2 0.1-0.1 0.1 0.2z" id="BQSE" name="St. Eustatius (Netherlands)"> | |
| 922 | + </path> | |
| 923 | + <path d="M632.8 388.7l-0.1 0 0.1-0.2 0.1 0.1-0.1 0.1z" id="BQSA" name="Saba (Netherlands)"> | |
| 924 | + </path> | |
| 925 | + <path d="M644 406.9l0 0.2 0.4-0.1-0.2 0.5 0.2 0.2 0 0.2 0.2 0.2 0.2 0.9-0.3 0.3-0.1-0.4-0.1 0.1-0.6-0.1-0.4 0-0.2-0.3 0.6-0.5-0.4 0-0.4-0.4-0.1-0.5-0.2-0.5 0.3-0.4 0.4 0.1 0.5 0.3 0.2 0.2z" id="MQ" name="Martinique"> | |
| 926 | + </path> | |
| 927 | + <path class="Canary Islands (Spain)" d="M 888.4 323.4 888.1 323.9 887.7 324.4 887.4 324 887 324 886.8 323.8 887 323.5 887.4 323.6 887.8 323.2 888.1 323 888.3 323.1 888.4 323.4 Z"> | |
| 928 | + </path> | |
| 929 | + <path class="Canary Islands (Spain)" d="M 902 321.1 902 321.6 902.2 322 902 322.7 902.1 323 901.7 323.4 901.2 323.6 901 323.8 900.4 323.6 899.9 323.1 899.7 322.7 899.7 322.1 900.3 321.7 900.4 321.2 900.4 321 901 321.1 901.4 321.1 901.7 321.2 902 321.1 Z"> | |
| 930 | + </path> | |
| 931 | + <path class="Canary Islands (Spain)" d="M 892.1 321.9 891.9 321.9 891.6 321.7 891.4 321.4 891.5 321 891.6 320.7 891.9 320.7 892.2 320.7 892.7 321.1 892.8 321.4 892.3 321.9 892.1 321.9 Z"> | |
| 932 | + </path> | |
| 933 | + <path class="Canary Islands (Spain)" d="M 898.2 318.4 898.2 318.6 897.6 318.9 897.2 319.4 896.9 319.6 896.9 320 896.5 320.7 896.4 321.1 895.9 321.7 895.8 321.9 895.6 321.9 895 322.1 894.9 322 894.8 321.6 894.5 321.2 894.4 321 894.2 320.7 894.2 320.4 893.8 319.8 894.3 319.5 894.6 319.7 895.2 319.5 895.6 319.5 896.1 319.3 896.6 318.9 896.6 318.7 897.2 318.4 897.8 318.4 898.1 318.3 898.2 318.4 Z"> | |
| 934 | + </path> | |
| 935 | + <path class="Canary Islands (Spain)" d="M 908.4 321.2 908.1 321.6 907.7 321.7 907.4 321.6 907 321.6 907 321.4 907.3 321.4 907.9 321.2 908.3 320.9 908.6 320.6 908.7 320.1 908.8 319.8 909 319.3 909.3 318.9 909.6 318.3 909.8 317.5 910 317.3 910.4 317.2 910.7 317.5 910.8 318 910.7 318.5 910.6 319 910.6 319.5 910.5 319.6 910.2 320.3 909.9 320.6 909.3 320.7 908.6 321 908.4 321.2 Z"> | |
| 936 | + </path> | |
| 937 | + <path class="Canary Islands (Spain)" d="M 888.8 316.7 889.1 316.6 889.3 316.9 889.5 317.4 889.3 317.7 889.4 318.1 888.8 319.1 888.7 319 888.6 318.6 888.2 317.7 888.1 317.4 888 317.2 888.2 316.8 888.5 316.6 888.8 316.7 Z"> | |
| 938 | + </path> | |
| 939 | + <path class="Canary Islands (Spain)" d="M 912.9 314.7 912.9 315.1 912.7 315.6 912 316.1 911.5 316.2 911.1 316.7 910.6 316.5 910.6 316.4 910.8 316 910.8 315.6 911 315.3 911.3 315.1 911.6 315.1 911.9 314.8 912.4 314.8 912.5 314.7 912.7 314.2 912.9 314.1 913.1 314.3 912.9 314.7 Z"> | |
| 940 | + </path> | |
| 941 | + <path d="M1240.2 583.1l0.2 0.3 0.5 0.2 0 0.3-0.2 0.2 0.1 0.2-0.3 0.6 0.1 0.2-0.3 0.1-0.2-0.3 0-0.3 0.2-0.2-0.2-0.7-0.1-0.1-0.1-0.2 0.3-0.3z" id="YT" name="Mayotte"> | |
| 942 | + </path> | |
| 943 | + <path d="M1295 635.8l0.4 0 0.4 0.2 0.3 0.3 0 0.3 0.1 0.5 0.3 0.2 0.2 0.2 0.1 0.2-0.2 0.6-0.1 0.4-0.2 0.2-0.4 0.1-0.9 0-0.2-0.2-0.8-0.4-0.3-0.5 0-0.3-0.3-0.6 0.1-0.4 0.2-0.2 0.2-0.4 0.1 0 0.5-0.2 0.5 0z" id="RE" name="Reunion"> | |
| 944 | + </path> | |
| 945 | + <path class="Guadeloupe" d="M 643 399.9 642.7 399.9 642.5 399.5 642.6 399.3 642.9 399 643.2 399.2 643.3 399.5 643.3 399.7 643 399.9 Z"> | |
| 946 | + </path> | |
| 947 | + <path class="Guadeloupe" d="M 641.2 397.3 641.5 397.3 641.5 397.5 641.5 397.6 641.3 397.7 641.3 398.5 641.3 398.9 641.1 399.1 640.5 399.4 640.5 399.2 640.3 399 640.2 398.5 640.2 398 640.1 397.5 640.1 397.2 640.3 396.9 640.6 396.8 641.1 397.1 641.2 397.3 Z"> | |
| 948 | + </path> | |
| 949 | + <path class="Guadeloupe" d="M 642.6 396.9 643 397 643.5 397.4 642.2 397.7 641.9 397.8 641.5 397.5 641.6 396.9 641.8 396.8 641.7 396.2 641.8 396 642.1 395.8 642.4 396.2 642.4 396.6 642.6 396.9 Z"> | |
| 950 | + </path> | |
| 951 | + <path class="Fiji" d="M 1989.1 624.9 1988.8 625 1988.9 624.6 1989.1 624.9 Z"> | |
| 952 | + </path> | |
| 953 | + <path class="Fiji" d="M 1981.6 623.3 1981.8 623.5 1982.1 623.6 1982.1 623.9 1981.6 624.1 1981.1 623.8 1980.6 624.1 1980.3 624.1 1980.1 624.4 1980.2 624.7 1979.8 624.6 1979.7 624.8 1979.3 624.7 1979.2 624.8 1978.8 624.6 1979.1 624.5 1979.3 624.4 1979.5 624.2 1979.9 624.3 1980.2 624 1980.4 623.7 1980.9 623.6 1981.4 623.3 1981.6 623.3 Z"> | |
| 954 | + </path> | |
| 955 | + <path class="Fiji" d="M 1982.5 623.2 1982.3 623.3 1982.1 623.2 1982.3 623 1982.5 623.2 Z"> | |
| 956 | + </path> | |
| 957 | + <path class="Fiji" d="M 1988.4 617.1 1988.5 617.6 1988.3 618.2 1988.2 618 1988.1 617.6 1987.9 617.6 1988 617.1 1988.2 617 1988.4 617.1 Z"> | |
| 958 | + </path> | |
| 959 | + <path class="Fiji" d="M 1985.8 615.7 1985.7 615.7 1985.6 615.3 1985.8 615.1 1986.2 615 1986.2 615.4 1986 615.6 1985.8 615.7 Z"> | |
| 960 | + </path> | |
| 961 | + <path class="Fiji" d="M 1983.6 613.2 1983.5 613.6 1983.8 613.8 1983.9 614.1 1984.2 614.5 1984.5 614.6 1984.6 614.9 1984.8 614.9 1984.9 615.3 1984.6 615.8 1984.7 616 1984.8 616.5 1984.5 616.7 1984.5 617.2 1984.7 617.2 1984.8 617.7 1984.5 618 1983.9 618.2 1983.8 618 1983.5 618.1 1983.4 618.3 1983.1 618 1982.3 618.3 1981.5 619 1981.2 618.9 1980.8 619.1 1980.5 619 1979.9 619.1 1979.6 618.9 1978.7 618.6 1978.4 618.5 1977.8 618.4 1977.6 618.3 1977.1 618.1 1976.9 617.1 1977.2 616.4 1977.5 616.4 1977.8 616.3 1977.9 615.8 1978.2 615.7 1978.3 615.3 1978.1 615.1 1978.7 614.7 1979.1 614.2 1979.2 614.3 1979.8 613.8 1980 613.8 1980.9 613.4 1981.5 613.6 1981.8 613.5 1982.3 613.3 1982.9 613.2 1983.2 612.9 1983.6 613.2 Z"> | |
| 962 | + </path> | |
| 963 | + <path class="Fiji" d="M 1989.9 613.4 1989.7 613.5 1989.9 612.6 1990.3 612.6 1990.2 613 1989.9 613.4 Z"> | |
| 964 | + </path> | |
| 965 | + <path class="Fiji" d="M 1978.5 611.4 1978.3 611.7 1977.8 612 1977.8 611.7 1978.2 611.3 1978.5 611.4 Z"> | |
| 966 | + </path> | |
| 967 | + <path class="Fiji" d="M 1993.4 611 1993.2 610.8 1993.3 610.6 1993.8 610.1 1994.1 609.6 1993.8 610.7 1993.4 611 Z"> | |
| 968 | + </path> | |
| 969 | + <path class="Fiji" d="M 1994.4 606 1994 606.4 1993.2 607.5 1992.9 607.6 1992.2 608 1992 608.6 1991.6 608.8 1991.4 609.1 1991.3 609.3 1991.6 609.4 1992.2 609.1 1992.3 609 1992.6 608.7 1992.8 608.4 1993.4 608.1 1993.7 607.8 1994.3 607.5 1994.3 607.8 1993.8 608.5 1993.6 608.6 1993.7 609.2 1993.4 609.5 1993.1 609.2 1992.7 609.2 1992.2 609.3 1991.8 609.6 1991.1 609.7 1990.1 609.7 1990.6 609.2 1990.2 609 1989.6 609.2 1989.2 609.4 1989.2 609.6 1988.9 609.7 1988.7 609.8 1988.6 610.2 1988.4 610.5 1988.1 610.4 1988.1 610.2 1987.7 610.1 1987.3 610.3 1987.1 610.8 1986.8 611 1986.5 611 1986.5 610.7 1986.5 610.3 1986.3 609.9 1986.4 609.7 1986.3 609.6 1985.7 609.8 1985.7 609.4 1986.1 609.1 1986.2 609.1 1986.1 608.6 1986.4 608.5 1986.9 608.8 1987.5 608.4 1987.7 608.4 1988 608.1 1988.2 608.1 1988.5 607.8 1988.5 607.6 1989.3 607.5 1990.2 607.2 1990.5 607.1 1990.9 607.2 1991.4 607 1991.6 606.6 1991.8 606.6 1992 606.2 1992.4 606.3 1992.5 606.1 1992.7 606.2 1993.7 605.7 1993.8 606 1994 605.9 1994.2 606 1994.5 605.7 1995 605.5 1995.1 605.6 1994.6 606 1994.4 606 Z"> | |
| 970 | + </path> | |
| 971 | + <circle cx="997.9" cy="189.1" id="0"> | |
| 972 | + </circle> | |
| 973 | + <circle cx="673.5" cy="724.1" id="1"> | |
| 974 | + </circle> | |
| 975 | + <circle cx="1798.2" cy="719.3" id="2"> | |
| 976 | + </circle> | |
| 977 | +</svg> |