mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-06-06 08:11:45 +00:00
feat: configurable auto-update check frequency
Adds a "Settings > Auto-update check" submenu with Disabled / Hourly / Daily / Weekly. Hourly is the default; existing settings files pick it up automatically via serde default. Manual "Check for updates" is unchanged and still fires when auto is disabled. The 24-hour hardcoded interval is replaced by reading Settings.update_check_interval_secs in both the startup scheduler and the post-check rearm path. None means auto is disabled and no timer is armed. Adds five new i18n keys across all eight locales.
This commit is contained in:
@@ -83,8 +83,9 @@ corner of your primary monitor on first launch. Drag it where you want it,
|
||||
release to snap to the nearest edge if you let go close to one.
|
||||
|
||||
- **Left-click** the bubble to open the expanded panel (5h + 7d + countdowns)
|
||||
- **Right-click** for refresh, models, update frequency, language, "Start
|
||||
with Windows", updates, exit
|
||||
- **Right-click** for refresh, models, refresh frequency, language, "Start
|
||||
with Windows", auto-update check (Disabled / Hourly / Daily / Weekly),
|
||||
manual "Check for updates", exit
|
||||
- **Drag** anywhere — it floats on top of all other windows
|
||||
- **Ctrl + MouseWheel** on the bubble to resize it
|
||||
- **Tray icon** (if enabled): left-click toggles the bubble visibility,
|
||||
|
||||
+93
-16
@@ -45,7 +45,8 @@ const STARTUP_REGISTRY_PATH: &str = r"Software\Microsoft\Windows\CurrentVersion\
|
||||
const STARTUP_VALUE_NAME: &str = "ClaudeCodeUsageBubble";
|
||||
const APP_CLASS_NAME: &str = "ClaudeCodeUsageBubbleApp";
|
||||
const HTTP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
||||
const UPDATE_CHECK_INTERVAL_SECS: u64 = 24 * 60 * 60;
|
||||
// The auto-check interval is user-configurable (see
|
||||
// `Settings::update_check_interval_secs`). `None` means auto-check is disabled.
|
||||
const BALLOON_COOLDOWN: Duration = Duration::from_secs(30 * 60);
|
||||
const REFRESH_TIMEOUT: Duration = Duration::from_secs(8);
|
||||
|
||||
@@ -63,7 +64,16 @@ const IDM_START_WITH_WINDOWS: u16 = 30;
|
||||
const IDM_RESET_POSITION: u16 = 31;
|
||||
const IDM_VERSION_ACTION: u16 = 32;
|
||||
const IDM_LANG_SYSTEM: u16 = 40;
|
||||
const IDM_LANG_BASE: u16 = 41;
|
||||
// 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).
|
||||
const IDM_UPDATE_AUTO_OFF: u16 = 60;
|
||||
const IDM_UPDATE_AUTO_HOURLY: u16 = 61;
|
||||
const IDM_UPDATE_AUTO_DAILY: u16 = 62;
|
||||
const IDM_UPDATE_AUTO_WEEKLY: u16 = 63;
|
||||
// IMPORTANT: language ids are dynamic and start at IDM_LANG_BASE.
|
||||
// Keep IDM_LANG_BASE the highest static id so the catch-all match arm
|
||||
// stays unambiguous.
|
||||
const IDM_LANG_BASE: u16 = 100;
|
||||
|
||||
// ---------- State ----------
|
||||
|
||||
@@ -349,6 +359,16 @@ pub fn on_menu_command(id: u32, _owner_hwnd: HWND) {
|
||||
IDM_START_WITH_WINDOWS => toggle_startup(),
|
||||
IDM_RESET_POSITION => reset_positions(),
|
||||
IDM_VERSION_ACTION => version_action(),
|
||||
IDM_UPDATE_AUTO_OFF => set_update_check_interval(None),
|
||||
IDM_UPDATE_AUTO_HOURLY => {
|
||||
set_update_check_interval(Some(settings::UPDATE_CHECK_HOURLY_SECS))
|
||||
}
|
||||
IDM_UPDATE_AUTO_DAILY => {
|
||||
set_update_check_interval(Some(settings::UPDATE_CHECK_DAILY_SECS))
|
||||
}
|
||||
IDM_UPDATE_AUTO_WEEKLY => {
|
||||
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),
|
||||
tray::IDM_TOGGLE_WIDGET => toggle_widget_visibility(),
|
||||
@@ -730,6 +750,7 @@ struct ContextMenuSnapshot {
|
||||
available: Vec<(String, String)>,
|
||||
language_override: Option<String>,
|
||||
current_interval: u32,
|
||||
update_check_interval_secs: Option<u64>,
|
||||
show_claude: bool,
|
||||
show_chatgpt: bool,
|
||||
widget_visible: bool,
|
||||
@@ -748,6 +769,7 @@ fn show_context_menu(owner_hwnd: HWND) {
|
||||
.collect(),
|
||||
language_override: s.settings.language.clone(),
|
||||
current_interval: s.settings.poll_interval_ms,
|
||||
update_check_interval_secs: s.settings.update_check_interval_secs,
|
||||
show_claude: s.settings.show_claude_code,
|
||||
show_chatgpt: s.settings.show_codex,
|
||||
widget_visible: s.settings.widget_visible,
|
||||
@@ -844,6 +866,34 @@ fn show_context_menu(owner_hwnd: HWND) {
|
||||
MENU_ITEM_FLAGS(0)
|
||||
};
|
||||
append_item(settings_menu, IDM_VERSION_ACTION, &version_label, version_flags);
|
||||
|
||||
let auto_update = CreatePopupMenu().unwrap();
|
||||
for (id, value, label) in [
|
||||
(IDM_UPDATE_AUTO_OFF, None, &snap.strings.auto_check_disabled),
|
||||
(
|
||||
IDM_UPDATE_AUTO_HOURLY,
|
||||
Some(settings::UPDATE_CHECK_HOURLY_SECS),
|
||||
&snap.strings.auto_check_hourly,
|
||||
),
|
||||
(
|
||||
IDM_UPDATE_AUTO_DAILY,
|
||||
Some(settings::UPDATE_CHECK_DAILY_SECS),
|
||||
&snap.strings.auto_check_daily,
|
||||
),
|
||||
(
|
||||
IDM_UPDATE_AUTO_WEEKLY,
|
||||
Some(settings::UPDATE_CHECK_WEEKLY_SECS),
|
||||
&snap.strings.auto_check_weekly,
|
||||
),
|
||||
] {
|
||||
let flags = if value == snap.update_check_interval_secs {
|
||||
MF_CHECKED
|
||||
} else {
|
||||
MENU_ITEM_FLAGS(0)
|
||||
};
|
||||
append_item(auto_update, id, label, flags);
|
||||
}
|
||||
append_submenu(settings_menu, auto_update, &snap.strings.auto_update_check);
|
||||
append_submenu(menu, settings_menu, &snap.strings.settings);
|
||||
|
||||
append_item(
|
||||
@@ -1067,18 +1117,27 @@ fn version_action() {
|
||||
}
|
||||
|
||||
fn schedule_update_check_timer(hwnd: HWND) {
|
||||
let last = lock_state()
|
||||
.as_ref()
|
||||
.and_then(|s| s.settings.last_update_check_unix);
|
||||
let (last, interval) = match lock_state().as_ref() {
|
||||
Some(s) => (
|
||||
s.settings.last_update_check_unix,
|
||||
s.settings.update_check_interval_secs,
|
||||
),
|
||||
None => return,
|
||||
};
|
||||
let Some(interval) = interval else {
|
||||
// Auto-check disabled. Manual "Check for updates" still works via
|
||||
// the menu; this just suppresses the timer.
|
||||
return;
|
||||
};
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let due = last.map_or(true, |t| now.saturating_sub(t) >= UPDATE_CHECK_INTERVAL_SECS);
|
||||
let due = last.map_or(true, |t| now.saturating_sub(t) >= interval);
|
||||
if due {
|
||||
begin_update_check(hwnd);
|
||||
} else {
|
||||
let remaining = UPDATE_CHECK_INTERVAL_SECS.saturating_sub(now.saturating_sub(last.unwrap_or(0)));
|
||||
let remaining = interval.saturating_sub(now.saturating_sub(last.unwrap_or(0)));
|
||||
let ms = (remaining.saturating_mul(1000).min(u32::MAX as u64)) as u32;
|
||||
unsafe {
|
||||
SetTimer(hwnd, TIMER_UPDATE_CHECK, ms, None);
|
||||
@@ -1117,23 +1176,41 @@ fn begin_update_check(hwnd: HWND) {
|
||||
s.update_status = UpdateStatus::Failed;
|
||||
}
|
||||
}
|
||||
s.settings.clone()
|
||||
(s.settings.clone(), s.settings.update_check_interval_secs)
|
||||
})
|
||||
};
|
||||
if let Some(snap) = snap_opt {
|
||||
let next_interval = snap_opt.as_ref().and_then(|(_, iv)| *iv);
|
||||
if let Some((snap, _)) = snap_opt {
|
||||
settings::save(&snap);
|
||||
}
|
||||
unsafe {
|
||||
SetTimer(
|
||||
send_hwnd.to_hwnd(),
|
||||
TIMER_UPDATE_CHECK,
|
||||
(UPDATE_CHECK_INTERVAL_SECS as u32) * 1000,
|
||||
None,
|
||||
);
|
||||
if let Some(interval) = next_interval {
|
||||
let ms = (interval.saturating_mul(1000).min(u32::MAX as u64)) as u32;
|
||||
unsafe {
|
||||
SetTimer(send_hwnd.to_hwnd(), TIMER_UPDATE_CHECK, ms, None);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn set_update_check_interval(value: Option<u64>) {
|
||||
let (snap, msg_hwnd) = {
|
||||
let mut s = lock_state();
|
||||
let Some(s) = s.as_mut() else {
|
||||
return;
|
||||
};
|
||||
s.settings.update_check_interval_secs = value;
|
||||
(s.settings.clone(), s.msg_hwnd)
|
||||
};
|
||||
settings::save(&snap);
|
||||
let hwnd = msg_hwnd.to_hwnd();
|
||||
unsafe {
|
||||
let _ = KillTimer(hwnd, TIMER_UPDATE_CHECK);
|
||||
}
|
||||
if value.is_some() {
|
||||
schedule_update_check_timer(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Start-with-Windows ----------
|
||||
|
||||
fn is_startup_enabled() -> bool {
|
||||
|
||||
@@ -23,6 +23,11 @@ update_failed = "Update fehlgeschlagen"
|
||||
applying_update = "Update wird angewendet…"
|
||||
update_available = "Update verfügbar"
|
||||
update_via_winget = "über WinGet"
|
||||
auto_update_check = "Automatische Updateprüfung"
|
||||
auto_check_disabled = "Deaktiviert"
|
||||
auto_check_hourly = "Stündlich"
|
||||
auto_check_daily = "Täglich"
|
||||
auto_check_weekly = "Wöchentlich"
|
||||
exit = "Beenden"
|
||||
show_widget = "Widget anzeigen"
|
||||
session_window = "5h"
|
||||
|
||||
@@ -23,6 +23,11 @@ update_failed = "Update failed"
|
||||
applying_update = "Applying update…"
|
||||
update_available = "Update available"
|
||||
update_via_winget = "via WinGet"
|
||||
auto_update_check = "Auto-update check"
|
||||
auto_check_disabled = "Disabled"
|
||||
auto_check_hourly = "Hourly"
|
||||
auto_check_daily = "Daily"
|
||||
auto_check_weekly = "Weekly"
|
||||
exit = "Exit"
|
||||
show_widget = "Show widget"
|
||||
session_window = "5h"
|
||||
|
||||
@@ -23,6 +23,11 @@ update_failed = "Actualización fallida"
|
||||
applying_update = "Aplicando actualización…"
|
||||
update_available = "Actualización disponible"
|
||||
update_via_winget = "vía WinGet"
|
||||
auto_update_check = "Búsqueda automática de actualizaciones"
|
||||
auto_check_disabled = "Desactivada"
|
||||
auto_check_hourly = "Cada hora"
|
||||
auto_check_daily = "Cada día"
|
||||
auto_check_weekly = "Cada semana"
|
||||
exit = "Salir"
|
||||
show_widget = "Mostrar widget"
|
||||
session_window = "5h"
|
||||
|
||||
@@ -23,6 +23,11 @@ update_failed = "Mise à jour échouée"
|
||||
applying_update = "Mise à jour en cours…"
|
||||
update_available = "Mise à jour disponible"
|
||||
update_via_winget = "via WinGet"
|
||||
auto_update_check = "Vérification automatique des mises à jour"
|
||||
auto_check_disabled = "Désactivée"
|
||||
auto_check_hourly = "Toutes les heures"
|
||||
auto_check_daily = "Quotidienne"
|
||||
auto_check_weekly = "Hebdomadaire"
|
||||
exit = "Quitter"
|
||||
show_widget = "Afficher le widget"
|
||||
session_window = "5h"
|
||||
|
||||
@@ -23,6 +23,11 @@ update_failed = "更新に失敗しました"
|
||||
applying_update = "更新を適用中…"
|
||||
update_available = "更新あり"
|
||||
update_via_winget = "WinGet経由"
|
||||
auto_update_check = "更新の自動確認"
|
||||
auto_check_disabled = "無効"
|
||||
auto_check_hourly = "1時間ごと"
|
||||
auto_check_daily = "毎日"
|
||||
auto_check_weekly = "毎週"
|
||||
exit = "終了"
|
||||
show_widget = "ウィジェットを表示"
|
||||
session_window = "5時間"
|
||||
|
||||
@@ -23,6 +23,11 @@ update_failed = "업데이트 실패"
|
||||
applying_update = "업데이트 적용 중…"
|
||||
update_available = "업데이트 있음"
|
||||
update_via_winget = "WinGet 사용"
|
||||
auto_update_check = "업데이트 자동 확인"
|
||||
auto_check_disabled = "사용 안 함"
|
||||
auto_check_hourly = "매시간"
|
||||
auto_check_daily = "매일"
|
||||
auto_check_weekly = "매주"
|
||||
exit = "종료"
|
||||
show_widget = "위젯 표시"
|
||||
session_window = "5시간"
|
||||
|
||||
@@ -23,6 +23,11 @@ update_failed = "Update mislukt"
|
||||
applying_update = "Update toepassen…"
|
||||
update_available = "Update beschikbaar"
|
||||
update_via_winget = "via WinGet"
|
||||
auto_update_check = "Automatische updatecontrole"
|
||||
auto_check_disabled = "Uitgeschakeld"
|
||||
auto_check_hourly = "Per uur"
|
||||
auto_check_daily = "Dagelijks"
|
||||
auto_check_weekly = "Wekelijks"
|
||||
exit = "Afsluiten"
|
||||
show_widget = "Widget tonen"
|
||||
session_window = "5u"
|
||||
|
||||
@@ -23,6 +23,11 @@ update_failed = "更新失敗"
|
||||
applying_update = "正在套用更新…"
|
||||
update_available = "有可用更新"
|
||||
update_via_winget = "透過 WinGet"
|
||||
auto_update_check = "自動檢查更新"
|
||||
auto_check_disabled = "停用"
|
||||
auto_check_hourly = "每小時"
|
||||
auto_check_daily = "每天"
|
||||
auto_check_weekly = "每週"
|
||||
exit = "結束"
|
||||
show_widget = "顯示小工具"
|
||||
session_window = "5 小時"
|
||||
|
||||
@@ -43,6 +43,11 @@ pub struct LocaleStrings {
|
||||
pub applying_update: String,
|
||||
pub update_available: String,
|
||||
pub update_via_winget: String,
|
||||
pub auto_update_check: String,
|
||||
pub auto_check_disabled: String,
|
||||
pub auto_check_hourly: String,
|
||||
pub auto_check_daily: String,
|
||||
pub auto_check_weekly: String,
|
||||
pub exit: String,
|
||||
pub show_widget: String,
|
||||
pub session_window: String,
|
||||
|
||||
@@ -14,6 +14,11 @@ pub const POLL_5_MIN: u32 = 5 * 60_000;
|
||||
pub const POLL_15_MIN: u32 = 15 * 60_000;
|
||||
pub const POLL_1_HOUR: u32 = 60 * 60_000;
|
||||
|
||||
// Update-check intervals (seconds). `None` means auto-check is disabled.
|
||||
pub const UPDATE_CHECK_HOURLY_SECS: u64 = 60 * 60;
|
||||
pub const UPDATE_CHECK_DAILY_SECS: u64 = 24 * 60 * 60;
|
||||
pub const UPDATE_CHECK_WEEKLY_SECS: u64 = 7 * 24 * 60 * 60;
|
||||
|
||||
fn default_show_claude() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -29,6 +34,9 @@ fn default_bubble_size() -> i32 {
|
||||
fn default_poll_interval_ms() -> u32 {
|
||||
POLL_5_MIN
|
||||
}
|
||||
fn default_update_check_interval_secs() -> Option<u64> {
|
||||
Some(UPDATE_CHECK_HOURLY_SECS)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct BubblePositions {
|
||||
@@ -77,6 +85,8 @@ pub struct Settings {
|
||||
pub language: Option<String>,
|
||||
#[serde(default)]
|
||||
pub last_update_check_unix: Option<u64>,
|
||||
#[serde(default = "default_update_check_interval_secs")]
|
||||
pub update_check_interval_secs: Option<u64>,
|
||||
#[serde(default = "default_widget_visible")]
|
||||
pub widget_visible: bool,
|
||||
}
|
||||
@@ -91,6 +101,7 @@ impl Default for Settings {
|
||||
poll_interval_ms: default_poll_interval_ms(),
|
||||
language: None,
|
||||
last_update_check_unix: None,
|
||||
update_check_interval_secs: default_update_check_interval_secs(),
|
||||
widget_visible: default_widget_visible(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user