Compare commits
2 Commits
20404d0145
...
72659eab2e
| Author | SHA1 | Date | |
|---|---|---|---|
|
72659eab2e
|
|||
|
9d14e63613
|
Generated
+94
-346
@@ -8,17 +8,6 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ahash"
|
|
||||||
version = "0.7.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.2.17",
|
|
||||||
"once_cell",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -49,23 +38,6 @@ version = "0.2.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "android_log-sys"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "android_logger"
|
|
||||||
version = "0.15.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
|
|
||||||
dependencies = [
|
|
||||||
"android_log-sys",
|
|
||||||
"env_filter",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -81,12 +53,6 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arrayvec"
|
|
||||||
version = "0.7.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atk"
|
name = "atk"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
@@ -189,25 +155,13 @@ 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",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitvec"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
|
||||||
dependencies = [
|
|
||||||
"funty",
|
|
||||||
"radium",
|
|
||||||
"tap",
|
|
||||||
"wyz",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -226,30 +180,6 @@ dependencies = [
|
|||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "borsh"
|
|
||||||
version = "1.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
|
|
||||||
dependencies = [
|
|
||||||
"borsh-derive",
|
|
||||||
"bytes",
|
|
||||||
"cfg_aliases",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "borsh-derive"
|
|
||||||
version = "1.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"proc-macro-crate 3.5.0",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "8.0.3"
|
version = "8.0.3"
|
||||||
@@ -286,40 +216,6 @@ version = "3.20.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byte-unit"
|
|
||||||
version = "5.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d"
|
|
||||||
dependencies = [
|
|
||||||
"rust_decimal",
|
|
||||||
"schemars 1.2.1",
|
|
||||||
"serde",
|
|
||||||
"utf8-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytecheck"
|
|
||||||
version = "0.6.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
|
|
||||||
dependencies = [
|
|
||||||
"bytecheck_derive",
|
|
||||||
"ptr_meta",
|
|
||||||
"simdutf8",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytecheck_derive"
|
|
||||||
version = "0.6.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.25.0"
|
version = "1.25.0"
|
||||||
@@ -347,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",
|
||||||
@@ -453,12 +349,6 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg_aliases"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20"
|
name = "chacha20"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -542,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",
|
||||||
@@ -555,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",
|
||||||
]
|
]
|
||||||
@@ -799,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",
|
||||||
@@ -949,16 +839,6 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "env_filter"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -1022,15 +902,6 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fern"
|
|
||||||
version = "0.7.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "field-offset"
|
name = "field-offset"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
@@ -1148,12 +1019,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "funty"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -1458,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",
|
||||||
@@ -1579,9 +1444,6 @@ name = "hashbrown"
|
|||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
dependencies = [
|
|
||||||
"ahash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
@@ -1616,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",
|
||||||
]
|
]
|
||||||
@@ -1942,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",
|
||||||
]
|
]
|
||||||
@@ -2113,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",
|
||||||
]
|
]
|
||||||
@@ -2134,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",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2233,11 +2095,14 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-log",
|
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tauri-plugin-store",
|
"tauri-plugin-store",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
"tracing-log",
|
||||||
|
"tracing-subscriber",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
@@ -2368,9 +2233,6 @@ name = "log"
|
|||||||
version = "0.4.32"
|
version = "0.4.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||||
dependencies = [
|
|
||||||
"value-bag",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
@@ -2477,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",
|
||||||
@@ -2507,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",
|
||||||
@@ -2525,7 +2387,16 @@ 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]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2610,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",
|
||||||
@@ -2623,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",
|
||||||
]
|
]
|
||||||
@@ -2644,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",
|
||||||
]
|
]
|
||||||
@@ -2655,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",
|
||||||
@@ -2688,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",
|
||||||
@@ -2715,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",
|
||||||
@@ -2728,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",
|
||||||
]
|
]
|
||||||
@@ -2739,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",
|
||||||
@@ -2751,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",
|
||||||
@@ -2782,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",
|
||||||
@@ -2987,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",
|
||||||
@@ -3096,26 +2967,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ptr_meta"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
|
|
||||||
dependencies = [
|
|
||||||
"ptr_meta_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ptr_meta_derive"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.39.4"
|
version = "0.39.4"
|
||||||
@@ -3146,12 +2997,6 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "radium"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -3160,22 +3005,11 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.1.16",
|
"getrandom 0.1.16",
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha 0.2.2",
|
"rand_chacha",
|
||||||
"rand_core 0.5.1",
|
"rand_core 0.5.1",
|
||||||
"rand_hc",
|
"rand_hc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.8.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"rand_chacha 0.3.1",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -3197,16 +3031,6 @@ dependencies = [
|
|||||||
"rand_core 0.5.1",
|
"rand_core 0.5.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_chacha"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
|
||||||
dependencies = [
|
|
||||||
"ppv-lite86",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -3216,15 +3040,6 @@ dependencies = [
|
|||||||
"getrandom 0.1.16",
|
"getrandom 0.1.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.6.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.2.17",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -3252,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]]
|
||||||
@@ -3315,15 +3130,6 @@ version = "0.8.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rend"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
|
|
||||||
dependencies = [
|
|
||||||
"bytecheck",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.4"
|
version = "0.13.4"
|
||||||
@@ -3396,52 +3202,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rkyv"
|
|
||||||
version = "0.7.46"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1"
|
|
||||||
dependencies = [
|
|
||||||
"bitvec",
|
|
||||||
"bytecheck",
|
|
||||||
"bytes",
|
|
||||||
"hashbrown 0.12.3",
|
|
||||||
"ptr_meta",
|
|
||||||
"rend",
|
|
||||||
"rkyv_derive",
|
|
||||||
"seahash",
|
|
||||||
"tinyvec",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rkyv_derive"
|
|
||||||
version = "0.7.46"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust_decimal"
|
|
||||||
version = "1.42.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995"
|
|
||||||
dependencies = [
|
|
||||||
"arrayvec",
|
|
||||||
"borsh",
|
|
||||||
"bytes",
|
|
||||||
"num-traits",
|
|
||||||
"rand 0.8.6",
|
|
||||||
"rkyv",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
@@ -3463,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",
|
||||||
@@ -3745,19 +3505,13 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "seahash"
|
|
||||||
version = "4.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "selectors"
|
name = "selectors"
|
||||||
version = "0.36.1"
|
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",
|
||||||
@@ -3949,6 +3703,15 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shared_child"
|
name = "shared_child"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -4003,12 +3766,6 @@ version = "0.3.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "simdutf8"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -4290,7 +4047,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4344,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",
|
||||||
@@ -4389,12 +4145,6 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tap"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.16"
|
version = "0.12.16"
|
||||||
@@ -4572,28 +4322,6 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tauri-plugin-log"
|
|
||||||
version = "2.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93"
|
|
||||||
dependencies = [
|
|
||||||
"android_logger",
|
|
||||||
"byte-unit",
|
|
||||||
"fern",
|
|
||||||
"log",
|
|
||||||
"objc2",
|
|
||||||
"objc2-foundation",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_repr",
|
|
||||||
"swift-rs",
|
|
||||||
"tauri",
|
|
||||||
"tauri-plugin",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-shell"
|
name = "tauri-plugin-shell"
|
||||||
version = "2.3.5"
|
version = "2.3.5"
|
||||||
@@ -4781,6 +4509,15 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.47"
|
version = "0.3.47"
|
||||||
@@ -5033,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",
|
||||||
@@ -5087,6 +4824,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||||
|
dependencies = [
|
||||||
|
"nu-ansi-term",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5231,12 +4994,6 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8-width"
|
|
||||||
version = "0.1.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -5256,10 +5013,10 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "value-bag"
|
name = "valuable"
|
||||||
version = "1.12.0"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
@@ -5444,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",
|
||||||
@@ -6135,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",
|
||||||
@@ -6215,15 +5972,6 @@ dependencies = [
|
|||||||
"x11-dl",
|
"x11-dl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wyz"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
|
||||||
dependencies = [
|
|
||||||
"tap",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x11"
|
name = "x11"
|
||||||
version = "2.21.0"
|
version = "2.21.0"
|
||||||
|
|||||||
+3
-1
@@ -37,12 +37,14 @@ sqlx = {
|
|||||||
strum = { version = "0.28", features = ["derive"] }
|
strum = { version = "0.28", features = ["derive"] }
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-log = "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-subscriber = "0.3"
|
||||||
uuid = { version = "1", features = ["v7"] }
|
uuid = { version = "1", features = ["v7"] }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
windows = {
|
windows = {
|
||||||
|
|||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
sessionId: session-260605-123454-ub3w
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
### Overview & Goals
|
||||||
|
Add a "main log output" viewer so users can inspect the application's runtime logs. Logs must still be written to stdout, must also be captured to a persistent single file in the app data directory, must be bounded to the last ~2 MB, and must be accessible via a dedicated window opened from the main UI.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
**In Scope:**
|
||||||
|
- Capture `log::` and `tracing::` output to a persistent log file in the app data directory.
|
||||||
|
- Enforce ~2 MB size limit on the stored log (keep the most recent data).
|
||||||
|
- Expose a Tauri command to retrieve the current log content.
|
||||||
|
- Add a new companion window (`?view=main-logs`) with a viewer UI modeled on the existing unpack logs feature.
|
||||||
|
- Support **live tailing** of the logs, displaying new log entries as they are emitted from the backend in real time.
|
||||||
|
- Support **dynamic log level filtering** in the UI to toggle/filter visible logs by log level (TRACE, DEBUG, INFO, WARN, ERROR).
|
||||||
|
- Add "Application logs" entry to the main window kebab menu that opens/focuses the logs window.
|
||||||
|
|
||||||
|
**Out of Scope:**
|
||||||
|
- Configurable log levels or per-module filters in the backend.
|
||||||
|
- Multi-file rotation or archival of old logs.
|
||||||
|
- Persisting logs across app reinstalls or explicit export.
|
||||||
|
|
||||||
|
### User Stories
|
||||||
|
- As a user troubleshooting an issue, I want to open the application logs from the menu so that I can see recent log output without needing external tools.
|
||||||
|
- As a power user, I want the log file to stay bounded (~2 MB) so that it does not grow unbounded on disk.
|
||||||
|
- As a developer or user experiencing an application crash, I want the logs to be persistently saved to a file on disk so that I can inspect them externally even if the application UI cannot be opened or crashes.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
- Logger writes to stdout, app-data file target (`lanspread.log`), and a Tauri event stream for real-time UI tailing.
|
||||||
|
- Log file is kept at ~2 MB; older content is dropped to keep the tail.
|
||||||
|
- `get_main_logs` command returns up to the last 2 MB of log text.
|
||||||
|
- Main logs window renders logs in real-time, automatically appending new log lines.
|
||||||
|
- Auto-scroll to the bottom of the log viewer container when new lines are appended (active by default, can be toggled).
|
||||||
|
- Pause/Resume control to freeze the log stream so the user can easily scroll, select, and read without being interrupted by new lines.
|
||||||
|
- Case-insensitive regex filtering of the displayed lines, using the exact same regex input validation, compilation, and error styling (red input border and error label) as the unpack logs window.
|
||||||
|
- Support **dynamic log level filtering** in the UI via a `SegmentedRadio` filter with options: "All", "Debug+", "Info+", "Warn+", "Error only".
|
||||||
|
- Support copying all/filtered logs to the clipboard and clearing the active log window's in-memory rows. Clearing the viewer does not delete the persisted log file.
|
||||||
|
- Window title: "Application Logs"; label: "main-logs".
|
||||||
|
- Menu item appears in the same kebab menu as "Unpack logs".
|
||||||
|
|
||||||
|
# Technical Design
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
- Logging setup: `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:2223` creates `tauri_plugin_log::Builder` with only `TargetKind::Stdout` + `LevelFilter::Info` (plus mdns off). Plugin registered at 2237.
|
||||||
|
- State & persistence pattern: `LanSpreadState` holds `unpack_logs: Arc<RwLock<Vec<UnpackLogEntry>>>`, `state_dir: OnceLock<PathBuf>`; `load_unpack_logs` / `persist_unpack_logs` + `trim_unpack_logs` (MAX=20) in the same file; exposed by `get_unpack_logs` (153).
|
||||||
|
- UI pattern: `src/windows/MainWindow.tsx:23` defines `openLogsWindow` using `WebviewWindow` with label `unpack-logs` and `?view=unpack-logs`; menu item at 110; `src/App.tsx:9` dispatches via `isUnpackLogsView`; full viewer in `src/UnpackLogsWindow.tsx` (filtering, list+detail, regex).
|
||||||
|
- The repo uses mostly `log::` in the Tauri and peer crates, with some `tracing::` in shared crates. The current Tauri app does not bridge or subscribe to `tracing::` output.
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
- Store the main log as a single plain-text file at `app.path().app_data_dir()?.join("lanspread.log")`.
|
||||||
|
- Do not use `TargetKind::LogDir` for this feature. It writes to the OS log directory and uses file replacement rotation, which does not match the requested app-data location or "keep the latest tail" behavior.
|
||||||
|
- Replace the current `tauri_plugin_log` logger setup with an app-owned logging setup initialized during `.setup` after the app data directory has been resolved.
|
||||||
|
- Install a `tracing_subscriber` subscriber as the single fan-out path for stdout, persistent file writes, and live UI events.
|
||||||
|
- Install `tracing_log::LogTracer` so existing `log::` calls are captured by the same subscriber as `tracing::` events.
|
||||||
|
- If `tauri-plugin-log` is kept registered for future frontend log commands, configure it with `skip_logger()` so it does not compete for the global logger.
|
||||||
|
- Use one backend formatter for both file and live-event output. Format lines as:
|
||||||
|
- `[YYYY-MM-DD][HH:MM:SS][target][LEVEL] message`
|
||||||
|
- This makes historical and live rows comparable and makes client-side level parsing deterministic.
|
||||||
|
- Implement a shared `MainLogSink` that:
|
||||||
|
- Appends formatted lines to `lanspread.log`.
|
||||||
|
- Emits the same formatted line to the frontend via a Tauri event, e.g. `main-log-line`.
|
||||||
|
- Bounds the file to the latest ~2 MB by rewriting the tail when the file exceeds the configured limit.
|
||||||
|
- Never logs from inside the logging path; write/emit failures are ignored or recorded without recursive logging.
|
||||||
|
- Handle the transition from loading history to receiving real-time logs in `MainLogsWindow.tsx` by using a **buffering and deduplication strategy**:
|
||||||
|
- Register the `main-log-line` listener before requesting history.
|
||||||
|
- Buffer incoming live rows while the initial `get_main_logs` file history is being requested.
|
||||||
|
- Once history is loaded, append buffered rows after removing any rows already present in the loaded history by exact line match.
|
||||||
|
- Transition to direct real-time appending with auto-scrolling to the bottom (if auto-scroll is enabled).
|
||||||
|
- Implement **dynamic log level filtering client-side** using the existing `SegmentedRadio` component.
|
||||||
|
- For live-streamed logs, use the structured event payload's level field.
|
||||||
|
- For historical log files loaded via `get_main_logs`, parse the level from the controlled `[LEVEL]` field, falling back to `Info`.
|
||||||
|
- Provide UI controls for "Pause / Resume", "Auto-scroll", and "Log Level Filter" to ensure high-quality user experience during busy logging periods.
|
||||||
|
- Model the new viewer on the unpack-logs style, retaining the identical case-insensitive regex filter ability and regex error reporting UI, but customize it for linear line streaming (sans split-pane list+detail structure, since main logs are a single continuous log flow).
|
||||||
|
|
||||||
|
### Proposed Changes
|
||||||
|
- **Rust dependencies:** add `tracing-subscriber` and `tracing-log` to the workspace/Tauri crate.
|
||||||
|
- **Rust (lib.rs):** initialize the custom logging pipeline in `.setup`, add bounded `MainLogSink`, add `get_main_logs`, emit `main-log-line`, and register the command.
|
||||||
|
- **Tauri capabilities:** add a `main-logs` capability file for the new window label with `core:default`.
|
||||||
|
- **Frontend (MainLogsWindow.tsx):** implement the scrollable log viewport using `listen('main-log-line')` for live tailing, supporting buffering/deduplication, pause/resume, auto-scroll, regex filtering, copying, and clearing.
|
||||||
|
- **Frontend (App.tsx & MainWindow.tsx):** dispatch the new companion view and add the kebab menu entry to open the window.
|
||||||
|
- No changes to peer crate, db, or protocol.
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
- Modified: `Cargo.toml`, `Cargo.lock`, `crates/lanspread-tauri-deno-ts/src-tauri/Cargo.toml`, `crates/lanspread-tauri-deno-ts/src-tauri/Cargo.lock`, `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs`, `crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx`, `crates/lanspread-tauri-deno-ts/src/App.tsx`
|
||||||
|
- Added: `crates/lanspread-tauri-deno-ts/src/MainLogsWindow.tsx` (and `.css` if custom styles needed beyond shared)
|
||||||
|
- Added: `crates/lanspread-tauri-deno-ts/src-tauri/capabilities/main-logs.json`
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
- Trimming on every write would be expensive; mitigate by trimming only when the file exceeds the 2 MB limit plus a small slack threshold, then rewriting the latest tail.
|
||||||
|
- The logging path is synchronous and global; keep it small, lock-protected, and non-recursive.
|
||||||
|
- Log lines may contain very long messages; UI should handle wrapping/horizontal scrolling with stable dimensions.
|
||||||
|
- On Windows release builds the console is hidden, so file becomes the only log source — acceptable.
|
||||||
|
- Installing `LogTracer` plus a `tracing_subscriber` subscriber must happen once. Tests should avoid double-initializing global logging.
|
||||||
|
|
||||||
|
# Delivery Steps
|
||||||
|
|
||||||
|
### Step 1: extend-logger-and-add-get-main-logs-command
|
||||||
|
Logger writes to stdout, persistent app-data `lanspread.log`, and a live UI event stream. A bounded sink enforces the 2 MB tail.
|
||||||
|
|
||||||
|
- Add workspace/Tauri dependencies for `tracing-subscriber` and `tracing-log`.
|
||||||
|
- Add constants `MAIN_LOG_FILE_NAME: &str = "lanspread.log"` and `MAX_MAIN_LOG_BYTES: u64 = 2 * 1024 * 1024` near the unpack log consts.
|
||||||
|
- Implement `main_log_path(state_dir: &Path) -> PathBuf`.
|
||||||
|
- Implement `trim_main_log_file(path: &Path)` that checks file size and rewrites only the last ~2 MB if exceeded, preserving valid UTF-8 by trimming to a character boundary when needed.
|
||||||
|
- Implement a `MainLogSink` or equivalent shared target that formats each log record once, appends it to `lanspread.log`, emits `main-log-line`, and trims when the file grows past the limit plus slack.
|
||||||
|
- Initialize logging during `.setup` after resolving `let state_dir = app.path().app_data_dir()?` and creating the app data directory.
|
||||||
|
- Install `LogTracer` for `log::` macros.
|
||||||
|
- Install a `tracing_subscriber` subscriber/layer that writes to stdout and the shared main-log sink.
|
||||||
|
- Preserve the current global level behavior: `Info` by default and `mdns_sd::service_daemon` off.
|
||||||
|
- Implement `#[tauri::command] async fn get_main_logs(app_handle: tauri::AppHandle) -> tauri::Result<String>` that resolves `app_handle.path().app_data_dir()`, trims `lanspread.log` if needed, reads the file (or returns empty string), and returns the content.
|
||||||
|
- Add `get_main_logs` to the `invoke_handler!` macro list.
|
||||||
|
- Add focused unit tests for tail trimming and level parsing/formatting helpers where practical.
|
||||||
|
|
||||||
|
### Step 2: implement-main-logs-frontend-window-and-dispatch
|
||||||
|
Implement `MainLogsWindow` with loading + real-time streaming and handle routing.
|
||||||
|
|
||||||
|
- Create `crates/lanspread-tauri-deno-ts/src/MainLogsWindow.tsx`:
|
||||||
|
- Fetch existing logs via `invoke('get_main_logs')` on load and split into an array of lines.
|
||||||
|
- Register `listen<MainLogLinePayload>('main-log-line', ...)` from `@tauri-apps/api/event` before fetching history.
|
||||||
|
- Buffer incoming logs while loading history, and append/deduplicate them once loaded.
|
||||||
|
- Store logs as objects containing the log line text and its associated `LogLevel`.
|
||||||
|
- Extract `LogLevel` for live logs from the event payload. For historical logs, parse `LogLevel` from the controlled `[LEVEL]` field (default to Info).
|
||||||
|
- Cap in-memory rows/bytes so a long-running logs window does not grow without bound.
|
||||||
|
- Render the log lines in a scrollable view.
|
||||||
|
- Implement the identical case-insensitive regex filter ability on lines (compiling regex with error catching, displaying error strings below header, and toggling an invalid input class name).
|
||||||
|
- Implement dynamic log level filtering in the UI using the shared `SegmentedRadio` component with options: "All", "Debug+", "Info+", "Warn+", "Error only".
|
||||||
|
- Implement UI controls: "Auto-scroll" checkbox (scrolls viewport to bottom when new logs arrive, active by default), "Pause / Resume" toggle (buffers/pauses incoming stream rendering), "Clear" button, "Copy to clipboard" button.
|
||||||
|
- Implement `isMainLogsView()` helper that checks the `view=main-logs` query param (export it).
|
||||||
|
- Update `crates/lanspread-tauri-deno-ts/src/App.tsx` to import `MainLogsWindow, isMainLogsView` and dispatch to it when the query matches (before falling back to MainWindow).
|
||||||
|
- Create `MainLogsWindow.css` for styling the viewer, search bars, controls, and scrollable log pane.
|
||||||
|
|
||||||
|
### Step 3: add-main-logs-capability
|
||||||
|
The new companion window can invoke commands and listen for log events.
|
||||||
|
|
||||||
|
- Add `crates/lanspread-tauri-deno-ts/src-tauri/capabilities/main-logs.json`.
|
||||||
|
- Set `"windows": ["main-logs"]`.
|
||||||
|
- Grant `"permissions": ["core:default"]`.
|
||||||
|
- No `log:default` permission is needed because the UI listens to the app-owned `main-log-line` event via Tauri core events.
|
||||||
|
|
||||||
|
### Step 4: add-menu-item-and-window-opener-in-mainwindow
|
||||||
|
Users can open the main logs viewer from the existing kebab menu.
|
||||||
|
|
||||||
|
- In `crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx`, add an `openMainLogsWindow` async function (copy of `openLogsWindow` but with label `'main-logs'`, title `'Application Logs'`, url `'/?view=main-logs'`).
|
||||||
|
- Add a new `KebabItem` for `'Application logs'` that calls `openMainLogsWindow`, placed near the existing `'Unpack logs'` entry in the `kebabItems` array.
|
||||||
|
- Verify that opening the window focuses an existing instance if already open (same pattern as unpack logs).
|
||||||
|
|
||||||
|
### Step 5: verify
|
||||||
|
Use the repo's just commands.
|
||||||
|
|
||||||
|
- `just fmt`
|
||||||
|
- `just clippy`
|
||||||
|
- `just frontend-test`
|
||||||
|
- `just test`
|
||||||
|
- Manual smoke test with `just run`: open "Application logs", verify history loads from app-data `lanspread.log`, new backend logs append live, filters work, pause/resume works, copy works, and the file remains bounded near 2 MB.
|
||||||
@@ -31,11 +31,14 @@ serde = { workspace = true }
|
|||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tauri = { workspace = true }
|
tauri = { workspace = true }
|
||||||
tauri-plugin-dialog = { workspace = true }
|
tauri-plugin-dialog = { workspace = true }
|
||||||
tauri-plugin-log = { 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-log = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
walkdir = { workspace = true }
|
walkdir = { workspace = true }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "main-logs",
|
||||||
|
"description": "Capability for the main-logs window",
|
||||||
|
"windows": [
|
||||||
|
"main-logs"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
|
fs::{self, OpenOptions},
|
||||||
|
io::{self, Read as _, Seek as _, SeekFrom, Write as _},
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
path::{Component, Path, PathBuf},
|
path::{Component, Path, PathBuf},
|
||||||
sync::{Arc, OnceLock},
|
sync::{Arc, Mutex, OnceLock},
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,6 +30,12 @@ use tokio::sync::{
|
|||||||
RwLock,
|
RwLock,
|
||||||
mpsc::{UnboundedReceiver, UnboundedSender},
|
mpsc::{UnboundedReceiver, UnboundedSender},
|
||||||
};
|
};
|
||||||
|
use tracing::{Event, Level, Metadata, Subscriber, field::Visit};
|
||||||
|
use tracing_subscriber::{
|
||||||
|
layer::{Context, Layer},
|
||||||
|
prelude::*,
|
||||||
|
registry::LookupSpan,
|
||||||
|
};
|
||||||
|
|
||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
|
|
||||||
@@ -79,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>>,
|
||||||
}
|
}
|
||||||
@@ -132,12 +141,29 @@ struct UnpackLogEntry {
|
|||||||
success: bool,
|
success: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Serialize)]
|
||||||
|
struct MainLogLinePayload {
|
||||||
|
line: 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 {
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_UNPACK_LOGS: usize = 20;
|
const MAX_UNPACK_LOGS: usize = 20;
|
||||||
const UNPACK_LOGS_FILE_NAME: &str = "unpack-logs.json";
|
const UNPACK_LOGS_FILE_NAME: &str = "unpack-logs.json";
|
||||||
|
const MAIN_LOG_FILE_NAME: &str = "lanspread.log";
|
||||||
|
const MAX_MAIN_LOG_BYTES: u64 = 2 * 1024 * 1024;
|
||||||
|
const MAIN_LOG_TRIM_SLACK_BYTES: u64 = 64 * 1024;
|
||||||
|
|
||||||
impl Unpacker for SidecarUnpacker {
|
impl Unpacker for SidecarUnpacker {
|
||||||
fn unpack<'a>(&'a self, archive: &'a Path, dest: &'a Path) -> UnpackFuture<'a> {
|
fn unpack<'a>(&'a self, archive: &'a Path, dest: &'a Path) -> UnpackFuture<'a> {
|
||||||
@@ -156,6 +182,32 @@ async fn get_unpack_logs(
|
|||||||
Ok(state.inner().unpack_logs.read().await.clone())
|
Ok(state.inner().unpack_logs.read().await.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
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()?;
|
||||||
|
fs::create_dir_all(&state_dir)?;
|
||||||
|
let path = main_log_path(&state_dir);
|
||||||
|
|
||||||
|
match read_main_log_file_to_limit(&path, MAX_MAIN_LOG_BYTES) {
|
||||||
|
Ok(contents) => Ok(MainLogHistoryPayload {
|
||||||
|
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||||
const GAME_SETUP_SCRIPT: &str = "game_setup.cmd";
|
const GAME_SETUP_SCRIPT: &str = "game_setup.cmd";
|
||||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||||
@@ -1349,6 +1401,362 @@ fn unpack_logs_path(state_dir: &Path) -> PathBuf {
|
|||||||
state_dir.join(UNPACK_LOGS_FILE_NAME)
|
state_dir.join(UNPACK_LOGS_FILE_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn main_log_path(state_dir: &Path) -> PathBuf {
|
||||||
|
state_dir.join(MAIN_LOG_FILE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
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,
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
|
||||||
|
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 {
|
||||||
|
file.seek(SeekFrom::End(0))?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let tail = if max_bytes == 0 {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
file.seek(SeekFrom::Start(metadata.len() - max_bytes))?;
|
||||||
|
|
||||||
|
let mut bytes = Vec::with_capacity(usize::try_from(max_bytes).unwrap_or(usize::MAX));
|
||||||
|
file.read_to_end(&mut bytes)?;
|
||||||
|
valid_utf8_tail(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 {
|
||||||
|
for offset in 0..bytes.len().min(4) {
|
||||||
|
if let Ok(tail) = std::str::from_utf8(&bytes[offset..]) {
|
||||||
|
return tail.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String::from_utf8_lossy(&bytes).into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct MainLogSink {
|
||||||
|
app_handle: AppHandle,
|
||||||
|
path: PathBuf,
|
||||||
|
file_state: Arc<Mutex<MainLogFileState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MainLogFileState {
|
||||||
|
file: Option<fs::File>,
|
||||||
|
last_sequence: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MainLogSink {
|
||||||
|
fn new(app_handle: AppHandle, path: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
app_handle,
|
||||||
|
path,
|
||||||
|
file_state: Arc::new(Mutex::new(MainLogFileState::default())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_line(&self, line: String, level: Level) {
|
||||||
|
write_main_log_stdout(&line);
|
||||||
|
let sequence = self.append_file_line(&line);
|
||||||
|
|
||||||
|
let _ = self.app_handle.emit(
|
||||||
|
"main-log-line",
|
||||||
|
MainLogLinePayload {
|
||||||
|
line,
|
||||||
|
level: level.as_str().to_string(),
|
||||||
|
sequence,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_history(&self) -> io::Result<MainLogHistoryPayload> {
|
||||||
|
let mut file_state = self
|
||||||
|
.file_state
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| io::Error::other("main log file lock poisoned"))?;
|
||||||
|
|
||||||
|
if file_state.file.is_none() && !self.path.exists() {
|
||||||
|
return Ok(MainLogHistoryPayload {
|
||||||
|
contents: String::new(),
|
||||||
|
last_sequence: file_state.last_sequence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = {
|
||||||
|
let file = self.cached_file(&mut file_state.file)?;
|
||||||
|
trim_main_log_file_to_limit_with_file(file, MAX_MAIN_LOG_BYTES)?;
|
||||||
|
read_main_log_file_to_limit_with_file(file, MAX_MAIN_LOG_BYTES)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(MainLogHistoryPayload {
|
||||||
|
contents,
|
||||||
|
last_sequence: file_state.last_sequence,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_file_line(&self, line: &str) -> Option<u64> {
|
||||||
|
let Ok(mut file_state) = self.file_state.lock() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let write_result = self.cached_file(&mut file_state.file).and_then(|file| {
|
||||||
|
file.seek(SeekFrom::End(0))
|
||||||
|
.and_then(|_| writeln!(file, "{line}"))
|
||||||
|
});
|
||||||
|
|
||||||
|
if write_result.is_err() {
|
||||||
|
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 {
|
||||||
|
sink: MainLogSink,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MainLogLayer {
|
||||||
|
fn new(sink: MainLogSink) -> Self {
|
||||||
|
Self { sink }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Layer<S> for MainLogLayer
|
||||||
|
where
|
||||||
|
S: Subscriber + for<'span> LookupSpan<'span>,
|
||||||
|
{
|
||||||
|
fn enabled(&self, metadata: &Metadata<'_>, _ctx: Context<'_, S>) -> bool {
|
||||||
|
should_capture_main_log_metadata(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||||||
|
let metadata = event.metadata();
|
||||||
|
if !should_capture_main_log_metadata(metadata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut visitor = MainLogFieldVisitor::default();
|
||||||
|
event.record(&mut visitor);
|
||||||
|
let target = visitor
|
||||||
|
.log_target
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| metadata.target().to_string());
|
||||||
|
let message = visitor.into_message();
|
||||||
|
let (date, time) = current_main_log_timestamp();
|
||||||
|
let line =
|
||||||
|
format_main_log_line_parts(&date, &time, &target, metadata.level().as_str(), &message);
|
||||||
|
|
||||||
|
self.sink.write_line(line, *metadata.level());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MainLogFieldVisitor {
|
||||||
|
message: Option<String>,
|
||||||
|
log_target: Option<String>,
|
||||||
|
fields: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MainLogFieldVisitor {
|
||||||
|
fn record_value(&mut self, field_name: &str, value: String) {
|
||||||
|
match field_name {
|
||||||
|
"message" => self.message = Some(value),
|
||||||
|
"log.target" => self.log_target = Some(value),
|
||||||
|
"log.module_path" | "log.file" | "log.line" => {}
|
||||||
|
_ => self.fields.push(format!("{field_name}={value}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_message(self) -> String {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
if let Some(message) = self.message
|
||||||
|
&& !message.is_empty()
|
||||||
|
{
|
||||||
|
parts.push(message);
|
||||||
|
}
|
||||||
|
parts.extend(self.fields);
|
||||||
|
|
||||||
|
if parts.is_empty() {
|
||||||
|
String::from("(no message)")
|
||||||
|
} else {
|
||||||
|
normalize_main_log_message(&parts.join(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Visit for MainLogFieldVisitor {
|
||||||
|
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
|
||||||
|
self.record_value(field.name(), value.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
|
||||||
|
self.record_value(field.name(), value.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
|
||||||
|
self.record_value(field.name(), value.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
|
||||||
|
self.record_value(field.name(), value.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||||
|
self.record_value(field.name(), format!("{value:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_capture_main_log_metadata(metadata: &Metadata<'_>) -> bool {
|
||||||
|
if metadata.target().starts_with("mdns_sd::service_daemon") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
matches!(*metadata.level(), Level::ERROR | Level::WARN | Level::INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_main_log_timestamp() -> (String, String) {
|
||||||
|
let now = time::OffsetDateTime::now_local().unwrap_or_else(|_| time::OffsetDateTime::now_utc());
|
||||||
|
let date = now.date();
|
||||||
|
let clock = now.time();
|
||||||
|
|
||||||
|
(
|
||||||
|
format!(
|
||||||
|
"{:04}-{:02}-{:02}",
|
||||||
|
date.year(),
|
||||||
|
u8::from(date.month()),
|
||||||
|
date.day()
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"{:02}:{:02}:{:02}",
|
||||||
|
clock.hour(),
|
||||||
|
clock.minute(),
|
||||||
|
clock.second()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_main_log_line_parts(
|
||||||
|
date: &str,
|
||||||
|
time: &str,
|
||||||
|
target: &str,
|
||||||
|
level: &str,
|
||||||
|
message: &str,
|
||||||
|
) -> String {
|
||||||
|
format!(
|
||||||
|
"[{date}][{time}][{}][{level}] {}",
|
||||||
|
normalize_main_log_target(target),
|
||||||
|
normalize_main_log_message(message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_main_log_target(target: &str) -> String {
|
||||||
|
target.replace(['\r', '\n'], " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_main_log_message(message: &str) -> String {
|
||||||
|
message.replace('\r', "\\r").replace('\n', "\\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_main_log_stdout(line: &str) {
|
||||||
|
let stdout = std::io::stdout();
|
||||||
|
let mut stdout = stdout.lock();
|
||||||
|
let _ = writeln!(stdout, "{line}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_main_logging(sink: MainLogSink) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let subscriber = tracing_subscriber::registry().with(MainLogLayer::new(sink));
|
||||||
|
tracing::subscriber::set_global_default(subscriber)?;
|
||||||
|
tracing_log::LogTracer::builder()
|
||||||
|
.with_max_level(log::LevelFilter::Info)
|
||||||
|
.init()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn load_unpack_logs(state_dir: &Path) -> Vec<UnpackLogEntry> {
|
fn load_unpack_logs(state_dir: &Path) -> Vec<UnpackLogEntry> {
|
||||||
let path = unpack_logs_path(state_dir);
|
let path = unpack_logs_path(state_dir);
|
||||||
let contents = match std::fs::read_to_string(&path) {
|
let contents = match std::fs::read_to_string(&path) {
|
||||||
@@ -1918,6 +2326,46 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(root);
|
let _ = std::fs::remove_dir_all(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn main_log_line_format_is_stable_and_single_line() {
|
||||||
|
let line = format_main_log_line_parts(
|
||||||
|
"2026-06-07",
|
||||||
|
"12:34:56",
|
||||||
|
"lanspread\napp",
|
||||||
|
"WARN",
|
||||||
|
"first line\nsecond line",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
line,
|
||||||
|
"[2026-06-07][12:34:56][lanspread app][WARN] first line\\nsecond line"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn main_log_trim_keeps_utf8_tail_at_char_boundary() {
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"lanspread-main-log-test-{}",
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("clock should be after epoch")
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&root).expect("test state dir should be created");
|
||||||
|
let path = main_log_path(&root);
|
||||||
|
std::fs::write(&path, format!("{}{}", "a".repeat(11), "é".repeat(20)))
|
||||||
|
.expect("main log should be written");
|
||||||
|
|
||||||
|
trim_main_log_file_to_limit(&path, 21).expect("main log should trim");
|
||||||
|
|
||||||
|
let trimmed = std::fs::read_to_string(&path).expect("trimmed log should remain utf-8");
|
||||||
|
assert!(trimmed.as_bytes().len() <= 21);
|
||||||
|
assert!(trimmed.starts_with('é'));
|
||||||
|
assert!(trimmed.ends_with('é'));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn active_operation_reconciliation_replaces_stale_ui_history() {
|
fn active_operation_reconciliation_replaces_stale_ui_history() {
|
||||||
let mut active_operations = HashMap::from([
|
let mut active_operations = HashMap::from([
|
||||||
@@ -2220,21 +2668,12 @@ mod tests {
|
|||||||
#[allow(clippy::missing_panics_doc)]
|
#[allow(clippy::missing_panics_doc)]
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let tauri_logger_builder = tauri_plugin_log::Builder::new()
|
|
||||||
.clear_targets()
|
|
||||||
.target(tauri_plugin_log::Target::new(
|
|
||||||
tauri_plugin_log::TargetKind::Stdout,
|
|
||||||
))
|
|
||||||
.level(log::LevelFilter::Info)
|
|
||||||
.level_for("mdns_sd::service_daemon", log::LevelFilter::Off);
|
|
||||||
|
|
||||||
// channel to receive events from the peer
|
// channel to receive events from the peer
|
||||||
let (tx_peer_event, rx_peer_event) = tokio::sync::mpsc::unbounded_channel::<PeerEvent>();
|
let (tx_peer_event, rx_peer_event) = tokio::sync::mpsc::unbounded_channel::<PeerEvent>();
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_store::Builder::new().build())
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_logger_builder.build())
|
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
request_games,
|
request_games,
|
||||||
@@ -2250,14 +2689,20 @@ pub fn run() {
|
|||||||
open_game_files,
|
open_game_files,
|
||||||
get_peer_count,
|
get_peer_count,
|
||||||
get_game_thumbnail,
|
get_game_thumbnail,
|
||||||
get_unpack_logs
|
get_unpack_logs,
|
||||||
|
get_main_logs
|
||||||
])
|
])
|
||||||
.manage(LanSpreadState::default())
|
.manage(LanSpreadState::default())
|
||||||
.manage(PeerEventTx(tx_peer_event))
|
.manage(PeerEventTx(tx_peer_event))
|
||||||
.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)?;
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { MainWindow } from './windows/MainWindow';
|
import { MainWindow } from './windows/MainWindow';
|
||||||
|
import { MainLogsWindow, isMainLogsView } from './MainLogsWindow';
|
||||||
import { UnpackLogsWindow, isUnpackLogsView } from './UnpackLogsWindow';
|
import { UnpackLogsWindow, isUnpackLogsView } from './UnpackLogsWindow';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tauri can spawn this bundle in either the main launcher window or the
|
* Tauri can spawn this bundle in either the main launcher window or the
|
||||||
* unpack-logs companion window. The URL query string disambiguates the two so
|
* companion log windows. The URL query string disambiguates the views so
|
||||||
* a single Vite build serves both.
|
* a single Vite build serves both.
|
||||||
*/
|
*/
|
||||||
const App = () => (isUnpackLogsView() ? <UnpackLogsWindow /> : <MainWindow />);
|
const App = () => {
|
||||||
|
if (isMainLogsView()) return <MainLogsWindow />;
|
||||||
|
if (isUnpackLogsView()) return <UnpackLogsWindow />;
|
||||||
|
return <MainWindow />;
|
||||||
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
.main-log-window {
|
||||||
|
height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 18px;
|
||||||
|
background: #000313;
|
||||||
|
color: #D5DBFE;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-controls,
|
||||||
|
.main-log-filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-controls {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-filter-row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-level-filter {
|
||||||
|
--accent: #4866b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #aeb7df;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-regex {
|
||||||
|
flex: 1 1 340px;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-copy-status {
|
||||||
|
color: #8ee6a6;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-load-error {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #ff8a8a;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: Consolas, "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-stats {
|
||||||
|
color: #8892b0;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-viewport {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid #2a3252;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #050813;
|
||||||
|
font-family: Consolas, "Courier New", monospace;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-line {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
color: #D5DBFE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-line.level-trace,
|
||||||
|
.main-log-line.level-debug {
|
||||||
|
color: #9aa6c8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-line.level-warn {
|
||||||
|
color: #ffd37a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-line.level-error {
|
||||||
|
color: #ff8a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-empty {
|
||||||
|
color: #8892b0;
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.main-log-window {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-log-controls {
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
export const isMainLogsView = (): boolean =>
|
||||||
|
new URLSearchParams(window.location.search).get('view') === 'main-logs';
|
||||||
|
|
||||||
|
export const MainLogsWindow = () => {
|
||||||
|
const [logs, setLogs] = useState<MainLogRow[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
const [regexInput, setRegexInput] = useState('');
|
||||||
|
const [levelFilter, setLevelFilter] = useState<LevelFilter>('all');
|
||||||
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
const [paused, setPaused] = useState(false);
|
||||||
|
const [pausedBufferCount, setPausedBufferCount] = useState(0);
|
||||||
|
const [copyStatus, setCopyStatus] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const viewportRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const historyLoadedRef = useRef(false);
|
||||||
|
const initialBufferRef = useRef<MainLogRow[]>([]);
|
||||||
|
const pausedBufferRef = useRef<MainLogRow[]>([]);
|
||||||
|
const pausedRef = useRef(false);
|
||||||
|
const lastHistorySequenceRef = useRef(0);
|
||||||
|
const historyLineCountsRef = useRef<Map<string, number>>(new Map());
|
||||||
|
|
||||||
|
const appendVisibleRows = useCallback((rows: MainLogRow[]) => {
|
||||||
|
setLogs(current => capLogRows([...current, ...rows]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const bufferPausedRows = useCallback((rows: MainLogRow[]) => {
|
||||||
|
pausedBufferRef.current = capLogRows([...pausedBufferRef.current, ...rows]);
|
||||||
|
setPausedBufferCount(pausedBufferRef.current.length);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
let unlisten: (() => void) | undefined;
|
||||||
|
|
||||||
|
const handleIncomingRow = (row: MainLogRow) => {
|
||||||
|
if (!historyLoadedRef.current) {
|
||||||
|
initialBufferRef.current = capLogRows([...initialBufferRef.current, row]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
consumeLoadedHistoryRow(
|
||||||
|
historyLineCountsRef.current,
|
||||||
|
row,
|
||||||
|
lastHistorySequenceRef.current,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pausedRef.current) {
|
||||||
|
bufferPausedRows([row]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appendVisibleRows([row]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setup = async () => {
|
||||||
|
try {
|
||||||
|
unlisten = await listen<MainLogLinePayload>('main-log-line', event => {
|
||||||
|
handleIncomingRow(rowFromPayload(event.payload));
|
||||||
|
});
|
||||||
|
|
||||||
|
const history = await invoke<MainLogHistoryPayload>('get_main_logs');
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
lastHistorySequenceRef.current = history.lastSequence;
|
||||||
|
const historyRows = rowsFromHistory(history.contents);
|
||||||
|
const historyLineCounts = lineCountsFromRows(historyRows);
|
||||||
|
const liveRows = dedupeBufferedRows(
|
||||||
|
historyLineCounts,
|
||||||
|
initialBufferRef.current,
|
||||||
|
lastHistorySequenceRef.current,
|
||||||
|
);
|
||||||
|
initialBufferRef.current = [];
|
||||||
|
historyLineCountsRef.current = historyLineCounts;
|
||||||
|
historyLoadedRef.current = true;
|
||||||
|
|
||||||
|
if (pausedRef.current) {
|
||||||
|
setLogs(capLogRows(historyRows));
|
||||||
|
bufferPausedRows(liveRows);
|
||||||
|
} else {
|
||||||
|
setLogs(capLogRows([...historyRows, ...liveRows]));
|
||||||
|
}
|
||||||
|
setLoadError(null);
|
||||||
|
} catch (err) {
|
||||||
|
if (!cancelled) {
|
||||||
|
historyLoadedRef.current = true;
|
||||||
|
setLoadError(err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void setup();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
historyLoadedRef.current = false;
|
||||||
|
initialBufferRef.current = [];
|
||||||
|
lastHistorySequenceRef.current = 0;
|
||||||
|
historyLineCountsRef.current = new Map();
|
||||||
|
unlisten?.();
|
||||||
|
};
|
||||||
|
}, [appendVisibleRows, bufferPausedRows]);
|
||||||
|
|
||||||
|
const { regex, regexError } = useMemo(() => {
|
||||||
|
if (!regexInput) {
|
||||||
|
return { regex: null as RegExp | null, regexError: null as string | null };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return { regex: new RegExp(regexInput, 'i'), regexError: null };
|
||||||
|
} catch (e) {
|
||||||
|
return { regex: null, regexError: e instanceof Error ? e.message : String(e) };
|
||||||
|
}
|
||||||
|
}, [regexInput]);
|
||||||
|
|
||||||
|
const filteredRows = useMemo(() => {
|
||||||
|
const minLevel = LEVEL_FILTER_MIN[levelFilter];
|
||||||
|
return logs.filter(row => {
|
||||||
|
if (LEVEL_ORDER[row.level] < minLevel) return false;
|
||||||
|
return regex ? regex.test(row.line) : true;
|
||||||
|
});
|
||||||
|
}, [levelFilter, logs, regex]);
|
||||||
|
|
||||||
|
const lastVisibleRow = filteredRows.length > 0 ? filteredRows[filteredRows.length - 1] : null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autoScroll) return;
|
||||||
|
const viewport = viewportRef.current;
|
||||||
|
if (!viewport) return;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
viewport.scrollTop = viewport.scrollHeight;
|
||||||
|
});
|
||||||
|
}, [autoScroll, filteredRows.length, lastVisibleRow?.id]);
|
||||||
|
|
||||||
|
const flushPausedRows = useCallback(() => {
|
||||||
|
const buffered = pausedBufferRef.current;
|
||||||
|
if (buffered.length === 0) return;
|
||||||
|
pausedBufferRef.current = [];
|
||||||
|
setPausedBufferCount(0);
|
||||||
|
appendVisibleRows(buffered);
|
||||||
|
}, [appendVisibleRows]);
|
||||||
|
|
||||||
|
const togglePaused = useCallback(() => {
|
||||||
|
if (paused) {
|
||||||
|
pausedRef.current = false;
|
||||||
|
setPaused(false);
|
||||||
|
flushPausedRows();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pausedRef.current = true;
|
||||||
|
setPaused(true);
|
||||||
|
}, [flushPausedRows, paused]);
|
||||||
|
|
||||||
|
const clearLogs = useCallback(() => {
|
||||||
|
setLogs([]);
|
||||||
|
initialBufferRef.current = [];
|
||||||
|
pausedBufferRef.current = [];
|
||||||
|
setPausedBufferCount(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const copyFilteredLogs = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(filteredRows.map(row => row.line).join('\n'));
|
||||||
|
setCopyStatus('Copied');
|
||||||
|
} catch {
|
||||||
|
setCopyStatus('Copy failed');
|
||||||
|
}
|
||||||
|
window.setTimeout(() => setCopyStatus(null), 1600);
|
||||||
|
}, [filteredRows]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="main-log-window">
|
||||||
|
<div className="main-log-header">
|
||||||
|
<h1>Application Logs</h1>
|
||||||
|
<div className="main-log-controls">
|
||||||
|
<label className="main-log-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={autoScroll}
|
||||||
|
onChange={(e) => setAutoScroll(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Auto-scroll
|
||||||
|
</label>
|
||||||
|
<button className="settings-button" onClick={togglePaused}>
|
||||||
|
{paused ? 'Resume' : 'Pause'}
|
||||||
|
</button>
|
||||||
|
<button className="settings-button" onClick={clearLogs} disabled={logs.length === 0}>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="settings-button"
|
||||||
|
onClick={() => void copyFilteredLogs()}
|
||||||
|
disabled={filteredRows.length === 0}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
{copyStatus && <span className="main-log-copy-status">{copyStatus}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="main-log-filter-row">
|
||||||
|
<div className="main-log-level-filter">
|
||||||
|
<SegmentedRadio
|
||||||
|
value={levelFilter}
|
||||||
|
options={LEVEL_FILTER_OPTIONS}
|
||||||
|
onChange={setLevelFilter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className={`unpack-log-regex main-log-regex ${regexError ? 'invalid' : ''}`}
|
||||||
|
type="text"
|
||||||
|
placeholder="Filter lines by regex (case-insensitive)..."
|
||||||
|
value={regexInput}
|
||||||
|
onChange={(e) => setRegexInput(e.target.value)}
|
||||||
|
title={regexError ?? ''}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{regexError && (
|
||||||
|
<div className="unpack-log-regex-error">regex error: {regexError}</div>
|
||||||
|
)}
|
||||||
|
{loadError && (
|
||||||
|
<div className="main-log-load-error">load error: {loadError}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="main-log-stats">
|
||||||
|
{loading ? 'loading' : `showing ${formatCount(filteredRows.length, 'line')} of ${formatCount(logs.length, 'line')}`}
|
||||||
|
{paused && pausedBufferCount > 0 && ` - ${formatCount(pausedBufferCount, 'paused line')}`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section ref={viewportRef} className="main-log-viewport" aria-live={paused ? 'off' : 'polite'}>
|
||||||
|
{filteredRows.length === 0 ? (
|
||||||
|
<div className="main-log-empty">
|
||||||
|
{logs.length === 0 ? 'No application logs recorded yet.' : 'No log lines match the current filters.'}
|
||||||
|
</div>
|
||||||
|
) : filteredRows.map(row => (
|
||||||
|
<div
|
||||||
|
key={row.id}
|
||||||
|
className={`main-log-line level-${row.level.toLowerCase()}`}
|
||||||
|
>
|
||||||
|
{row.line}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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'}`;
|
||||||
@@ -43,6 +43,29 @@ const openLogsWindow = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openMainLogsWindow = async () => {
|
||||||
|
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow');
|
||||||
|
try {
|
||||||
|
const existing = await WebviewWindow.getByLabel('main-logs');
|
||||||
|
if (existing) {
|
||||||
|
await existing.setFocus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const win = new WebviewWindow('main-logs', {
|
||||||
|
url: '/?view=main-logs',
|
||||||
|
title: 'Application Logs',
|
||||||
|
width: 980,
|
||||||
|
height: 720,
|
||||||
|
resizable: true,
|
||||||
|
});
|
||||||
|
await win.once<unknown>('tauri://error', (event) => {
|
||||||
|
console.error('Error opening application logs window:', event.payload);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error opening application logs window:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const MainWindow = () => {
|
export const MainWindow = () => {
|
||||||
const { settings, set: setSetting } = useSettings();
|
const { settings, set: setSetting } = useSettings();
|
||||||
const { gameDir, hasGameDirectory, setGameDir, rescan } = useGameDirectory();
|
const { gameDir, hasGameDirectory, setGameDir, rescan } = useGameDirectory();
|
||||||
@@ -107,6 +130,7 @@ export const MainWindow = () => {
|
|||||||
{ kind: 'item', label: 'Settings', onClick: () => setSettingsOpen(true) },
|
{ kind: 'item', label: 'Settings', onClick: () => setSettingsOpen(true) },
|
||||||
{ kind: 'item', label: 'Refresh library', onClick: () => rescan() },
|
{ kind: 'item', label: 'Refresh library', onClick: () => rescan() },
|
||||||
{ kind: 'separator' },
|
{ kind: 'separator' },
|
||||||
|
{ kind: 'item', label: 'Application logs', onClick: () => void openMainLogsWindow() },
|
||||||
{ kind: 'item', label: 'Unpack logs', onClick: () => void openLogsWindow() },
|
{ kind: 'item', label: 'Unpack logs', onClick: () => void openLogsWindow() },
|
||||||
], [rescan]);
|
], [rescan]);
|
||||||
|
|
||||||
|
|||||||
@@ -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