mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-06-06 14:12:14 +00:00
feat(menu): add Restart action between separator and Exit
One-click relaunch of the running binary via a detached cmd.exe handoff: timeout /t 1 /nobreak >/dev/null & start "" "<exe>" — the 1s wait outlives the parent so the relaunched instance acquires Global\ClaudeCodeUsageBubble without ERROR_ALREADY_EXISTS. - New IDM_RESTART (33) wired into show_context_menu + on_menu_command. - Match arm placed above the IDM_LANG_BASE guard so future ids in the static band can't be swallowed by the dynamic-language catch-all. - Settings flushed defensively before quit (clone-then-save to avoid blocking the UI thread on disk I/O while holding lock_state). - Rejects current_exe paths containing '%' (same defense as update::install — cmd.exe expands %var% inside quotes). - New 'restart' string in LocaleStrings + translation in all 8 locales.
This commit is contained in:
+65
-1
@@ -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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1034,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();
|
||||
@@ -1361,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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -29,6 +29,7 @@ auto_check_hourly = "1時間ごと"
|
||||
auto_check_daily = "毎日"
|
||||
auto_check_weekly = "毎週"
|
||||
exit = "終了"
|
||||
restart = "再起動"
|
||||
show_widget = "ウィジェットを表示"
|
||||
session_window = "5時間"
|
||||
weekly_window = "7日"
|
||||
|
||||
@@ -29,6 +29,7 @@ auto_check_hourly = "매시간"
|
||||
auto_check_daily = "매일"
|
||||
auto_check_weekly = "매주"
|
||||
exit = "종료"
|
||||
restart = "다시 시작"
|
||||
show_widget = "위젯 표시"
|
||||
session_window = "5시간"
|
||||
weekly_window = "7일"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -29,6 +29,7 @@ auto_check_hourly = "每小時"
|
||||
auto_check_daily = "每天"
|
||||
auto_check_weekly = "每週"
|
||||
exit = "結束"
|
||||
restart = "重新啟動"
|
||||
show_widget = "顯示小工具"
|
||||
session_window = "5 小時"
|
||||
weekly_window = "7 日"
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user