Skip to content

Commit e28bc51

Browse files
committed
v1.2.1
- Added WinGet support
1 parent 832e70f commit e28bc51

File tree

8 files changed

+215
-25
lines changed

8 files changed

+215
-25
lines changed

.github/workflows/release.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ on:
88
permissions:
99
contents: write
1010

11+
env:
12+
RELEASE_ASSET_NAME: claude-code-usage-monitor.exe
13+
WINGET_PACKAGE_ID: CodeZeno.ClaudeCodeUsageMonitor
14+
1115
jobs:
1216
build:
1317
runs-on: windows-latest
@@ -26,3 +30,48 @@ jobs:
2630
with:
2731
files: target/release/claude-code-usage-monitor.exe
2832
generate_release_notes: true
33+
34+
winget:
35+
needs: build
36+
runs-on: windows-latest
37+
env:
38+
WINGETCREATE_GITHUB_TOKEN: ${{ secrets.WINGETCREATE_GITHUB_TOKEN }}
39+
steps:
40+
- name: Prepare release metadata
41+
shell: pwsh
42+
run: |
43+
$tag = "${{ github.ref_name }}"
44+
$version = $tag.TrimStart('v')
45+
$packageUrl = "https://github.com/${{ github.repository }}/releases/download/$tag/${{ env.RELEASE_ASSET_NAME }}"
46+
47+
"PACKAGE_VERSION=$version" >> $env:GITHUB_ENV
48+
"PACKAGE_URL=$packageUrl" >> $env:GITHUB_ENV
49+
50+
- name: Skip when winget token is not configured
51+
if: env.WINGETCREATE_GITHUB_TOKEN == ''
52+
shell: pwsh
53+
run: |
54+
Write-Warning "Skipping WinGet submission because WINGETCREATE_GITHUB_TOKEN is not configured."
55+
56+
- name: Install .NET 6 runtime
57+
if: env.WINGETCREATE_GITHUB_TOKEN != ''
58+
uses: actions/setup-dotnet@v4
59+
with:
60+
dotnet-version: "6.0.x"
61+
62+
- name: Download wingetcreate
63+
if: env.WINGETCREATE_GITHUB_TOKEN != ''
64+
shell: pwsh
65+
run: |
66+
Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
67+
68+
- name: Submit WinGet manifest update
69+
if: env.WINGETCREATE_GITHUB_TOKEN != ''
70+
shell: pwsh
71+
run: |
72+
.\wingetcreate.exe update `
73+
--submit `
74+
--token $env:WINGETCREATE_GITHUB_TOKEN `
75+
--urls $env:PACKAGE_URL `
76+
--version $env:PACKAGE_VERSION `
77+
$env:WINGET_PACKAGE_ID

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ Desktop.ini
2121

2222
# Local release workflow helper
2323
workflow.cmd
24+
25+
# Local WinGet manifest generation output
26+
/manifests/

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "claude-code-usage-monitor"
3-
version = "1.2.0"
3+
version = "1.2.1"
44
edition = "2021"
55
license = "MIT"
66
description = "Windows taskbar widget for monitoring Claude Code usage and rate limits"

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Run the executable — the widget appears in your taskbar.
4949

5050
- **Drag** the left divider to reposition the widget along the taskbar
5151
- **Right-click** for a context menu with **Refresh**, **Update Frequency**, **Settings** (Start with Windows, Reset Position), and **Exit**
52+
- Installations managed by WinGet defer upgrades to `winget upgrade` instead of replacing the executable in place
5253

5354
## Project structure
5455

@@ -66,9 +67,7 @@ src/
6667

6768
Pre-built Windows executables are available on the [Releases](../../releases) page. Download `claude-code-usage-monitor.exe` and run it directly — no Rust toolchain required.
6869

69-
New releases are published automatically when a version tag is pushed:
70+
After the GitHub Release is published, the release workflow also attempts to submit a WinGet manifest update for `CodeZeno.ClaudeCodeUsageMonitor`.
7071

71-
```bash
72-
git tag v1.0.0
73-
git push origin v1.0.0
74-
```
72+
- Configure a classic PAT as the `WINGETCREATE_GITHUB_TOKEN` repository secret with `public_repo` scope so `wingetcreate` can submit to `microsoft/winget-pkgs`
73+
- The first WinGet submission still needs to be created manually; after the package exists, later tagged releases can update it automatically

src/localization.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,16 @@ pub fn detect_system_language() -> LanguageId {
275275
.unwrap_or(LanguageId::English)
276276
}
277277

278+
pub fn update_via_winget(language: LanguageId) -> &'static str {
279+
match language {
280+
LanguageId::English => "Update via WinGet",
281+
LanguageId::Spanish => "Actualizar con WinGet",
282+
LanguageId::French => "Mettre a jour avec WinGet",
283+
LanguageId::German => "Mit WinGet aktualisieren",
284+
LanguageId::Japanese => "WinGet",
285+
}
286+
}
287+
278288
fn preferred_ui_languages() -> Vec<String> {
279289
unsafe {
280290
let mut num_languages = 0u32;

src/updater.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ const RELEASE_ASSET_NAME: &str = "claude-code-usage-monitor.exe";
1717
const HELPER_EXE_NAME: &str = "updater-helper.exe";
1818
const DOWNLOAD_EXE_NAME: &str = "update-download.exe";
1919
const CREATE_NO_WINDOW: u32 = 0x08000000;
20+
const CREATE_NEW_CONSOLE: u32 = 0x00000010;
21+
// Keep this aligned with the package identifier used in winget-pkgs.
22+
const WINGET_PACKAGE_ID: &str = "CodeZeno.ClaudeCodeUsageMonitor";
23+
24+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25+
pub enum InstallChannel {
26+
Portable,
27+
Winget,
28+
}
2029

2130
#[derive(Clone, Debug)]
2231
pub struct ReleaseDescriptor {
@@ -60,13 +69,34 @@ pub fn handle_cli_mode(args: &[String]) -> Option<i32> {
6069
None
6170
}
6271

72+
pub fn current_install_channel() -> InstallChannel {
73+
match std::env::current_exe() {
74+
Ok(path) if is_winget_install_path(&path) => InstallChannel::Winget,
75+
_ => InstallChannel::Portable,
76+
}
77+
}
78+
6379
pub fn check_for_updates() -> Result<UpdateCheckResult, String> {
6480
match fetch_latest_release()? {
6581
Some(release) => Ok(UpdateCheckResult::Available(release)),
6682
None => Ok(UpdateCheckResult::UpToDate),
6783
}
6884
}
6985

86+
pub fn begin_winget_update() -> Result<(), String> {
87+
let command = winget_upgrade_command();
88+
Command::new("powershell.exe")
89+
.arg("-NoLogo")
90+
.arg("-NoExit")
91+
.arg("-Command")
92+
.arg(&command)
93+
.creation_flags(CREATE_NEW_CONSOLE)
94+
.spawn()
95+
.map_err(|e| format!("Unable to launch WinGet update command: {e}"))?;
96+
97+
Ok(())
98+
}
99+
70100
pub fn begin_self_update(release: &ReleaseDescriptor) -> Result<(), String> {
71101
let current_exe =
72102
std::env::current_exe().map_err(|e| format!("Unable to locate current executable: {e}"))?;
@@ -305,6 +335,10 @@ fn updates_dir() -> Result<PathBuf, String> {
305335
.ok_or_else(|| "Unable to resolve a writable local updates directory.".to_string())
306336
}
307337

338+
fn winget_upgrade_command() -> String {
339+
format!("winget upgrade --id {WINGET_PACKAGE_ID} --exact")
340+
}
341+
308342
fn backup_path_for(target: &Path) -> PathBuf {
309343
let file_name = target
310344
.file_name()
@@ -350,6 +384,52 @@ fn user_agent() -> &'static str {
350384
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))
351385
}
352386

387+
fn is_winget_install_path(path: &Path) -> bool {
388+
let normalized_path = normalize_path(path);
389+
winget_install_roots()
390+
.into_iter()
391+
.map(|root| normalize_path(&root))
392+
.any(|root| normalized_path.starts_with(&root))
393+
}
394+
395+
fn winget_install_roots() -> Vec<PathBuf> {
396+
let mut roots = Vec::new();
397+
398+
if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") {
399+
roots.push(
400+
PathBuf::from(local_app_data)
401+
.join("Microsoft")
402+
.join("WinGet")
403+
.join("Packages"),
404+
);
405+
}
406+
407+
if let Ok(program_files) = std::env::var("ProgramFiles") {
408+
roots.push(PathBuf::from(program_files).join("WinGet").join("Packages"));
409+
} else {
410+
roots.push(PathBuf::from(r"C:\Program Files\WinGet\Packages"));
411+
}
412+
413+
if let Ok(program_files_x86) = std::env::var("ProgramFiles(x86)") {
414+
roots.push(
415+
PathBuf::from(program_files_x86)
416+
.join("WinGet")
417+
.join("Packages"),
418+
);
419+
} else {
420+
roots.push(PathBuf::from(r"C:\Program Files (x86)\WinGet\Packages"));
421+
}
422+
423+
roots
424+
}
425+
426+
fn normalize_path(path: &Path) -> String {
427+
path.to_string_lossy()
428+
.replace('/', "\\")
429+
.trim_end_matches('\\')
430+
.to_ascii_lowercase()
431+
}
432+
353433
fn is_version_newer(candidate: &str, current: &str) -> bool {
354434
parse_version(candidate) > parse_version(current)
355435
}

0 commit comments

Comments
 (0)