Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
72659eab2e
|
|||
|
9d14e63613
|
Generated
+33
-33
@@ -155,9 +155,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.12.1"
|
version = "2.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
|
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
@@ -243,7 +243,7 @@ version = "0.18.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
|
checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"cairo-sys-rs",
|
"cairo-sys-rs",
|
||||||
"glib",
|
"glib",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -432,7 +432,7 @@ version = "0.25.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
@@ -445,7 +445,7 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@@ -689,7 +689,7 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
"libc",
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
@@ -1323,7 +1323,7 @@ version = "0.18.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
|
checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-executor",
|
"futures-executor",
|
||||||
@@ -1478,9 +1478,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230"
|
checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
]
|
]
|
||||||
@@ -1804,7 +1804,7 @@ version = "0.11.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "533e68a5842e734946fe159fb03fc9bbbb254f590dd0d8ad321ae5ff7beca2c1"
|
checksum = "533e68a5842e734946fe159fb03fc9bbbb254f590dd0d8ad321ae5ff7beca2c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"inotify-sys",
|
"inotify-sys",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@@ -1975,7 +1975,7 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
|
checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"serde",
|
"serde",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
@@ -1996,7 +1996,7 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
|
checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2339,7 +2339,7 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"jni-sys 0.3.1",
|
"jni-sys 0.3.1",
|
||||||
"log",
|
"log",
|
||||||
"ndk-sys",
|
"ndk-sys",
|
||||||
@@ -2369,7 +2369,7 @@ version = "8.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"fsevent-sys",
|
"fsevent-sys",
|
||||||
"inotify",
|
"inotify",
|
||||||
"kqueue",
|
"kqueue",
|
||||||
@@ -2387,7 +2387,7 @@ version = "2.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
|
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2481,7 +2481,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
@@ -2494,7 +2494,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
@@ -2515,7 +2515,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
@@ -2526,7 +2526,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
@@ -2559,7 +2559,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-core-graphics",
|
"objc2-core-graphics",
|
||||||
@@ -2586,7 +2586,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
"libc",
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
@@ -2599,7 +2599,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
@@ -2610,7 +2610,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
@@ -2622,7 +2622,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-cloud-kit",
|
"objc2-cloud-kit",
|
||||||
@@ -2653,7 +2653,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
|
checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
@@ -2858,7 +2858,7 @@ version = "0.18.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"fdeflate",
|
"fdeflate",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -3067,7 +3067,7 @@ version = "0.5.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3223,7 +3223,7 @@ version = "1.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
@@ -3511,7 +3511,7 @@ version = "0.36.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
|
checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"cssparser",
|
"cssparser",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"log",
|
"log",
|
||||||
@@ -4100,7 +4100,7 @@ version = "0.35.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
|
checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
@@ -4770,7 +4770,7 @@ version = "0.6.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
@@ -5201,7 +5201,7 @@ version = "0.244.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"hashbrown 0.15.5",
|
"hashbrown 0.15.5",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
"semver",
|
"semver",
|
||||||
@@ -5892,7 +5892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags 2.12.1",
|
"bitflags 2.13.0",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
+1
-1
@@ -39,12 +39,12 @@ tauri = { version = "2", features = [] }
|
|||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
tauri-plugin-store = "2"
|
tauri-plugin-store = "2"
|
||||||
|
time = { version = "0.3", features = ["local-offset"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-util = { version = "0.7", features = ["codec", "rt"] }
|
tokio-util = { version = "0.7", features = ["codec", "rt"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-log = "0.2"
|
tracing-log = "0.2"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
time = { version = "0.3", features = ["local-offset"] }
|
|
||||||
uuid = { version = "1", features = ["v7"] }
|
uuid = { version = "1", features = ["v7"] }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
windows = {
|
windows = {
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ tauri = { workspace = true }
|
|||||||
tauri-plugin-dialog = { workspace = true }
|
tauri-plugin-dialog = { workspace = true }
|
||||||
tauri-plugin-shell = { workspace = true }
|
tauri-plugin-shell = { workspace = true }
|
||||||
tauri-plugin-store = { workspace = true }
|
tauri-plugin-store = { workspace = true }
|
||||||
|
time = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tokio-util = { workspace = true }
|
tokio-util = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-log = { workspace = true }
|
tracing-log = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
time = { workspace = true }
|
|
||||||
walkdir = { workspace = true }
|
walkdir = { workspace = true }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
fs::{self, OpenOptions},
|
fs::{self, OpenOptions},
|
||||||
io::{Read as _, Seek as _, SeekFrom, Write as _},
|
io::{self, Read as _, Seek as _, SeekFrom, Write as _},
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
path::{Component, Path, PathBuf},
|
path::{Component, Path, PathBuf},
|
||||||
sync::{Arc, Mutex, OnceLock},
|
sync::{Arc, Mutex, OnceLock},
|
||||||
@@ -87,6 +87,7 @@ struct LanSpreadState {
|
|||||||
catalog: Arc<RwLock<GameCatalog>>,
|
catalog: Arc<RwLock<GameCatalog>>,
|
||||||
unpack_logs: Arc<RwLock<Vec<UnpackLogEntry>>>,
|
unpack_logs: Arc<RwLock<Vec<UnpackLogEntry>>>,
|
||||||
state_dir: OnceLock<PathBuf>,
|
state_dir: OnceLock<PathBuf>,
|
||||||
|
main_log_sink: OnceLock<MainLogSink>,
|
||||||
active_outbound_transfers: OutboundTransfers,
|
active_outbound_transfers: OutboundTransfers,
|
||||||
outbound_transfer_emit: Arc<RwLock<OutboundTransferEmitState>>,
|
outbound_transfer_emit: Arc<RwLock<OutboundTransferEmitState>>,
|
||||||
}
|
}
|
||||||
@@ -144,6 +145,14 @@ struct UnpackLogEntry {
|
|||||||
struct MainLogLinePayload {
|
struct MainLogLinePayload {
|
||||||
line: String,
|
line: String,
|
||||||
level: String,
|
level: String,
|
||||||
|
sequence: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct MainLogHistoryPayload {
|
||||||
|
contents: String,
|
||||||
|
last_sequence: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SidecarUnpacker {
|
struct SidecarUnpacker {
|
||||||
@@ -174,15 +183,27 @@ async fn get_unpack_logs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_main_logs(app_handle: tauri::AppHandle) -> tauri::Result<String> {
|
async fn get_main_logs(
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
state: tauri::State<'_, LanSpreadState>,
|
||||||
|
) -> tauri::Result<MainLogHistoryPayload> {
|
||||||
|
if let Some(sink) = state.inner().main_log_sink.get() {
|
||||||
|
return Ok(sink.read_history()?);
|
||||||
|
}
|
||||||
|
|
||||||
let state_dir = app_handle.path().app_data_dir()?;
|
let state_dir = app_handle.path().app_data_dir()?;
|
||||||
fs::create_dir_all(&state_dir)?;
|
fs::create_dir_all(&state_dir)?;
|
||||||
let path = main_log_path(&state_dir);
|
let path = main_log_path(&state_dir);
|
||||||
trim_main_log_file(&path)?;
|
|
||||||
|
|
||||||
match fs::read_to_string(&path) {
|
match read_main_log_file_to_limit(&path, MAX_MAIN_LOG_BYTES) {
|
||||||
Ok(contents) => Ok(contents),
|
Ok(contents) => Ok(MainLogHistoryPayload {
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
|
contents,
|
||||||
|
last_sequence: 0,
|
||||||
|
}),
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(MainLogHistoryPayload {
|
||||||
|
contents: String::new(),
|
||||||
|
last_sequence: 0,
|
||||||
|
}),
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1384,33 +1405,70 @@ fn main_log_path(state_dir: &Path) -> PathBuf {
|
|||||||
state_dir.join(MAIN_LOG_FILE_NAME)
|
state_dir.join(MAIN_LOG_FILE_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trim_main_log_file(path: &Path) -> std::io::Result<()> {
|
#[cfg(test)]
|
||||||
trim_main_log_file_to_limit(path, MAX_MAIN_LOG_BYTES)
|
fn trim_main_log_file_to_limit(path: &Path, max_bytes: u64) -> io::Result<()> {
|
||||||
}
|
let mut file = match OpenOptions::new().read(true).write(true).open(path) {
|
||||||
|
Ok(file) => file,
|
||||||
fn trim_main_log_file_to_limit(path: &Path, max_bytes: u64) -> std::io::Result<()> {
|
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
|
||||||
let metadata = match fs::metadata(path) {
|
|
||||||
Ok(metadata) => metadata,
|
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
trim_main_log_file_to_limit_with_file(&mut file, max_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trim_main_log_file_to_limit_with_file(file: &mut fs::File, max_bytes: u64) -> io::Result<()> {
|
||||||
|
let metadata = file.metadata()?;
|
||||||
|
|
||||||
if metadata.len() <= max_bytes {
|
if metadata.len() <= max_bytes {
|
||||||
|
file.seek(SeekFrom::End(0))?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if max_bytes == 0 {
|
let tail = if max_bytes == 0 {
|
||||||
fs::write(path, [])?;
|
String::new()
|
||||||
return Ok(());
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = fs::File::open(path)?;
|
|
||||||
file.seek(SeekFrom::Start(metadata.len() - max_bytes))?;
|
file.seek(SeekFrom::Start(metadata.len() - max_bytes))?;
|
||||||
|
|
||||||
let mut bytes = Vec::with_capacity(usize::try_from(max_bytes).unwrap_or(usize::MAX));
|
let mut bytes = Vec::with_capacity(usize::try_from(max_bytes).unwrap_or(usize::MAX));
|
||||||
file.read_to_end(&mut bytes)?;
|
file.read_to_end(&mut bytes)?;
|
||||||
let tail = valid_utf8_tail(bytes);
|
valid_utf8_tail(bytes)
|
||||||
fs::write(path, tail.as_bytes())
|
};
|
||||||
|
|
||||||
|
file.set_len(0)?;
|
||||||
|
file.seek(SeekFrom::Start(0))?;
|
||||||
|
file.write_all(tail.as_bytes())?;
|
||||||
|
file.seek(SeekFrom::End(0))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_main_log_file_to_limit(path: &Path, max_bytes: u64) -> io::Result<String> {
|
||||||
|
let mut file = fs::File::open(path)?;
|
||||||
|
read_main_log_file_to_limit_with_file(&mut file, max_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_main_log_file_to_limit_with_file(
|
||||||
|
file: &mut fs::File,
|
||||||
|
max_bytes: u64,
|
||||||
|
) -> io::Result<String> {
|
||||||
|
let metadata = file.metadata()?;
|
||||||
|
if metadata.len() == 0 || max_bytes == 0 {
|
||||||
|
file.seek(SeekFrom::End(0))?;
|
||||||
|
return Ok(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = metadata.len().saturating_sub(max_bytes);
|
||||||
|
file.seek(SeekFrom::Start(start))?;
|
||||||
|
|
||||||
|
let capacity = usize::try_from(metadata.len() - start).unwrap_or(usize::MAX);
|
||||||
|
let mut bytes = Vec::with_capacity(capacity);
|
||||||
|
file.read_to_end(&mut bytes)?;
|
||||||
|
file.seek(SeekFrom::End(0))?;
|
||||||
|
|
||||||
|
if start == 0 {
|
||||||
|
Ok(String::from_utf8_lossy(&bytes).into_owned())
|
||||||
|
} else {
|
||||||
|
Ok(valid_utf8_tail(bytes))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_utf8_tail(bytes: Vec<u8>) -> String {
|
fn valid_utf8_tail(bytes: Vec<u8>) -> String {
|
||||||
@@ -1427,7 +1485,13 @@ fn valid_utf8_tail(bytes: Vec<u8>) -> String {
|
|||||||
struct MainLogSink {
|
struct MainLogSink {
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
file_lock: Arc<Mutex<()>>,
|
file_state: Arc<Mutex<MainLogFileState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MainLogFileState {
|
||||||
|
file: Option<fs::File>,
|
||||||
|
last_sequence: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MainLogSink {
|
impl MainLogSink {
|
||||||
@@ -1435,54 +1499,101 @@ impl MainLogSink {
|
|||||||
Self {
|
Self {
|
||||||
app_handle,
|
app_handle,
|
||||||
path,
|
path,
|
||||||
file_lock: Arc::new(Mutex::new(())),
|
file_state: Arc::new(Mutex::new(MainLogFileState::default())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_line(&self, line: String, level: Level) {
|
fn write_line(&self, line: String, level: Level) {
|
||||||
write_main_log_stdout(&line);
|
write_main_log_stdout(&line);
|
||||||
self.append_file_line(&line);
|
let sequence = self.append_file_line(&line);
|
||||||
|
|
||||||
let _ = self.app_handle.emit(
|
let _ = self.app_handle.emit(
|
||||||
"main-log-line",
|
"main-log-line",
|
||||||
MainLogLinePayload {
|
MainLogLinePayload {
|
||||||
line,
|
line,
|
||||||
level: level.as_str().to_string(),
|
level: level.as_str().to_string(),
|
||||||
|
sequence,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_file_line(&self, line: &str) {
|
fn read_history(&self) -> io::Result<MainLogHistoryPayload> {
|
||||||
let Ok(_guard) = self.file_lock.lock() else {
|
let mut file_state = self
|
||||||
return;
|
.file_state
|
||||||
};
|
.lock()
|
||||||
|
.map_err(|_| io::Error::other("main log file lock poisoned"))?;
|
||||||
|
|
||||||
if let Some(parent) = self.path.parent() {
|
if file_state.file.is_none() && !self.path.exists() {
|
||||||
let _ = fs::create_dir_all(parent);
|
return Ok(MainLogHistoryPayload {
|
||||||
|
contents: String::new(),
|
||||||
|
last_sequence: file_state.last_sequence,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(mut file) = OpenOptions::new()
|
let contents = {
|
||||||
.create(true)
|
let file = self.cached_file(&mut file_state.file)?;
|
||||||
.append(true)
|
trim_main_log_file_to_limit_with_file(file, MAX_MAIN_LOG_BYTES)?;
|
||||||
.open(&self.path)
|
read_main_log_file_to_limit_with_file(file, MAX_MAIN_LOG_BYTES)?
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if writeln!(file, "{line}").is_err() {
|
Ok(MainLogHistoryPayload {
|
||||||
return;
|
contents,
|
||||||
|
last_sequence: file_state.last_sequence,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let should_trim = file
|
fn append_file_line(&self, line: &str) -> Option<u64> {
|
||||||
.metadata()
|
let Ok(mut file_state) = self.file_state.lock() else {
|
||||||
.is_ok_and(|metadata| {
|
return None;
|
||||||
metadata.len() > MAX_MAIN_LOG_BYTES.saturating_add(MAIN_LOG_TRIM_SLACK_BYTES)
|
};
|
||||||
|
|
||||||
|
let write_result = self.cached_file(&mut file_state.file).and_then(|file| {
|
||||||
|
file.seek(SeekFrom::End(0))
|
||||||
|
.and_then(|_| writeln!(file, "{line}"))
|
||||||
});
|
});
|
||||||
|
|
||||||
if should_trim {
|
if write_result.is_err() {
|
||||||
let _ = trim_main_log_file(&self.path);
|
file_state.file = None;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_state.last_sequence = file_state.last_sequence.saturating_add(1);
|
||||||
|
let sequence = file_state.last_sequence;
|
||||||
|
|
||||||
|
let should_trim = file_state.file.as_ref().is_some_and(|file| {
|
||||||
|
file.metadata().is_ok_and(|metadata| {
|
||||||
|
metadata.len() > MAX_MAIN_LOG_BYTES.saturating_add(MAIN_LOG_TRIM_SLACK_BYTES)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if should_trim && let Some(file) = file_state.file.as_mut() {
|
||||||
|
let _ = trim_main_log_file_to_limit_with_file(file, MAX_MAIN_LOG_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(sequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cached_file<'a>(&self, file: &'a mut Option<fs::File>) -> io::Result<&'a mut fs::File> {
|
||||||
|
if file.is_none() {
|
||||||
|
*file = Some(open_main_log_file(&self.path)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
file.as_mut()
|
||||||
|
.ok_or_else(|| io::Error::other("main log file was not opened"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_main_log_file(path: &Path) -> io::Result<fs::File> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.truncate(false)
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MainLogLayer {
|
struct MainLogLayer {
|
||||||
@@ -1517,13 +1628,8 @@ where
|
|||||||
.unwrap_or_else(|| metadata.target().to_string());
|
.unwrap_or_else(|| metadata.target().to_string());
|
||||||
let message = visitor.into_message();
|
let message = visitor.into_message();
|
||||||
let (date, time) = current_main_log_timestamp();
|
let (date, time) = current_main_log_timestamp();
|
||||||
let line = format_main_log_line_parts(
|
let line =
|
||||||
&date,
|
format_main_log_line_parts(&date, &time, &target, metadata.level().as_str(), &message);
|
||||||
&time,
|
|
||||||
&target,
|
|
||||||
metadata.level().as_str(),
|
|
||||||
&message,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.sink.write_line(line, *metadata.level());
|
self.sink.write_line(line, *metadata.level());
|
||||||
}
|
}
|
||||||
@@ -1622,8 +1728,9 @@ fn format_main_log_line_parts(
|
|||||||
message: &str,
|
message: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
format!(
|
format!(
|
||||||
"[{date}][{time}][{}][{level}] {message}",
|
"[{date}][{time}][{}][{level}] {}",
|
||||||
normalize_main_log_target(target)
|
normalize_main_log_target(target),
|
||||||
|
normalize_main_log_message(message)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1641,12 +1748,8 @@ fn write_main_log_stdout(line: &str) {
|
|||||||
let _ = writeln!(stdout, "{line}");
|
let _ = writeln!(stdout, "{line}");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_main_logging(
|
fn init_main_logging(sink: MainLogSink) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
app_handle: AppHandle,
|
let subscriber = tracing_subscriber::registry().with(MainLogLayer::new(sink));
|
||||||
path: PathBuf,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let subscriber =
|
|
||||||
tracing_subscriber::registry().with(MainLogLayer::new(MainLogSink::new(app_handle, path)));
|
|
||||||
tracing::subscriber::set_global_default(subscriber)?;
|
tracing::subscriber::set_global_default(subscriber)?;
|
||||||
tracing_log::LogTracer::builder()
|
tracing_log::LogTracer::builder()
|
||||||
.with_max_level(log::LevelFilter::Info)
|
.with_max_level(log::LevelFilter::Info)
|
||||||
@@ -2594,8 +2697,12 @@ pub fn run() {
|
|||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
let state_dir = app.path().app_data_dir()?;
|
let state_dir = app.path().app_data_dir()?;
|
||||||
std::fs::create_dir_all(&state_dir)?;
|
std::fs::create_dir_all(&state_dir)?;
|
||||||
init_main_logging(app.handle().clone(), main_log_path(&state_dir))?;
|
let main_log_sink = MainLogSink::new(app.handle().clone(), main_log_path(&state_dir));
|
||||||
let state = app.state::<LanSpreadState>();
|
let state = app.state::<LanSpreadState>();
|
||||||
|
if state.main_log_sink.set(main_log_sink.clone()).is_err() {
|
||||||
|
log::warn!("main log sink was already initialized");
|
||||||
|
}
|
||||||
|
init_main_logging(main_log_sink)?;
|
||||||
let unpack_logs = load_unpack_logs(&state_dir);
|
let unpack_logs = load_unpack_logs(&state_dir);
|
||||||
tauri::async_runtime::block_on(async {
|
tauri::async_runtime::block_on(async {
|
||||||
*state.unpack_logs.write().await = unpack_logs;
|
*state.unpack_logs.write().await = unpack_logs;
|
||||||
|
|||||||
@@ -3,114 +3,28 @@ import { invoke } from '@tauri-apps/api/core';
|
|||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
import { SegmentedRadio } from './components/SegmentedRadio';
|
import { SegmentedRadio } from './components/SegmentedRadio';
|
||||||
|
import {
|
||||||
|
capLogRows,
|
||||||
|
consumeLoadedHistoryRow,
|
||||||
|
dedupeBufferedRows,
|
||||||
|
formatCount,
|
||||||
|
LEVEL_FILTER_MIN,
|
||||||
|
LEVEL_FILTER_OPTIONS,
|
||||||
|
LEVEL_ORDER,
|
||||||
|
lineCountsFromRows,
|
||||||
|
type LevelFilter,
|
||||||
|
type MainLogHistoryPayload,
|
||||||
|
type MainLogLinePayload,
|
||||||
|
type MainLogRow,
|
||||||
|
rowFromPayload,
|
||||||
|
rowsFromHistory,
|
||||||
|
} from './lib/mainLogs';
|
||||||
|
|
||||||
import './MainLogsWindow.css';
|
import './MainLogsWindow.css';
|
||||||
|
|
||||||
export const isMainLogsView = (): boolean =>
|
export const isMainLogsView = (): boolean =>
|
||||||
new URLSearchParams(window.location.search).get('view') === 'main-logs';
|
new URLSearchParams(window.location.search).get('view') === 'main-logs';
|
||||||
|
|
||||||
const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'] as const;
|
|
||||||
type LogLevel = typeof LOG_LEVELS[number];
|
|
||||||
type LevelFilter = 'all' | 'debug' | 'info' | 'warn' | 'error';
|
|
||||||
|
|
||||||
interface MainLogLinePayload {
|
|
||||||
line: string;
|
|
||||||
level: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MainLogRow {
|
|
||||||
line: string;
|
|
||||||
level: LogLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LEVEL_ORDER: Record<LogLevel, number> = {
|
|
||||||
TRACE: 0,
|
|
||||||
DEBUG: 1,
|
|
||||||
INFO: 2,
|
|
||||||
WARN: 3,
|
|
||||||
ERROR: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
const LEVEL_FILTER_MIN: Record<LevelFilter, number> = {
|
|
||||||
all: LEVEL_ORDER.TRACE,
|
|
||||||
debug: LEVEL_ORDER.DEBUG,
|
|
||||||
info: LEVEL_ORDER.INFO,
|
|
||||||
warn: LEVEL_ORDER.WARN,
|
|
||||||
error: LEVEL_ORDER.ERROR,
|
|
||||||
};
|
|
||||||
|
|
||||||
const LEVEL_FILTER_OPTIONS: ReadonlyArray<{ value: LevelFilter; label: string }> = [
|
|
||||||
{ value: 'all', label: 'All' },
|
|
||||||
{ value: 'debug', label: 'Debug+' },
|
|
||||||
{ value: 'info', label: 'Info+' },
|
|
||||||
{ value: 'warn', label: 'Warn+' },
|
|
||||||
{ value: 'error', label: 'Error only' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const MAX_IN_MEMORY_LOG_ROWS = 12_000;
|
|
||||||
const MAX_IN_MEMORY_LOG_CHARS = 2 * 1024 * 1024;
|
|
||||||
|
|
||||||
const isLogLevel = (value: string | undefined): value is LogLevel =>
|
|
||||||
typeof value === 'string' && (LOG_LEVELS as readonly string[]).includes(value);
|
|
||||||
|
|
||||||
const normalizeLogLevel = (value: string | undefined): LogLevel => {
|
|
||||||
const upper = value?.toUpperCase();
|
|
||||||
return isLogLevel(upper) ? upper : 'INFO';
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseLogLevelFromLine = (line: string): LogLevel => {
|
|
||||||
const match = line.match(/\[(TRACE|DEBUG|INFO|WARN|ERROR)\](?:\s|$)/);
|
|
||||||
return normalizeLogLevel(match?.[1]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rowFromPayload = (payload: MainLogLinePayload): MainLogRow => ({
|
|
||||||
line: payload.line,
|
|
||||||
level: normalizeLogLevel(payload.level),
|
|
||||||
});
|
|
||||||
|
|
||||||
const rowsFromHistory = (text: string): MainLogRow[] =>
|
|
||||||
text
|
|
||||||
.split(/\r?\n/)
|
|
||||||
.filter(line => line.length > 0)
|
|
||||||
.map(line => ({ line, level: parseLogLevelFromLine(line) }));
|
|
||||||
|
|
||||||
const capLogRows = (rows: MainLogRow[]): MainLogRow[] => {
|
|
||||||
let charCount = 0;
|
|
||||||
const capped: MainLogRow[] = [];
|
|
||||||
|
|
||||||
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
||||||
const row = rows[index];
|
|
||||||
const rowChars = row.line.length + 1;
|
|
||||||
const wouldExceedRows = capped.length >= MAX_IN_MEMORY_LOG_ROWS;
|
|
||||||
const wouldExceedChars = charCount + rowChars > MAX_IN_MEMORY_LOG_CHARS;
|
|
||||||
if (wouldExceedRows || (wouldExceedChars && capped.length > 0)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
capped.push(row);
|
|
||||||
charCount += rowChars;
|
|
||||||
}
|
|
||||||
|
|
||||||
return capped.reverse();
|
|
||||||
};
|
|
||||||
|
|
||||||
const dedupeBufferedRows = (historyRows: MainLogRow[], bufferedRows: MainLogRow[]): MainLogRow[] => {
|
|
||||||
const lineCounts = new Map<string, number>();
|
|
||||||
historyRows.forEach(row => {
|
|
||||||
lineCounts.set(row.line, (lineCounts.get(row.line) ?? 0) + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
return bufferedRows.filter(row => {
|
|
||||||
const count = lineCounts.get(row.line) ?? 0;
|
|
||||||
if (count <= 0) return true;
|
|
||||||
lineCounts.set(row.line, count - 1);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCount = (count: number, noun: string): string =>
|
|
||||||
`${count.toLocaleString()} ${noun}${count === 1 ? '' : 's'}`;
|
|
||||||
|
|
||||||
export const MainLogsWindow = () => {
|
export const MainLogsWindow = () => {
|
||||||
const [logs, setLogs] = useState<MainLogRow[]>([]);
|
const [logs, setLogs] = useState<MainLogRow[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -127,6 +41,8 @@ export const MainLogsWindow = () => {
|
|||||||
const initialBufferRef = useRef<MainLogRow[]>([]);
|
const initialBufferRef = useRef<MainLogRow[]>([]);
|
||||||
const pausedBufferRef = useRef<MainLogRow[]>([]);
|
const pausedBufferRef = useRef<MainLogRow[]>([]);
|
||||||
const pausedRef = useRef(false);
|
const pausedRef = useRef(false);
|
||||||
|
const lastHistorySequenceRef = useRef(0);
|
||||||
|
const historyLineCountsRef = useRef<Map<string, number>>(new Map());
|
||||||
|
|
||||||
const appendVisibleRows = useCallback((rows: MainLogRow[]) => {
|
const appendVisibleRows = useCallback((rows: MainLogRow[]) => {
|
||||||
setLogs(current => capLogRows([...current, ...rows]));
|
setLogs(current => capLogRows([...current, ...rows]));
|
||||||
@@ -146,6 +62,15 @@ export const MainLogsWindow = () => {
|
|||||||
initialBufferRef.current = capLogRows([...initialBufferRef.current, row]);
|
initialBufferRef.current = capLogRows([...initialBufferRef.current, row]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
consumeLoadedHistoryRow(
|
||||||
|
historyLineCountsRef.current,
|
||||||
|
row,
|
||||||
|
lastHistorySequenceRef.current,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (pausedRef.current) {
|
if (pausedRef.current) {
|
||||||
bufferPausedRows([row]);
|
bufferPausedRows([row]);
|
||||||
return;
|
return;
|
||||||
@@ -159,12 +84,19 @@ export const MainLogsWindow = () => {
|
|||||||
handleIncomingRow(rowFromPayload(event.payload));
|
handleIncomingRow(rowFromPayload(event.payload));
|
||||||
});
|
});
|
||||||
|
|
||||||
const history = await invoke<string>('get_main_logs');
|
const history = await invoke<MainLogHistoryPayload>('get_main_logs');
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
const historyRows = rowsFromHistory(history);
|
lastHistorySequenceRef.current = history.lastSequence;
|
||||||
const liveRows = dedupeBufferedRows(historyRows, initialBufferRef.current);
|
const historyRows = rowsFromHistory(history.contents);
|
||||||
|
const historyLineCounts = lineCountsFromRows(historyRows);
|
||||||
|
const liveRows = dedupeBufferedRows(
|
||||||
|
historyLineCounts,
|
||||||
|
initialBufferRef.current,
|
||||||
|
lastHistorySequenceRef.current,
|
||||||
|
);
|
||||||
initialBufferRef.current = [];
|
initialBufferRef.current = [];
|
||||||
|
historyLineCountsRef.current = historyLineCounts;
|
||||||
historyLoadedRef.current = true;
|
historyLoadedRef.current = true;
|
||||||
|
|
||||||
if (pausedRef.current) {
|
if (pausedRef.current) {
|
||||||
@@ -192,6 +124,8 @@ export const MainLogsWindow = () => {
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
historyLoadedRef.current = false;
|
historyLoadedRef.current = false;
|
||||||
initialBufferRef.current = [];
|
initialBufferRef.current = [];
|
||||||
|
lastHistorySequenceRef.current = 0;
|
||||||
|
historyLineCountsRef.current = new Map();
|
||||||
unlisten?.();
|
unlisten?.();
|
||||||
};
|
};
|
||||||
}, [appendVisibleRows, bufferPausedRows]);
|
}, [appendVisibleRows, bufferPausedRows]);
|
||||||
@@ -215,6 +149,8 @@ export const MainLogsWindow = () => {
|
|||||||
});
|
});
|
||||||
}, [levelFilter, logs, regex]);
|
}, [levelFilter, logs, regex]);
|
||||||
|
|
||||||
|
const lastVisibleRow = filteredRows.length > 0 ? filteredRows[filteredRows.length - 1] : null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autoScroll) return;
|
if (!autoScroll) return;
|
||||||
const viewport = viewportRef.current;
|
const viewport = viewportRef.current;
|
||||||
@@ -223,7 +159,7 @@ export const MainLogsWindow = () => {
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
viewport.scrollTop = viewport.scrollHeight;
|
viewport.scrollTop = viewport.scrollHeight;
|
||||||
});
|
});
|
||||||
}, [autoScroll, filteredRows.length]);
|
}, [autoScroll, filteredRows.length, lastVisibleRow?.id]);
|
||||||
|
|
||||||
const flushPausedRows = useCallback(() => {
|
const flushPausedRows = useCallback(() => {
|
||||||
const buffered = pausedBufferRef.current;
|
const buffered = pausedBufferRef.current;
|
||||||
@@ -328,9 +264,9 @@ export const MainLogsWindow = () => {
|
|||||||
<div className="main-log-empty">
|
<div className="main-log-empty">
|
||||||
{logs.length === 0 ? 'No application logs recorded yet.' : 'No log lines match the current filters.'}
|
{logs.length === 0 ? 'No application logs recorded yet.' : 'No log lines match the current filters.'}
|
||||||
</div>
|
</div>
|
||||||
) : filteredRows.map((row, index) => (
|
) : filteredRows.map(row => (
|
||||||
<div
|
<div
|
||||||
key={`${index}-${row.line}`}
|
key={row.id}
|
||||||
className={`main-log-line level-${row.level.toLowerCase()}`}
|
className={`main-log-line level-${row.level.toLowerCase()}`}
|
||||||
>
|
>
|
||||||
{row.line}
|
{row.line}
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
export const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'] as const;
|
||||||
|
export type LogLevel = typeof LOG_LEVELS[number];
|
||||||
|
export type LevelFilter = 'all' | 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
|
||||||
|
export interface MainLogLinePayload {
|
||||||
|
line: string;
|
||||||
|
level: string;
|
||||||
|
sequence?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainLogHistoryPayload {
|
||||||
|
contents: string;
|
||||||
|
lastSequence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainLogRow {
|
||||||
|
id: string;
|
||||||
|
line: string;
|
||||||
|
level: LogLevel;
|
||||||
|
sequence?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LEVEL_ORDER: Record<LogLevel, number> = {
|
||||||
|
TRACE: 0,
|
||||||
|
DEBUG: 1,
|
||||||
|
INFO: 2,
|
||||||
|
WARN: 3,
|
||||||
|
ERROR: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LEVEL_FILTER_MIN: Record<LevelFilter, number> = {
|
||||||
|
all: LEVEL_ORDER.TRACE,
|
||||||
|
debug: LEVEL_ORDER.DEBUG,
|
||||||
|
info: LEVEL_ORDER.INFO,
|
||||||
|
warn: LEVEL_ORDER.WARN,
|
||||||
|
error: LEVEL_ORDER.ERROR,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LEVEL_FILTER_OPTIONS: ReadonlyArray<{ value: LevelFilter; label: string }> = [
|
||||||
|
{ value: 'all', label: 'All' },
|
||||||
|
{ value: 'debug', label: 'Debug+' },
|
||||||
|
{ value: 'info', label: 'Info+' },
|
||||||
|
{ value: 'warn', label: 'Warn+' },
|
||||||
|
{ value: 'error', label: 'Error only' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const MAX_IN_MEMORY_LOG_ROWS = 12_000;
|
||||||
|
const MAX_IN_MEMORY_LOG_CHARS = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
let nextSyntheticLogRowId = 0;
|
||||||
|
|
||||||
|
const syntheticLogRowId = (): string => {
|
||||||
|
nextSyntheticLogRowId += 1;
|
||||||
|
return `live-synthetic-${nextSyntheticLogRowId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLogLevel = (value: string | undefined): value is LogLevel =>
|
||||||
|
typeof value === 'string' && (LOG_LEVELS as readonly string[]).includes(value);
|
||||||
|
|
||||||
|
export const normalizeLogLevel = (value: string | undefined): LogLevel => {
|
||||||
|
const upper = value?.toUpperCase();
|
||||||
|
return isLogLevel(upper) ? upper : 'INFO';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseLogLevelFromLine = (line: string): LogLevel => {
|
||||||
|
const match = line.match(/\[(TRACE|DEBUG|INFO|WARN|ERROR)\](?:\s|$)/);
|
||||||
|
return normalizeLogLevel(match?.[1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rowFromPayload = (payload: MainLogLinePayload): MainLogRow => {
|
||||||
|
const sequence = typeof payload.sequence === 'number' ? payload.sequence : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: sequence === undefined ? syntheticLogRowId() : `live-${sequence}`,
|
||||||
|
line: payload.line,
|
||||||
|
level: normalizeLogLevel(payload.level),
|
||||||
|
sequence,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rowsFromHistory = (text: string): MainLogRow[] =>
|
||||||
|
text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.filter(line => line.length > 0)
|
||||||
|
.map((line, index) => ({
|
||||||
|
id: `history-${index}`,
|
||||||
|
line,
|
||||||
|
level: parseLogLevelFromLine(line),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const capLogRows = (rows: MainLogRow[]): MainLogRow[] => {
|
||||||
|
let charCount = 0;
|
||||||
|
const capped: MainLogRow[] = [];
|
||||||
|
|
||||||
|
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
||||||
|
const row = rows[index];
|
||||||
|
const rowChars = row.line.length + 1;
|
||||||
|
const wouldExceedRows = capped.length >= MAX_IN_MEMORY_LOG_ROWS;
|
||||||
|
const wouldExceedChars = charCount + rowChars > MAX_IN_MEMORY_LOG_CHARS;
|
||||||
|
if (wouldExceedRows || (wouldExceedChars && capped.length > 0)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
capped.push(row);
|
||||||
|
charCount += rowChars;
|
||||||
|
}
|
||||||
|
|
||||||
|
return capped.reverse();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rowWasLoadedInHistory = (row: MainLogRow, lastHistorySequence: number): boolean =>
|
||||||
|
typeof row.sequence === 'number' && row.sequence <= lastHistorySequence;
|
||||||
|
|
||||||
|
export const lineCountsFromRows = (rows: MainLogRow[]): Map<string, number> => {
|
||||||
|
const lineCounts = new Map<string, number>();
|
||||||
|
rows.forEach(row => {
|
||||||
|
lineCounts.set(row.line, (lineCounts.get(row.line) ?? 0) + 1);
|
||||||
|
});
|
||||||
|
return lineCounts;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const consumeLoadedHistoryRow = (
|
||||||
|
historyLineCounts: Map<string, number>,
|
||||||
|
row: MainLogRow,
|
||||||
|
lastHistorySequence: number,
|
||||||
|
): boolean => {
|
||||||
|
if (!rowWasLoadedInHistory(row, lastHistorySequence)) return false;
|
||||||
|
|
||||||
|
const count = historyLineCounts.get(row.line) ?? 0;
|
||||||
|
if (count <= 0) return false;
|
||||||
|
|
||||||
|
if (count === 1) {
|
||||||
|
historyLineCounts.delete(row.line);
|
||||||
|
} else {
|
||||||
|
historyLineCounts.set(row.line, count - 1);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dedupeBufferedRows = (
|
||||||
|
historyLineCounts: Map<string, number>,
|
||||||
|
bufferedRows: MainLogRow[],
|
||||||
|
lastHistorySequence: number,
|
||||||
|
): MainLogRow[] =>
|
||||||
|
bufferedRows.filter(row => !consumeLoadedHistoryRow(historyLineCounts, row, lastHistorySequence));
|
||||||
|
|
||||||
|
export const formatCount = (count: number, noun: string): string =>
|
||||||
|
`${count.toLocaleString()} ${noun}${count === 1 ? '' : 's'}`;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
dedupeBufferedRows,
|
||||||
|
lineCountsFromRows,
|
||||||
|
rowFromPayload,
|
||||||
|
rowsFromHistory,
|
||||||
|
rowWasLoadedInHistory,
|
||||||
|
} from '../src/lib/mainLogs.ts';
|
||||||
|
|
||||||
|
const assertEquals = <T>(actual: T, expected: T, message: string) => {
|
||||||
|
if (actual !== expected) {
|
||||||
|
throw new Error(`${message}: expected ${expected}, got ${actual}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Deno.test('history rows parse levels and stable ids', () => {
|
||||||
|
const rows = rowsFromHistory('[2026-06-07][12:00:00][app][WARN] careful\nplain line\n');
|
||||||
|
|
||||||
|
assertEquals(rows.length, 2, 'history should skip trailing empty line');
|
||||||
|
assertEquals(rows[0].id, 'history-0', 'history id should include row position');
|
||||||
|
assertEquals(rows[0].level, 'WARN', 'explicit level should be parsed');
|
||||||
|
assertEquals(rows[1].level, 'INFO', 'unknown level should default to info');
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('buffered main log rows covered by history sequence are removed', () => {
|
||||||
|
const historyRows = rowsFromHistory('[2026-06-07][12:00:01][app][INFO] included\n');
|
||||||
|
const included = rowFromPayload({
|
||||||
|
line: '[2026-06-07][12:00:01][app][INFO] included',
|
||||||
|
level: 'INFO',
|
||||||
|
sequence: 4,
|
||||||
|
});
|
||||||
|
const fresh = rowFromPayload({
|
||||||
|
line: '[2026-06-07][12:00:02][app][INFO] fresh',
|
||||||
|
level: 'INFO',
|
||||||
|
sequence: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deduped = dedupeBufferedRows(lineCountsFromRows(historyRows), [included, fresh], 4);
|
||||||
|
|
||||||
|
assertEquals(rowWasLoadedInHistory(included, 4), true, 'included row should match history');
|
||||||
|
assertEquals(deduped.length, 1, 'only fresh row should remain');
|
||||||
|
assertEquals(deduped[0].line, fresh.line, 'fresh row should not be dropped');
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('buffered rows missing from trimmed history are retained', () => {
|
||||||
|
const retained = rowFromPayload({
|
||||||
|
line: '[2026-06-07][12:00:00][app][INFO] trimmed out',
|
||||||
|
level: 'INFO',
|
||||||
|
sequence: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deduped = dedupeBufferedRows(new Map(), [retained], 4);
|
||||||
|
|
||||||
|
assertEquals(deduped.length, 1, 'trimmed row should remain visible');
|
||||||
|
assertEquals(deduped[0].line, retained.line, 'trimmed row should be preserved');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user