Compare commits
1 Commits
main
...
20404d0145
| Author | SHA1 | Date | |
|---|---|---|---|
| 20404d0145 |
Generated
+61
-313
@@ -8,17 +8,6 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -49,23 +38,6 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@@ -81,12 +53,6 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.18.2"
|
||||
@@ -196,18 +162,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -226,30 +180,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "brotli"
|
||||
version = "8.0.3"
|
||||
@@ -286,40 +216,6 @@ version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
@@ -453,12 +349,6 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
@@ -949,16 +839,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -1022,15 +902,6 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fern"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "field-offset"
|
||||
version = "0.3.6"
|
||||
@@ -1148,12 +1019,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
@@ -1579,9 +1444,6 @@ name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
@@ -2233,11 +2095,14 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-store",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"walkdir",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
@@ -2368,9 +2233,6 @@ name = "log"
|
||||
version = "0.4.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||
dependencies = [
|
||||
"value-bag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
@@ -2528,6 +2390,15 @@ dependencies = [
|
||||
"bitflags 2.12.1",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
@@ -3096,26 +2967,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.4"
|
||||
@@ -3146,12 +2997,6 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
@@ -3160,22 +3005,11 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
"libc",
|
||||
"rand_chacha 0.2.2",
|
||||
"rand_chacha",
|
||||
"rand_core 0.5.1",
|
||||
"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]]
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
@@ -3197,16 +3031,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
@@ -3216,15 +3040,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
@@ -3315,15 +3130,6 @@ version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
|
||||
dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.4"
|
||||
@@ -3396,52 +3202,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
@@ -3745,12 +3505,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.36.1"
|
||||
@@ -3949,6 +3703,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "shared_child"
|
||||
version = "1.1.1"
|
||||
@@ -4003,12 +3766,6 @@ version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.3"
|
||||
@@ -4290,7 +4047,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@@ -4389,12 +4145,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -4572,28 +4322,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.3.5"
|
||||
@@ -4781,6 +4509,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "time"
|
||||
version = "0.3.47"
|
||||
@@ -5087,6 +4824,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"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]]
|
||||
@@ -5231,12 +4994,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -5256,10 +5013,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.12.0"
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
@@ -6215,15 +5972,6 @@ dependencies = [
|
||||
"x11-dl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||
dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11"
|
||||
version = "2.21.0"
|
||||
|
||||
+3
-1
@@ -37,12 +37,14 @@ sqlx = {
|
||||
strum = { version = "0.28", features = ["derive"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-log = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-store = "2"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["codec", "rt"] }
|
||||
tracing = "0.1"
|
||||
tracing-log = "0.2"
|
||||
tracing-subscriber = "0.3"
|
||||
time = { version = "0.3", features = ["local-offset"] }
|
||||
uuid = { version = "1", features = ["v7"] }
|
||||
walkdir = "2"
|
||||
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 }
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-dialog = { workspace = true }
|
||||
tauri-plugin-log = { workspace = true }
|
||||
tauri-plugin-shell = { workspace = true }
|
||||
tauri-plugin-store = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-log = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
time = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
|
||||
[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::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs::{self, OpenOptions},
|
||||
io::{Read as _, Seek as _, SeekFrom, Write as _},
|
||||
net::SocketAddr,
|
||||
path::{Component, Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
@@ -28,6 +30,12 @@ use tokio::sync::{
|
||||
RwLock,
|
||||
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/
|
||||
|
||||
@@ -132,12 +140,21 @@ struct UnpackLogEntry {
|
||||
success: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
struct MainLogLinePayload {
|
||||
line: String,
|
||||
level: String,
|
||||
}
|
||||
|
||||
struct SidecarUnpacker {
|
||||
app_handle: AppHandle,
|
||||
}
|
||||
|
||||
const MAX_UNPACK_LOGS: usize = 20;
|
||||
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 {
|
||||
fn unpack<'a>(&'a self, archive: &'a Path, dest: &'a Path) -> UnpackFuture<'a> {
|
||||
@@ -156,6 +173,20 @@ async fn get_unpack_logs(
|
||||
Ok(state.inner().unpack_logs.read().await.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_main_logs(app_handle: tauri::AppHandle) -> tauri::Result<String> {
|
||||
let state_dir = app_handle.path().app_data_dir()?;
|
||||
fs::create_dir_all(&state_dir)?;
|
||||
let path = main_log_path(&state_dir);
|
||||
trim_main_log_file(&path)?;
|
||||
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(contents) => Ok(contents),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
const GAME_SETUP_SCRIPT: &str = "game_setup.cmd";
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
@@ -1349,6 +1380,280 @@ fn unpack_logs_path(state_dir: &Path) -> PathBuf {
|
||||
state_dir.join(UNPACK_LOGS_FILE_NAME)
|
||||
}
|
||||
|
||||
fn main_log_path(state_dir: &Path) -> PathBuf {
|
||||
state_dir.join(MAIN_LOG_FILE_NAME)
|
||||
}
|
||||
|
||||
fn trim_main_log_file(path: &Path) -> std::io::Result<()> {
|
||||
trim_main_log_file_to_limit(path, MAX_MAIN_LOG_BYTES)
|
||||
}
|
||||
|
||||
fn trim_main_log_file_to_limit(path: &Path, max_bytes: u64) -> std::io::Result<()> {
|
||||
let metadata = match fs::metadata(path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
if metadata.len() <= max_bytes {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if max_bytes == 0 {
|
||||
fs::write(path, [])?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut file = fs::File::open(path)?;
|
||||
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)?;
|
||||
let tail = valid_utf8_tail(bytes);
|
||||
fs::write(path, tail.as_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_lock: Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
impl MainLogSink {
|
||||
fn new(app_handle: AppHandle, path: PathBuf) -> Self {
|
||||
Self {
|
||||
app_handle,
|
||||
path,
|
||||
file_lock: Arc::new(Mutex::new(())),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_line(&self, line: String, level: Level) {
|
||||
write_main_log_stdout(&line);
|
||||
self.append_file_line(&line);
|
||||
|
||||
let _ = self.app_handle.emit(
|
||||
"main-log-line",
|
||||
MainLogLinePayload {
|
||||
line,
|
||||
level: level.as_str().to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn append_file_line(&self, line: &str) {
|
||||
let Ok(_guard) = self.file_lock.lock() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(parent) = self.path.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.path)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if writeln!(file, "{line}").is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let should_trim = file
|
||||
.metadata()
|
||||
.is_ok_and(|metadata| {
|
||||
metadata.len() > MAX_MAIN_LOG_BYTES.saturating_add(MAIN_LOG_TRIM_SLACK_BYTES)
|
||||
});
|
||||
|
||||
if should_trim {
|
||||
let _ = trim_main_log_file(&self.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}] {message}",
|
||||
normalize_main_log_target(target)
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
app_handle: AppHandle,
|
||||
path: PathBuf,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let subscriber =
|
||||
tracing_subscriber::registry().with(MainLogLayer::new(MainLogSink::new(app_handle, path)));
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
tracing_log::LogTracer::builder()
|
||||
.with_max_level(log::LevelFilter::Info)
|
||||
.init()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_unpack_logs(state_dir: &Path) -> Vec<UnpackLogEntry> {
|
||||
let path = unpack_logs_path(state_dir);
|
||||
let contents = match std::fs::read_to_string(&path) {
|
||||
@@ -1918,6 +2223,46 @@ mod tests {
|
||||
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]
|
||||
fn active_operation_reconciliation_replaces_stale_ui_history() {
|
||||
let mut active_operations = HashMap::from([
|
||||
@@ -2220,21 +2565,12 @@ mod tests {
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
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
|
||||
let (tx_peer_event, rx_peer_event) = tokio::sync::mpsc::unbounded_channel::<PeerEvent>();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_logger_builder.build())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
request_games,
|
||||
@@ -2250,13 +2586,15 @@ pub fn run() {
|
||||
open_game_files,
|
||||
get_peer_count,
|
||||
get_game_thumbnail,
|
||||
get_unpack_logs
|
||||
get_unpack_logs,
|
||||
get_main_logs
|
||||
])
|
||||
.manage(LanSpreadState::default())
|
||||
.manage(PeerEventTx(tx_peer_event))
|
||||
.setup(move |app| {
|
||||
let state_dir = app.path().app_data_dir()?;
|
||||
std::fs::create_dir_all(&state_dir)?;
|
||||
init_main_logging(app.handle().clone(), main_log_path(&state_dir))?;
|
||||
let state = app.state::<LanSpreadState>();
|
||||
let unpack_logs = load_unpack_logs(&state_dir);
|
||||
tauri::async_runtime::block_on(async {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { MainWindow } from './windows/MainWindow';
|
||||
import { MainLogsWindow, isMainLogsView } from './MainLogsWindow';
|
||||
import { UnpackLogsWindow, isUnpackLogsView } from './UnpackLogsWindow';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const App = () => (isUnpackLogsView() ? <UnpackLogsWindow /> : <MainWindow />);
|
||||
const App = () => {
|
||||
if (isMainLogsView()) return <MainLogsWindow />;
|
||||
if (isUnpackLogsView()) return <UnpackLogsWindow />;
|
||||
return <MainWindow />;
|
||||
};
|
||||
|
||||
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,342 @@
|
||||
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 './MainLogsWindow.css';
|
||||
|
||||
export const isMainLogsView = (): boolean =>
|
||||
new URLSearchParams(window.location.search).get('view') === 'main-logs';
|
||||
|
||||
const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'] as const;
|
||||
type LogLevel = typeof LOG_LEVELS[number];
|
||||
type LevelFilter = 'all' | 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
interface MainLogLinePayload {
|
||||
line: string;
|
||||
level: string;
|
||||
}
|
||||
|
||||
interface MainLogRow {
|
||||
line: string;
|
||||
level: LogLevel;
|
||||
}
|
||||
|
||||
const LEVEL_ORDER: Record<LogLevel, number> = {
|
||||
TRACE: 0,
|
||||
DEBUG: 1,
|
||||
INFO: 2,
|
||||
WARN: 3,
|
||||
ERROR: 4,
|
||||
};
|
||||
|
||||
const LEVEL_FILTER_MIN: Record<LevelFilter, number> = {
|
||||
all: LEVEL_ORDER.TRACE,
|
||||
debug: LEVEL_ORDER.DEBUG,
|
||||
info: LEVEL_ORDER.INFO,
|
||||
warn: LEVEL_ORDER.WARN,
|
||||
error: LEVEL_ORDER.ERROR,
|
||||
};
|
||||
|
||||
const LEVEL_FILTER_OPTIONS: ReadonlyArray<{ value: LevelFilter; label: string }> = [
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'debug', label: 'Debug+' },
|
||||
{ value: 'info', label: 'Info+' },
|
||||
{ value: 'warn', label: 'Warn+' },
|
||||
{ value: 'error', label: 'Error only' },
|
||||
];
|
||||
|
||||
const MAX_IN_MEMORY_LOG_ROWS = 12_000;
|
||||
const MAX_IN_MEMORY_LOG_CHARS = 2 * 1024 * 1024;
|
||||
|
||||
const isLogLevel = (value: string | undefined): value is LogLevel =>
|
||||
typeof value === 'string' && (LOG_LEVELS as readonly string[]).includes(value);
|
||||
|
||||
const normalizeLogLevel = (value: string | undefined): LogLevel => {
|
||||
const upper = value?.toUpperCase();
|
||||
return isLogLevel(upper) ? upper : 'INFO';
|
||||
};
|
||||
|
||||
const parseLogLevelFromLine = (line: string): LogLevel => {
|
||||
const match = line.match(/\[(TRACE|DEBUG|INFO|WARN|ERROR)\](?:\s|$)/);
|
||||
return normalizeLogLevel(match?.[1]);
|
||||
};
|
||||
|
||||
const rowFromPayload = (payload: MainLogLinePayload): MainLogRow => ({
|
||||
line: payload.line,
|
||||
level: normalizeLogLevel(payload.level),
|
||||
});
|
||||
|
||||
const rowsFromHistory = (text: string): MainLogRow[] =>
|
||||
text
|
||||
.split(/\r?\n/)
|
||||
.filter(line => line.length > 0)
|
||||
.map(line => ({ line, level: parseLogLevelFromLine(line) }));
|
||||
|
||||
const capLogRows = (rows: MainLogRow[]): MainLogRow[] => {
|
||||
let charCount = 0;
|
||||
const capped: MainLogRow[] = [];
|
||||
|
||||
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
||||
const row = rows[index];
|
||||
const rowChars = row.line.length + 1;
|
||||
const wouldExceedRows = capped.length >= MAX_IN_MEMORY_LOG_ROWS;
|
||||
const wouldExceedChars = charCount + rowChars > MAX_IN_MEMORY_LOG_CHARS;
|
||||
if (wouldExceedRows || (wouldExceedChars && capped.length > 0)) {
|
||||
break;
|
||||
}
|
||||
|
||||
capped.push(row);
|
||||
charCount += rowChars;
|
||||
}
|
||||
|
||||
return capped.reverse();
|
||||
};
|
||||
|
||||
const dedupeBufferedRows = (historyRows: MainLogRow[], bufferedRows: MainLogRow[]): MainLogRow[] => {
|
||||
const lineCounts = new Map<string, number>();
|
||||
historyRows.forEach(row => {
|
||||
lineCounts.set(row.line, (lineCounts.get(row.line) ?? 0) + 1);
|
||||
});
|
||||
|
||||
return bufferedRows.filter(row => {
|
||||
const count = lineCounts.get(row.line) ?? 0;
|
||||
if (count <= 0) return true;
|
||||
lineCounts.set(row.line, count - 1);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const formatCount = (count: number, noun: string): string =>
|
||||
`${count.toLocaleString()} ${noun}${count === 1 ? '' : 's'}`;
|
||||
|
||||
export const MainLogsWindow = () => {
|
||||
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 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 (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<string>('get_main_logs');
|
||||
if (cancelled) return;
|
||||
|
||||
const historyRows = rowsFromHistory(history);
|
||||
const liveRows = dedupeBufferedRows(historyRows, initialBufferRef.current);
|
||||
initialBufferRef.current = [];
|
||||
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 = [];
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoScroll) return;
|
||||
const viewport = viewportRef.current;
|
||||
if (!viewport) return;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
viewport.scrollTop = viewport.scrollHeight;
|
||||
});
|
||||
}, [autoScroll, filteredRows.length]);
|
||||
|
||||
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, index) => (
|
||||
<div
|
||||
key={`${index}-${row.line}`}
|
||||
className={`main-log-line level-${row.level.toLowerCase()}`}
|
||||
>
|
||||
{row.line}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -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 = () => {
|
||||
const { settings, set: setSetting } = useSettings();
|
||||
const { gameDir, hasGameDirectory, setGameDir, rescan } = useGameDirectory();
|
||||
@@ -107,6 +130,7 @@ export const MainWindow = () => {
|
||||
{ kind: 'item', label: 'Settings', onClick: () => setSettingsOpen(true) },
|
||||
{ kind: 'item', label: 'Refresh library', onClick: () => rescan() },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'item', label: 'Application logs', onClick: () => void openMainLogsWindow() },
|
||||
{ kind: 'item', label: 'Unpack logs', onClick: () => void openLogsWindow() },
|
||||
], [rescan]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user