irongit

chore: initial commit

huncholanehuncholaneauthored
commit 974418762e90a91905961ebd5e720b72faefa6b3Browse files

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(&current_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 == &current_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, &current_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, &current_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, &current_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, &current_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+}
+1024-0src/views/shared_browser.rs
@@ -0,0 +1,1024 @@
1+use egui::{self, RichText, Ui};
2+
3+use crate::schema::{SchemaOverview, SharedStruct};
4+use crate::theme::CatppuccinMocha;
5+use crate::types::InferredType;
6+
7+pub struct SharedBrowserView {
8+ /// Navigation path: e.g. ["Shared", "Address", "city"]
9+ path: Vec<String>,
10+ selection: usize,
11+ scroll_to_selection: bool,
12+ restore_key: Option<String>,
13+ sort_alpha: bool,
14+}
15+
16+/// An entry at a given level of the schema tree.
17+struct Entry {
18+ label: String,
19+ type_short: String,
20+ color: egui::Color32,
21+ is_container: bool,
22+ count: Option<usize>,
23+}
24+
25+impl SharedBrowserView {
26+ pub fn new() -> Self {
27+ Self {
28+ path: Vec::new(),
29+ selection: 0,
30+ scroll_to_selection: false,
31+ restore_key: None,
32+ sort_alpha: false,
33+ }
34+ }
35+
36+ pub fn invalidate(&mut self) {
37+ self.path.clear();
38+ self.selection = 0;
39+ }
40+
41+ pub fn show(
42+ &mut self,
43+ ui: &mut Ui,
44+ schema: &SchemaOverview,
45+ files: &[(String, serde_json::Value)],
46+ ) {
47+ if schema.structs.is_empty() && schema.unique_structs.is_empty() {
48+ ui.centered_and_justified(|ui| {
49+ ui.label(
50+ RichText::new("Import multiple files to see shared types")
51+ .color(CatppuccinMocha::OVERLAY0)
52+ .size(16.0),
53+ );
54+ });
55+ return;
56+ }
57+
58+ // Build entries for current level and parent level
59+ let mut current_entries = build_entries_at_path(&self.path, schema);
60+ if self.sort_alpha && self.path.len() == 1 {
61+ current_entries.sort_by(|a, b| a.label.cmp(&b.label));
62+ }
63+ let parent_entries = if self.path.is_empty() {
64+ None
65+ } else {
66+ Some(build_entries_at_path(
67+ &self.path[..self.path.len() - 1],
68+ schema,
69+ ))
70+ };
71+
72+ // Restore selection by key after going back
73+ if let Some(key) = self.restore_key.take() {
74+ if let Some(idx) = current_entries.iter().position(|e| e.label == key) {
75+ self.selection = idx;
76+ }
77+ }
78+
79+ // Clamp selection
80+ if !current_entries.is_empty() && self.selection >= current_entries.len() {
81+ self.selection = current_entries.len().saturating_sub(1);
82+ }
83+
84+ // Keyboard navigation
85+ let down = ui.input(|i| {
86+ i.key_pressed(egui::Key::J) || i.key_pressed(egui::Key::ArrowDown)
87+ });
88+ let up = ui.input(|i| {
89+ i.key_pressed(egui::Key::K) || i.key_pressed(egui::Key::ArrowUp)
90+ });
91+ let right = ui.input(|i| {
92+ i.key_pressed(egui::Key::L)
93+ || i.key_pressed(egui::Key::ArrowRight)
94+ || i.key_pressed(egui::Key::Enter)
95+ });
96+ let left = ui.input(|i| {
97+ i.key_pressed(egui::Key::H)
98+ || i.key_pressed(egui::Key::ArrowLeft)
99+ || i.key_pressed(egui::Key::Backspace)
100+ });
101+
102+ if down && self.selection + 1 < current_entries.len() {
103+ self.selection += 1;
104+ self.scroll_to_selection = true;
105+ }
106+ if up && self.selection > 0 {
107+ self.selection -= 1;
108+ self.scroll_to_selection = true;
109+ }
110+ if right {
111+ if let Some(entry) = current_entries.get(self.selection) {
112+ if entry.is_container {
113+ self.path.push(entry.label.clone());
114+ self.selection = 0;
115+ self.scroll_to_selection = true;
116+ }
117+ }
118+ }
119+ if left && !self.path.is_empty() {
120+ let popped = self.path.pop().unwrap();
121+ self.restore_key = Some(popped);
122+ self.scroll_to_selection = true;
123+ }
124+
125+ // 'A' toggles alphabetical sort on the structs pane
126+ let toggle_sort = ui.input(|i| i.key_pressed(egui::Key::A));
127+ if toggle_sort && self.path.len() == 1 {
128+ let selected_label = current_entries.get(self.selection).map(|e| e.label.clone());
129+ self.sort_alpha = !self.sort_alpha;
130+ // Re-sort in place
131+ if self.sort_alpha {
132+ current_entries.sort_by(|a, b| a.label.cmp(&b.label));
133+ } else {
134+ current_entries = build_entries_at_path(&self.path, schema);
135+ }
136+ // Restore selection to the same struct after reordering
137+ if let Some(label) = selected_label {
138+ if let Some(idx) = current_entries.iter().position(|e| e.label == label) {
139+ self.selection = idx;
140+ }
141+ }
142+ }
143+
144+ // --- Three-column miller layout ---
145+ let avail = ui.available_rect_before_wrap();
146+ let total_w = avail.width() - 12.0;
147+ let col_widths = [total_w * 0.22, total_w * 0.38, total_w * 0.38];
148+ let col_height = avail.height();
149+
150+ let mut clicked_entry: Option<usize> = None;
151+ let mut dbl_clicked_entry: Option<usize> = None;
152+ let mut type_nav: Option<Vec<String>> = None;
153+
154+ // Compute pane titles based on current path
155+ let (left_title, mid_title, right_title) = pane_titles(
156+ &self.path,
157+ current_entries.get(self.selection).map(|e| e.label.as_str()),
158+ );
159+
160+ ui.horizontal(|ui| {
161+ ui.set_height(col_height);
162+
163+ // Left: parent (history)
164+ ui.vertical(|ui| {
165+ ui.set_width(col_widths[0]);
166+ ui.set_height(col_height);
167+ if !left_title.is_empty() {
168+ render_pane_title(ui, &left_title);
169+ }
170+ render_parent_column(ui, &self.path, &parent_entries, col_height);
171+ });
172+
173+ draw_separator(ui, col_height);
174+
175+ // Middle: current entries
176+ ui.vertical(|ui| {
177+ ui.set_width(col_widths[1]);
178+ ui.set_height(col_height);
179+ render_pane_title(ui, &mid_title);
180+ let (c, d) = render_current_column(
181+ ui,
182+ &current_entries,
183+ self.selection,
184+ self.scroll_to_selection,
185+ col_height,
186+ );
187+ clicked_entry = c;
188+ dbl_clicked_entry = d;
189+ });
190+
191+ draw_separator(ui, col_height);
192+
193+ // Right: values for selected entry
194+ ui.vertical(|ui| {
195+ let remaining = ui.available_width();
196+ ui.set_width(remaining);
197+ ui.set_height(col_height);
198+ render_pane_title(ui, &right_title);
199+ if let Some(entry) = current_entries.get(self.selection) {
200+ if let Some(nav) = render_values_column(
201+ ui,
202+ &self.path,
203+ &entry.label,
204+ schema,
205+ files,
206+ col_height,
207+ ) {
208+ type_nav = Some(nav);
209+ }
210+ }
211+ });
212+ });
213+
214+ self.scroll_to_selection = false;
215+
216+ if let Some(idx) = clicked_entry {
217+ self.selection = idx;
218+ }
219+ if let Some(idx) = dbl_clicked_entry {
220+ self.selection = idx;
221+ if let Some(entry) = current_entries.get(idx) {
222+ if entry.is_container {
223+ self.path.push(entry.label.clone());
224+ self.selection = 0;
225+ self.scroll_to_selection = true;
226+ }
227+ }
228+ }
229+ // Navigate to a struct via type link click
230+ if let Some(nav_path) = type_nav {
231+ self.path = nav_path;
232+ self.selection = 0;
233+ self.scroll_to_selection = true;
234+ }
235+ }
236+}
237+
238+/// Build the list of entries at a given path in the schema tree.
239+fn build_entries_at_path(path: &[String], schema: &SchemaOverview) -> Vec<Entry> {
240+ match path.len() {
241+ 0 => {
242+ // Root: show "Shared" and "Unique" categories
243+ let mut entries = Vec::new();
244+ if !schema.structs.is_empty() {
245+ entries.push(Entry {
246+ label: "Shared".to_string(),
247+ type_short: String::new(),
248+ color: CatppuccinMocha::BLUE,
249+ is_container: true,
250+ count: Some(schema.structs.len()),
251+ });
252+ }
253+ if !schema.unique_structs.is_empty() {
254+ entries.push(Entry {
255+ label: "Unique".to_string(),
256+ type_short: String::new(),
257+ color: CatppuccinMocha::LAVENDER,
258+ is_container: true,
259+ count: Some(schema.unique_structs.len()),
260+ });
261+ }
262+ entries
263+ }
264+ 1 => {
265+ // Inside a category: list structs
266+ let structs = match path[0].as_str() {
267+ "Shared" => &schema.structs,
268+ "Unique" => &schema.unique_structs,
269+ _ => return Vec::new(),
270+ };
271+ structs
272+ .iter()
273+ .map(|s| Entry {
274+ label: s.name.clone(),
275+ type_short: format!("{} fields", s.fields.len()),
276+ color: CatppuccinMocha::LAVENDER,
277+ is_container: true,
278+ count: Some(s.occurrence_count),
279+ })
280+ .collect()
281+ }
282+ _ => {
283+ // Inside a struct (or nested): list fields
284+ let structs = match path[0].as_str() {
285+ "Shared" => &schema.structs,
286+ "Unique" => &schema.unique_structs,
287+ _ => return Vec::new(),
288+ };
289+ let Some(root_struct) = structs.iter().find(|s| s.name == path[1]) else {
290+ return Vec::new();
291+ };
292+
293+ // Use all structs (shared + unique) for type resolution and display
294+ let all = schema.all_structs();
295+
296+ // Walk remaining path segments to find the current type
297+ let current_type = resolve_type_at_path(&root_struct.fields, &path[2..], &all);
298+
299+ match current_type {
300+ Some(fields) => fields
301+ .iter()
302+ .map(|(name, typ)| {
303+ let short = typ.short_name(&all);
304+ let color = crate::theme::type_color(&short);
305+ let is_container = is_navigable_type(typ, &all);
306+ Entry {
307+ label: name.clone(),
308+ type_short: short,
309+ color,
310+ is_container,
311+ count: None,
312+ }
313+ })
314+ .collect(),
315+ None => Vec::new(),
316+ }
317+ }
318+ }
319+}
320+
321+/// Resolve the fields available at a given sub-path within a struct's fields.
322+/// Returns the BTreeMap of fields if the path resolves to a struct-like type.
323+fn resolve_type_at_path<'a>(
324+ fields: &'a std::collections::BTreeMap<String, InferredType>,
325+ sub_path: &[String],
326+ all_structs: &'a [SharedStruct],
327+) -> Option<&'a std::collections::BTreeMap<String, InferredType>> {
328+ if sub_path.is_empty() {
329+ return Some(fields);
330+ }
331+
332+ let field_type = fields.get(&sub_path[0])?;
333+ let inner = unwrap_container(field_type);
334+
335+ // If this type references another struct, navigate into it
336+ if let InferredType::Object(obj_fields) = inner {
337+ return resolve_type_at_path(obj_fields, &sub_path[1..], all_structs);
338+ }
339+
340+ // Check if it matches a named struct in the schema
341+ let type_name = inner.short_name(all_structs);
342+ if let Some(s) = all_structs.iter().find(|s| s.name == type_name) {
343+ return resolve_type_at_path(&s.fields, &sub_path[1..], all_structs);
344+ }
345+
346+ None
347+}
348+
349+/// Unwrap Option<T>, Vec<T> etc. to get the inner type
350+fn unwrap_container(typ: &InferredType) -> &InferredType {
351+ match typ {
352+ InferredType::Option(inner) => unwrap_container(inner),
353+ InferredType::Array(inner) => unwrap_container(inner),
354+ _ => typ,
355+ }
356+}
357+
358+/// Check if a type can be navigated into (has sub-fields)
359+fn is_navigable_type(typ: &InferredType, all_structs: &[SharedStruct]) -> bool {
360+ let inner = unwrap_container(typ);
361+ match inner {
362+ InferredType::Object(_) => true,
363+ _ => {
364+ let name = inner.short_name(all_structs);
365+ all_structs.iter().any(|s| s.name == name)
366+ }
367+ }
368+}
369+
370+fn draw_separator(ui: &mut Ui, height: f32) {
371+ let sep_rect = ui.available_rect_before_wrap();
372+ ui.painter().line_segment(
373+ [
374+ egui::pos2(sep_rect.left(), sep_rect.top()),
375+ egui::pos2(sep_rect.left(), sep_rect.top() + height),
376+ ],
377+ egui::Stroke::new(1.0, CatppuccinMocha::SURFACE0),
378+ );
379+ ui.add_space(4.0);
380+}
381+
382+/// Compute titles for each of the three panes based on the current navigation path.
383+fn pane_titles(path: &[String], selected_label: Option<&str>) -> (String, String, String) {
384+ let selected = selected_label.unwrap_or("");
385+ match path.len() {
386+ 0 => (
387+ String::new(),
388+ "Categories".to_string(),
389+ format!("{} Preview", selected),
390+ ),
391+ 1 => (
392+ "Categories".to_string(),
393+ format!("{} Structs", path[0]),
394+ format!("{} Fields", selected),
395+ ),
396+ 2 => (
397+ format!("{} Structs", path[0]),
398+ format!("{} Fields", path[1]),
399+ format!("{} Values", selected),
400+ ),
401+ _ => {
402+ let parent_field = &path[path.len() - 1];
403+ let grandparent = &path[path.len() - 2];
404+ (
405+ format!("{} Fields", grandparent),
406+ format!("{} Fields", parent_field),
407+ format!("{} Values", selected),
408+ )
409+ }
410+ }
411+}
412+
413+fn render_pane_title(ui: &mut Ui, title: &str) {
414+ ui.horizontal(|ui| {
415+ ui.label(
416+ RichText::new(title)
417+ .color(CatppuccinMocha::OVERLAY1)
418+ .family(egui::FontFamily::Monospace)
419+ .size(10.0),
420+ );
421+ });
422+ ui.add_space(2.0);
423+}
424+
425+fn render_parent_column(
426+ ui: &mut Ui,
427+ path: &[String],
428+ parent_entries: &Option<Vec<Entry>>,
429+ height: f32,
430+) {
431+ let Some(entries) = parent_entries else {
432+ return;
433+ };
434+
435+ let active_label = path.last().map(|s| s.as_str()).unwrap_or("");
436+ let row_height = ui.text_style_height(&egui::TextStyle::Monospace) + 4.0;
437+
438+ egui::ScrollArea::vertical()
439+ .id_salt("shared_parent")
440+ .auto_shrink(false)
441+ .max_height(height)
442+ .show_rows(ui, row_height, entries.len(), |ui, range| {
443+ for i in range {
444+ let entry = &entries[i];
445+ let is_active = entry.label == active_label;
446+ let bg = if is_active {
447+ CatppuccinMocha::SURFACE0
448+ } else {
449+ egui::Color32::TRANSPARENT
450+ };
451+ let text_color = if is_active {
452+ CatppuccinMocha::TEXT
453+ } else {
454+ CatppuccinMocha::OVERLAY0
455+ };
456+
457+ egui::Frame::new()
458+ .fill(bg)
459+ .corner_radius(4.0)
460+ .inner_margin(egui::Margin::symmetric(6, 1))
461+ .show(ui, |ui| {
462+ ui.horizontal(|ui| {
463+ ui.label(
464+ RichText::new(&entry.label)
465+ .color(text_color)
466+ .family(egui::FontFamily::Monospace)
467+ .size(12.0),
468+ );
469+ });
470+ });
471+ }
472+ });
473+}
474+
475+fn render_current_column(
476+ ui: &mut Ui,
477+ entries: &[Entry],
478+ selection: usize,
479+ scroll_to_selection: bool,
480+ height: f32,
481+) -> (Option<usize>, Option<usize>) {
482+ let mut clicked = None;
483+ let mut dbl_clicked = None;
484+
485+ if entries.is_empty() {
486+ ui.centered_and_justified(|ui| {
487+ ui.label(
488+ RichText::new("(empty)")
489+ .color(CatppuccinMocha::OVERLAY0)
490+ .family(egui::FontFamily::Monospace),
491+ );
492+ });
493+ return (None, None);
494+ }
495+
496+ let row_height = ui.text_style_height(&egui::TextStyle::Monospace) + 6.0;
497+
498+ egui::ScrollArea::vertical()
499+ .id_salt("shared_current")
500+ .auto_shrink(false)
501+ .max_height(height)
502+ .show_rows(ui, row_height, entries.len(), |ui, range| {
503+ for i in range {
504+ let entry = &entries[i];
505+ let is_selected = i == selection;
506+
507+ // Check hover from previous frame via temp storage
508+ let row_id = ui.id().with(("row", i));
509+ let is_hovered = ui.ctx().data(|d| d.get_temp::<bool>(row_id)).unwrap_or(false);
510+
511+ let bg = if is_selected {
512+ CatppuccinMocha::SURFACE0
513+ } else if is_hovered {
514+ egui::Color32::from_rgba_unmultiplied(
515+ entry.color.r(), entry.color.g(), entry.color.b(), 15,
516+ )
517+ } else {
518+ egui::Color32::TRANSPARENT
519+ };
520+ let label_color = if is_selected || is_hovered {
521+ entry.color
522+ } else {
523+ CatppuccinMocha::SUBTEXT0
524+ };
525+ let font_size = if is_hovered && !is_selected { 12.5 } else { 12.0 };
526+ let type_size = if is_hovered && !is_selected { 11.5 } else { 11.0 };
527+ let icon_size = if is_hovered && !is_selected { 12.5 } else { 12.0 };
528+
529+ let r = egui::Frame::new()
530+ .fill(bg)
531+ .corner_radius(4.0)
532+ .inner_margin(egui::Margin::symmetric(6, 2))
533+ .show(ui, |ui| {
534+ ui.horizontal(|ui| {
535+ // Container indicator
536+ if entry.is_container {
537+ ui.label(
538+ RichText::new(egui_phosphor::regular::BRACKETS_CURLY)
539+ .color(entry.color)
540+ .size(icon_size),
541+ );
542+ }
543+
544+ ui.label(
545+ RichText::new(&entry.label)
546+ .color(label_color)
547+ .family(egui::FontFamily::Monospace)
548+ .size(font_size),
549+ );
550+
551+ ui.with_layout(
552+ egui::Layout::right_to_left(egui::Align::Center),
553+ |ui| {
554+ if let Some(count) = entry.count {
555+ ui.label(
556+ RichText::new(format!("{}", count))
557+ .color(CatppuccinMocha::OVERLAY0)
558+ .family(egui::FontFamily::Monospace)
559+ .size(10.0),
560+ );
561+ }
562+ if !entry.type_short.is_empty() {
563+ ui.label(
564+ RichText::new(&entry.type_short)
565+ .color(entry.color)
566+ .family(egui::FontFamily::Monospace)
567+ .size(type_size),
568+ );
569+ }
570+ },
571+ );
572+ });
573+ })
574+ .response
575+ .interact(egui::Sense::click());
576+
577+ // Store hover state for next frame
578+ ui.ctx().data_mut(|d| d.insert_temp(row_id, r.hovered()));
579+
580+ if scroll_to_selection && is_selected {
581+ r.scroll_to_me(Some(egui::Align::Center));
582+ }
583+
584+ if r.clicked() {
585+ clicked = Some(i);
586+ }
587+ if r.double_clicked() {
588+ dbl_clicked = Some(i);
589+ }
590+ }
591+ });
592+
593+ (clicked, dbl_clicked)
594+}
595+
596+/// Render the right column showing all values found for the selected entry.
597+/// Returns an optional navigation path if the user clicks a type link.
598+fn render_values_column(
599+ ui: &mut Ui,
600+ path: &[String],
601+ selected_label: &str,
602+ schema: &SchemaOverview,
603+ files: &[(String, serde_json::Value)],
604+ height: f32,
605+) -> Option<Vec<String>> {
606+ let mut nav_target: Option<Vec<String>> = None;
607+ // Determine what we're looking at
608+ match path.len() {
609+ 0 => {
610+ // Root level — selected is "Shared" or "Unique", show summary
611+ let structs = match selected_label {
612+ "Shared" => &schema.structs,
613+ "Unique" => &schema.unique_structs,
614+ _ => return None,
615+ };
616+ ui.label(
617+ RichText::new(format!("{} — {} structs", selected_label, structs.len()))
618+ .color(CatppuccinMocha::BLUE)
619+ .small(),
620+ );
621+ ui.add_space(4.0);
622+
623+ egui::ScrollArea::vertical()
624+ .id_salt("shared_values")
625+ .auto_shrink(false)
626+ .max_height(height - 24.0)
627+ .show(ui, |ui| {
628+ for s in structs {
629+ ui.horizontal(|ui| {
630+ ui.label(
631+ RichText::new(egui_phosphor::regular::BRACKETS_CURLY)
632+ .color(CatppuccinMocha::LAVENDER)
633+ .size(11.0),
634+ );
635+ ui.label(
636+ RichText::new(&s.name)
637+ .color(CatppuccinMocha::LAVENDER)
638+ .family(egui::FontFamily::Monospace)
639+ .size(11.0),
640+ );
641+ ui.label(
642+ RichText::new(format!("{}x in {} files", s.occurrence_count, s.source_files.len()))
643+ .color(CatppuccinMocha::OVERLAY0)
644+ .family(egui::FontFamily::Monospace)
645+ .size(10.0),
646+ );
647+ });
648+ }
649+ });
650+ }
651+ 1 => {
652+ // Category level — selected is a struct name, show its fields preview
653+ let structs = match path[0].as_str() {
654+ "Shared" => &schema.structs,
655+ "Unique" => &schema.unique_structs,
656+ _ => return None,
657+ };
658+ let Some(s) = structs.iter().find(|s| s.name == selected_label) else {
659+ return None;
660+ };
661+
662+ ui.label(
663+ RichText::new(format!(
664+ "{} {} — {} fields, {}x",
665+ egui_phosphor::regular::BRACKETS_CURLY,
666+ s.name,
667+ s.fields.len(),
668+ s.occurrence_count,
669+ ))
670+ .color(CatppuccinMocha::LAVENDER)
671+ .small(),
672+ );
673+ ui.add_space(2.0);
674+ ui.label(
675+ RichText::new(format!("in: {}", s.source_files.join(", ")))
676+ .color(CatppuccinMocha::OVERLAY0)
677+ .size(10.0),
678+ );
679+ ui.add_space(4.0);
680+
681+ egui::ScrollArea::vertical()
682+ .id_salt("shared_values")
683+ .auto_shrink(false)
684+ .max_height(height - 40.0)
685+ .show(ui, |ui| {
686+ let all = schema.all_structs();
687+ for (idx, (name, typ)) in s.fields.iter().enumerate() {
688+ let short = typ.short_name(&all);
689+ let color = crate::theme::type_color(&short);
690+ let is_nav = is_navigable_type(typ, &all);
691+
692+ // Hover state from previous frame
693+ let row_id = ui.id().with(("val_row", idx));
694+ let is_hovered = ui.ctx().data(|d| d.get_temp::<bool>(row_id)).unwrap_or(false);
695+
696+ let bg = if is_hovered {
697+ egui::Color32::from_rgba_unmultiplied(
698+ color.r(), color.g(), color.b(), 15,
699+ )
700+ } else {
701+ egui::Color32::TRANSPARENT
702+ };
703+ let name_color = if is_hovered { color } else { CatppuccinMocha::SUBTEXT0 };
704+ let font_size = if is_hovered { 11.5 } else { 11.0 };
705+
706+ let row_r = egui::Frame::new()
707+ .fill(bg)
708+ .corner_radius(3.0)
709+ .inner_margin(egui::Margin::symmetric(4, 1))
710+ .show(ui, |ui| {
711+ ui.horizontal(|ui| {
712+ ui.label(
713+ RichText::new(name)
714+ .color(name_color)
715+ .family(egui::FontFamily::Monospace)
716+ .size(font_size),
717+ );
718+ if is_nav {
719+ let r = ui.add(
720+ egui::Label::new(
721+ RichText::new(&short)
722+ .color(color)
723+ .family(egui::FontFamily::Monospace)
724+ .size(font_size)
725+ .underline(),
726+ )
727+ .sense(egui::Sense::click()),
728+ );
729+ if r.hovered() {
730+ ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
731+ }
732+ if r.clicked() {
733+ let inner = unwrap_container(typ);
734+ let struct_name = inner.short_name(&all);
735+ if let Some(cat) = find_struct_category(&struct_name, schema) {
736+ nav_target = Some(vec![cat.to_string(), struct_name]);
737+ }
738+ }
739+ } else {
740+ ui.label(
741+ RichText::new(&short)
742+ .color(color)
743+ .family(egui::FontFamily::Monospace)
744+ .size(font_size),
745+ );
746+ }
747+ });
748+ })
749+ .response;
750+
751+ ui.ctx().data_mut(|d| d.insert_temp(row_id, row_r.hovered()));
752+ }
753+ });
754+ }
755+ _ => {
756+ // Inside a struct — selected is a field name, show all values across files
757+ let structs = match path[0].as_str() {
758+ "Shared" => &schema.structs,
759+ "Unique" => &schema.unique_structs,
760+ _ => return None,
761+ };
762+ let Some(root_struct) = structs.iter().find(|s| s.name == path[1]) else {
763+ return None;
764+ };
765+
766+ // Build the full field path from path[2..] + selected_label
767+ let mut field_path: Vec<&str> = path[2..].iter().map(|s| s.as_str()).collect();
768+ field_path.push(selected_label);
769+
770+ let all = schema.all_structs();
771+
772+ // Get the type of the selected field
773+ let selected_field_type = resolve_type_at_path(&root_struct.fields, &path[2..], &all)
774+ .and_then(|fields| fields.get(selected_label).cloned());
775+
776+ let field_type_str = selected_field_type
777+ .as_ref()
778+ .map(|t| t.short_name(&all))
779+ .unwrap_or_default();
780+
781+ let is_primitive = selected_field_type
782+ .as_ref()
783+ .map(|t| !is_navigable_type(t, &all))
784+ .unwrap_or(true);
785+
786+ ui.label(
787+ RichText::new(format!("{}: {}", selected_label, field_type_str))
788+ .color(CatppuccinMocha::BLUE)
789+ .small(),
790+ );
791+ ui.add_space(4.0);
792+
793+ // Collect field keys for shape matching
794+ let field_keys: Vec<&str> = root_struct.fields.keys().map(|k| k.as_str()).collect();
795+
796+ // Collect all values across all files
797+ let mut all_values: Vec<(String, String)> = Vec::new(); // (value, filename)
798+ for (filename, value) in files {
799+ if !root_struct.source_files.contains(filename) {
800+ continue;
801+ }
802+ let mut values: Vec<String> = Vec::new();
803+ collect_nested_field_values(value, &field_keys, &field_path, &mut values);
804+ for v in values {
805+ all_values.push((v, filename.clone()));
806+ }
807+ }
808+
809+ // Group values per file for display
810+ let mut per_file: Vec<(&str, Vec<&str>)> = Vec::new();
811+ for (val, filename) in &all_values {
812+ if let Some(entry) = per_file.last_mut().filter(|(f, _)| *f == filename.as_str()) {
813+ entry.1.push(val.as_str());
814+ } else {
815+ per_file.push((filename.as_str(), vec![val.as_str()]));
816+ }
817+ }
818+
819+ egui::ScrollArea::vertical()
820+ .id_salt("shared_values")
821+ .auto_shrink(false)
822+ .max_height(height - 24.0)
823+ .show(ui, |ui| {
824+ for (filename, values) in &per_file {
825+ let display_name = filename.strip_suffix(".json").unwrap_or(filename);
826+
827+ // File separator header
828+ ui.add_space(2.0);
829+ ui.horizontal(|ui| {
830+ let sep_rect = ui.available_rect_before_wrap();
831+ let y = sep_rect.center().y;
832+ let left = sep_rect.left();
833+
834+ ui.painter().line_segment(
835+ [egui::pos2(left, y), egui::pos2(left + 8.0, y)],
836+ egui::Stroke::new(1.0, CatppuccinMocha::SURFACE2),
837+ );
838+
839+ ui.add_space(10.0);
840+ ui.label(
841+ RichText::new(display_name)
842+ .color(CatppuccinMocha::YELLOW)
843+ .family(egui::FontFamily::Monospace)
844+ .size(11.0),
845+ );
846+
847+ let after = ui.available_rect_before_wrap();
848+ ui.painter().line_segment(
849+ [egui::pos2(after.left(), y), egui::pos2(after.right(), y)],
850+ egui::Stroke::new(1.0, CatppuccinMocha::SURFACE2),
851+ );
852+ });
853+ ui.add_space(2.0);
854+
855+ if is_primitive {
856+ // Group identical values within this file
857+ let mut counts: Vec<(&str, usize)> = Vec::new();
858+ for val in values {
859+ if let Some(entry) = counts.iter_mut().find(|(v, _)| *v == *val) {
860+ entry.1 += 1;
861+ } else {
862+ counts.push((val, 1));
863+ }
864+ }
865+ counts.sort_by(|a, b| b.1.cmp(&a.1));
866+
867+ for (val_str, count) in &counts {
868+ let display = if val_str.len() > 60 {
869+ format!("{}…", &val_str[..57])
870+ } else {
871+ val_str.to_string()
872+ };
873+
874+ let color = value_color(val_str);
875+
876+ ui.horizontal(|ui| {
877+ ui.add_space(4.0);
878+ ui.label(
879+ RichText::new(&display)
880+ .color(color)
881+ .family(egui::FontFamily::Monospace)
882+ .size(11.0),
883+ );
884+ if *count > 1 {
885+ ui.label(
886+ RichText::new(format!("×{}", count))
887+ .color(CatppuccinMocha::OVERLAY0)
888+ .family(egui::FontFamily::Monospace)
889+ .size(10.0),
890+ );
891+ }
892+ });
893+ }
894+ } else {
895+ // Non-primitive: list each value
896+ for val_str in values {
897+ let display = if val_str.len() > 80 {
898+ format!("{}…", &val_str[..77])
899+ } else {
900+ val_str.to_string()
901+ };
902+
903+ let color = value_color(val_str);
904+
905+ ui.horizontal(|ui| {
906+ ui.add_space(4.0);
907+ ui.label(
908+ RichText::new(&display)
909+ .color(color)
910+ .family(egui::FontFamily::Monospace)
911+ .size(11.0),
912+ );
913+ });
914+ }
915+ }
916+ }
917+ });
918+ }
919+ }
920+ nav_target
921+}
922+
923+/// Resolve which category ("Shared" or "Unique") a struct name belongs to.
924+fn find_struct_category(name: &str, schema: &SchemaOverview) -> Option<&'static str> {
925+ if schema.structs.iter().any(|s| s.name == name) {
926+ Some("Shared")
927+ } else if schema.unique_structs.iter().any(|s| s.name == name) {
928+ Some("Unique")
929+ } else {
930+ None
931+ }
932+}
933+
934+/// Recursively collect values at a nested field path from objects matching the struct shape.
935+fn collect_nested_field_values(
936+ value: &serde_json::Value,
937+ struct_field_keys: &[&str],
938+ field_path: &[&str],
939+ out: &mut Vec<String>,
940+) {
941+ match value {
942+ serde_json::Value::Object(map) => {
943+ // Check if this object matches the struct shape
944+ let matching = struct_field_keys
945+ .iter()
946+ .filter(|k| map.contains_key(**k))
947+ .count();
948+ let ratio = if struct_field_keys.is_empty() {
949+ 0.0
950+ } else {
951+ matching as f64 / struct_field_keys.len() as f64
952+ };
953+
954+ if ratio >= 0.5 && !field_path.is_empty() {
955+ // Navigate through the field path
956+ let mut current: Option<&serde_json::Value> = Some(value);
957+ for segment in field_path {
958+ match current {
959+ Some(serde_json::Value::Object(m)) => {
960+ current = m.get(*segment);
961+ }
962+ Some(serde_json::Value::Array(arr)) => {
963+ for item in arr {
964+ if let serde_json::Value::Object(m) = item {
965+ if let Some(v) = m.get(*segment) {
966+ out.push(format_value(v));
967+ }
968+ }
969+ }
970+ return;
971+ }
972+ _ => {
973+ current = None;
974+ break;
975+ }
976+ }
977+ }
978+
979+ match current {
980+ Some(v) => out.push(format_value(v)),
981+ None => out.push("(missing)".to_string()),
982+ }
983+ }
984+
985+ // Recurse into all values to find more matching objects
986+ for (_, v) in map {
987+ collect_nested_field_values(v, struct_field_keys, field_path, out);
988+ }
989+ }
990+ serde_json::Value::Array(arr) => {
991+ for v in arr {
992+ collect_nested_field_values(v, struct_field_keys, field_path, out);
993+ }
994+ }
995+ _ => {}
996+ }
997+}
998+
999+fn format_value(v: &serde_json::Value) -> String {
1000+ match v {
1001+ serde_json::Value::String(s) => format!("\"{}\"", s),
1002+ serde_json::Value::Number(n) => n.to_string(),
1003+ serde_json::Value::Bool(b) => b.to_string(),
1004+ serde_json::Value::Null => "null".to_string(),
1005+ serde_json::Value::Array(a) => format!("[…] {} items", a.len()),
1006+ serde_json::Value::Object(m) => format!("{{…}} {} fields", m.len()),
1007+ }
1008+}
1009+
1010+fn value_color(val_str: &str) -> egui::Color32 {
1011+ if val_str == "(missing)" {
1012+ CatppuccinMocha::OVERLAY0
1013+ } else if val_str.starts_with('"') {
1014+ CatppuccinMocha::GREEN
1015+ } else if val_str == "true" || val_str == "false" {
1016+ CatppuccinMocha::MAUVE
1017+ } else if val_str == "null" {
1018+ CatppuccinMocha::OVERLAY0
1019+ } else if val_str.starts_with('[') || val_str.starts_with('{') {
1020+ CatppuccinMocha::YELLOW
1021+ } else {
1022+ CatppuccinMocha::PEACH
1023+ }
1024+}
+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>