mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-09-09 00:20:21 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4022d3f2de | ||
|
|
67f3b8a7ec | ||
|
|
c46ab57f73 |
Generated
+312
-69
@@ -8,6 +8,12 @@ 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 = "anyhow"
|
||||||
|
version = "1.0.102"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "arrayref"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -26,6 +32,12 @@ version = "1.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -59,18 +71,18 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "claude-code-usage-bubble"
|
name = "claude-code-usage-bubble"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"embed-resource",
|
"embed-resource",
|
||||||
"log",
|
"log",
|
||||||
|
"self-replace",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"simplelog",
|
"simplelog",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
"toml 0.8.23",
|
|
||||||
"windows",
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -151,7 +163,7 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 1.1.2+spec-1.1.0",
|
"toml",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
@@ -162,6 +174,22 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fdeflate"
|
name = "fdeflate"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@@ -187,6 +215,12 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@@ -208,12 +242,46 @@ dependencies = [
|
|||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
"wasip3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "id-arena"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.0"
|
version = "2.14.0"
|
||||||
@@ -221,7 +289,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.17.1",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -230,6 +300,12 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leb128fmt"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.186"
|
version = "0.2.186"
|
||||||
@@ -245,6 +321,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.29"
|
||||||
@@ -282,6 +364,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -294,7 +382,7 @@ version = "0.17.16"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"fdeflate",
|
"fdeflate",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -307,6 +395,16 @@ 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 = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -325,13 +423,19 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.17",
|
||||||
"libredox",
|
"libredox",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
@@ -345,6 +449,30 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "self-replace"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"tempfile",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.28"
|
version = "1.0.28"
|
||||||
@@ -394,15 +522,6 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_spanned"
|
|
||||||
version = "0.6.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -463,6 +582,19 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.4.2",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
@@ -551,18 +683,6 @@ dependencies = [
|
|||||||
"strict-num",
|
"strict-num",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml"
|
|
||||||
version = "0.8.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"serde_spanned 0.6.9",
|
|
||||||
"toml_datetime 0.6.11",
|
|
||||||
"toml_edit",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
@@ -571,20 +691,11 @@ checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned 1.1.1",
|
"serde_spanned",
|
||||||
"toml_datetime 1.1.1+spec-1.1.0",
|
"toml_datetime",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow 1.0.3",
|
"winnow",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_datetime"
|
|
||||||
version = "0.6.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -596,35 +707,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_edit"
|
|
||||||
version = "0.22.27"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap",
|
|
||||||
"serde",
|
|
||||||
"serde_spanned 0.6.9",
|
|
||||||
"toml_datetime 0.6.11",
|
|
||||||
"toml_write",
|
|
||||||
"winnow 0.7.15",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow 1.0.3",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_write"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
@@ -643,6 +734,12 @@ version = "1.0.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
@@ -675,6 +772,58 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.3+wasi-0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen 0.57.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip3"
|
||||||
|
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen 0.51.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-encoder"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||||
|
dependencies = [
|
||||||
|
"leb128fmt",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-metadata"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"indexmap",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasmparser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"indexmap",
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
@@ -754,6 +903,15 @@ dependencies = [
|
|||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
@@ -836,15 +994,6 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winnow"
|
|
||||||
version = "0.7.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -861,6 +1010,100 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rust-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.57.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-core"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"indexmap",
|
||||||
|
"prettyplease",
|
||||||
|
"syn",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-component",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust-macro"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-bindgen-rust",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-component"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wasmparser",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-parser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"id-arena",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"unicode-xid",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "claude-code-usage-bubble"
|
name = "claude-code-usage-bubble"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
description = "Floating bubble showing Claude Code and Codex usage on Windows"
|
description = "Floating bubble showing Claude Code and Codex usage on Windows"
|
||||||
@@ -14,9 +14,9 @@ dirs = "6"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
simplelog = "0.12"
|
simplelog = "0.12"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
toml = "0.8"
|
|
||||||
tiny-skia = "0.11"
|
tiny-skia = "0.11"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
self-replace = "1.5"
|
||||||
|
|
||||||
[dependencies.windows]
|
[dependencies.windows]
|
||||||
version = "0.58"
|
version = "0.58"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ which solves the same "how close am I to the Claude Code limit?" problem
|
|||||||
with a horizontal taskbar widget. This project takes the UX in a different
|
with a horizontal taskbar widget. This project takes the UX in a different
|
||||||
direction — a floating, draggable circular bubble that the user can place
|
direction — a floating, draggable circular bubble that the user can place
|
||||||
anywhere on screen — and is a clean-room implementation: the HTTP client,
|
anywhere on screen — and is a clean-room implementation: the HTTP client,
|
||||||
provider polling, credential discovery, localisation, tray rendering, and
|
provider polling, credential discovery, tray rendering, and
|
||||||
self-updater are all written from scratch against the same public APIs
|
self-updater are all written from scratch against the same public APIs
|
||||||
(Anthropic, ChatGPT, GitHub Releases).
|
(Anthropic, ChatGPT, GitHub Releases).
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ self-updater are all written from scratch against the same public APIs
|
|||||||
(140–360 logical pixels)
|
(140–360 logical pixels)
|
||||||
- Left-click the bubble for an expanded panel with both **5h** and **7d**
|
- Left-click the bubble for an expanded panel with both **5h** and **7d**
|
||||||
bars plus reset countdowns
|
bars plus reset countdowns
|
||||||
- Right-click for refresh, displayed providers, update frequency, language,
|
- Right-click for refresh, displayed providers, update frequency, startup,
|
||||||
startup, updates, exit
|
updates, exit
|
||||||
- Optional system tray icons (one per enabled provider)
|
- Optional system tray icons (one per enabled provider)
|
||||||
- Auto-hide when a fullscreen app is in the foreground (games, video,
|
- Auto-hide when a fullscreen app is in the foreground (games, video,
|
||||||
presentations) — reappears when you leave fullscreen
|
presentations) — reappears when you leave fullscreen
|
||||||
@@ -42,12 +42,8 @@ self-updater are all written from scratch against the same public APIs
|
|||||||
## Who this is for
|
## Who this is for
|
||||||
|
|
||||||
Windows 10/11 users who already have **Claude Code (CLI or App) installed
|
Windows 10/11 users who already have **Claude Code (CLI or App) installed
|
||||||
and signed in**. Codex support is optional — install and sign in to the
|
and signed in on Windows**. Codex support is optional — install and sign in
|
||||||
Codex CLI, then enable Codex from the right-click **Providers** menu.
|
to the Codex CLI, then enable Codex from the right-click **Providers** menu.
|
||||||
|
|
||||||
If you use Claude Code through WSL, that is supported too. The monitor
|
|
||||||
can read your Claude Code credentials from Windows or from your WSL
|
|
||||||
environment.
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -85,9 +81,9 @@ corner of your primary monitor on first launch. Drag it where you want it,
|
|||||||
release to snap to the nearest edge if you let go close to one.
|
release to snap to the nearest edge if you let go close to one.
|
||||||
|
|
||||||
- **Left-click** the bubble to open the expanded panel (5h + 7d + countdowns)
|
- **Left-click** the bubble to open the expanded panel (5h + 7d + countdowns)
|
||||||
- **Right-click** for refresh, providers, refresh frequency, language, "Start
|
- **Right-click** for refresh, providers, refresh frequency, "Start with
|
||||||
with Windows", controls, auto-update check (Disabled / Hourly / Daily /
|
Windows", controls, auto-update check (Disabled / Hourly / Daily / Weekly),
|
||||||
Weekly), manual "Check for updates", exit
|
manual "Check for updates", exit
|
||||||
- **Drag** anywhere — it floats on top of all other windows
|
- **Drag** anywhere — it floats on top of all other windows
|
||||||
- **Ctrl + MouseWheel** on the bubble, or **Controls** in the right-click
|
- **Ctrl + MouseWheel** on the bubble, or **Controls** in the right-click
|
||||||
menu, to resize it
|
menu, to resize it
|
||||||
@@ -130,7 +126,6 @@ Settings are saved to:
|
|||||||
What the app reads:
|
What the app reads:
|
||||||
|
|
||||||
- Your local Claude Code OAuth credentials from `~/.claude/.credentials.json`
|
- Your local Claude Code OAuth credentials from `~/.claude/.credentials.json`
|
||||||
- If needed, the same credentials file inside an installed WSL distro
|
|
||||||
- If Codex is enabled, your local Codex credentials from `$CODEX_HOME/auth.json`
|
- If Codex is enabled, your local Codex credentials from `$CODEX_HOME/auth.json`
|
||||||
or `~/.codex/auth.json`
|
or `~/.codex/auth.json`
|
||||||
|
|
||||||
@@ -145,7 +140,6 @@ What the app stores locally:
|
|||||||
- Bubble position(s) per model
|
- Bubble position(s) per model
|
||||||
- Bubble size
|
- Bubble size
|
||||||
- Polling frequency
|
- Polling frequency
|
||||||
- Language preference
|
|
||||||
- Last update check time
|
- Last update check time
|
||||||
- Displayed provider preferences
|
- Displayed provider preferences
|
||||||
|
|
||||||
|
|||||||
+2
-72
@@ -74,17 +74,12 @@ const IDM_RESTART: u16 = 33;
|
|||||||
const IDM_SIZE_SMALLER: u16 = 34;
|
const IDM_SIZE_SMALLER: u16 = 34;
|
||||||
const IDM_SIZE_LARGER: u16 = 35;
|
const IDM_SIZE_LARGER: u16 = 35;
|
||||||
const IDM_RESET_SIZE: u16 = 36;
|
const IDM_RESET_SIZE: u16 = 36;
|
||||||
const IDM_LANG_SYSTEM: u16 = 40;
|
|
||||||
// 50 is reserved by tray::IDM_TOGGLE_WIDGET — keep the auto-update range
|
// 50 is reserved by tray::IDM_TOGGLE_WIDGET — keep the auto-update range
|
||||||
// clear of it (and any future tray ids in the 5x band).
|
// clear of it (and any future tray ids in the 5x band).
|
||||||
const IDM_UPDATE_AUTO_OFF: u16 = 60;
|
const IDM_UPDATE_AUTO_OFF: u16 = 60;
|
||||||
const IDM_UPDATE_AUTO_HOURLY: u16 = 61;
|
const IDM_UPDATE_AUTO_HOURLY: u16 = 61;
|
||||||
const IDM_UPDATE_AUTO_DAILY: u16 = 62;
|
const IDM_UPDATE_AUTO_DAILY: u16 = 62;
|
||||||
const IDM_UPDATE_AUTO_WEEKLY: u16 = 63;
|
const IDM_UPDATE_AUTO_WEEKLY: u16 = 63;
|
||||||
// IMPORTANT: language ids are dynamic and start at IDM_LANG_BASE.
|
|
||||||
// Keep IDM_LANG_BASE the highest static id so the catch-all match arm
|
|
||||||
// stays unambiguous.
|
|
||||||
const IDM_LANG_BASE: u16 = 100;
|
|
||||||
|
|
||||||
// ---------- State ----------
|
// ---------- State ----------
|
||||||
|
|
||||||
@@ -213,7 +208,7 @@ pub fn run(args: crate::AppArgs) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let settings = settings::load();
|
let settings = settings::load();
|
||||||
let i18n = I18n::load(settings.language.as_deref());
|
let i18n = I18n::load();
|
||||||
let is_dark = os::theme::is_dark();
|
let is_dark = os::theme::is_dark();
|
||||||
let install_channel = update::current_channel();
|
let install_channel = update::current_channel();
|
||||||
let http = match net::Client::new(HTTP_USER_AGENT) {
|
let http = match net::Client::new(HTTP_USER_AGENT) {
|
||||||
@@ -436,13 +431,8 @@ fn on_menu_command(id: u32, _owner_hwnd: HWND) {
|
|||||||
IDM_UPDATE_AUTO_WEEKLY => {
|
IDM_UPDATE_AUTO_WEEKLY => {
|
||||||
set_update_check_interval(Some(settings::UPDATE_CHECK_WEEKLY_SECS))
|
set_update_check_interval(Some(settings::UPDATE_CHECK_WEEKLY_SECS))
|
||||||
}
|
}
|
||||||
IDM_LANG_SYSTEM => set_language(None),
|
|
||||||
// Static ids in the 30-99 band must match BEFORE the dynamic
|
|
||||||
// language guard, otherwise `x >= IDM_LANG_BASE` would swallow any
|
|
||||||
// future id that creeps into the >=100 range.
|
|
||||||
tray::IDM_TOGGLE_WIDGET => toggle_widget_visibility(),
|
tray::IDM_TOGGLE_WIDGET => toggle_widget_visibility(),
|
||||||
IDM_RESTART => restart_app(),
|
IDM_RESTART => restart_app(),
|
||||||
x if x >= IDM_LANG_BASE => set_language_by_index((x - IDM_LANG_BASE) as usize),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -710,7 +700,7 @@ fn build_panel_data_from(snap: &UiSnapshot, model: ProviderId, p: &ProviderUiSta
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn placeholder_panel(model: ProviderId) -> PanelData {
|
fn placeholder_panel(model: ProviderId) -> PanelData {
|
||||||
let strings = i18n::I18n::load(None).strings().clone();
|
let strings = i18n::I18n::load().strings().clone();
|
||||||
PanelData {
|
PanelData {
|
||||||
model,
|
model,
|
||||||
session_pct: 0.0,
|
session_pct: 0.0,
|
||||||
@@ -934,8 +924,6 @@ fn announce_update_applied(_msg_hwnd: HWND, version: &str) {
|
|||||||
|
|
||||||
struct ContextMenuSnapshot {
|
struct ContextMenuSnapshot {
|
||||||
strings: LocaleStrings,
|
strings: LocaleStrings,
|
||||||
available: Vec<(String, String)>,
|
|
||||||
language_override: Option<String>,
|
|
||||||
current_interval: u32,
|
current_interval: u32,
|
||||||
update_check_interval_secs: Option<u64>,
|
update_check_interval_secs: Option<u64>,
|
||||||
show_claude: bool,
|
show_claude: bool,
|
||||||
@@ -951,12 +939,6 @@ fn show_context_menu(owner_hwnd: HWND) {
|
|||||||
let snap = match lock_state().as_ref() {
|
let snap = match lock_state().as_ref() {
|
||||||
Some(s) => ContextMenuSnapshot {
|
Some(s) => ContextMenuSnapshot {
|
||||||
strings: s.i18n.strings().clone(),
|
strings: s.i18n.strings().clone(),
|
||||||
available: s
|
|
||||||
.i18n
|
|
||||||
.available()
|
|
||||||
.map(|(c, n)| (c.to_string(), n.to_string()))
|
|
||||||
.collect(),
|
|
||||||
language_override: s.settings.language.clone(),
|
|
||||||
current_interval: s.settings.poll_interval_ms,
|
current_interval: s.settings.poll_interval_ms,
|
||||||
update_check_interval_secs: s.settings.update_check_interval_secs,
|
update_check_interval_secs: s.settings.update_check_interval_secs,
|
||||||
show_claude: s.settings.show_claude_code,
|
show_claude: s.settings.show_claude_code,
|
||||||
@@ -1057,39 +1039,6 @@ fn show_context_menu(owner_hwnd: HWND) {
|
|||||||
MENU_ITEM_FLAGS(0),
|
MENU_ITEM_FLAGS(0),
|
||||||
);
|
);
|
||||||
|
|
||||||
let Ok(lang) = CreatePopupMenu() else {
|
|
||||||
log::error!("CreatePopupMenu(lang) failed");
|
|
||||||
let _ = DestroyMenu(settings_menu);
|
|
||||||
let _ = DestroyMenu(menu);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
append_item(
|
|
||||||
lang,
|
|
||||||
IDM_LANG_SYSTEM,
|
|
||||||
&snap.strings.system_default,
|
|
||||||
if snap.language_override.is_none() {
|
|
||||||
MF_CHECKED
|
|
||||||
} else {
|
|
||||||
MENU_ITEM_FLAGS(0)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
for (i, (code, name)) in snap.available.iter().enumerate() {
|
|
||||||
let id = IDM_LANG_BASE + i as u16;
|
|
||||||
let flags = if snap
|
|
||||||
.language_override
|
|
||||||
.as_deref()
|
|
||||||
.map(|c| c == code)
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
MF_CHECKED
|
|
||||||
} else {
|
|
||||||
MENU_ITEM_FLAGS(0)
|
|
||||||
};
|
|
||||||
append_item(lang, id, name, flags);
|
|
||||||
}
|
|
||||||
append_submenu(settings_menu, lang, &snap.strings.language);
|
|
||||||
let _ = AppendMenuW(settings_menu, MF_SEPARATOR, 0, PCWSTR::null());
|
|
||||||
|
|
||||||
let version_label = version_action_label(&snap);
|
let version_label = version_action_label(&snap);
|
||||||
let version_flags = if matches!(
|
let version_flags = if matches!(
|
||||||
snap.update_status,
|
snap.update_status,
|
||||||
@@ -1372,25 +1321,6 @@ fn set_bubble_size(size_logical: i32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_language(_dummy: Option<()>) {
|
|
||||||
update_settings(|s| {
|
|
||||||
s.i18n.set_active(None);
|
|
||||||
s.settings.language = None;
|
|
||||||
});
|
|
||||||
propagate_to_ui();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_language_by_index(idx: usize) {
|
|
||||||
update_settings(|s| {
|
|
||||||
let code = s.i18n.available().nth(idx).map(|(c, _)| c.to_string());
|
|
||||||
if let Some(c) = code.as_deref() {
|
|
||||||
s.i18n.set_active(Some(c));
|
|
||||||
}
|
|
||||||
s.settings.language = code;
|
|
||||||
});
|
|
||||||
propagate_to_ui();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn version_action() {
|
fn version_action() {
|
||||||
enum Act {
|
enum Act {
|
||||||
Apply(update::Release, InstallChannel),
|
Apply(update::Release, InstallChannel),
|
||||||
|
|||||||
+4
-8
@@ -1246,12 +1246,8 @@ mod fullscreen_tests {
|
|||||||
|
|
||||||
// ---------- Painting ----------
|
// ---------- Painting ----------
|
||||||
|
|
||||||
// Sized for the widest countdown across all shipped locales. Korean
|
// Sized for the widest English countdown text the bubble renders.
|
||||||
// "999시간" (3 digits + 2 CJK chars for the hour suffix) is the current
|
const COUNTDOWN_TEMPLATE: &str = "999d";
|
||||||
// worst case; ASCII-only "999d" was too narrow and let CJK text spill
|
|
||||||
// out of the column. Update this when adding a locale with a longer
|
|
||||||
// suffix.
|
|
||||||
const COUNTDOWN_TEMPLATE: &str = "999시간";
|
|
||||||
|
|
||||||
/// Geometry for the bubble's "circle head + pill tail" shape, in DPI-scaled pixels.
|
/// Geometry for the bubble's "circle head + pill tail" shape, in DPI-scaled pixels.
|
||||||
///
|
///
|
||||||
@@ -1885,8 +1881,8 @@ fn paint_bubble_text(hdc: HDC, layout: &BubbleLayout, inputs: &PaintInputs) {
|
|||||||
// Head: 5h countdown text if available, otherwise the static "5h" tag.
|
// Head: 5h countdown text if available, otherwise the static "5h" tag.
|
||||||
// The ring already signals "this is the 5h window", so the countdown
|
// The ring already signals "this is the 5h window", so the countdown
|
||||||
// is the more useful glanceable info when we have it. Fall back to
|
// is the more useful glanceable info when we have it. Fall back to
|
||||||
// "5h" when the localized countdown would overflow the rect (e.g.,
|
// "5h" when the countdown would overflow the rect at the
|
||||||
// wide CJK strings like "999시간" at the 140-logical minimum width) —
|
// 140-logical minimum width —
|
||||||
// DT_NOCLIP would otherwise leak the glyphs onto the ring stroke.
|
// DT_NOCLIP would otherwise leak the glyphs onto the ring stroke.
|
||||||
SetTextColor(hdc, COLORREF(muted_color.into_colorref()));
|
SetTextColor(hdc, COLORREF(muted_color.into_colorref()));
|
||||||
let head_label_rect_w = layout.head_label_rect.right - layout.head_label_rect.left;
|
let head_label_rect_w = layout.head_label_rect.right - layout.head_label_rect.left;
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ impl super::CredentialSource for LocalClaudeCreds {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared between local-fs and wsl-bridge sources — both parse the same
|
/// Parse the Claude credential JSON shape written by the official CLI.
|
||||||
/// JSON shape, the only difference is how they get to the file content.
|
|
||||||
pub(crate) fn parse_claude_json(content: &str) -> Result<super::Token, super::Error> {
|
pub(crate) fn parse_claude_json(content: &str) -> Result<super::Token, super::Error> {
|
||||||
let value: serde_json::Value = serde_json::from_str(content)?;
|
let value: serde_json::Value = serde_json::from_str(content)?;
|
||||||
let oauth = value
|
let oauth = value
|
||||||
|
|||||||
+3
-14
@@ -1,13 +1,12 @@
|
|||||||
// Pluggable credential discovery.
|
// Pluggable credential discovery.
|
||||||
//
|
//
|
||||||
// Each `CredentialSource` knows how to read a single OAuth token from
|
// Each `CredentialSource` knows how to read a single OAuth token from
|
||||||
// somewhere (a local JSON file, a WSL filesystem, …). The `Locator`
|
// somewhere, such as a local JSON file. The `Locator`
|
||||||
// holds a priority-ordered list and serves the first source that
|
// holds a priority-ordered list and serves the first source that
|
||||||
// actually has a token. New sources drop in without touching the locator.
|
// actually has a token. New sources drop in without touching the locator.
|
||||||
|
|
||||||
pub mod codex_auth;
|
pub mod codex_auth;
|
||||||
pub mod local_fs;
|
pub mod local_fs;
|
||||||
pub mod wsl_bridge;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Token {
|
pub struct Token {
|
||||||
@@ -23,8 +22,6 @@ pub struct Token {
|
|||||||
pub enum RefreshHint {
|
pub enum RefreshHint {
|
||||||
/// `claude.cmd` / `claude.exe` on PATH.
|
/// `claude.cmd` / `claude.exe` on PATH.
|
||||||
LocalClaudeCli,
|
LocalClaudeCli,
|
||||||
/// Run `claude -p .` inside a specific WSL distro.
|
|
||||||
WslClaudeCli { distro: String },
|
|
||||||
/// `codex` / `codex.cmd` / `codex.ps1` on PATH.
|
/// `codex` / `codex.cmd` / `codex.ps1` on PATH.
|
||||||
LocalCodexCli,
|
LocalCodexCli,
|
||||||
}
|
}
|
||||||
@@ -37,10 +34,6 @@ pub enum Error {
|
|||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
#[error("required field missing from credential JSON: {0}")]
|
#[error("required field missing from credential JSON: {0}")]
|
||||||
MissingField(&'static str),
|
MissingField(&'static str),
|
||||||
#[error("WSL command in {distro:?} failed: {detail}")]
|
|
||||||
WslCommand { distro: String, detail: String },
|
|
||||||
#[error("timeout while talking to WSL")]
|
|
||||||
WslTimeout,
|
|
||||||
#[error("credential source unavailable")]
|
#[error("credential source unavailable")]
|
||||||
Unavailable,
|
Unavailable,
|
||||||
}
|
}
|
||||||
@@ -50,7 +43,7 @@ pub trait CredentialSource: Send + Sync {
|
|||||||
/// signatures (e.g. `"local:C:\\Users\\me\\.claude\\.credentials.json"`).
|
/// signatures (e.g. `"local:C:\\Users\\me\\.claude\\.credentials.json"`).
|
||||||
fn id(&self) -> &str;
|
fn id(&self) -> &str;
|
||||||
|
|
||||||
/// Read the current token. May spawn subprocesses (for WSL).
|
/// Read the current token.
|
||||||
fn read(&self) -> Result<Token, Error>;
|
fn read(&self) -> Result<Token, Error>;
|
||||||
|
|
||||||
/// Cheap change-detection fingerprint. `None` means "source is missing".
|
/// Cheap change-detection fingerprint. `None` means "source is missing".
|
||||||
@@ -70,16 +63,12 @@ impl Locator {
|
|||||||
Self { sources }
|
Self { sources }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a Claude locator with the standard search order: Windows
|
/// Build a Claude locator with the standard Windows credential path.
|
||||||
/// home directory first, then every installed WSL distro.
|
|
||||||
pub fn for_claude() -> Self {
|
pub fn for_claude() -> Self {
|
||||||
let mut sources: Vec<Box<dyn CredentialSource>> = Vec::new();
|
let mut sources: Vec<Box<dyn CredentialSource>> = Vec::new();
|
||||||
if let Some(s) = local_fs::LocalClaudeCreds::detect() {
|
if let Some(s) = local_fs::LocalClaudeCreds::detect() {
|
||||||
sources.push(Box::new(s));
|
sources.push(Box::new(s));
|
||||||
}
|
}
|
||||||
for distro in wsl_bridge::list_distros() {
|
|
||||||
sources.push(Box::new(wsl_bridge::WslClaudeCreds::new(distro)));
|
|
||||||
}
|
|
||||||
Self { sources }
|
Self { sources }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
// Reach into installed WSL distros to read their Claude credentials.
|
|
||||||
//
|
|
||||||
// We never mount the WSL filesystem ourselves — instead we shell out to
|
|
||||||
// `wsl.exe -d <distro> -- sh -lc '...'` and read stdout. Every call has
|
|
||||||
// a hard timeout so a hung WSL doesn't freeze the poll thread.
|
|
||||||
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
use std::process::{Command, Output, Stdio};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use super::local_fs::parse_claude_json;
|
|
||||||
|
|
||||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
|
||||||
const COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
/// Enumerate installed WSL distributions. Returns an empty vec if WSL is
|
|
||||||
/// not installed or the probe fails.
|
|
||||||
pub fn list_distros() -> Vec<String> {
|
|
||||||
let Some(output) = run_with_timeout(
|
|
||||||
Command::new("wsl.exe").args(["-l", "-q"]),
|
|
||||||
COMMAND_TIMEOUT,
|
|
||||||
) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
if !output.status.success() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
decode_wsl_text(&output.stdout)
|
|
||||||
.lines()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|l| !l.is_empty())
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WslClaudeCreds {
|
|
||||||
distro: String,
|
|
||||||
id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WslClaudeCreds {
|
|
||||||
pub fn new(distro: String) -> Self {
|
|
||||||
let id = format!("wsl:{distro}");
|
|
||||||
Self { distro, id }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn distro(&self) -> &str {
|
|
||||||
&self.distro
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::CredentialSource for WslClaudeCreds {
|
|
||||||
fn id(&self) -> &str {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read(&self) -> Result<super::Token, super::Error> {
|
|
||||||
let output = wsl_run(&self.distro, "cat ~/.claude/.credentials.json")?;
|
|
||||||
if !output.status.success() {
|
|
||||||
return Err(super::Error::WslCommand {
|
|
||||||
distro: self.distro.clone(),
|
|
||||||
detail: format!("cat exited {}", output.status),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let content = String::from_utf8(output.stdout).map_err(|_| super::Error::WslCommand {
|
|
||||||
distro: self.distro.clone(),
|
|
||||||
detail: "non-UTF-8 stdout".into(),
|
|
||||||
})?;
|
|
||||||
parse_claude_json(&content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn signature(&self) -> Option<String> {
|
|
||||||
let output = wsl_run(
|
|
||||||
&self.distro,
|
|
||||||
"if [ -f ~/.claude/.credentials.json ]; then \
|
|
||||||
stat -c '%s|%Y' ~/.claude/.credentials.json; \
|
|
||||||
else echo MISSING; fi",
|
|
||||||
)
|
|
||||||
.ok()?;
|
|
||||||
if !output.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let body = decode_wsl_text(&output.stdout).trim().to_string();
|
|
||||||
if body == "MISSING" {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(format!("{}|{}", self.id, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn refresh_hint(&self) -> super::RefreshHint {
|
|
||||||
super::RefreshHint::WslClaudeCli {
|
|
||||||
distro: self.distro.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wsl_run(distro: &str, script: &str) -> Result<Output, super::Error> {
|
|
||||||
run_with_timeout(
|
|
||||||
Command::new("wsl.exe")
|
|
||||||
.arg("-d")
|
|
||||||
.arg(distro)
|
|
||||||
.arg("--")
|
|
||||||
.arg("sh")
|
|
||||||
.arg("-lc")
|
|
||||||
.arg(script),
|
|
||||||
COMMAND_TIMEOUT,
|
|
||||||
)
|
|
||||||
.ok_or(super::Error::WslTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_with_timeout(cmd: &mut Command, timeout: Duration) -> Option<Output> {
|
|
||||||
let mut child = cmd
|
|
||||||
.creation_flags(CREATE_NO_WINDOW)
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.spawn()
|
|
||||||
.ok()?;
|
|
||||||
let start = Instant::now();
|
|
||||||
loop {
|
|
||||||
match child.try_wait() {
|
|
||||||
Ok(Some(_)) => return child.wait_with_output().ok(),
|
|
||||||
Ok(None) => {
|
|
||||||
if start.elapsed() > timeout {
|
|
||||||
let _ = child.kill();
|
|
||||||
let _ = child.wait();
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
std::thread::sleep(Duration::from_millis(80));
|
|
||||||
}
|
|
||||||
Err(_) => return None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `wsl.exe -l -q` historically emits UTF-16LE on stdout; other commands
|
|
||||||
/// emit UTF-8. Detect by sampling high bytes and decode appropriately.
|
|
||||||
pub(crate) fn decode_wsl_text(bytes: &[u8]) -> String {
|
|
||||||
if bytes.len() >= 2 && bytes.len() % 2 == 0 {
|
|
||||||
let sample_end = bytes.len().min(256);
|
|
||||||
let mut high_nul = 0usize;
|
|
||||||
for chunk in bytes[..sample_end].chunks_exact(2) {
|
|
||||||
if chunk[1] == 0 {
|
|
||||||
high_nul += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if high_nul * 2 >= sample_end / 2 {
|
|
||||||
let units: Vec<u16> = bytes
|
|
||||||
.chunks_exact(2)
|
|
||||||
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
|
||||||
.collect();
|
|
||||||
return String::from_utf16_lossy(&units);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String::from_utf8_lossy(bytes).into_owned()
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
// Discover the user's preferred Windows UI language.
|
|
||||||
//
|
|
||||||
// We try three sources in priority order and return the first non-empty
|
|
||||||
// result. Callers normalise the returned BCP-47-ish code against the
|
|
||||||
// list of locales we actually ship.
|
|
||||||
|
|
||||||
use windows::core::PWSTR;
|
|
||||||
use windows::Win32::Globalization::{
|
|
||||||
GetUserDefaultLocaleName, GetUserDefaultUILanguage, GetUserPreferredUILanguages,
|
|
||||||
LCIDToLocaleName, LOCALE_ALLOW_NEUTRAL_NAMES, MAX_LOCALE_NAME, MUI_LANGUAGE_NAME,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// First non-empty locale code from the user's preferences. May be
|
|
||||||
/// `Some("en-US")` style; callers do prefix normalisation.
|
|
||||||
pub fn detect_system_locale() -> Option<String> {
|
|
||||||
preferred_ui()
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.or_else(default_ui_language)
|
|
||||||
.or_else(default_locale_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preferred_ui() -> Vec<String> {
|
|
||||||
unsafe {
|
|
||||||
let mut count: u32 = 0;
|
|
||||||
let mut buf_len: u32 = 0;
|
|
||||||
if GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &mut count, PWSTR::null(), &mut buf_len)
|
|
||||||
.is_err()
|
|
||||||
|| buf_len == 0
|
|
||||||
{
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
let mut buffer = vec![0u16; buf_len as usize];
|
|
||||||
if GetUserPreferredUILanguages(
|
|
||||||
MUI_LANGUAGE_NAME,
|
|
||||||
&mut count,
|
|
||||||
PWSTR(buffer.as_mut_ptr()),
|
|
||||||
&mut buf_len,
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
buffer
|
|
||||||
.split(|u| *u == 0)
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.map(String::from_utf16_lossy)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_ui_language() -> Option<String> {
|
|
||||||
unsafe {
|
|
||||||
let lcid = GetUserDefaultUILanguage();
|
|
||||||
let mut buf = [0u16; MAX_LOCALE_NAME as usize];
|
|
||||||
let len = LCIDToLocaleName(lcid as u32, Some(&mut buf), LOCALE_ALLOW_NEUTRAL_NAMES);
|
|
||||||
if len <= 1 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(String::from_utf16_lossy(&buf[..(len as usize - 1)]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_locale_name() -> Option<String> {
|
|
||||||
unsafe {
|
|
||||||
let mut buf = [0u16; MAX_LOCALE_NAME as usize];
|
|
||||||
let len = GetUserDefaultLocaleName(&mut buf);
|
|
||||||
if len <= 1 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(String::from_utf16_lossy(&buf[..(len as usize - 1)]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
code = "en"
|
|
||||||
native_name = "English"
|
|
||||||
|
|
||||||
window_title = "Claude Code Usage Bubble"
|
|
||||||
refresh = "Refresh"
|
|
||||||
update_frequency = "Update frequency"
|
|
||||||
one_minute = "1 minute"
|
|
||||||
five_minutes = "5 minutes"
|
|
||||||
fifteen_minutes = "15 minutes"
|
|
||||||
one_hour = "1 hour"
|
|
||||||
providers = "Providers"
|
|
||||||
claude_label = "Claude Code"
|
|
||||||
chatgpt_label = "Codex"
|
|
||||||
opencode_go_label = "OpenCode Go"
|
|
||||||
settings = "Settings"
|
|
||||||
start_with_windows = "Start with Windows"
|
|
||||||
reset_position = "Reset position"
|
|
||||||
size_smaller = "Make smaller"
|
|
||||||
size_larger = "Make larger"
|
|
||||||
reset_size = "Reset size"
|
|
||||||
controls = "Controls"
|
|
||||||
control_left_click = "Left-click: details"
|
|
||||||
control_right_click = "Right-click: menu"
|
|
||||||
control_drag = "Drag: move/snap"
|
|
||||||
control_ctrl_wheel = "Ctrl+Wheel: resize"
|
|
||||||
control_tray_click = "Tray click: show/hide"
|
|
||||||
tray_left_click = "Left-click: show/hide"
|
|
||||||
language = "Language"
|
|
||||||
system_default = "System default"
|
|
||||||
check_for_updates = "Check for updates"
|
|
||||||
checking_for_updates = "Checking for updates…"
|
|
||||||
up_to_date = "Up to date"
|
|
||||||
update_failed = "Update failed"
|
|
||||||
applying_update = "Applying update…"
|
|
||||||
update_available = "Update available"
|
|
||||||
update_via_winget = "via WinGet"
|
|
||||||
auto_update_check = "Auto-update check"
|
|
||||||
auto_check_disabled = "Disabled"
|
|
||||||
auto_check_hourly = "Hourly"
|
|
||||||
auto_check_daily = "Daily"
|
|
||||||
auto_check_weekly = "Weekly"
|
|
||||||
exit = "Exit"
|
|
||||||
restart = "Restart"
|
|
||||||
show_widget = "Show widget"
|
|
||||||
session_window = "5h"
|
|
||||||
weekly_window = "7d"
|
|
||||||
now = "now"
|
|
||||||
day_suffix = "d"
|
|
||||||
hour_suffix = "h"
|
|
||||||
minute_suffix = "m"
|
|
||||||
second_suffix = "s"
|
|
||||||
token_expired_title = "Claude Code session expired"
|
|
||||||
token_expired_body = "Sign in again to keep tracking your usage."
|
|
||||||
chatgpt_token_expired_title = "Codex session expired"
|
|
||||||
chatgpt_token_expired_body = "Sign in again to keep tracking your usage."
|
|
||||||
threshold_80_body = "Approaching the 5-hour limit."
|
|
||||||
threshold_95_body = "Limit is close — consider easing up."
|
|
||||||
update_applied_title = "Update applied"
|
|
||||||
update_applied_body = "Updated to v"
|
|
||||||
update_rollback_failed_body = "Update failed. Your original binary is saved at: "
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
code = "ja"
|
|
||||||
native_name = "日本語"
|
|
||||||
|
|
||||||
window_title = "Claude Code Usage Bubble"
|
|
||||||
refresh = "更新"
|
|
||||||
update_frequency = "更新間隔"
|
|
||||||
one_minute = "1分"
|
|
||||||
five_minutes = "5分"
|
|
||||||
fifteen_minutes = "15分"
|
|
||||||
one_hour = "1時間"
|
|
||||||
providers = "プロバイダー"
|
|
||||||
claude_label = "Claude Code"
|
|
||||||
chatgpt_label = "Codex"
|
|
||||||
opencode_go_label = "OpenCode Go"
|
|
||||||
settings = "設定"
|
|
||||||
start_with_windows = "Windows起動時に開始"
|
|
||||||
reset_position = "位置をリセット"
|
|
||||||
size_smaller = "小さくする"
|
|
||||||
size_larger = "大きくする"
|
|
||||||
reset_size = "サイズをリセット"
|
|
||||||
controls = "操作"
|
|
||||||
control_left_click = "左クリック: 詳細"
|
|
||||||
control_right_click = "右クリック: メニュー"
|
|
||||||
control_drag = "ドラッグ: 移動/吸着"
|
|
||||||
control_ctrl_wheel = "Ctrl+ホイール: サイズ変更"
|
|
||||||
control_tray_click = "トレイ左クリック: 表示/非表示"
|
|
||||||
tray_left_click = "左クリック: 表示/非表示"
|
|
||||||
language = "言語"
|
|
||||||
system_default = "システム既定"
|
|
||||||
check_for_updates = "更新を確認"
|
|
||||||
checking_for_updates = "確認中…"
|
|
||||||
up_to_date = "最新です"
|
|
||||||
update_failed = "更新に失敗しました"
|
|
||||||
applying_update = "更新を適用中…"
|
|
||||||
update_available = "更新あり"
|
|
||||||
update_via_winget = "WinGet経由"
|
|
||||||
auto_update_check = "更新の自動確認"
|
|
||||||
auto_check_disabled = "無効"
|
|
||||||
auto_check_hourly = "1時間ごと"
|
|
||||||
auto_check_daily = "毎日"
|
|
||||||
auto_check_weekly = "毎週"
|
|
||||||
exit = "終了"
|
|
||||||
restart = "再起動"
|
|
||||||
show_widget = "ウィジェットを表示"
|
|
||||||
session_window = "5時間"
|
|
||||||
weekly_window = "7日"
|
|
||||||
now = "今"
|
|
||||||
day_suffix = "日"
|
|
||||||
hour_suffix = "時"
|
|
||||||
minute_suffix = "分"
|
|
||||||
second_suffix = "秒"
|
|
||||||
token_expired_title = "Claude Codeのセッションが切れました"
|
|
||||||
token_expired_body = "使用状況の追跡を続けるには再度サインインしてください。"
|
|
||||||
chatgpt_token_expired_title = "Codexのセッションが切れました"
|
|
||||||
chatgpt_token_expired_body = "使用状況の追跡を続けるには再度サインインしてください。"
|
|
||||||
threshold_80_body = "5時間の上限に近づいています。"
|
|
||||||
threshold_95_body = "上限に近づきました — ペースを落としましょう。"
|
|
||||||
update_applied_title = "更新を適用しました"
|
|
||||||
update_applied_body = "バージョン v"
|
|
||||||
update_rollback_failed_body = "更新に失敗しました。元のバイナリは次の場所に保存されています: "
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
code = "ko"
|
|
||||||
native_name = "한국어"
|
|
||||||
|
|
||||||
window_title = "Claude Code Usage Bubble"
|
|
||||||
refresh = "새로 고침"
|
|
||||||
update_frequency = "업데이트 주기"
|
|
||||||
one_minute = "1분"
|
|
||||||
five_minutes = "5분"
|
|
||||||
fifteen_minutes = "15분"
|
|
||||||
one_hour = "1시간"
|
|
||||||
providers = "제공자"
|
|
||||||
claude_label = "Claude Code"
|
|
||||||
chatgpt_label = "Codex"
|
|
||||||
opencode_go_label = "OpenCode Go"
|
|
||||||
settings = "설정"
|
|
||||||
start_with_windows = "Windows 시작 시 실행"
|
|
||||||
reset_position = "위치 초기화"
|
|
||||||
size_smaller = "작게"
|
|
||||||
size_larger = "크게"
|
|
||||||
reset_size = "크기 초기화"
|
|
||||||
controls = "조작"
|
|
||||||
control_left_click = "왼쪽 클릭: 상세"
|
|
||||||
control_right_click = "오른쪽 클릭: 메뉴"
|
|
||||||
control_drag = "드래그: 이동/스냅"
|
|
||||||
control_ctrl_wheel = "Ctrl+휠: 크기 조절"
|
|
||||||
control_tray_click = "트레이 클릭: 표시/숨김"
|
|
||||||
tray_left_click = "왼쪽 클릭: 표시/숨김"
|
|
||||||
language = "언어"
|
|
||||||
system_default = "시스템 기본값"
|
|
||||||
check_for_updates = "업데이트 확인"
|
|
||||||
checking_for_updates = "확인 중…"
|
|
||||||
up_to_date = "최신 상태"
|
|
||||||
update_failed = "업데이트 실패"
|
|
||||||
applying_update = "업데이트 적용 중…"
|
|
||||||
update_available = "업데이트 있음"
|
|
||||||
update_via_winget = "WinGet 사용"
|
|
||||||
auto_update_check = "업데이트 자동 확인"
|
|
||||||
auto_check_disabled = "사용 안 함"
|
|
||||||
auto_check_hourly = "매시간"
|
|
||||||
auto_check_daily = "매일"
|
|
||||||
auto_check_weekly = "매주"
|
|
||||||
exit = "종료"
|
|
||||||
restart = "다시 시작"
|
|
||||||
show_widget = "위젯 표시"
|
|
||||||
session_window = "5시간"
|
|
||||||
weekly_window = "7일"
|
|
||||||
now = "지금"
|
|
||||||
day_suffix = "일"
|
|
||||||
hour_suffix = "시간"
|
|
||||||
minute_suffix = "분"
|
|
||||||
second_suffix = "초"
|
|
||||||
token_expired_title = "Claude Code 세션 만료"
|
|
||||||
token_expired_body = "사용량을 계속 추적하려면 다시 로그인하세요."
|
|
||||||
chatgpt_token_expired_title = "Codex 세션 만료"
|
|
||||||
chatgpt_token_expired_body = "사용량을 계속 추적하려면 다시 로그인하세요."
|
|
||||||
threshold_80_body = "5시간 한도에 가까워지고 있어요."
|
|
||||||
threshold_95_body = "한도 임박 — 잠시 쉬어가세요."
|
|
||||||
update_applied_title = "업데이트가 적용되었습니다"
|
|
||||||
update_applied_body = "버전 v"
|
|
||||||
update_rollback_failed_body = "업데이트 실패. 원본 바이너리는 다음 위치에 저장되었습니다: "
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
code = "vi"
|
|
||||||
native_name = "Tiếng Việt"
|
|
||||||
|
|
||||||
window_title = "Claude Code Usage Bubble"
|
|
||||||
refresh = "Làm mới"
|
|
||||||
update_frequency = "Tần suất cập nhật"
|
|
||||||
one_minute = "1 phút"
|
|
||||||
five_minutes = "5 phút"
|
|
||||||
fifteen_minutes = "15 phút"
|
|
||||||
one_hour = "1 giờ"
|
|
||||||
providers = "Nhà cung cấp"
|
|
||||||
claude_label = "Claude Code"
|
|
||||||
chatgpt_label = "Codex"
|
|
||||||
opencode_go_label = "OpenCode Go"
|
|
||||||
settings = "Cài đặt"
|
|
||||||
start_with_windows = "Khởi động cùng Windows"
|
|
||||||
reset_position = "Đặt lại vị trí"
|
|
||||||
size_smaller = "Thu nhỏ"
|
|
||||||
size_larger = "Phóng to"
|
|
||||||
reset_size = "Đặt lại kích thước"
|
|
||||||
controls = "Điều khiển"
|
|
||||||
control_left_click = "Nhấp trái: chi tiết"
|
|
||||||
control_right_click = "Nhấp phải: menu"
|
|
||||||
control_drag = "Kéo: di chuyển/bám"
|
|
||||||
control_ctrl_wheel = "Ctrl+cuộn: đổi cỡ"
|
|
||||||
control_tray_click = "Khay: hiện/ẩn"
|
|
||||||
tray_left_click = "Nhấp trái: hiện/ẩn"
|
|
||||||
language = "Ngôn ngữ"
|
|
||||||
system_default = "Mặc định hệ thống"
|
|
||||||
check_for_updates = "Kiểm tra cập nhật"
|
|
||||||
checking_for_updates = "Đang kiểm tra cập nhật…"
|
|
||||||
up_to_date = "Đã là phiên bản mới nhất"
|
|
||||||
update_failed = "Cập nhật thất bại"
|
|
||||||
applying_update = "Đang áp dụng cập nhật…"
|
|
||||||
update_available = "Có bản cập nhật mới"
|
|
||||||
update_via_winget = "qua WinGet"
|
|
||||||
auto_update_check = "Tự động kiểm tra cập nhật"
|
|
||||||
auto_check_disabled = "Tắt"
|
|
||||||
auto_check_hourly = "Mỗi giờ"
|
|
||||||
auto_check_daily = "Hằng ngày"
|
|
||||||
auto_check_weekly = "Hằng tuần"
|
|
||||||
exit = "Thoát"
|
|
||||||
restart = "Khởi động lại"
|
|
||||||
show_widget = "Hiện widget"
|
|
||||||
session_window = "5g"
|
|
||||||
weekly_window = "7n"
|
|
||||||
now = "ngay"
|
|
||||||
day_suffix = "n"
|
|
||||||
hour_suffix = "g"
|
|
||||||
minute_suffix = "p"
|
|
||||||
second_suffix = "s"
|
|
||||||
token_expired_title = "Phiên Claude Code đã hết hạn"
|
|
||||||
token_expired_body = "Hãy đăng nhập lại để tiếp tục theo dõi mức sử dụng."
|
|
||||||
chatgpt_token_expired_title = "Phiên Codex đã hết hạn"
|
|
||||||
chatgpt_token_expired_body = "Hãy đăng nhập lại để tiếp tục theo dõi mức sử dụng."
|
|
||||||
threshold_80_body = "Sắp chạm giới hạn 5 giờ."
|
|
||||||
threshold_95_body = "Sắp tới giới hạn — hãy cân nhắc giảm tốc."
|
|
||||||
update_applied_title = "Đã áp dụng cập nhật"
|
|
||||||
update_applied_body = "Đã cập nhật lên v"
|
|
||||||
update_rollback_failed_body = "Cập nhật thất bại. Tệp gốc của bạn được lưu tại: "
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
code = "zh-TW"
|
|
||||||
native_name = "繁體中文"
|
|
||||||
|
|
||||||
window_title = "Claude Code Usage Bubble"
|
|
||||||
refresh = "重新整理"
|
|
||||||
update_frequency = "更新頻率"
|
|
||||||
one_minute = "1 分鐘"
|
|
||||||
five_minutes = "5 分鐘"
|
|
||||||
fifteen_minutes = "15 分鐘"
|
|
||||||
one_hour = "1 小時"
|
|
||||||
providers = "提供者"
|
|
||||||
claude_label = "Claude Code"
|
|
||||||
chatgpt_label = "Codex"
|
|
||||||
opencode_go_label = "OpenCode Go"
|
|
||||||
settings = "設定"
|
|
||||||
start_with_windows = "隨 Windows 啟動"
|
|
||||||
reset_position = "重設位置"
|
|
||||||
size_smaller = "縮小"
|
|
||||||
size_larger = "放大"
|
|
||||||
reset_size = "重設大小"
|
|
||||||
controls = "操作"
|
|
||||||
control_left_click = "左鍵: 詳細"
|
|
||||||
control_right_click = "右鍵: 選單"
|
|
||||||
control_drag = "拖曳: 移動/吸附"
|
|
||||||
control_ctrl_wheel = "Ctrl+滾輪: 調整大小"
|
|
||||||
control_tray_click = "系統匣: 顯示/隱藏"
|
|
||||||
tray_left_click = "左鍵: 顯示/隱藏"
|
|
||||||
language = "語言"
|
|
||||||
system_default = "系統預設"
|
|
||||||
check_for_updates = "檢查更新"
|
|
||||||
checking_for_updates = "檢查中…"
|
|
||||||
up_to_date = "已是最新"
|
|
||||||
update_failed = "更新失敗"
|
|
||||||
applying_update = "正在套用更新…"
|
|
||||||
update_available = "有可用更新"
|
|
||||||
update_via_winget = "透過 WinGet"
|
|
||||||
auto_update_check = "自動檢查更新"
|
|
||||||
auto_check_disabled = "停用"
|
|
||||||
auto_check_hourly = "每小時"
|
|
||||||
auto_check_daily = "每天"
|
|
||||||
auto_check_weekly = "每週"
|
|
||||||
exit = "結束"
|
|
||||||
restart = "重新啟動"
|
|
||||||
show_widget = "顯示小工具"
|
|
||||||
session_window = "5 小時"
|
|
||||||
weekly_window = "7 日"
|
|
||||||
now = "現在"
|
|
||||||
day_suffix = "日"
|
|
||||||
hour_suffix = "時"
|
|
||||||
minute_suffix = "分"
|
|
||||||
second_suffix = "秒"
|
|
||||||
token_expired_title = "Claude Code 工作階段已過期"
|
|
||||||
token_expired_body = "請重新登入以繼續追蹤使用量。"
|
|
||||||
chatgpt_token_expired_title = "Codex 工作階段已過期"
|
|
||||||
chatgpt_token_expired_body = "請重新登入以繼續追蹤使用量。"
|
|
||||||
threshold_80_body = "接近 5 小時上限。"
|
|
||||||
threshold_95_body = "上限將至 — 建議稍作休息。"
|
|
||||||
update_applied_title = "已套用更新"
|
|
||||||
update_applied_body = "已更新至 v"
|
|
||||||
update_rollback_failed_body = "更新失敗。您的原始執行檔已保存於: "
|
|
||||||
+85
-212
@@ -1,25 +1,9 @@
|
|||||||
// Embedded TOML-based localisation.
|
// English UI strings and formatting helpers.
|
||||||
//
|
|
||||||
// Each supported language lives in `locales/<code>.toml`. At startup we
|
|
||||||
// `include_str!` every file, parse them with `toml`, and stash them in a
|
|
||||||
// HashMap keyed by language code. The active language defaults to whatever
|
|
||||||
// Windows reports for the user's preferred UI language; the menu lets the
|
|
||||||
// user override that.
|
|
||||||
//
|
|
||||||
// Adding a translation: copy `en.toml` to `<code>.toml`, translate the
|
|
||||||
// strings, then add one `include_str!` entry to `RAW_LOCALES` below.
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use serde::Deserialize;
|
/// The strings every UI module needs.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub mod detect;
|
|
||||||
|
|
||||||
const FALLBACK_CODE: &str = "en";
|
|
||||||
|
|
||||||
/// The strings every UI module needs. Field names map 1:1 to TOML keys.
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct LocaleStrings {
|
pub struct LocaleStrings {
|
||||||
pub window_title: String,
|
pub window_title: String,
|
||||||
pub refresh: String,
|
pub refresh: String,
|
||||||
@@ -45,8 +29,6 @@ pub struct LocaleStrings {
|
|||||||
pub control_ctrl_wheel: String,
|
pub control_ctrl_wheel: String,
|
||||||
pub control_tray_click: String,
|
pub control_tray_click: String,
|
||||||
pub tray_left_click: String,
|
pub tray_left_click: String,
|
||||||
pub language: String,
|
|
||||||
pub system_default: String,
|
|
||||||
pub check_for_updates: String,
|
pub check_for_updates: String,
|
||||||
pub checking_for_updates: String,
|
pub checking_for_updates: String,
|
||||||
pub up_to_date: String,
|
pub up_to_date: String,
|
||||||
@@ -74,147 +56,93 @@ pub struct LocaleStrings {
|
|||||||
pub chatgpt_token_expired_title: String,
|
pub chatgpt_token_expired_title: String,
|
||||||
pub chatgpt_token_expired_body: String,
|
pub chatgpt_token_expired_body: String,
|
||||||
/// Body text for "your usage just crossed 80% of the 5h limit". The
|
/// Body text for "your usage just crossed 80% of the 5h limit". The
|
||||||
/// title is composed from the provider label + percent so it does not
|
/// title is composed from the provider label + percent.
|
||||||
/// need to be translated separately.
|
|
||||||
pub threshold_80_body: String,
|
pub threshold_80_body: String,
|
||||||
/// Body text for the 95% threshold balloon.
|
/// Body text for the 95% threshold balloon.
|
||||||
pub threshold_95_body: String,
|
pub threshold_95_body: String,
|
||||||
/// Title for the tray balloon shown on first launch after an auto-update.
|
/// Title for the tray balloon shown on first launch after an auto-update.
|
||||||
pub update_applied_title: String,
|
pub update_applied_title: String,
|
||||||
/// Prefix for the tray balloon body. Call site appends the version (e.g. "0.1.10").
|
/// Prefix for the tray balloon body. Call site appends the version.
|
||||||
pub update_applied_body: String,
|
pub update_applied_body: String,
|
||||||
/// Prefix for the rollback-failed MessageBox body. Call site appends
|
/// Prefix for the rollback-failed MessageBox body. Call site appends
|
||||||
/// the backup path and a separator with the expected target filename.
|
/// the backup path and a separator with the expected target filename.
|
||||||
pub update_rollback_failed_body: String,
|
pub update_rollback_failed_body: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
pub struct I18n {
|
||||||
struct LocaleFile {
|
|
||||||
code: String,
|
|
||||||
native_name: String,
|
|
||||||
#[serde(flatten)]
|
|
||||||
strings: LocaleStrings,
|
strings: LocaleStrings,
|
||||||
}
|
}
|
||||||
|
|
||||||
const RAW_LOCALES: &[(&str, &str)] = &[
|
|
||||||
("en", include_str!("locales/en.toml")),
|
|
||||||
("ja", include_str!("locales/ja.toml")),
|
|
||||||
("ko", include_str!("locales/ko.toml")),
|
|
||||||
("vi", include_str!("locales/vi.toml")),
|
|
||||||
("zh-TW", include_str!("locales/zh-TW.toml")),
|
|
||||||
];
|
|
||||||
|
|
||||||
pub struct I18n {
|
|
||||||
/// Sorted by code so menus list languages deterministically.
|
|
||||||
available: BTreeMap<String, (String, LocaleStrings)>,
|
|
||||||
active: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl I18n {
|
impl I18n {
|
||||||
/// Load all embedded TOML files and pick an active language.
|
pub fn load() -> Self {
|
||||||
///
|
Self {
|
||||||
/// `requested` overrides system detection. `None` means "ask Windows".
|
strings: english_strings(),
|
||||||
pub fn load(requested: Option<&str>) -> Self {
|
|
||||||
let mut available = BTreeMap::new();
|
|
||||||
for (code, body) in RAW_LOCALES {
|
|
||||||
match toml::from_str::<LocaleFile>(body) {
|
|
||||||
Ok(file) => {
|
|
||||||
available.insert(file.code.clone(), (file.native_name, file.strings));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("failed to parse locale {code}: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !available.contains_key(FALLBACK_CODE) {
|
|
||||||
// Embedded TOMLs are validated by tests; this should never
|
|
||||||
// happen in practice. Fall through with whatever we have.
|
|
||||||
log::error!("fallback locale '{FALLBACK_CODE}' missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
let active = requested
|
|
||||||
.and_then(|c| normalise(c, &available))
|
|
||||||
.or_else(|| detect::detect_system_locale().and_then(|c| normalise(&c, &available)))
|
|
||||||
.unwrap_or_else(|| FALLBACK_CODE.to_string());
|
|
||||||
|
|
||||||
Self { available, active }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn strings(&self) -> &LocaleStrings {
|
pub fn strings(&self) -> &LocaleStrings {
|
||||||
self.available
|
&self.strings
|
||||||
.get(&self.active)
|
|
||||||
.map(|(_, s)| s)
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
// Defensive: if `active` was set to something unavailable
|
|
||||||
// (shouldn't happen given `load` validates) — fall back.
|
|
||||||
&self
|
|
||||||
.available
|
|
||||||
.get(FALLBACK_CODE)
|
|
||||||
.expect("fallback locale must exist")
|
|
||||||
.1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn active_code(&self) -> &str {
|
|
||||||
&self.active
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterate `(code, native_name)` pairs in stable order.
|
|
||||||
pub fn available(&self) -> impl Iterator<Item = (&str, &str)> {
|
|
||||||
self.available
|
|
||||||
.iter()
|
|
||||||
.map(|(code, (name, _))| (code.as_str(), name.as_str()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_active(&mut self, requested: Option<&str>) {
|
|
||||||
let new_active = requested
|
|
||||||
.and_then(|c| normalise(c, &self.available))
|
|
||||||
.or_else(|| detect::detect_system_locale().and_then(|c| normalise(&c, &self.available)))
|
|
||||||
.unwrap_or_else(|| FALLBACK_CODE.to_string());
|
|
||||||
self.active = new_active;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a user-supplied or system-supplied locale code to one we have.
|
fn english_strings() -> LocaleStrings {
|
||||||
///
|
LocaleStrings {
|
||||||
/// Handles `en_US`, `en-US`, `EN`, `zh-Hant-TW`, etc. by progressive
|
window_title: "Claude Code Usage Bubble".into(),
|
||||||
/// fallback: exact → ASCII-lower exact → prefix match.
|
refresh: "Refresh".into(),
|
||||||
fn normalise(input: &str, available: &BTreeMap<String, (String, LocaleStrings)>) -> Option<String> {
|
update_frequency: "Update frequency".into(),
|
||||||
let cleaned = input.trim().replace('_', "-");
|
one_minute: "1 minute".into(),
|
||||||
if cleaned.is_empty() || cleaned.eq_ignore_ascii_case("system") {
|
five_minutes: "5 minutes".into(),
|
||||||
return None;
|
fifteen_minutes: "15 minutes".into(),
|
||||||
|
one_hour: "1 hour".into(),
|
||||||
|
providers: "Providers".into(),
|
||||||
|
claude_label: "Claude Code".into(),
|
||||||
|
chatgpt_label: "Codex".into(),
|
||||||
|
opencode_go_label: "OpenCode Go".into(),
|
||||||
|
settings: "Settings".into(),
|
||||||
|
start_with_windows: "Start with Windows".into(),
|
||||||
|
reset_position: "Reset position".into(),
|
||||||
|
size_smaller: "Make smaller".into(),
|
||||||
|
size_larger: "Make larger".into(),
|
||||||
|
reset_size: "Reset size".into(),
|
||||||
|
controls: "Controls".into(),
|
||||||
|
control_left_click: "Left-click: details".into(),
|
||||||
|
control_right_click: "Right-click: menu".into(),
|
||||||
|
control_drag: "Drag: move/snap".into(),
|
||||||
|
control_ctrl_wheel: "Ctrl+Wheel: resize".into(),
|
||||||
|
control_tray_click: "Tray click: show/hide".into(),
|
||||||
|
tray_left_click: "Left-click: show/hide".into(),
|
||||||
|
check_for_updates: "Check for updates".into(),
|
||||||
|
checking_for_updates: "Checking for updates...".into(),
|
||||||
|
up_to_date: "Up to date".into(),
|
||||||
|
update_failed: "Update failed".into(),
|
||||||
|
applying_update: "Applying update...".into(),
|
||||||
|
update_available: "Update available".into(),
|
||||||
|
update_via_winget: "via WinGet".into(),
|
||||||
|
auto_update_check: "Auto-update check".into(),
|
||||||
|
auto_check_disabled: "Disabled".into(),
|
||||||
|
auto_check_hourly: "Hourly".into(),
|
||||||
|
auto_check_daily: "Daily".into(),
|
||||||
|
auto_check_weekly: "Weekly".into(),
|
||||||
|
exit: "Exit".into(),
|
||||||
|
restart: "Restart".into(),
|
||||||
|
show_widget: "Show widget".into(),
|
||||||
|
session_window: "5h".into(),
|
||||||
|
weekly_window: "7d".into(),
|
||||||
|
now: "now".into(),
|
||||||
|
day_suffix: "d".into(),
|
||||||
|
hour_suffix: "h".into(),
|
||||||
|
minute_suffix: "m".into(),
|
||||||
|
second_suffix: "s".into(),
|
||||||
|
token_expired_title: "Claude Code session expired".into(),
|
||||||
|
token_expired_body: "Sign in again to keep tracking your usage.".into(),
|
||||||
|
chatgpt_token_expired_title: "Codex session expired".into(),
|
||||||
|
chatgpt_token_expired_body: "Sign in again to keep tracking your usage.".into(),
|
||||||
|
threshold_80_body: "Approaching the 5-hour limit.".into(),
|
||||||
|
threshold_95_body: "Limit is close - consider easing up.".into(),
|
||||||
|
update_applied_title: "Update applied".into(),
|
||||||
|
update_applied_body: "Updated to v".into(),
|
||||||
|
update_rollback_failed_body: "Update failed. Your original binary is saved at: ".into(),
|
||||||
}
|
}
|
||||||
// Exact (case-insensitive)
|
|
||||||
for key in available.keys() {
|
|
||||||
if key.eq_ignore_ascii_case(&cleaned) {
|
|
||||||
return Some(key.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Special-case: Traditional Chinese variants → zh-TW
|
|
||||||
let lower = cleaned.to_ascii_lowercase();
|
|
||||||
if lower.starts_with("zh")
|
|
||||||
&& (lower.contains("tw") || lower.contains("hk") || lower.contains("hant"))
|
|
||||||
{
|
|
||||||
if available.contains_key("zh-TW") {
|
|
||||||
return Some("zh-TW".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Prefix fallback (e.g. "en-US" → "en")
|
|
||||||
let prefix = lower.split('-').next().unwrap_or("");
|
|
||||||
if !prefix.is_empty() {
|
|
||||||
for key in available.keys() {
|
|
||||||
if key
|
|
||||||
.split('-')
|
|
||||||
.next()
|
|
||||||
.map(str::to_ascii_lowercase)
|
|
||||||
.as_deref()
|
|
||||||
== Some(prefix)
|
|
||||||
{
|
|
||||||
return Some(key.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Free-function helpers ----------
|
// ---------- Free-function helpers ----------
|
||||||
@@ -282,79 +210,24 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn embedded_locales_parse_and_include_required_control_strings() {
|
fn english_strings_include_required_menu_labels() {
|
||||||
let mut has_fallback = false;
|
let strings = english_strings();
|
||||||
for (expected_code, body) in RAW_LOCALES {
|
for (name, value) in [
|
||||||
let file = toml::from_str::<LocaleFile>(body)
|
("refresh", strings.refresh.as_str()),
|
||||||
.unwrap_or_else(|e| panic!("locale {expected_code} failed to parse: {e}"));
|
("providers", strings.providers.as_str()),
|
||||||
assert_eq!(file.code, *expected_code);
|
("settings", strings.settings.as_str()),
|
||||||
has_fallback |= file.code == FALLBACK_CODE;
|
("size_smaller", strings.size_smaller.as_str()),
|
||||||
|
("size_larger", strings.size_larger.as_str()),
|
||||||
let strings = file.strings;
|
("reset_size", strings.reset_size.as_str()),
|
||||||
for (name, value) in [
|
("controls", strings.controls.as_str()),
|
||||||
("size_smaller", strings.size_smaller.as_str()),
|
("control_left_click", strings.control_left_click.as_str()),
|
||||||
("providers", strings.providers.as_str()),
|
("control_right_click", strings.control_right_click.as_str()),
|
||||||
("size_larger", strings.size_larger.as_str()),
|
("control_drag", strings.control_drag.as_str()),
|
||||||
("opencode_go_label", strings.opencode_go_label.as_str()),
|
("control_ctrl_wheel", strings.control_ctrl_wheel.as_str()),
|
||||||
("reset_size", strings.reset_size.as_str()),
|
("control_tray_click", strings.control_tray_click.as_str()),
|
||||||
("controls", strings.controls.as_str()),
|
("tray_left_click", strings.tray_left_click.as_str()),
|
||||||
("control_left_click", strings.control_left_click.as_str()),
|
] {
|
||||||
("control_right_click", strings.control_right_click.as_str()),
|
assert!(!value.trim().is_empty(), "empty string: {name}");
|
||||||
("control_drag", strings.control_drag.as_str()),
|
|
||||||
("control_ctrl_wheel", strings.control_ctrl_wheel.as_str()),
|
|
||||||
("control_tray_click", strings.control_tray_click.as_str()),
|
|
||||||
("tray_left_click", strings.tray_left_click.as_str()),
|
|
||||||
] {
|
|
||||||
assert!(
|
|
||||||
!value.trim().is_empty(),
|
|
||||||
"locale {expected_code} has empty {name}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
assert!(has_fallback, "fallback locale {FALLBACK_CODE} missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn locale_schema_rejects_missing_or_malformed_control_strings() {
|
|
||||||
let (_, fallback_body) = RAW_LOCALES
|
|
||||||
.iter()
|
|
||||||
.find(|(code, _)| *code == FALLBACK_CODE)
|
|
||||||
.expect("fallback locale fixture must exist");
|
|
||||||
|
|
||||||
let missing_control =
|
|
||||||
fallback_body.replace("tray_left_click = \"Left-click: show/hide\"\n", "");
|
|
||||||
assert!(
|
|
||||||
toml::from_str::<LocaleFile>(&missing_control).is_err(),
|
|
||||||
"missing tray_left_click should fail locale deserialization"
|
|
||||||
);
|
|
||||||
|
|
||||||
let malformed_control = fallback_body.replace(
|
|
||||||
"control_tray_click = \"Tray click: show/hide\"",
|
|
||||||
"control_tray_click = [\"Tray click: show/hide\"]",
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
toml::from_str::<LocaleFile>(&malformed_control).is_err(),
|
|
||||||
"malformed control_tray_click should fail locale deserialization"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn locale_schema_rejects_missing_provider_strings() {
|
|
||||||
let (_, fallback_body) = RAW_LOCALES
|
|
||||||
.iter()
|
|
||||||
.find(|(code, _)| *code == FALLBACK_CODE)
|
|
||||||
.expect("fallback locale fixture must exist");
|
|
||||||
|
|
||||||
let missing_providers = fallback_body.replace("providers = \"Providers\"\n", "");
|
|
||||||
assert!(
|
|
||||||
toml::from_str::<LocaleFile>(&missing_providers).is_err(),
|
|
||||||
"missing providers should fail locale deserialization"
|
|
||||||
);
|
|
||||||
|
|
||||||
let missing_opencode = fallback_body.replace("opencode_go_label = \"OpenCode Go\"\n", "");
|
|
||||||
assert!(
|
|
||||||
toml::from_str::<LocaleFile>(&missing_opencode).is_err(),
|
|
||||||
"missing opencode_go_label should fail locale deserialization"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,8 +139,6 @@ pub struct Settings {
|
|||||||
#[serde(default = "default_poll_interval_ms")]
|
#[serde(default = "default_poll_interval_ms")]
|
||||||
pub poll_interval_ms: u32,
|
pub poll_interval_ms: u32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub language: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub last_update_check_unix: Option<u64>,
|
pub last_update_check_unix: Option<u64>,
|
||||||
#[serde(default = "default_update_check_interval_secs")]
|
#[serde(default = "default_update_check_interval_secs")]
|
||||||
pub update_check_interval_secs: Option<u64>,
|
pub update_check_interval_secs: Option<u64>,
|
||||||
@@ -157,7 +155,6 @@ impl Default for Settings {
|
|||||||
bubble_positions: BubblePositions::default(),
|
bubble_positions: BubblePositions::default(),
|
||||||
bubble_size_logical: default_bubble_size(),
|
bubble_size_logical: default_bubble_size(),
|
||||||
poll_interval_ms: default_poll_interval_ms(),
|
poll_interval_ms: default_poll_interval_ms(),
|
||||||
language: None,
|
|
||||||
last_update_check_unix: None,
|
last_update_check_unix: None,
|
||||||
update_check_interval_secs: default_update_check_interval_secs(),
|
update_check_interval_secs: default_update_check_interval_secs(),
|
||||||
widget_visible: default_widget_visible(),
|
widget_visible: default_widget_visible(),
|
||||||
|
|||||||
+65
-11
@@ -1,10 +1,10 @@
|
|||||||
// Download a release asset and swap it in via a detached helper process.
|
// Download a release asset and swap it in through the proven `self_replace`
|
||||||
|
// handoff path.
|
||||||
//
|
//
|
||||||
// The running process cannot reliably replace its own mapped image on
|
// The running process cannot reliably replace its own mapped image on
|
||||||
// Windows. Instead, it writes the new .exe to a staging path, copies the
|
// Windows. `self_replace` handles the platform-specific rename/copy sequence
|
||||||
// current executable to a helper path, starts that helper with
|
// for that case. After the replacement succeeds, we start the installed path
|
||||||
// `--apply-update`, and then exits. The helper waits for the parent process
|
// with `--wait-pid` and let the current process exit.
|
||||||
// to exit before replacing the install target with the staged new binary.
|
|
||||||
|
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -22,6 +22,16 @@ use crate::net::Client;
|
|||||||
use crate::os::to_utf16_nul;
|
use crate::os::to_utf16_nul;
|
||||||
|
|
||||||
pub fn begin(http: &Client, release: &super::Release) -> Result<(), super::Error> {
|
pub fn begin(http: &Client, release: &super::Release) -> Result<(), super::Error> {
|
||||||
|
match begin_inner(http, release) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
write_update_error(&e);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_inner(http: &Client, release: &super::Release) -> Result<(), super::Error> {
|
||||||
let current = std::env::current_exe()?;
|
let current = std::env::current_exe()?;
|
||||||
ensure_writable(¤t)?;
|
ensure_writable(¤t)?;
|
||||||
let staging = stage_path()?;
|
let staging = stage_path()?;
|
||||||
@@ -39,10 +49,9 @@ pub fn begin(http: &Client, release: &super::Release) -> Result<(), super::Error
|
|||||||
&staging,
|
&staging,
|
||||||
release.asset_sha256.as_ref(),
|
release.asset_sha256.as_ref(),
|
||||||
)?;
|
)?;
|
||||||
let helper = helper_path()?;
|
replace_current_exe(&staging)?;
|
||||||
reject_unsafe_path(&helper)?;
|
spawn_replaced_exe(¤t, &release.version)?;
|
||||||
prepare_update_helper(¤t, &helper)?;
|
let _ = std::fs::remove_file(&staging);
|
||||||
spawn_update_helper(&helper, &staging, ¤t, &release.version)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +152,26 @@ fn spawn_update_helper(
|
|||||||
super::handoff::spawn_detached(helper, &args).map_err(super::Error::Io)
|
super::handoff::spawn_detached(helper, &args).map_err(super::Error::Io)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replace_current_exe(staging: &Path) -> Result<(), super::Error> {
|
||||||
|
self_replace::self_replace(staging)
|
||||||
|
.map_err(|e| super::Error::SwapFailed(format!("self_replace({}): {e}", staging.display())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_replaced_exe(
|
||||||
|
target: &Path,
|
||||||
|
version: &super::release::Version,
|
||||||
|
) -> Result<(), super::Error> {
|
||||||
|
let pid = unsafe { GetCurrentProcessId() };
|
||||||
|
let version_str = format!("{}.{}.{}", version.major, version.minor, version.patch);
|
||||||
|
let args = vec![
|
||||||
|
OsString::from("--wait-pid"),
|
||||||
|
OsString::from(pid.to_string()),
|
||||||
|
OsString::from("--updated-to"),
|
||||||
|
OsString::from(version_str),
|
||||||
|
];
|
||||||
|
super::handoff::spawn_detached(target, &args).map_err(super::Error::Io)
|
||||||
|
}
|
||||||
|
|
||||||
fn replace_from_helper(source: &Path, target: &Path, version: &str) -> Result<(), super::Error> {
|
fn replace_from_helper(source: &Path, target: &Path, version: &str) -> Result<(), super::Error> {
|
||||||
let backup = backup_path(target);
|
let backup = backup_path(target);
|
||||||
// Parent has exited, so the install target is no longer mapped.
|
// Parent has exited, so the install target is no longer mapped.
|
||||||
@@ -216,9 +245,9 @@ fn backup_path(target: &Path) -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn surface_rollback_failure(backup: &Path, target_name: &str) {
|
fn surface_rollback_failure(backup: &Path, target_name: &str) {
|
||||||
// Pull the localized body from i18n; the caller passes the
|
// Pull the UI body text from i18n; the caller passes the
|
||||||
// user-meaningful filename so we can format it in-place.
|
// user-meaningful filename so we can format it in-place.
|
||||||
let strings = crate::i18n::I18n::load(None).strings().clone();
|
let strings = crate::i18n::I18n::load().strings().clone();
|
||||||
let body = format!(
|
let body = format!(
|
||||||
"{}{}\n\n{}",
|
"{}{}\n\n{}",
|
||||||
strings.update_rollback_failed_body,
|
strings.update_rollback_failed_body,
|
||||||
@@ -247,6 +276,31 @@ fn stage_path() -> Result<PathBuf, super::Error> {
|
|||||||
.join("update.exe"))
|
.join("update.exe"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_error_log_path() -> Result<PathBuf, super::Error> {
|
||||||
|
let base = dirs::data_local_dir().ok_or_else(|| {
|
||||||
|
super::Error::NotWritable("no local data directory available".to_string())
|
||||||
|
})?;
|
||||||
|
Ok(base
|
||||||
|
.join("ClaudeCodeUsageBubble")
|
||||||
|
.join("updates")
|
||||||
|
.join("update-error.log"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_update_error(error: &super::Error) {
|
||||||
|
let Ok(path) = update_error_log_path() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
let ts = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let body = format!("unix_time={ts}\nerror={error}\n");
|
||||||
|
let _ = std::fs::write(path, body);
|
||||||
|
}
|
||||||
|
|
||||||
fn helper_path() -> Result<PathBuf, super::Error> {
|
fn helper_path() -> Result<PathBuf, super::Error> {
|
||||||
let base = dirs::data_local_dir().ok_or_else(|| {
|
let base = dirs::data_local_dir().ok_or_else(|| {
|
||||||
super::Error::NotWritable("no local data directory available".to_string())
|
super::Error::NotWritable("no local data directory available".to_string())
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
// Self-update subsystem.
|
// Self-update subsystem.
|
||||||
//
|
//
|
||||||
// Two stages: `release::fetch_latest` checks GitHub releases for a newer
|
// Two stages: `release::fetch_latest` checks GitHub releases for a newer
|
||||||
// build; `install::begin` downloads the .exe, swaps it in via native
|
// build; `install::begin` downloads the .exe, swaps it in via `self_replace`,
|
||||||
// `MoveFileExW`, then spawns the new binary detached via
|
// then spawns the new binary detached via `CreateProcessW`. No shell handoff —
|
||||||
// `CreateProcessW`. No shell handoff — nothing can flash a console.
|
// nothing can flash a console.
|
||||||
|
|
||||||
pub mod channel;
|
pub mod channel;
|
||||||
pub mod handoff;
|
pub mod handoff;
|
||||||
|
|||||||
+11
-26
@@ -56,11 +56,13 @@ impl Orchestrator {
|
|||||||
|
|
||||||
fn spawn_cli(hint: &RefreshHint) -> bool {
|
fn spawn_cli(hint: &RefreshHint) -> bool {
|
||||||
match hint {
|
match hint {
|
||||||
RefreshHint::LocalClaudeCli => spawn_local(&["claude.cmd", "claude.exe", "claude"], &["-p", "."]),
|
RefreshHint::LocalClaudeCli => {
|
||||||
RefreshHint::WslClaudeCli { distro } => spawn_wsl(distro),
|
spawn_local(&["claude.cmd", "claude.exe", "claude"], &["-p", "."])
|
||||||
RefreshHint::LocalCodexCli => {
|
|
||||||
spawn_local(&["codex.cmd", "codex.ps1", "codex.exe", "codex"], &["exec", "."])
|
|
||||||
}
|
}
|
||||||
|
RefreshHint::LocalCodexCli => spawn_local(
|
||||||
|
&["codex.cmd", "codex.ps1", "codex.exe", "codex"],
|
||||||
|
&["exec", "."],
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +71,11 @@ fn spawn_local(candidates: &[&str], args: &[&str]) -> bool {
|
|||||||
let lower = name.to_ascii_lowercase();
|
let lower = name.to_ascii_lowercase();
|
||||||
let mut cmd = if lower.ends_with(".ps1") {
|
let mut cmd = if lower.ends_with(".ps1") {
|
||||||
let mut c = Command::new("powershell.exe");
|
let mut c = Command::new("powershell.exe");
|
||||||
c.arg("-NoProfile").arg("-ExecutionPolicy").arg("Bypass").arg("-File").arg(name);
|
c.arg("-NoProfile")
|
||||||
|
.arg("-ExecutionPolicy")
|
||||||
|
.arg("Bypass")
|
||||||
|
.arg("-File")
|
||||||
|
.arg(name);
|
||||||
for a in args {
|
for a in args {
|
||||||
c.arg(a);
|
c.arg(a);
|
||||||
}
|
}
|
||||||
@@ -100,24 +106,3 @@ fn spawn_local(candidates: &[&str], args: &[&str]) -> bool {
|
|||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_wsl(distro: &str) -> bool {
|
|
||||||
let script = "if command -v claude >/dev/null 2>&1; then claude -p .; \
|
|
||||||
elif [ -x \"$HOME/.local/bin/claude\" ]; then \"$HOME/.local/bin/claude\" -p .; \
|
|
||||||
else exit 127; fi";
|
|
||||||
Command::new("wsl.exe")
|
|
||||||
.arg("-d")
|
|
||||||
.arg(distro)
|
|
||||||
.arg("--")
|
|
||||||
.arg("bash")
|
|
||||||
.arg("-lic")
|
|
||||||
.arg(script)
|
|
||||||
.env_remove("CLAUDECODE")
|
|
||||||
.env_remove("CLAUDE_CODE_ENTRYPOINT")
|
|
||||||
.creation_flags(CREATE_NO_WINDOW)
|
|
||||||
.stdin(Stdio::null())
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.spawn()
|
|
||||||
.is_ok()
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user