mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-09-09 12:17:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcce939f72 | ||
|
|
e089a1b420 | ||
|
|
457d5274da | ||
|
|
f1dfe15000 | ||
|
|
38ae4dff09 | ||
|
|
3c0878f6cc | ||
|
|
eca430ccc6 | ||
|
|
2791022e7a | ||
|
|
ed9b8b2042 |
Generated
+4
-722
@@ -8,12 +8,6 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
@@ -26,24 +20,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -77,12 +59,11 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "claude-code-usage-bubble"
|
||||
version = "0.1.4"
|
||||
version = "0.1.8"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"embed-resource",
|
||||
"log",
|
||||
"native-tls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -90,26 +71,9 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tiny-skia",
|
||||
"toml 0.8.23",
|
||||
"ureq",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -178,17 +142,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "3.0.9"
|
||||
@@ -209,22 +162,6 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
@@ -250,36 +187,6 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -301,149 +208,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"utf8_iter",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@@ -451,9 +221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -462,12 +230,6 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
@@ -483,18 +245,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
@@ -517,23 +267,6 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.1"
|
||||
@@ -549,111 +282,31 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.115"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -672,19 +325,13 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -698,51 +345,6 @@ dependencies = [
|
||||
"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 = "schannel"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
@@ -844,18 +446,6 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "strict-num"
|
||||
version = "0.1.1"
|
||||
@@ -873,30 +463,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
@@ -985,16 +551,6 @@ dependencies = [
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
@@ -1087,51 +643,6 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"log",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -1164,58 +675,6 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
@@ -1402,183 +861,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "writeable"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
+1
-6
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "claude-code-usage-bubble"
|
||||
version = "0.1.4"
|
||||
version = "0.1.8"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
description = "Floating bubble showing Claude Code and Codex usage on Windows"
|
||||
@@ -8,11 +8,6 @@ homepage = "https://github.com/tiennm99/claude-code-usage-bubble"
|
||||
repository = "https://github.com/tiennm99/claude-code-usage-bubble"
|
||||
|
||||
[dependencies]
|
||||
# `ureq` + `native-tls` are kept while the legacy `poller.rs` and `updater.rs`
|
||||
# modules survive. Phase 4 deletes `poller.rs` and Phase 6 deletes `updater.rs`,
|
||||
# at which point these two deps go away in favour of `net::winhttp`.
|
||||
ureq = { version = "2", default-features = false, features = ["native-tls", "json", "proxy-from-env"] }
|
||||
native-tls = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
dirs = "6"
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# Phase 01 — Implement Restart Action
|
||||
|
||||
## Context Links
|
||||
|
||||
- Reused pattern: `src/update/install.rs:1-120` (cmd-handoff swap-and-restart). Documented in `docs/release-process.md` if it exists.
|
||||
- Menu wiring reference: `src/app.rs:870-1045` (`show_context_menu`) and `src/app.rs:363-392` (`on_menu_command`).
|
||||
- Mutex acquisition: `src/app.rs:152-168` (`Global\ClaudeCodeUsageBubble`).
|
||||
- i18n schema: `src/i18n/mod.rs` (`LocaleStrings` struct around line 22-80).
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** Low (UX enhancement).
|
||||
- **Status:** Done. Code-reviewer DONE_WITH_CONCERNS — M1 (match-arm ordering) + L3 (lock-during-save) addressed in follow-up edits.
|
||||
- **Size:** ~50 LOC across 3 files (+ 8 locale TOMLs, one line each).
|
||||
|
||||
## Key Insights
|
||||
|
||||
- The existing mutex check rejects a second instance immediately. A naive "spawn-then-exit" races. The `cmd.exe /c timeout` handoff (1 s sleep, then `start ""`) is the simplest decoupling — same trick `update::install::begin` already uses.
|
||||
- `cmd.exe` expands `%var%` in argument strings. Current `current_exe()` path containing `%` is an injection vector; reject it (existing precedent: `update::install` rejects too).
|
||||
- `std::process::Command` with `creation_flags(DETACHED_PROCESS | CREATE_NO_WINDOW)` ensures the helper outlives the parent silently.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
|
||||
- Right-click context menu shows a "Restart" item directly above "Exit".
|
||||
- Clicking "Restart" closes the current process and a new instance starts within ~1–2 seconds, restoring tray icons and bubbles.
|
||||
- No confirmation prompt.
|
||||
- Item label is i18n-aware: all 8 locales get a translation.
|
||||
|
||||
### Non-functional
|
||||
|
||||
- No regression in mutex single-instance behavior — second instance must still be blocked if user accidentally launches manually mid-restart.
|
||||
- No console window flashes during handoff.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User clicks "Restart"
|
||||
→ WM_COMMAND with IDM_RESTART
|
||||
→ app::on_menu_command → app::restart_app()
|
||||
→ settings::save current snapshot (defensive flush)
|
||||
→ spawn detached cmd.exe with delayed `start ""` for current_exe
|
||||
→ PostQuitMessage(0)
|
||||
→ message loop exits → mutex released
|
||||
→ cmd.exe wakes up → new instance launches → acquires mutex → run()
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Modify:**
|
||||
|
||||
- `src/app.rs` — add `IDM_RESTART: u16 = 33` const (next free in the 30-39 band), match arm in `on_menu_command`, menu append in `show_context_menu` between `IDM_TOGGLE_WIDGET` row and the separator before `IDM_EXIT`, new `fn restart_app()`.
|
||||
- `src/i18n/mod.rs` — add `pub restart: String,` field to `LocaleStrings` (place near `exit`).
|
||||
- `src/i18n/locales/en.toml`, `de.toml`, `es.toml`, `fr.toml`, `ja.toml`, `ko.toml`, `nl.toml`, `zh-TW.toml` — add `restart = "<translation>"`.
|
||||
|
||||
**Create:** none.
|
||||
|
||||
**Delete:** none.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Add menu ID and i18n field.**
|
||||
- `app.rs`: `const IDM_RESTART: u16 = 33;` (after `IDM_VERSION_ACTION`).
|
||||
- `i18n/mod.rs`: add `pub restart: String,` to `LocaleStrings`. Place adjacent to `exit`.
|
||||
- Add `restart = "Restart"` to `en.toml`. Translate for the other 7 locales (Vietnamese-quality is acceptable; native fluency not required for a single-word menu item).
|
||||
|
||||
2. **Wire the menu entry.**
|
||||
- `app.rs::show_context_menu` — between the `show_widget` append and the `MF_SEPARATOR` before `IDM_EXIT`, add `append_item(menu, IDM_RESTART, &snap.strings.restart, MENU_ITEM_FLAGS(0));`.
|
||||
- Add `IDM_RESTART => restart_app(),` arm in `on_menu_command` before the `_ => {}` catch-all.
|
||||
|
||||
3. **Implement `restart_app()`.**
|
||||
- Persist a final settings snapshot (defensive flush). Read current state, call `settings::save(&snap)`.
|
||||
- Resolve `std::env::current_exe()`. If `Err`, log error and `PostQuitMessage(0)` (degrade to plain Exit).
|
||||
- Convert path to string. If it contains `%`, log error and return (refuse — matches `update::install` precedent).
|
||||
- Build the cmd line: `timeout /t 1 >nul & start "" "<exe>"`.
|
||||
- Spawn via `std::process::Command::new("cmd.exe")` with `.creation_flags(DETACHED_PROCESS | CREATE_NO_WINDOW)` and a `raw_arg` payload `/c "<cmd>"` (mirrors `install.rs:104-114`).
|
||||
- On spawn success: `PostQuitMessage(0)`. On failure: log error and return (app stays running).
|
||||
|
||||
4. **Verify.**
|
||||
- `cargo check` — no warnings.
|
||||
- Manual smoke test: build release, right-click tray, Restart, observe close + relaunch within ~2 s, mutex acquired by new instance, bubbles + tray icons rendered.
|
||||
|
||||
## Todo List
|
||||
|
||||
- [x] Add `IDM_RESTART` const in `app.rs`.
|
||||
- [x] Add `restart: String` to `LocaleStrings` in `i18n/mod.rs`.
|
||||
- [x] Update all 8 locale `.toml` files.
|
||||
- [x] Append menu item in `show_context_menu`.
|
||||
- [x] Add match arm in `on_menu_command` (placed before `IDM_LANG_BASE` guard per reviewer M1).
|
||||
- [x] Implement `restart_app()` in `app.rs` (clone-then-save per reviewer L3).
|
||||
- [x] `cargo check` clean.
|
||||
- [ ] Manual smoke test on Windows (deferred to user; needs release build).
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Right-click tray → menu shows "Restart" between "Show widget" group and "Exit".
|
||||
- Clicking it closes the process and a new one starts within 2 s with identical state (settings honored, bubble positions persisted, tray icons restored).
|
||||
- No console window flashes.
|
||||
- `cargo check` passes with no new warnings.
|
||||
- All 8 locales include the new key (no fallback to English).
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Severity | Mitigation |
|
||||
|------|----------|------------|
|
||||
| Mutex race — new instance starts before old releases | Medium | 1 s `timeout` in cmd handoff; matches update module precedent. |
|
||||
| `current_exe()` path contains `%` (injection) | Low | Reject, log, abort (same as `install.rs:90-94`). |
|
||||
| `cmd.exe` not on PATH (broken Windows install) | Very Low | Log error, app stays running. User can Exit manually. |
|
||||
| Settings not flushed before quit | Low | Explicit `settings::save()` before `PostQuitMessage`. Bubble positions already persist on drag, so worst case is a no-op. |
|
||||
| User restart-spams the menu | Low | Each click queues a new cmd handoff; the timeout dedupes via mutex. Worst case: one extra instance attempt that exits immediately on `ERROR_ALREADY_EXISTS`. |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- `%`-in-path rejection prevents `cmd.exe` variable expansion injection.
|
||||
- No user-supplied input enters the cmd line — only `std::env::current_exe()` output.
|
||||
- Detached process flags prevent inherited stdio from leaking.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- After merge: bump version (semver patch — UX addition with no API change).
|
||||
- Consider an analogous restart action for the bubble's context menu (currently the bubble also fires `on_menu_command` via `WM_COMMAND`, so the same menu id works there for free).
|
||||
@@ -0,0 +1,41 @@
|
||||
# Plan: Menu Restart Button
|
||||
|
||||
**Slug:** menu-restart-button
|
||||
**Created:** 2026-05-18 09:45
|
||||
**Branch:** main
|
||||
**Status:** Implemented (awaiting commit)
|
||||
|
||||
## Goal
|
||||
|
||||
Add a "Restart" entry to the tray right-click context menu, positioned directly above "Exit". Clicking it relaunches the running binary in-place without prompting for confirmation.
|
||||
|
||||
## Why
|
||||
|
||||
User-requested. Current flow to apply a config/locale tweak that doesn't hot-reload (or to recover after a hang) is Exit → relaunch from Start menu. A one-click restart is symmetric with Exit and avoids hunting for the binary again.
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Title | Status |
|
||||
|---|-------|--------|
|
||||
| 01 | Implement Restart action | Done — [phase-01-implement-restart-action.md](phase-01-implement-restart-action.md) |
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- **Placement:** main menu, between `Show widget` separator and `Exit`. NOT inside Settings submenu — keeps top-level discoverability.
|
||||
- **No confirmation dialog.** Settings auto-save on every change (settings.rs:138-152); restart is non-destructive.
|
||||
- **Mechanism:** detached `cmd.exe /c timeout /t 1 >nul & start "" "<exe>"` handoff, then `PostQuitMessage(0)`. Same pattern as `update::install::begin` minus the swap step. The 1-second wait lets the current process release `Global\ClaudeCodeUsageBubble` mutex before the new instance's `CreateMutexW` runs.
|
||||
- **Reject paths containing `%`** — cmd.exe expands `%var%`, same defense the update module already uses (install.rs:90-94).
|
||||
|
||||
## Dependencies
|
||||
|
||||
- None. Pure Rust + existing `windows` crate features.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Restart after settings change auto-trigger (would be a separate feature).
|
||||
- Restart-with-args (e.g., toggle `--diagnose`).
|
||||
- Cross-platform — Windows-only by design.
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
None.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Brainstorm: Off-Screen Bubble Recovery
|
||||
|
||||
## 1. Recovery strategies ranked
|
||||
|
||||
### RECOMMEND — Layered: validate on load + clamp on create
|
||||
|
||||
**A. Validate position in `settings::load`** (primary defense)
|
||||
- After deserialize, walk `bubble_positions`. For each `Some((x,y))`, build a probe rect `(x, y, x+min_w, y+min_h)` and check via `MonitorFromRect(... MONITOR_DEFAULTTONULL)`. If null → set to `None`.
|
||||
- One pass, ~15 lines, runs before any window code sees the value.
|
||||
- Pro: KISS, no race with `ShowWindow`, fixes related bugs (hand-edited JSON, dpi-changed coords).
|
||||
|
||||
**B. Clamp on create** (defense-in-depth, kept as proposed)
|
||||
- After `CreateWindowExW`, before `ShowWindow`, call `clamp_into_work_area(hwnd)`.
|
||||
- Catches monitor unplug **between** `load()` and `create()` (rare but possible: laptop closed mid-startup).
|
||||
- Cost: one extra call, idempotent.
|
||||
|
||||
Both together are the right answer. Neither alone covers all cases.
|
||||
|
||||
### CONSIDER — Visual cue
|
||||
|
||||
**C. Tray balloon "Widget repositioned to primary monitor"**
|
||||
- Only when validator actually relocated. Uses existing `Shell_NotifyIconW NIF_INFO`. ~20 lines.
|
||||
- Risk: balloon spam on dock/undock cycles. Fire only when *saved* position was killed.
|
||||
|
||||
### AVOID
|
||||
|
||||
**D. Topology fingerprint** — overkill, doesn't preserve intent better than (A).
|
||||
**E. Per-monitor relative pinning** — future feature, not a fix. YAGNI.
|
||||
|
||||
## 2. Trade-off matrix
|
||||
|
||||
| Approach | Preserves intent on replug | Surprise on cold start | LOC | Risk |
|
||||
|----------|---------------------------|------------------------|-----|------|
|
||||
| A (validate-on-load) | No — wipes saved coord | Low | ~15 | None |
|
||||
| B (clamp-on-create) | Partial — moves to nearest edge | Low | ~3 | None |
|
||||
| A+B | No | Low | ~18 | None |
|
||||
| A+B+C | Same + explains itself | Very low | ~38 | Balloon fatigue |
|
||||
| D (topology hash) | Yes if same monitor before next launch | Medium | ~80 | Maintenance |
|
||||
| E (relative pin) | Yes | Medium | ~150 | Premature |
|
||||
|
||||
## 3. Edge cases proposed fix misses
|
||||
|
||||
1. **Saved-pos monitor asleep / no input** — `MonitorFromRect` still returns handle; A+B no-op. Correct.
|
||||
2. **DPI change while app closed** — px coords technically valid by topology; A passes, B no-op. Acceptable.
|
||||
3. **Negative-coord monitors (secondary left of primary)** — validator MUST use `MONITOR_DEFAULTTONULL`, not `DEFAULTTONEAREST` (would silently snap valid secondary coord to primary).
|
||||
4. **Dual bubbles overlap after relocate** — both clamped to bottom-right of primary → stacked. `default_position` staggers Codex; clamp doesn't. Minor.
|
||||
5. **User drags to secondary, unplugs, restarts** — A+B: bubble at primary default; saved pos destroyed. No recovery on replug. Acceptable for v1.
|
||||
6. **Dock-daily multi-monitor user** — every undock wipes pos; every dock back gives default. Annoying. Case where E would win. Punt unless reported.
|
||||
|
||||
## 4. Logging strategy (minimal)
|
||||
|
||||
On the visibility-affecting path only:
|
||||
|
||||
- `info`: `bubble create model={} pos=({},{}) size={}x{} dpi={}` — one line per bubble at create.
|
||||
- `warn`: `bubble position ({},{}) outside all monitors, resetting to default` — fires in validator. **This is the line that would have solved this bug in 5 seconds.**
|
||||
- `warn`: `clamp_into_work_area moved bubble from ({},{}) to ({},{})` — fires on create-time clamp.
|
||||
- `debug`: monitor enumeration on startup.
|
||||
|
||||
Skip: per-render logs, drag logs, timer ticks.
|
||||
|
||||
## 5. "Reset position" discoverability — secondary
|
||||
|
||||
Menu item exists but buried. If A+B work, this path is unreachable. Don't add a "Reset position" balloon prompt — confirmation fatigue. Just fix silently and the §4 warn + §C balloon explain it once.
|
||||
|
||||
## Recommended action
|
||||
|
||||
1. Add `BubblePositions::validate(&mut self)` called from `settings::load`. Use `MonitorFromRect(... MONITOR_DEFAULTTONULL)` with `(x, y, x+MIN_BUBBLE_SIZE, y+MIN_BUBBLE_SIZE)`. Set to `None` on miss. Log `warn`.
|
||||
2. Call `clamp_into_work_area(hwnd)` in `bubble::create` between `CreateWindowExW` and `ShowWindow`. Log `warn` on movement.
|
||||
3. Add the 4 log lines from §4.
|
||||
4. Single tray balloon "Widget repositioned: previous monitor not connected" once per launch when validator killed any saved position.
|
||||
5. Defer monitor-index pinning (E) and topology hash (D).
|
||||
|
||||
Total: ~40 lines, one new function, two log statements, one balloon call.
|
||||
|
||||
## Unresolved questions
|
||||
|
||||
- Balloon (item 4): opt-in or always-on? Default always-on.
|
||||
- `MIN_BUBBLE_SIZE` as probe rect, or account for current `bubble_size_logical`? Min safer.
|
||||
- Re-attempt last-known coord on replug? Probably no — YAGNI.
|
||||
- Codex/Claude stagger preservation on auto-relocate? Currently they'd stack. Worth fixing in same patch?
|
||||
@@ -0,0 +1,60 @@
|
||||
# Code Review: Bubble Off-Screen Clamp Fix
|
||||
|
||||
**Scope:** Proposed bug-fix for v0.1.7 "widget enabled but not shown" — saved positions on disconnected monitor.
|
||||
**Files:** `src/bubble.rs` (create, clamp_into_work_area, set_user_visible, default_position), `src/app.rs` (spawn_bubble, toggle_widget_visibility, reset_positions).
|
||||
|
||||
## Overall Assessment
|
||||
|
||||
**Fix is correct and minimal. Ship it with two small refinements.** Root-cause matches code (verified: `bubble.rs:143-160` passes saved `position` straight into `CreateWindowExW`; `clamp_into_work_area` at `:770` only wired into `WM_SETTINGCHANGE` at `:486`). Approach is the right shape: clamp post-create, pre-show.
|
||||
|
||||
## Critical Issues
|
||||
|
||||
None.
|
||||
|
||||
## High Priority
|
||||
|
||||
1. **Call order — clamp must run BEFORE `render(hwnd)` at `bubble.rs:220`, not just before `ShowWindow` at `:222`.** `render` calls `GetWindowRect` (`:1108`) for the `UpdateLayeredWindow` destination point. If clamp runs after `render`, the first frame paints at the off-screen coords; second paint only happens on next update_data tick. Move `clamp_into_work_area(hwnd)` to between line `:218` (state insert) and `:220` (render).
|
||||
|
||||
## Medium Priority
|
||||
|
||||
2. **`MonitorFromWindow` on a not-yet-shown off-screen window — verified safe.** Win32 sets the window rect immediately at `CreateWindowExW` return (visibility is irrelevant to `GetWindowRect`). With `MONITOR_DEFAULTTONEAREST` and a window whose entire rect lies on a disconnected monitor, the OS computes intersection with each *currently attached* monitor's rect; none intersect → falls back to nearest by Euclidean distance → returns the primary on a single-monitor setup. Saved `[2407,1282]` on a 1920-wide primary → nearest = primary → clamp pulls to `(1920-w, …)`. Correct.
|
||||
|
||||
3. **Multi-monitor edge case is preserved.** If the saved position is on a still-connected secondary, `MonitorFromWindow` returns that secondary monitor and clamps within its work area — no unwanted pull to primary. Good.
|
||||
|
||||
4. **Partial off-screen.** `clamp_into_work_area` only adjusts when fully outside (clamps each axis independently to `[wa.left, wa.right-w]`). A window whose top-left is on-screen but bottom-right spills off → it pulls the whole window inside. Behaviour is fine; matches `snap_to_edge` (`:644-645`).
|
||||
|
||||
5. **DPI mismatch (saved from 4K → 1080p primary):** the saved coords are physical pixels but the new bubble's `width_px/height_px` are recomputed against the *current* primary DPI (`:140-142`). Clamp uses the new size against the new monitor's work area — correct. No DPI bug.
|
||||
|
||||
6. **`default_position` case:** no-op (already inside work area). Safe.
|
||||
|
||||
## Low Priority
|
||||
|
||||
7. **Log levels are appropriate.** `info!` in `create` (fires once per bubble creation), `set_user_visible` (fires only on user-toggle — verified at `app.rs:1152` only called from `toggle_widget_visibility`), and `toggle_widget_visibility` (one event per click). None on the render hot path. Approved.
|
||||
|
||||
8. **Alternative call site (clamp in `app::spawn_bubble`):** Less attractive. `spawn_bubble` doesn't own the HWND lifecycle and would need a fresh `GetWindowRect` round-trip. Keeping the clamp inside `bubble::create` keeps the bubble module the sole owner of window geometry and means future call sites (e.g. tests, a hypothetical re-create-on-DPI-change) also benefit for free. The "bubble module stays position-agnostic" argument is weak — it already calls `default_position`, `snap_to_edge`, and `clamp_into_work_area`. Position-aware is the status quo.
|
||||
|
||||
## Side Effects
|
||||
|
||||
- No callers of `bubble::create` assert the returned HWND is at the exact requested coords. `app::spawn_bubble` (`:277`) ignores position post-create; `reset_positions` (`:1156`) destroys + recreates. Safe.
|
||||
- `position(hwnd)` (`:322`) reads live `GetWindowRect`, so any subsequent `on_bubble_moved` save reflects the clamped coords — this self-heals the persisted bad value on first drag.
|
||||
|
||||
## Positive Observations
|
||||
|
||||
- Clamp helper already exists and is correct (`:770-809`).
|
||||
- Fix is one line + three log statements; minimal blast radius.
|
||||
- Persisted-corruption auto-heal via first interaction is a nice property.
|
||||
|
||||
## Recommended Actions
|
||||
|
||||
1. **MUST:** Place `clamp_into_work_area(hwnd)` between `lock_bubbles().insert(...)` (`:218`) and `render(hwnd)` (`:220`) — not after `render`.
|
||||
2. **SHOULD:** Add an `info!` in `clamp_into_work_area` that fires only when `nx != r.left || ny != r.top` (i.e. the actual reposition path). Free diagnostic for future "bubble moved itself" reports.
|
||||
3. **CONSIDER:** Persist the clamped position immediately after `create` so `settings.json` is self-healed on next launch, not only after a drag. Trade-off: writes settings on every startup; current behaviour writes only on user action. Probably YAGNI — drift gets repaired on first interaction.
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
- Should we also persist the corrected position eagerly (action 3)? Default to no per YAGNI; flag for user.
|
||||
- Does Windows ever defer `CreateWindowExW` window-rect commit until `ShowWindow`? Per MSDN and verified by existing `snap_to_edge` using the same pattern in `WM_EXITSIZEMOVE`, no — rect is committed synchronously.
|
||||
|
||||
**Status:** DONE_WITH_CONCERNS
|
||||
**Summary:** Fix is correct and small. One ordering bug: clamp must precede `render`, not just `ShowWindow`, otherwise the first paint targets the off-screen coords.
|
||||
**Concerns:** Action 1 (clamp before render) is a real correctness issue — the proposal as written ("after CreateWindowExW succeeds and before ShowWindow") technically permits ordering after `render`, which would defeat the fix until the next data update.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Code Review — Tray "Restart" Action
|
||||
|
||||
**Scope:** uncommitted changes on clean tree
|
||||
**Files:** `src/app.rs`, `src/i18n/mod.rs`, 8x `src/i18n/locales/*.toml`
|
||||
**Plan:** `plans/260518-0945-menu-restart-button/phase-01-implement-restart-action.md`
|
||||
|
||||
## Verdict
|
||||
Clean implementation. All 7 acceptance criteria met. Build is `cargo check`-clean. No security regressions. Pattern faithfully borrowed from `update/install.rs`.
|
||||
|
||||
## Acceptance Criteria — all PASS
|
||||
1. Menu order verified `app.rs:1040-1042`: separator → `IDM_RESTART` → `IDM_EXIT`.
|
||||
2. `IDM_RESTART => restart_app()` arm wired `app.rs:393`.
|
||||
3. `restart_app()` `app.rs:1382-1421` flushes settings, gets `current_exe`, rejects `%`, spawns detached `cmd.exe`, `PostQuitMessage(0)`.
|
||||
4. 1 s `timeout` matches install.rs precedent (2 s there; 1 s sufficient — current process exits as soon as `PostQuitMessage(0)` drains the loop).
|
||||
5. Verified `restart = "..."` in all 8 TOMLs at line 32 (en/de/es/fr/ja/ko/nl/zh-TW). `LocaleStrings` field at `mod.rs:52`. No `#[serde(default)]` → missing key = hard fail; all present.
|
||||
6. Match-arm ordering unambiguous: `IDM_RESTART=33` < guard `x >= IDM_LANG_BASE=100`. Guard won't match 33. `tray::IDM_TOGGLE_WIDGET=50` likewise < 100. Safe.
|
||||
7. No new clippy issues; no new unsafe blocks (`PostQuitMessage(0)` already unsafe at `IDM_EXIT`; matches that idiom).
|
||||
|
||||
## Critical
|
||||
None.
|
||||
|
||||
## High
|
||||
None.
|
||||
|
||||
## Medium
|
||||
**M1. Restart arm sits below the `IDM_LANG_BASE` guard arm.** `app.rs:391-393`. The guard `x if x >= IDM_LANG_BASE => …` is exhaustive for any id `>= 100`. Today `IDM_RESTART=33` is fine, but future readers adding a static id `>= 100` between lines 392 and 393 would silently route into language switching. Cheap fix: move `IDM_RESTART => restart_app()` and `tray::IDM_TOGGLE_WIDGET => …` ABOVE the guard arm. Plan note at `app.rs:82-84` already warns about this — the new arm violates that guidance.
|
||||
|
||||
## Low / Info
|
||||
**L1. Double-restart not deduped.** Rapid clicks queue multiple `cmd.exe` children. First wins the mutex; second's `start ""` succeeds, the resulting bubble process exits at `ERROR_ALREADY_EXISTS`. Acceptable per plan §Risk Assessment. No fix needed.
|
||||
|
||||
**L2. `to_string_lossy()` on `current_exe()` will mangle non-UTF-8 paths.** Same pattern in `install.rs:100`. On real Windows installs paths are UTF-16; lossy → UTF-8 is virtually always faithful. Consistent with existing precedent.
|
||||
|
||||
**L3. `settings::save()` runs while `lock_state()` read-guard is held** (`app.rs:1385-1387`). If `save` ever takes a lock on the same mutex this would deadlock — it currently does not, but the pattern elsewhere (e.g. `set_poll_interval` at 1087-1100) clones, releases, then saves. Recommend matching that pattern: clone snapshot inside scope, drop guard, then `settings::save(&snap)`. Defensive only.
|
||||
|
||||
**L4. No regression to existing menu wiring** — verified by inspection: `show_widget` append at 1034-1039 still preceded by no separator, then separator 1040, then Restart, then Exit. Matches plan exactly.
|
||||
|
||||
## Pattern-Parity Check vs `install.rs`
|
||||
- Flags: `CREATE_NO_WINDOW | DETACHED_PROCESS` → identical bit pattern (`0x0800_0000 | 0x0000_0008`). New constants `RESTART_*` duplicate the values; minor DRY nit but they're file-local and the comment explains why. Acceptable.
|
||||
- `raw_arg` quoting: `/c` then `"<cmd>"` with inner `"` preserved → byte-for-byte same shape as `install.rs:113-114`. Correct.
|
||||
- `%` rejection: present, logs and aborts. Matches `install.rs:89-96`.
|
||||
- `stdin/out/err = Null`: present, matches.
|
||||
|
||||
## PostQuitMessage on UI thread
|
||||
`IDM_EXIT` does the same at `app.rs:370`, called from `on_menu_command` via WM_COMMAND on the UI thread. `restart_app()` is reached the same way. Safe — identical control-flow shape.
|
||||
|
||||
## Metrics
|
||||
- New code: ~40 LOC in `app.rs`, 1 field in `mod.rs`, 8x 1-line TOML adds.
|
||||
- Type coverage: 100%.
|
||||
- New warnings: 0 (`cargo check` clean per user).
|
||||
|
||||
## Recommended Actions
|
||||
1. **M1** (nice-to-have): reorder match arms so `IDM_RESTART` / `IDM_TOGGLE_WIDGET` precede the `x if x >= IDM_LANG_BASE` guard. Defends against future id collisions.
|
||||
2. **L3** (optional): mirror `set_poll_interval`'s clone-then-save pattern in `restart_app()` for consistency.
|
||||
|
||||
## Unresolved Questions
|
||||
- None blocking. Plan §Next Steps suggests a semver patch bump and analogous bubble-menu entry; out of scope for this review.
|
||||
|
||||
---
|
||||
|
||||
**Status:** DONE_WITH_CONCERNS
|
||||
**Summary:** Implementation matches plan and acceptance criteria; cmd-handoff faithfully mirrors `update/install.rs`; all 8 locales updated; no critical or high issues. One medium suggestion (reorder match arms to defend against future static-id collisions with the `IDM_LANG_BASE` guard) and two low/optional refinements.
|
||||
**Concerns:** M1 — new `IDM_RESTART` and `tray::IDM_TOGGLE_WIDGET` arms sit below a catch-all `x >= IDM_LANG_BASE` guard. Today safe (33, 50 < 100); future-fragile. Code comment at `app.rs:82-84` already flags the rule that was bent.
|
||||
+173
-33
@@ -6,6 +6,8 @@
|
||||
// message-only window owned by this module.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
@@ -69,6 +71,7 @@ const IDM_MODEL_CHATGPT: u16 = 21;
|
||||
const IDM_START_WITH_WINDOWS: u16 = 30;
|
||||
const IDM_RESET_POSITION: u16 = 31;
|
||||
const IDM_VERSION_ACTION: u16 = 32;
|
||||
const IDM_RESTART: u16 = 33;
|
||||
const IDM_LANG_SYSTEM: u16 = 40;
|
||||
// 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).
|
||||
@@ -385,8 +388,12 @@ pub fn on_menu_command(id: u32, _owner_hwnd: HWND) {
|
||||
set_update_check_interval(Some(settings::UPDATE_CHECK_WEEKLY_SECS))
|
||||
}
|
||||
IDM_LANG_SYSTEM => set_language(None),
|
||||
x if x >= IDM_LANG_BASE => set_language_by_index((x - IDM_LANG_BASE) as usize),
|
||||
// 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(),
|
||||
IDM_RESTART => restart_app(),
|
||||
x if x >= IDM_LANG_BASE => set_language_by_index((x - IDM_LANG_BASE) as usize),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -468,39 +475,57 @@ fn apply_results(
|
||||
results: Vec<(ProviderId, Result<UsageWindows, usage::Error>)>,
|
||||
) -> Vec<ProviderId> {
|
||||
let mut auth_failures = Vec::new();
|
||||
let mut s = lock_state();
|
||||
let Some(s) = s.as_mut() else {
|
||||
return auth_failures;
|
||||
};
|
||||
if results.is_empty() {
|
||||
return auth_failures;
|
||||
}
|
||||
let strings = s.i18n.strings().clone();
|
||||
let mut any_ok = false;
|
||||
for (id, outcome) in results {
|
||||
match outcome {
|
||||
Ok(windows) => {
|
||||
let entry = s.snapshots.entry(id).or_default();
|
||||
entry.windows = windows;
|
||||
entry.primary_text = i18n::format_window(&windows.primary, &strings);
|
||||
entry.secondary_text = i18n::format_window(&windows.secondary, &strings);
|
||||
any_ok = true;
|
||||
}
|
||||
Err(usage::Error::AuthRequired | usage::Error::TokenExpired) => {
|
||||
auth_failures.push(id);
|
||||
let entry = s.snapshots.entry(id).or_default();
|
||||
entry.primary_text = "!".into();
|
||||
entry.secondary_text = "!".into();
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("provider {id:?} poll failed: {e}");
|
||||
let entry = s.snapshots.entry(id).or_default();
|
||||
entry.primary_text = "…".into();
|
||||
entry.secondary_text = "…".into();
|
||||
let mut crossings: Vec<(ProviderId, u8)> = Vec::new();
|
||||
{
|
||||
let mut guard = lock_state();
|
||||
let Some(state) = guard.as_mut() else {
|
||||
return auth_failures;
|
||||
};
|
||||
if results.is_empty() {
|
||||
return auth_failures;
|
||||
}
|
||||
let strings = state.i18n.strings().clone();
|
||||
let mut any_ok = false;
|
||||
for (id, outcome) in results {
|
||||
match outcome {
|
||||
Ok(windows) => {
|
||||
let entry = state.snapshots.entry(id).or_default();
|
||||
let old_pct = entry.windows.primary.utilization;
|
||||
let new_pct = windows.primary.utilization;
|
||||
// Fire a balloon only the cycle a provider CROSSES a
|
||||
// threshold so the user is nudged once, not on every
|
||||
// subsequent poll while parked above it.
|
||||
for threshold in [80u8, 95u8] {
|
||||
if old_pct < threshold as f64 && new_pct >= threshold as f64 {
|
||||
crossings.push((id, threshold));
|
||||
}
|
||||
}
|
||||
entry.windows = windows;
|
||||
entry.primary_text = i18n::format_window(&windows.primary, &strings);
|
||||
entry.secondary_text = i18n::format_window(&windows.secondary, &strings);
|
||||
any_ok = true;
|
||||
}
|
||||
Err(usage::Error::AuthRequired | usage::Error::TokenExpired) => {
|
||||
auth_failures.push(id);
|
||||
let entry = state.snapshots.entry(id).or_default();
|
||||
entry.primary_text = "!".into();
|
||||
entry.secondary_text = "!".into();
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("provider {id:?} poll failed: {e}");
|
||||
let entry = state.snapshots.entry(id).or_default();
|
||||
entry.primary_text = "…".into();
|
||||
entry.secondary_text = "…".into();
|
||||
}
|
||||
}
|
||||
}
|
||||
state.last_poll_ok = any_ok;
|
||||
}
|
||||
// Lock released. Fire any threshold balloons outside the critical
|
||||
// section so tray::notify and i18n cloning don't block the UI thread.
|
||||
for (id, threshold) in crossings {
|
||||
show_threshold_balloon(id, threshold);
|
||||
}
|
||||
s.last_poll_ok = any_ok;
|
||||
auth_failures
|
||||
}
|
||||
|
||||
@@ -752,6 +777,60 @@ fn handle_tray_action(action: TrayAction) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-read the system theme and, if it changed, push the new value into
|
||||
/// the UI. Called from each bubble's WM_SETTINGCHANGE handler — Windows
|
||||
/// posts that to every top-level window when the user toggles light/dark
|
||||
/// in Settings, so this naturally fires once per change.
|
||||
pub fn recheck_theme() {
|
||||
let now_dark = os::theme::is_dark();
|
||||
let changed = {
|
||||
let mut s = lock_state();
|
||||
let Some(s) = s.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if s.is_dark == now_dark {
|
||||
false
|
||||
} else {
|
||||
s.is_dark = now_dark;
|
||||
true
|
||||
}
|
||||
};
|
||||
if changed {
|
||||
propagate_to_ui();
|
||||
refresh_tray_icons();
|
||||
}
|
||||
}
|
||||
|
||||
fn show_threshold_balloon(provider: ProviderId, threshold: u8) {
|
||||
let payload = {
|
||||
let mut s = lock_state();
|
||||
let Some(s) = s.as_mut() else {
|
||||
return;
|
||||
};
|
||||
// Reuse the same cooldown as the token-expired balloon: any one
|
||||
// balloon at a time keeps notifications calm.
|
||||
if let Some(last) = s.last_balloon_at {
|
||||
if last.elapsed() < BALLOON_COOLDOWN {
|
||||
return;
|
||||
}
|
||||
}
|
||||
s.last_balloon_at = Some(Instant::now());
|
||||
let strings = s.i18n.strings();
|
||||
let provider_label = match provider {
|
||||
ProviderId::Claude => strings.claude_label.clone(),
|
||||
ProviderId::ChatGpt => strings.chatgpt_label.clone(),
|
||||
};
|
||||
let title = format!("{provider_label} · {threshold}%");
|
||||
let body = if threshold >= 95 {
|
||||
strings.threshold_95_body.clone()
|
||||
} else {
|
||||
strings.threshold_80_body.clone()
|
||||
};
|
||||
(s.msg_hwnd, provider, title, body)
|
||||
};
|
||||
tray::notify(payload.0.to_hwnd(), payload.1, &payload.2, &payload.3);
|
||||
}
|
||||
|
||||
fn show_token_expired_balloon(failed: ProviderId) {
|
||||
let payload = {
|
||||
let mut s = lock_state();
|
||||
@@ -962,6 +1041,7 @@ fn show_context_menu(owner_hwnd: HWND) {
|
||||
if snap.widget_visible { MF_CHECKED } else { MENU_ITEM_FLAGS(0) },
|
||||
);
|
||||
let _ = AppendMenuW(menu, MF_SEPARATOR, 0, PCWSTR::null());
|
||||
append_item(menu, IDM_RESTART, &snap.strings.restart, MENU_ITEM_FLAGS(0));
|
||||
append_item(menu, IDM_EXIT, &snap.strings.exit, MENU_ITEM_FLAGS(0));
|
||||
|
||||
let mut pt = POINT::default();
|
||||
@@ -995,9 +1075,13 @@ fn version_action_label(snap: &ContextMenuSnapshot) -> String {
|
||||
UpdateStatus::Applying => snap.strings.applying_update.clone(),
|
||||
UpdateStatus::Failed => snap.strings.update_failed.clone(),
|
||||
};
|
||||
// Append the running binary's version so the user can see what
|
||||
// they are on without opening an About dialog. Using middle-dot as
|
||||
// the separator matches the bubble's countdown formatting.
|
||||
let with_version = format!("{base} \u{00b7} v{}", env!("CARGO_PKG_VERSION"));
|
||||
match snap.install_channel {
|
||||
InstallChannel::Winget => format!("{base} ({})", snap.strings.update_via_winget),
|
||||
InstallChannel::Portable => base,
|
||||
InstallChannel::Winget => format!("{with_version} ({})", snap.strings.update_via_winget),
|
||||
InstallChannel::Portable => with_version,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1285,6 +1369,62 @@ fn set_update_check_interval(value: Option<u64>) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Restart ----------
|
||||
|
||||
// Windows CreateProcess flags. Match the values used by `update::install`
|
||||
// so the cmd-handoff child detaches cleanly without flashing a console.
|
||||
const RESTART_CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
const RESTART_DETACHED_PROCESS: u32 = 0x0000_0008;
|
||||
|
||||
/// Relaunch the running binary via a detached cmd.exe handoff.
|
||||
///
|
||||
/// The 1-second `timeout` gives the current process time to release the
|
||||
/// `Global\ClaudeCodeUsageBubble` mutex before the relaunched instance's
|
||||
/// `CreateMutexW` runs, otherwise the new instance would see
|
||||
/// `ERROR_ALREADY_EXISTS` and exit immediately.
|
||||
fn restart_app() {
|
||||
// Defensive flush — bubble positions and most settings already persist
|
||||
// on change, but a final save is cheap insurance. Snapshot then drop the
|
||||
// lock before the disk write so the UI thread doesn't block on I/O.
|
||||
let snap = lock_state().as_ref().map(|s| s.settings.clone());
|
||||
if let Some(s) = snap {
|
||||
settings::save(&s);
|
||||
}
|
||||
|
||||
let exe = match std::env::current_exe() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::error!("restart: current_exe failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let exe_str = exe.to_string_lossy();
|
||||
// cmd.exe expands `%var%` inside double quotes, so a path containing `%`
|
||||
// would let the environment leak into the relaunch. Refuse — matches the
|
||||
// defense already used in `update::install`.
|
||||
if exe_str.contains('%') {
|
||||
log::error!("restart: refusing path containing '%': {exe_str}");
|
||||
return;
|
||||
}
|
||||
let exe_str = exe_str.replace('"', "");
|
||||
let cmd = format!(r#"timeout /t 1 /nobreak >nul & start "" "{exe_str}""#);
|
||||
let spawned = Command::new("cmd.exe")
|
||||
.raw_arg("/c")
|
||||
.raw_arg(format!("\"{cmd}\""))
|
||||
.creation_flags(RESTART_CREATE_NO_WINDOW | RESTART_DETACHED_PROCESS)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
match spawned {
|
||||
Ok(_) => {
|
||||
log::info!("restart: cmd handoff spawned, posting quit");
|
||||
unsafe { PostQuitMessage(0) };
|
||||
}
|
||||
Err(e) => log::error!("restart: cmd spawn failed: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Start-with-Windows ----------
|
||||
|
||||
fn is_startup_enabled() -> bool {
|
||||
|
||||
+41
-10
@@ -133,16 +133,16 @@ pub fn create(config: BubbleConfig) -> HWND {
|
||||
let initial_size_logical = config
|
||||
.size_logical
|
||||
.clamp(MIN_BUBBLE_SIZE, MAX_BUBBLE_SIZE);
|
||||
let dpi_for_create = primary_dpi();
|
||||
let width_px = scale_to_dpi(initial_size_logical, dpi_for_create);
|
||||
let height_px = scale_to_dpi(bubble_height_logical(initial_size_logical), dpi_for_create);
|
||||
let (x, y) = config
|
||||
.position
|
||||
.unwrap_or_else(|| default_position(width_px, height_px, config.model));
|
||||
let hwnd = unsafe {
|
||||
let class_w = wide_str(CLASS_NAME);
|
||||
let title_w = wide_str("Claude Code Usage Bubble");
|
||||
let hinstance = GetModuleHandleW(PCWSTR::null()).unwrap_or_default();
|
||||
let dpi = primary_dpi();
|
||||
let width_px = scale_to_dpi(initial_size_logical, dpi);
|
||||
let height_px = scale_to_dpi(bubble_height_logical(initial_size_logical), dpi);
|
||||
let (x, y) = config
|
||||
.position
|
||||
.unwrap_or_else(|| default_position(width_px, height_px, config.model));
|
||||
CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
|
||||
PCWSTR::from_raw(class_w.as_ptr()),
|
||||
@@ -217,6 +217,16 @@ pub fn create(config: BubbleConfig) -> HWND {
|
||||
},
|
||||
);
|
||||
|
||||
log::info!(
|
||||
"bubble create model={:?} pos=({x},{y}) size={width_px}x{height_px} dpi={dpi}",
|
||||
config.model
|
||||
);
|
||||
|
||||
// Defense in depth: settings::load already validates positions against
|
||||
// currently-connected monitors, but a monitor unplug between load and
|
||||
// create (or a partially-off-screen saved position) is still possible.
|
||||
clamp_into_work_area(hwnd);
|
||||
|
||||
render(hwnd);
|
||||
unsafe {
|
||||
let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
|
||||
@@ -478,10 +488,13 @@ unsafe extern "system" fn wnd_proc(
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_SETTINGCHANGE => {
|
||||
// Taskbar move / auto-hide toggle / DPI change all post this.
|
||||
// Just re-clamp into the new work area so the bubble can't end up
|
||||
// hidden behind the new taskbar position.
|
||||
// Taskbar move / auto-hide toggle / DPI change / theme toggle
|
||||
// all post this. Re-clamp into the new work area (bubble must
|
||||
// not end up hidden behind the new taskbar position) and ask
|
||||
// the app to re-read the light/dark setting — Windows fires
|
||||
// this message when the user flips the OS theme in Settings.
|
||||
clamp_into_work_area(hwnd);
|
||||
crate::app::recheck_theme();
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
@@ -789,8 +802,26 @@ fn clamp_into_work_area(hwnd: HWND) {
|
||||
let w = r.right - r.left;
|
||||
let h = r.bottom - r.top;
|
||||
let nx = r.left.clamp(wa.left, (wa.right - w).max(wa.left));
|
||||
let ny = r.top.clamp(wa.top, (wa.bottom - h).max(wa.top));
|
||||
let mut ny = r.top.clamp(wa.top, (wa.bottom - h).max(wa.top));
|
||||
|
||||
// When both bubbles get clamped to the same bottom-right corner (e.g.,
|
||||
// saved positions were on a disconnected monitor and the validator missed
|
||||
// them), keep the Codex-above-Claude stagger that `default_position` uses
|
||||
// so they don't visually stack.
|
||||
let is_codex = lock_bubbles()
|
||||
.get(&(hwnd.0 as isize))
|
||||
.is_some_and(|b| matches!(b.model, TrayIconKind::ChatGpt));
|
||||
if is_codex && nx == wa.right - w && ny == wa.bottom - h {
|
||||
const STAGGER_GAP: i32 = 24;
|
||||
ny = (ny - h - STAGGER_GAP).max(wa.top);
|
||||
}
|
||||
|
||||
if nx != r.left || ny != r.top {
|
||||
log::warn!(
|
||||
"clamp_into_work_area moved bubble from ({}, {}) to ({nx}, {ny})",
|
||||
r.left,
|
||||
r.top
|
||||
);
|
||||
unsafe {
|
||||
let _ = SetWindowPos(
|
||||
hwnd,
|
||||
|
||||
@@ -29,6 +29,7 @@ auto_check_hourly = "Stündlich"
|
||||
auto_check_daily = "Täglich"
|
||||
auto_check_weekly = "Wöchentlich"
|
||||
exit = "Beenden"
|
||||
restart = "Neu starten"
|
||||
show_widget = "Widget anzeigen"
|
||||
session_window = "5h"
|
||||
weekly_window = "7d"
|
||||
@@ -41,3 +42,5 @@ token_expired_title = "Claude Code-Sitzung abgelaufen"
|
||||
token_expired_body = "Melde dich erneut an, um die Nutzung weiter zu verfolgen."
|
||||
chatgpt_token_expired_title = "Codex-Sitzung abgelaufen"
|
||||
chatgpt_token_expired_body = "Melde dich erneut an, um die Nutzung weiter zu verfolgen."
|
||||
threshold_80_body = "5-Stunden-Limit naht."
|
||||
threshold_95_body = "Limit fast erreicht — gönn dir eine Pause."
|
||||
|
||||
@@ -29,6 +29,7 @@ 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"
|
||||
@@ -41,3 +42,5 @@ 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."
|
||||
|
||||
@@ -29,6 +29,7 @@ auto_check_hourly = "Cada hora"
|
||||
auto_check_daily = "Cada día"
|
||||
auto_check_weekly = "Cada semana"
|
||||
exit = "Salir"
|
||||
restart = "Reiniciar"
|
||||
show_widget = "Mostrar widget"
|
||||
session_window = "5h"
|
||||
weekly_window = "7d"
|
||||
@@ -41,3 +42,5 @@ token_expired_title = "Sesión de Claude Code caducada"
|
||||
token_expired_body = "Vuelve a iniciar sesión para seguir registrando el uso."
|
||||
chatgpt_token_expired_title = "Sesión de Codex caducada"
|
||||
chatgpt_token_expired_body = "Vuelve a iniciar sesión para seguir registrando el uso."
|
||||
threshold_80_body = "Cerca del límite de 5 horas."
|
||||
threshold_95_body = "Límite casi alcanzado — reduce el ritmo."
|
||||
|
||||
@@ -29,6 +29,7 @@ auto_check_hourly = "Toutes les heures"
|
||||
auto_check_daily = "Quotidienne"
|
||||
auto_check_weekly = "Hebdomadaire"
|
||||
exit = "Quitter"
|
||||
restart = "Redémarrer"
|
||||
show_widget = "Afficher le widget"
|
||||
session_window = "5h"
|
||||
weekly_window = "7j"
|
||||
@@ -41,3 +42,5 @@ token_expired_title = "Session Claude Code expirée"
|
||||
token_expired_body = "Reconnectez-vous pour continuer à suivre votre utilisation."
|
||||
chatgpt_token_expired_title = "Session Codex expirée"
|
||||
chatgpt_token_expired_body = "Reconnectez-vous pour continuer à suivre votre utilisation."
|
||||
threshold_80_body = "Approche de la limite de 5 heures."
|
||||
threshold_95_body = "Limite proche — pensez à lever le pied."
|
||||
|
||||
@@ -29,6 +29,7 @@ auto_check_hourly = "1時間ごと"
|
||||
auto_check_daily = "毎日"
|
||||
auto_check_weekly = "毎週"
|
||||
exit = "終了"
|
||||
restart = "再起動"
|
||||
show_widget = "ウィジェットを表示"
|
||||
session_window = "5時間"
|
||||
weekly_window = "7日"
|
||||
@@ -41,3 +42,5 @@ token_expired_title = "Claude Codeのセッションが切れました"
|
||||
token_expired_body = "使用状況の追跡を続けるには再度サインインしてください。"
|
||||
chatgpt_token_expired_title = "Codexのセッションが切れました"
|
||||
chatgpt_token_expired_body = "使用状況の追跡を続けるには再度サインインしてください。"
|
||||
threshold_80_body = "5時間の上限に近づいています。"
|
||||
threshold_95_body = "上限に近づきました — ペースを落としましょう。"
|
||||
|
||||
@@ -29,6 +29,7 @@ auto_check_hourly = "매시간"
|
||||
auto_check_daily = "매일"
|
||||
auto_check_weekly = "매주"
|
||||
exit = "종료"
|
||||
restart = "다시 시작"
|
||||
show_widget = "위젯 표시"
|
||||
session_window = "5시간"
|
||||
weekly_window = "7일"
|
||||
@@ -41,3 +42,5 @@ token_expired_title = "Claude Code 세션 만료"
|
||||
token_expired_body = "사용량을 계속 추적하려면 다시 로그인하세요."
|
||||
chatgpt_token_expired_title = "Codex 세션 만료"
|
||||
chatgpt_token_expired_body = "사용량을 계속 추적하려면 다시 로그인하세요."
|
||||
threshold_80_body = "5시간 한도에 가까워지고 있어요."
|
||||
threshold_95_body = "한도 임박 — 잠시 쉬어가세요."
|
||||
|
||||
@@ -29,6 +29,7 @@ auto_check_hourly = "Per uur"
|
||||
auto_check_daily = "Dagelijks"
|
||||
auto_check_weekly = "Wekelijks"
|
||||
exit = "Afsluiten"
|
||||
restart = "Opnieuw starten"
|
||||
show_widget = "Widget tonen"
|
||||
session_window = "5u"
|
||||
weekly_window = "7d"
|
||||
@@ -41,3 +42,5 @@ token_expired_title = "Claude Code-sessie verlopen"
|
||||
token_expired_body = "Meld je opnieuw aan om gebruik te blijven volgen."
|
||||
chatgpt_token_expired_title = "Codex-sessie verlopen"
|
||||
chatgpt_token_expired_body = "Meld je opnieuw aan om gebruik te blijven volgen."
|
||||
threshold_80_body = "Je nadert de 5-uurslimiet."
|
||||
threshold_95_body = "Limiet bijna bereikt — overweeg even gas terug te nemen."
|
||||
|
||||
@@ -29,6 +29,7 @@ auto_check_hourly = "每小時"
|
||||
auto_check_daily = "每天"
|
||||
auto_check_weekly = "每週"
|
||||
exit = "結束"
|
||||
restart = "重新啟動"
|
||||
show_widget = "顯示小工具"
|
||||
session_window = "5 小時"
|
||||
weekly_window = "7 日"
|
||||
@@ -41,3 +42,5 @@ token_expired_title = "Claude Code 工作階段已過期"
|
||||
token_expired_body = "請重新登入以繼續追蹤使用量。"
|
||||
chatgpt_token_expired_title = "Codex 工作階段已過期"
|
||||
chatgpt_token_expired_body = "請重新登入以繼續追蹤使用量。"
|
||||
threshold_80_body = "接近 5 小時上限。"
|
||||
threshold_95_body = "上限將至 — 建議稍作休息。"
|
||||
|
||||
@@ -49,6 +49,7 @@ pub struct LocaleStrings {
|
||||
pub auto_check_daily: String,
|
||||
pub auto_check_weekly: String,
|
||||
pub exit: String,
|
||||
pub restart: String,
|
||||
pub show_widget: String,
|
||||
pub session_window: String,
|
||||
pub weekly_window: String,
|
||||
@@ -61,6 +62,12 @@ pub struct LocaleStrings {
|
||||
pub token_expired_body: String,
|
||||
pub chatgpt_token_expired_title: String,
|
||||
pub chatgpt_token_expired_body: String,
|
||||
/// 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
|
||||
/// need to be translated separately.
|
||||
pub threshold_80_body: String,
|
||||
/// Body text for the 95% threshold balloon.
|
||||
pub threshold_95_body: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use windows::Win32::Foundation::RECT;
|
||||
use windows::Win32::Graphics::Gdi::{MonitorFromRect, MONITOR_DEFAULTTONULL};
|
||||
|
||||
use crate::bubble::DEFAULT_BUBBLE_SIZE;
|
||||
use crate::usage::ProviderId;
|
||||
type TrayIconKind = ProviderId;
|
||||
|
||||
// 140px matches MIN_BUBBLE_SIZE — a saved top-left a few px past the work-area
|
||||
// edge still passes the validator, but a position fully on a disconnected
|
||||
// monitor (the bug we're guarding against) fails.
|
||||
const POSITION_PROBE_PX: i32 = 140;
|
||||
|
||||
const APP_DIR_NAME: &str = "ClaudeCodeUsageBubble";
|
||||
const SETTINGS_FILE: &str = "settings.json";
|
||||
|
||||
@@ -67,6 +74,37 @@ impl BubblePositions {
|
||||
self.claude = None;
|
||||
self.codex = None;
|
||||
}
|
||||
|
||||
/// Drop any saved position whose top-left no longer falls on a connected
|
||||
/// monitor. Guards against `bubble::create` placing the window on a
|
||||
/// disconnected secondary monitor (where the user can't see or recover it).
|
||||
pub fn validate(&mut self) {
|
||||
if let Some((x, y)) = self.claude {
|
||||
if !position_on_any_monitor(x, y) {
|
||||
log::warn!("bubble position claude ({x},{y}) outside all monitors; resetting to default");
|
||||
self.claude = None;
|
||||
}
|
||||
}
|
||||
if let Some((x, y)) = self.codex {
|
||||
if !position_on_any_monitor(x, y) {
|
||||
log::warn!("bubble position codex ({x},{y}) outside all monitors; resetting to default");
|
||||
self.codex = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn position_on_any_monitor(x: i32, y: i32) -> bool {
|
||||
// MONITOR_DEFAULTTONULL returns a null HMONITOR when the rect intersects
|
||||
// no connected monitor — exactly the signal we want.
|
||||
let probe = RECT {
|
||||
left: x,
|
||||
top: y,
|
||||
right: x + POSITION_PROBE_PX,
|
||||
bottom: y + POSITION_PROBE_PX,
|
||||
};
|
||||
let monitor = unsafe { MonitorFromRect(&probe, MONITOR_DEFAULTTONULL) };
|
||||
!monitor.is_invalid()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -132,6 +170,8 @@ pub fn load() -> Settings {
|
||||
settings.bubble_size_logical = settings
|
||||
.bubble_size_logical
|
||||
.clamp(crate::bubble::MIN_BUBBLE_SIZE, crate::bubble::MAX_BUBBLE_SIZE);
|
||||
// Drop positions on monitors that have since been disconnected.
|
||||
settings.bubble_positions.validate();
|
||||
settings
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user