- Added check for updates and ability to self-update
This commit is contained in:
Craig Constable 2026-03-18 14:53:30 +10:00
parent d6ea143f05
commit 832e70fd72
6 changed files with 650 additions and 7 deletions

2
Cargo.lock generated
View File

@ -38,7 +38,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "claude-code-usage-monitor"
version = "1.1.0"
version = "1.2.0"
dependencies = [
"dirs",
"native-tls",

View File

@ -1,6 +1,6 @@
[package]
name = "claude-code-usage-monitor"
version = "1.1.0"
version = "1.2.0"
edition = "2021"
license = "MIT"
description = "Windows taskbar widget for monitoring Claude Code usage and rate limits"

View File

@ -83,6 +83,15 @@ pub struct Strings {
pub reset_position: &'static str,
pub language: &'static str,
pub system_default: &'static str,
pub check_for_updates: &'static str,
pub checking_for_updates: &'static str,
pub updates: &'static str,
pub update_in_progress: &'static str,
pub up_to_date: &'static str,
pub up_to_date_short: &'static str,
pub update_failed: &'static str,
pub applying_update: &'static str,
pub update_to: &'static str,
pub exit: &'static str,
pub session_window: &'static str,
pub weekly_window: &'static str,
@ -106,6 +115,15 @@ const ENGLISH: Strings = Strings {
reset_position: "Reset Position",
language: "Language",
system_default: "System Default",
check_for_updates: "Check for Updates",
checking_for_updates: "Checking for Updates...",
updates: "Updates",
update_in_progress: "An update check is already in progress.",
up_to_date: "You already have the latest version.",
up_to_date_short: "Up to date",
update_failed: "Unable to update automatically",
applying_update: "Applying update...",
update_to: "Update to",
exit: "Exit",
session_window: "5h",
weekly_window: "7d",
@ -129,6 +147,15 @@ const SPANISH: Strings = Strings {
reset_position: "Restablecer posicion",
language: "Idioma",
system_default: "Predeterminado del sistema",
check_for_updates: "Buscar actualizaciones",
checking_for_updates: "Buscando actualizaciones...",
updates: "Actualizaciones",
update_in_progress: "Ya hay una comprobacion de actualizacion en curso.",
up_to_date: "Ya tienes la version mas reciente.",
up_to_date_short: "Actualizado",
update_failed: "No se pudo actualizar automaticamente",
applying_update: "Aplicando actualizacion...",
update_to: "Actualizar a",
exit: "Salir",
session_window: "5h",
weekly_window: "7d",
@ -152,6 +179,15 @@ const FRENCH: Strings = Strings {
reset_position: "Reinitialiser la position",
language: "Langue",
system_default: "Par defaut du systeme",
check_for_updates: "Verifier les mises a jour",
checking_for_updates: "Verification des mises a jour...",
updates: "Mises a jour",
update_in_progress: "Une verification de mise a jour est deja en cours.",
up_to_date: "Vous utilisez deja la version la plus recente.",
up_to_date_short: "A jour",
update_failed: "Impossible d'effectuer la mise a jour automatiquement",
applying_update: "Application de la mise a jour...",
update_to: "Mettre a jour vers",
exit: "Quitter",
session_window: "5h",
weekly_window: "7d",
@ -175,6 +211,15 @@ const GERMAN: Strings = Strings {
reset_position: "Position zurucksetzen",
language: "Sprache",
system_default: "Systemstandard",
check_for_updates: "Nach Updates suchen",
checking_for_updates: "Suche nach Updates...",
updates: "Updates",
update_in_progress: "Eine Update-Prufung lauft bereits.",
up_to_date: "Sie verwenden bereits die neueste Version.",
up_to_date_short: "Aktuell",
update_failed: "Automatisches Update war nicht moglich",
applying_update: "Update wird installiert...",
update_to: "Aktualisieren auf",
exit: "Beenden",
session_window: "5h",
weekly_window: "7d",
@ -198,6 +243,15 @@ const JAPANESE: Strings = Strings {
reset_position: "位置をリセット",
language: "言語",
system_default: "システム既定",
check_for_updates: "更新を確認",
checking_for_updates: "更新を確認しています...",
updates: "更新",
update_in_progress: "更新確認は既に実行中です。",
up_to_date: "既に最新バージョンです。",
up_to_date_short: "最新です",
update_failed: "自動更新を完了できませんでした",
applying_update: "更新を適用しています...",
update_to: "更新先",
exit: "終了",
session_window: "5h",
weekly_window: "7d",

View File

@ -5,8 +5,13 @@ mod models;
mod native_interop;
mod poller;
mod theme;
mod updater;
mod window;
fn main() {
let args: Vec<String> = std::env::args().collect();
if let Some(exit_code) = updater::handle_cli_mode(&args) {
std::process::exit(exit_code);
}
window::run();
}

383
src/updater.rs Normal file
View File

@ -0,0 +1,383 @@
use std::fs::File;
use std::io::{self, Write};
use std::os::windows::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use serde::Deserialize;
use windows::core::PCWSTR;
use windows::Win32::Foundation::{HWND, WAIT_OBJECT_0, WAIT_TIMEOUT};
use windows::Win32::System::Threading::{OpenProcess, WaitForSingleObject, PROCESS_SYNCHRONIZE};
use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_ICONERROR, MB_OK};
const GITHUB_API_ACCEPT: &str = "application/vnd.github+json";
const GITHUB_API_VERSION: &str = "2022-11-28";
const RELEASE_ASSET_NAME: &str = "claude-code-usage-monitor.exe";
const HELPER_EXE_NAME: &str = "updater-helper.exe";
const DOWNLOAD_EXE_NAME: &str = "update-download.exe";
const CREATE_NO_WINDOW: u32 = 0x08000000;
#[derive(Clone, Debug)]
pub struct ReleaseDescriptor {
pub latest_version: String,
asset_url: String,
}
#[derive(Debug)]
pub enum UpdateCheckResult {
UpToDate,
Available(ReleaseDescriptor),
}
#[derive(Deserialize)]
struct GitHubRelease {
tag_name: String,
assets: Vec<GitHubAsset>,
}
#[derive(Deserialize)]
struct GitHubAsset {
name: String,
browser_download_url: String,
}
pub fn handle_cli_mode(args: &[String]) -> Option<i32> {
if args.len() == 5 && args[1] == "--apply-update" {
let target = PathBuf::from(&args[2]);
let source = PathBuf::from(&args[3]);
let pid = args[4].parse::<u32>().unwrap_or(0);
return Some(match apply_update(target, source, pid) {
Ok(()) => 0,
Err(error) => {
show_error_message("Update failed", &error);
1
}
});
}
None
}
pub fn check_for_updates() -> Result<UpdateCheckResult, String> {
match fetch_latest_release()? {
Some(release) => Ok(UpdateCheckResult::Available(release)),
None => Ok(UpdateCheckResult::UpToDate),
}
}
pub fn begin_self_update(release: &ReleaseDescriptor) -> Result<(), String> {
let current_exe =
std::env::current_exe().map_err(|e| format!("Unable to locate current executable: {e}"))?;
ensure_target_location_writable(&current_exe)?;
let stage_dir = updates_dir()?;
std::fs::create_dir_all(&stage_dir)
.map_err(|e| format!("Unable to create updater working directory: {e}"))?;
let helper_path = stage_dir.join(HELPER_EXE_NAME);
let download_path = stage_dir.join(DOWNLOAD_EXE_NAME);
let partial_download_path = stage_dir.join(format!("{DOWNLOAD_EXE_NAME}.part"));
if helper_path.exists() {
let _ = std::fs::remove_file(&helper_path);
}
if download_path.exists() {
let _ = std::fs::remove_file(&download_path);
}
if partial_download_path.exists() {
let _ = std::fs::remove_file(&partial_download_path);
}
download_release_asset(&release.asset_url, &partial_download_path, &download_path)?;
std::fs::copy(&current_exe, &helper_path)
.map_err(|e| format!("Unable to prepare updater helper: {e}"))?;
let pid = std::process::id().to_string();
let target = current_exe.to_string_lossy().to_string();
let source = download_path.to_string_lossy().to_string();
Command::new(&helper_path)
.arg("--apply-update")
.arg(target)
.arg(source)
.arg(pid)
.creation_flags(CREATE_NO_WINDOW)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| format!("Unable to launch updater helper: {e}"))?;
Ok(())
}
fn apply_update(target: PathBuf, source: PathBuf, pid: u32) -> Result<(), String> {
if !source.exists() {
return Err(format!(
"Downloaded update not found at {}",
source.display()
));
}
let _ = wait_for_process_exit(pid, Duration::from_secs(30));
replace_target_binary(&target, &source)?;
relaunch_target(&target)?;
let _ = std::fs::remove_file(&source);
Ok(())
}
fn fetch_latest_release() -> Result<Option<ReleaseDescriptor>, String> {
let (owner, repo) = github_repo()?;
let url = format!("https://api.github.com/repos/{owner}/{repo}/releases/latest");
let agent = build_agent()?;
let response = agent
.get(&url)
.set("Accept", GITHUB_API_ACCEPT)
.set("User-Agent", user_agent())
.set("X-GitHub-Api-Version", GITHUB_API_VERSION)
.call()
.map_err(|e| format!("Unable to check GitHub releases: {e}"))?;
let release: GitHubRelease = response
.into_json()
.map_err(|e| format!("Unable to parse GitHub release data: {e}"))?;
let latest_version = release.tag_name.trim_start_matches('v').to_string();
if !is_version_newer(&latest_version, env!("CARGO_PKG_VERSION")) {
return Ok(None);
}
let asset = release
.assets
.iter()
.find(|asset| asset.name.eq_ignore_ascii_case(RELEASE_ASSET_NAME))
.or_else(|| {
release
.assets
.iter()
.find(|asset| asset.name.to_ascii_lowercase().ends_with(".exe"))
})
.ok_or_else(|| {
"No Windows executable asset was found in the latest release.".to_string()
})?;
Ok(Some(ReleaseDescriptor {
latest_version,
asset_url: asset.browser_download_url.clone(),
}))
}
fn build_agent() -> Result<ureq::Agent, String> {
let tls = native_tls::TlsConnector::new()
.map_err(|e| format!("Unable to initialize TLS support for update checks: {e}"))?;
Ok(ureq::AgentBuilder::new()
.timeout(Duration::from_secs(30))
.tls_connector(std::sync::Arc::new(tls))
.build())
}
fn download_release_asset(url: &str, partial_path: &Path, final_path: &Path) -> Result<(), String> {
let agent = build_agent()?;
let response = agent
.get(url)
.set("User-Agent", user_agent())
.call()
.map_err(|e| format!("Unable to download the latest release: {e}"))?;
let mut reader = response.into_reader();
let mut file = File::create(partial_path)
.map_err(|e| format!("Unable to create temporary download file: {e}"))?;
io::copy(&mut reader, &mut file)
.map_err(|e| format!("Unable to write the downloaded update: {e}"))?;
file.flush()
.map_err(|e| format!("Unable to finalize the downloaded update: {e}"))?;
std::fs::rename(partial_path, final_path)
.map_err(|e| format!("Unable to finalize the downloaded update file: {e}"))?;
Ok(())
}
fn replace_target_binary(target: &Path, source: &Path) -> Result<(), String> {
let backup_path = backup_path_for(target);
let mut last_error = None;
for _ in 0..60 {
let _ = std::fs::remove_file(&backup_path);
let renamed_existing = match std::fs::rename(target, &backup_path) {
Ok(()) => true,
Err(error) if error.kind() == io::ErrorKind::NotFound => false,
Err(error) => {
last_error = Some(error);
std::thread::sleep(Duration::from_millis(500));
continue;
}
};
match std::fs::copy(source, target) {
Ok(_) => {
let _ = std::fs::remove_file(&backup_path);
return Ok(());
}
Err(error) => {
last_error = Some(error);
let _ = std::fs::remove_file(target);
if renamed_existing {
let _ = std::fs::rename(&backup_path, target);
}
}
}
std::thread::sleep(Duration::from_millis(500));
}
Err(format!(
"Unable to replace {}. {}",
target.display(),
last_error
.map(|error| error.to_string())
.unwrap_or_else(|| {
"The file may still be locked or the install directory may not be writable."
.to_string()
})
))
}
fn relaunch_target(target: &Path) -> Result<(), String> {
let mut command = Command::new(target);
if let Some(parent) = target.parent() {
command.current_dir(parent);
}
command
.creation_flags(CREATE_NO_WINDOW)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| {
format!(
"The update was installed, but the app could not be restarted automatically: {e}"
)
})?;
Ok(())
}
fn wait_for_process_exit(pid: u32, timeout: Duration) -> Result<(), String> {
if pid == 0 {
return Ok(());
}
unsafe {
let handle = OpenProcess(PROCESS_SYNCHRONIZE, false, pid)
.map_err(|e| format!("Unable to monitor the running app process: {e}"))?;
let result = WaitForSingleObject(handle, timeout.as_millis().min(u32::MAX as u128) as u32);
let _ = windows::Win32::Foundation::CloseHandle(handle);
if result == WAIT_OBJECT_0 {
Ok(())
} else if result == WAIT_TIMEOUT {
Err("Timed out waiting for the running app to exit.".to_string())
} else {
Err("Unable to confirm that the running app has exited.".to_string())
}
}
}
fn updates_dir() -> Result<PathBuf, String> {
dirs::data_local_dir()
.map(|dir| dir.join("ClaudeCodeUsageMonitor").join("updates"))
.or_else(|| {
Some(
std::env::temp_dir()
.join("ClaudeCodeUsageMonitor")
.join("updates"),
)
})
.ok_or_else(|| "Unable to resolve a writable local updates directory.".to_string())
}
fn backup_path_for(target: &Path) -> PathBuf {
let file_name = target
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("app.exe");
target.with_file_name(format!("{file_name}.old"))
}
fn ensure_target_location_writable(target: &Path) -> Result<(), String> {
let parent = target.parent().ok_or_else(|| {
"Unable to determine the install directory for the current executable.".to_string()
})?;
let probe_path = parent.join(".__ccum_update_probe");
match File::create(&probe_path) {
Ok(_) => {
let _ = std::fs::remove_file(&probe_path);
Ok(())
}
Err(error) => Err(format!(
"The current install location is not writable. Move the app to a user-writable folder or install it somewhere outside Program Files. {error}"
)),
}
}
fn github_repo() -> Result<(&'static str, &'static str), String> {
let repository = env!("CARGO_PKG_REPOSITORY").trim_end_matches('/');
let parts: Vec<&str> = repository.split('/').collect();
if parts.len() < 2 {
return Err("Package repository URL is not configured for GitHub releases.".to_string());
}
let owner = parts[parts.len() - 2];
let repo = parts[parts.len() - 1];
if owner.is_empty() || repo.is_empty() {
return Err("Package repository URL is not configured for GitHub releases.".to_string());
}
Ok((owner, repo))
}
fn user_agent() -> &'static str {
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))
}
fn is_version_newer(candidate: &str, current: &str) -> bool {
parse_version(candidate) > parse_version(current)
}
fn parse_version(version: &str) -> (u32, u32, u32) {
let core = version.split('-').next().unwrap_or(version);
let mut parts = core.split('.').map(|part| part.parse::<u32>().unwrap_or(0));
(
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
)
}
fn show_error_message(title: &str, message: &str) {
unsafe {
let title_wide = wide_str(title);
let message_wide = wide_str(message);
let _ = MessageBoxW(
HWND::default(),
PCWSTR::from_raw(message_wide.as_ptr()),
PCWSTR::from_raw(title_wide.as_ptr()),
MB_OK | MB_ICONERROR,
);
}
}
fn wide_str(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}

View File

@ -22,6 +22,7 @@ use crate::native_interop::{
};
use crate::poller;
use crate::theme;
use crate::updater::{self, ReleaseDescriptor, UpdateCheckResult};
/// Wrapper to make HWND sendable across threads (safe for PostMessage usage)
#[derive(Clone, Copy)]
@ -59,6 +60,7 @@ struct AppState {
poll_interval_ms: u32,
retry_count: u32,
last_poll_ok: bool,
update_status: UpdateStatus,
tray_offset: i32,
dragging: bool,
@ -66,6 +68,15 @@ struct AppState {
drag_start_offset: i32,
}
#[derive(Clone, Debug)]
enum UpdateStatus {
Idle,
Checking,
Applying,
UpToDate,
Available(ReleaseDescriptor),
}
const RETRY_BASE_MS: u32 = 30_000; // 30 seconds
const POLL_1_MIN: u32 = 60_000;
@ -80,6 +91,7 @@ const IDM_FREQ_15MIN: u16 = 12;
const IDM_FREQ_1HOUR: u16 = 13;
const IDM_START_WITH_WINDOWS: u16 = 20;
const IDM_RESET_POSITION: u16 = 30;
const IDM_VERSION_ACTION: u16 = 31;
const IDM_LANG_SYSTEM: u16 = 40;
const IDM_LANG_ENGLISH: u16 = 41;
const IDM_LANG_SPANISH: u16 = 42;
@ -90,6 +102,7 @@ const IDM_LANG_JAPANESE: u16 = 45;
const DIVIDER_HIT_ZONE: i32 = 13; // LEFT_DIVIDER_W + DIVIDER_RIGHT_MARGIN
const WM_DPICHANGED_MSG: u32 = 0x02E0;
const WM_APP_UPDATE_CHECK_COMPLETE: u32 = WM_APP + 2;
/// Current system DPI (96 = 100% scaling, 144 = 150%, 192 = 200%, etc.)
static CURRENT_DPI: AtomicU32 = AtomicU32::new(96);
@ -213,6 +226,32 @@ fn set_window_title(hwnd: HWND, strings: Strings) {
}
}
fn show_info_message(hwnd: HWND, title: &str, message: &str) {
unsafe {
let title_wide = native_interop::wide_str(title);
let message_wide = native_interop::wide_str(message);
let _ = MessageBoxW(
hwnd,
PCWSTR::from_raw(message_wide.as_ptr()),
PCWSTR::from_raw(title_wide.as_ptr()),
MB_OK | MB_ICONINFORMATION,
);
}
}
fn show_error_message(hwnd: HWND, title: &str, message: &str) {
unsafe {
let title_wide = native_interop::wide_str(title);
let message_wide = native_interop::wide_str(message);
let _ = MessageBoxW(
hwnd,
PCWSTR::from_raw(message_wide.as_ptr()),
PCWSTR::from_raw(title_wide.as_ptr()),
MB_OK | MB_ICONERROR,
);
}
}
fn apply_language_to_state(state: &mut AppState, language_override: Option<LanguageId>) {
state.language_override = language_override;
state.language = localization::resolve_language(language_override);
@ -239,6 +278,136 @@ fn update_language_change() -> bool {
true
}
fn version_action_label(strings: Strings, status: &UpdateStatus) -> String {
let current = env!("CARGO_PKG_VERSION");
match status {
UpdateStatus::Idle => format!("v{current} - {}", strings.check_for_updates),
UpdateStatus::Checking => format!("v{current} - {}", strings.checking_for_updates),
UpdateStatus::Applying => format!("v{current} - {}", strings.applying_update),
UpdateStatus::UpToDate => format!("v{current} - {}", strings.up_to_date_short),
UpdateStatus::Available(release) => {
format!(
"v{current} - {} v{}",
strings.update_to, release.latest_version
)
}
}
}
fn begin_update_check(hwnd: HWND) {
let send_hwnd = SendHwnd::from_hwnd(hwnd);
let strings = {
let mut state = lock_state();
let Some(app_state) = state.as_mut() else {
return;
};
if matches!(
app_state.update_status,
UpdateStatus::Checking | UpdateStatus::Applying
) {
show_info_message(
hwnd,
app_state.language.strings().updates,
app_state.language.strings().update_in_progress,
);
return;
}
app_state.update_status = UpdateStatus::Applying;
app_state.language.strings()
};
std::thread::spawn(move || {
let hwnd = send_hwnd.to_hwnd();
match updater::check_for_updates() {
Ok(UpdateCheckResult::UpToDate) => {
{
let mut state = lock_state();
if let Some(s) = state.as_mut() {
s.update_status = UpdateStatus::UpToDate;
}
}
show_info_message(hwnd, strings.updates, strings.up_to_date);
unsafe {
let _ = PostMessageW(hwnd, WM_APP_UPDATE_CHECK_COMPLETE, WPARAM(0), LPARAM(0));
}
}
Ok(UpdateCheckResult::Available(release)) => {
{
let mut state = lock_state();
if let Some(s) = state.as_mut() {
s.update_status = UpdateStatus::Available(release);
}
}
unsafe {
let _ = PostMessageW(hwnd, WM_APP_UPDATE_CHECK_COMPLETE, WPARAM(0), LPARAM(0));
}
}
Err(error) => {
{
let mut state = lock_state();
if let Some(s) = state.as_mut() {
s.update_status = UpdateStatus::Idle;
}
}
let message = format!("{}.\n\n{}", strings.update_failed, error);
show_error_message(hwnd, strings.updates, &message);
unsafe {
let _ = PostMessageW(hwnd, WM_APP_UPDATE_CHECK_COMPLETE, WPARAM(0), LPARAM(0));
}
}
}
});
}
fn begin_update_apply(hwnd: HWND, release: ReleaseDescriptor) {
let send_hwnd = SendHwnd::from_hwnd(hwnd);
let strings = {
let mut state = lock_state();
let Some(app_state) = state.as_mut() else {
return;
};
if matches!(
app_state.update_status,
UpdateStatus::Checking | UpdateStatus::Applying
) {
show_info_message(
hwnd,
app_state.language.strings().updates,
app_state.language.strings().update_in_progress,
);
return;
}
app_state.update_status = UpdateStatus::Checking;
app_state.language.strings()
};
std::thread::spawn(move || {
let hwnd = send_hwnd.to_hwnd();
match updater::begin_self_update(&release) {
Ok(()) => unsafe {
let _ = PostMessageW(hwnd, WM_CLOSE, WPARAM(0), LPARAM(0));
},
Err(error) => {
{
let mut state = lock_state();
if let Some(s) = state.as_mut() {
s.update_status = UpdateStatus::Available(release);
}
}
let message = format!("{}.\n\n{}", strings.update_failed, error);
show_error_message(hwnd, strings.updates, &message);
unsafe {
let _ = PostMessageW(hwnd, WM_APP_UPDATE_CHECK_COMPLETE, WPARAM(0), LPARAM(0));
}
}
}
});
}
const STARTUP_REGISTRY_PATH: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
const STARTUP_REGISTRY_KEY: &str = "ClaudeCodeUsageMonitor";
@ -465,6 +634,7 @@ pub fn run() {
poll_interval_ms: settings.poll_interval_ms,
retry_count: 0,
last_poll_ok: false,
update_status: UpdateStatus::Idle,
tray_offset: settings.tray_offset,
dragging: false,
drag_start_mouse_x: 0,
@ -1115,6 +1285,7 @@ unsafe extern "system" fn wnd_proc(
schedule_countdown_timer();
LRESULT(0)
}
WM_APP_UPDATE_CHECK_COMPLETE => LRESULT(0),
WM_SETCURSOR => {
let is_dragging = {
let state = lock_state();
@ -1289,6 +1460,21 @@ unsafe extern "system" fn wnd_proc(
do_poll(sh);
});
}
IDM_VERSION_ACTION => {
let release = {
let state = lock_state();
match state.as_ref().map(|s| &s.update_status) {
Some(UpdateStatus::Available(release)) => Some(release.clone()),
_ => None,
}
};
if let Some(release) = release {
begin_update_apply(hwnd, release);
} else {
begin_update_check(hwnd);
}
}
2 => {
let hook = {
let state = lock_state();
@ -1371,15 +1557,21 @@ unsafe extern "system" fn wnd_proc(
fn show_context_menu(hwnd: HWND) {
unsafe {
let (current_interval, strings, language_override) = {
let (current_interval, strings, language_override, update_status) = {
let state = lock_state();
match state.as_ref() {
Some(s) => (
s.poll_interval_ms,
s.language.strings(),
s.language_override,
s.update_status.clone(),
),
None => (
POLL_15_MIN,
LanguageId::English.strings(),
None,
UpdateStatus::Idle,
),
None => (POLL_15_MIN, LanguageId::English.strings(), None),
}
};
@ -1494,11 +1686,20 @@ fn show_context_menu(hwnd: HWND) {
let _ = AppendMenuW(settings_menu, MF_SEPARATOR, 0, PCWSTR::null());
let version_str = native_interop::wide_str(&format!("v{}", env!("CARGO_PKG_VERSION")));
let version_label = version_action_label(strings, &update_status);
let version_str = native_interop::wide_str(&version_label);
let version_flags = if matches!(
update_status,
UpdateStatus::Checking | UpdateStatus::Applying
) {
MF_GRAYED
} else {
MENU_ITEM_FLAGS(0)
};
let _ = AppendMenuW(
settings_menu,
MF_GRAYED,
0,
version_flags,
IDM_VERSION_ACTION as usize,
PCWSTR::from_raw(version_str.as_ptr()),
);