merge: sync with upstream v1.3.4, resolve conflicts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
yure 2026-04-20 13:40:06 +08:00
commit cae0950301
13 changed files with 394 additions and 119 deletions

2
Cargo.lock generated
View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "claude-code-usage-monitor"
version = "1.3.1"
version = "1.3.4"
edition = "2021"
license = "MIT"
description = "Claude Code Usage Monitor"

View File

@ -34,6 +34,8 @@ pub(super) const STRINGS: Strings = Strings {
day_suffix: "d",
hour_suffix: "h",
minute_suffix: "m",
token_expired_title: "Claude Code Auth Error",
token_expired_body: "Run 'claude' in a terminal, then use '/login' and follow the prompts. After that, refresh or restart this app.",
second_suffix: "s",
quiet_hours: "Quiet Hours",
quiet_start: "Start",

View File

@ -34,6 +34,8 @@ pub(super) const STRINGS: Strings = Strings {
day_suffix: "j",
hour_suffix: "h",
minute_suffix: "m",
token_expired_title: "Erreur d'authentification",
token_expired_body: "Exécutez 'claude' dans un terminal, puis utilisez '/login' et suivez les instructions. Ensuite, actualisez ou redémarrez cette application.",
second_suffix: "s",
quiet_hours: "Quiet Hours",
quiet_start: "Start",

View File

@ -34,6 +34,8 @@ pub(super) const STRINGS: Strings = Strings {
day_suffix: "T",
hour_suffix: "h",
minute_suffix: "m",
token_expired_title: "Authentifizierungsfehler",
token_expired_body: "Führen Sie 'claude' in einem Terminal aus, verwenden Sie dann '/login' und folgen Sie den Anweisungen. Aktualisieren oder starten Sie diese App anschließend neu.",
second_suffix: "s",
quiet_hours: "Quiet Hours",
quiet_start: "Start",

View File

@ -34,6 +34,8 @@ pub(super) const STRINGS: Strings = Strings {
day_suffix: "日",
hour_suffix: "時間",
minute_suffix: "分",
token_expired_title: "認証エラー",
token_expired_body: "ターミナルで 'claude' を実行し、'/login' を使って案内に従ってください。その後、このアプリを更新するか再起動してください。",
second_suffix: "秒",
quiet_hours: "サイレント時間",
quiet_start: "開始",

View File

@ -34,6 +34,8 @@ pub(super) const STRINGS: Strings = Strings {
day_suffix: "일",
hour_suffix: "시간",
minute_suffix: "분",
token_expired_title: "인증 오류",
token_expired_body: "터미널에서 'claude'를 실행한 다음 '/login'을 사용하고 안내에 따라 진행하세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요.",
second_suffix: "초",
quiet_hours: "조용한 시간",
quiet_start: "시작",

View File

@ -147,6 +147,8 @@ pub struct Strings {
pub hour_suffix: &'static str,
pub minute_suffix: &'static str,
pub second_suffix: &'static str,
pub token_expired_title: &'static str,
pub token_expired_body: &'static str,
pub quiet_hours: &'static str,
pub quiet_start: &'static str,
pub quiet_end: &'static str,

View File

@ -34,6 +34,8 @@ pub(super) const STRINGS: Strings = Strings {
day_suffix: "d",
hour_suffix: "h",
minute_suffix: "m",
token_expired_title: "Error de autenticación",
token_expired_body: "Ejecuta 'claude' en una terminal, luego usa '/login' y sigue las indicaciones. Después, actualiza o reinicia esta aplicación.",
second_suffix: "s",
quiet_hours: "Quiet Hours",
quiet_start: "Start",

View File

@ -34,6 +34,8 @@ pub(super) const STRINGS: Strings = Strings {
day_suffix: "天",
hour_suffix: "時",
minute_suffix: "分",
token_expired_title: "驗證錯誤",
token_expired_body: "請在終端機中執行 'claude',然後使用 '/login' 並依照提示操作。完成後,請重新整理或重新啟動此應用程式。",
second_suffix: "秒",
quiet_hours: "安靜時刻",
quiet_start: "開始時間",

View File

@ -17,11 +17,20 @@ const MODEL_FALLBACK_CHAIN: &[&str] = &["claude-3-haiku-20240307", "claude-haiku
#[derive(Debug)]
pub enum PollError {
AuthRequired,
NoCredentials,
TokenExpired,
RequestFailed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CredentialWatchMode {
ActiveSource,
AllSources,
}
pub type CredentialWatchSnapshot = Vec<String>;
#[derive(Deserialize)]
struct UsageResponse {
five_hour: Option<UsageBucket>,
@ -234,23 +243,112 @@ fn build_agent() -> Result<ureq::Agent, PollError> {
.build())
}
pub fn credential_watch_snapshot(mode: CredentialWatchMode) -> CredentialWatchSnapshot {
let sources = match mode {
CredentialWatchMode::ActiveSource => read_credentials()
.map(|creds| vec![creds.source])
.unwrap_or_else(all_known_credential_sources),
CredentialWatchMode::AllSources => all_known_credential_sources(),
};
let mut snapshot: CredentialWatchSnapshot = sources
.into_iter()
.filter_map(|source| credential_watch_signature(&source))
.collect();
snapshot.sort();
snapshot.dedup();
snapshot
}
fn all_known_credential_sources() -> Vec<CredentialSource> {
let mut sources = Vec::new();
if let Some(source) = windows_credential_source() {
sources.push(source);
}
for distro in list_wsl_distros() {
sources.push(CredentialSource::Wsl { distro });
}
sources
}
fn windows_credential_source() -> Option<CredentialSource> {
let home = dirs::home_dir()?;
Some(CredentialSource::Windows(
home.join(".claude").join(".credentials.json"),
))
}
fn credential_watch_signature(source: &CredentialSource) -> Option<String> {
match source {
CredentialSource::Windows(path) => Some(windows_credential_watch_signature(path)),
CredentialSource::Wsl { distro } => wsl_credential_watch_signature(distro),
}
}
fn windows_credential_watch_signature(path: &PathBuf) -> String {
let key = format!("win:{}", path.display());
match std::fs::metadata(path) {
Ok(metadata) => {
let modified = metadata
.modified()
.ok()
.and_then(|value| value.duration_since(UNIX_EPOCH).ok())
.map(|value| value.as_secs())
.unwrap_or(0);
format!("{key}|present|{}|{modified}", metadata.len())
}
Err(_) => format!("{key}|missing"),
}
}
fn wsl_credential_watch_signature(distro: &str) -> Option<String> {
let output = run_with_timeout(
Command::new("wsl.exe")
.arg("-d")
.arg(distro)
.arg("--")
.arg("sh")
.arg("-lc")
.arg(
"if [ -f ~/.claude/.credentials.json ]; then \
stat -c 'present|%s|%Y' ~/.claude/.credentials.json; \
else echo missing; fi",
)
.creation_flags(CREATE_NO_WINDOW)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null()),
Duration::from_secs(5),
)?;
let state = if output.status.success() {
decode_wsl_text(&output.stdout).trim().to_string()
} else {
format!("status-{}", output.status)
};
Some(format!("wsl:{distro}|{state}"))
}
fn fetch_usage_with_fallback(token: &str) -> Result<UsageData, PollError> {
// Try the dedicated usage endpoint first
if let Some(data) = try_usage_endpoint(token) {
match try_usage_endpoint(token)? {
Some(data) => {
// If reset timers are missing, fill them in from the Messages API
if data.session.resets_at.is_none() || data.weekly.resets_at.is_none() {
if let Ok(fallback) = fetch_usage_via_messages(token) {
let mut merged = data;
if merged.session.resets_at.is_none() {
merged.session.resets_at = fallback.session.resets_at;
if data.session.resets_at.is_none() || data.weekly.resets_at.is_none() {
if let Ok(fallback) = fetch_usage_via_messages(token) {
let mut merged = data;
if merged.session.resets_at.is_none() {
merged.session.resets_at = fallback.session.resets_at;
}
if merged.weekly.resets_at.is_none() {
merged.weekly.resets_at = fallback.weekly.resets_at;
}
return Ok(merged);
}
if merged.weekly.resets_at.is_none() {
merged.weekly.resets_at = fallback.weekly.resets_at;
}
return Ok(merged);
}
return Ok(data);
}
return Ok(data);
None => {}
}
// Fall back to Messages API with rate limit headers
@ -261,8 +359,8 @@ fn fetch_usage_with_fallback(token: &str) -> Result<UsageData, PollError> {
result
}
fn try_usage_endpoint(token: &str) -> Option<UsageData> {
let agent = build_agent().ok()?;
fn try_usage_endpoint(token: &str) -> Result<Option<UsageData>, PollError> {
let agent = build_agent()?;
let resp = match agent
.get(USAGE_URL)
@ -271,10 +369,19 @@ fn try_usage_endpoint(token: &str) -> Option<UsageData> {
.call()
{
Ok(resp) => resp,
_ => return None,
Err(ureq::Error::Status(code, _)) if code == 401 || code == 403 => {
diagnose::log(format!(
"usage endpoint returned auth error status {code}; re-login required"
));
return Err(PollError::AuthRequired);
}
Err(_) => return Ok(None),
};
let response: UsageResponse = resp.into_json().ok()?;
let response: UsageResponse = match resp.into_json() {
Ok(response) => response,
Err(_) => return Ok(None),
};
let mut data = UsageData::default();
if let Some(bucket) = &response.five_hour {
@ -287,7 +394,7 @@ fn try_usage_endpoint(token: &str) -> Option<UsageData> {
data.weekly.resets_at = parse_iso8601(bucket.resets_at.as_deref());
}
Some(data)
Ok(Some(data))
}
fn fetch_usage_via_messages(token: &str) -> Result<UsageData, PollError> {
@ -308,6 +415,12 @@ fn fetch_usage_via_messages(token: &str) -> Result<UsageData, PollError> {
.send_json(&body)
{
Ok(resp) => resp,
Err(ureq::Error::Status(code, _)) if code == 401 || code == 403 => {
diagnose::log(format!(
"messages endpoint returned auth error status {code}; re-login required"
));
return Err(PollError::AuthRequired);
}
Err(ureq::Error::Status(_code, resp)) => resp,
Err(_) => continue,
};
@ -410,8 +523,9 @@ fn read_credentials() -> Option<Credentials> {
}
fn read_windows_credentials() -> Option<Credentials> {
let home = dirs::home_dir()?;
let cred_path = home.join(".claude").join(".credentials.json");
let CredentialSource::Windows(cred_path) = windows_credential_source()? else {
return None;
};
let content = match std::fs::read_to_string(&cred_path) {
Ok(content) => content,
Err(error) => {

View File

@ -2,8 +2,8 @@ use windows::Win32::Foundation::*;
use windows::Win32::Graphics::Gdi::*;
use windows::Win32::System::LibraryLoader::GetModuleFileNameW;
use windows::Win32::UI::Shell::{
ExtractIconExW, NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY,
NOTIFYICONDATAW, Shell_NotifyIconW,
ExtractIconExW, NIF_ICON, NIF_INFO, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY,
NIIF_WARNING, NOTIFYICONDATAW, Shell_NotifyIconW,
};
use windows::Win32::UI::WindowsAndMessaging::*;
use windows::core::PCWSTR;
@ -249,6 +249,35 @@ fn load_embedded_app_icon() -> HICON {
}
}
/// Show a Windows balloon notification from the tray icon.
/// Used to alert the user when re-authentication is required.
pub fn notify_balloon(hwnd: HWND, title: &str, message: &str) {
unsafe {
let mut nid: NOTIFYICONDATAW = std::mem::zeroed();
nid.cbSize = std::mem::size_of::<NOTIFYICONDATAW>() as u32;
nid.hWnd = hwnd;
nid.uID = TRAY_ICON_ID;
nid.uFlags = NIF_INFO;
nid.dwInfoFlags = NIIF_WARNING;
copy_wide(title, &mut nid.szInfoTitle);
copy_wide_256(message, &mut nid.szInfo);
let _ = Shell_NotifyIconW(NIM_MODIFY, &nid);
}
}
/// Copy a string into a fixed-size wide buffer (truncates to fit).
fn copy_wide<const N: usize>(s: &str, buf: &mut [u16; N]) {
let wide: Vec<u16> = s.encode_utf16().collect();
let len = wide.len().min(N - 1);
buf[..len].copy_from_slice(&wide[..len]);
buf[len] = 0;
}
/// Copy a string into a 256-wide buffer.
fn copy_wide_256(s: &str, buf: &mut [u16; 256]) {
copy_wide(s, buf)
}
/// Register the tray icon with the shell.
pub fn add(hwnd: HWND, percent: Option<f64>, tooltip: &str) {
let hicon = create_icon(percent);

View File

@ -65,6 +65,10 @@ struct AppState {
poll_interval_ms: u32,
retry_count: u32,
force_notify_auth_error: bool,
auth_error_paused_polling: bool,
auth_watch_mode: poller::CredentialWatchMode,
auth_watch_snapshot: poller::CredentialWatchSnapshot,
last_poll_ok: bool,
update_status: UpdateStatus,
last_update_check_unix: Option<u64>,
@ -1348,6 +1352,10 @@ pub fn run() {
data: None,
poll_interval_ms: settings.poll_interval_ms,
retry_count: 0,
force_notify_auth_error: false,
auth_error_paused_polling: false,
auth_watch_mode: poller::CredentialWatchMode::ActiveSource,
auth_watch_snapshot: Vec::new(),
last_poll_ok: false,
update_status: UpdateStatus::Idle,
last_update_check_unix: settings.last_update_check_unix,
@ -1766,30 +1774,87 @@ fn do_poll(send_hwnd: SendHwnd) {
SetTimer(hwnd, TIMER_POLL, interval, None);
}
}
s.force_notify_auth_error = false;
s.auth_error_paused_polling = false;
s.auth_watch_mode = poller::CredentialWatchMode::ActiveSource;
s.auth_watch_snapshot.clear();
}
unsafe {
let _ = PostMessageW(hwnd, WM_APP_USAGE_UPDATED, WPARAM(0), LPARAM(0));
}
}
Err(_e) => {
// Show refresh indicator — retry will recover silently
let mut state = lock_state();
if let Some(s) = state.as_mut() {
s.session_text = "...".to_string();
s.weekly_text = "...".to_string();
s.last_poll_ok = false;
Err(e) => {
let auth_watch = match e {
poller::PollError::AuthRequired | poller::PollError::TokenExpired => Some((
poller::CredentialWatchMode::ActiveSource,
poller::credential_watch_snapshot(poller::CredentialWatchMode::ActiveSource),
)),
poller::PollError::NoCredentials => Some((
poller::CredentialWatchMode::AllSources,
poller::credential_watch_snapshot(poller::CredentialWatchMode::AllSources),
)),
poller::PollError::RequestFailed => None,
};
// Distinguish auth-required errors from transient errors.
let notify_auth_error = {
let mut state = lock_state();
let mut should_notify = false;
if let Some(s) = state.as_mut() {
s.last_poll_ok = false;
match auth_watch {
Some((watch_mode, watch_snapshot)) => {
// Only show the balloon on the first failure so it doesn't spam.
if s.retry_count == 0 || s.force_notify_auth_error {
should_notify = true;
}
s.force_notify_auth_error = false;
s.auth_error_paused_polling = true;
s.auth_watch_mode = watch_mode;
s.auth_watch_snapshot = watch_snapshot;
s.session_text = "⚠".to_string();
s.weekly_text = "⚠".to_string();
s.retry_count = s.retry_count.saturating_add(1);
unsafe {
let _ = KillTimer(hwnd, TIMER_POLL);
let _ = KillTimer(hwnd, TIMER_RESET_POLL);
let _ = KillTimer(hwnd, TIMER_COUNTDOWN);
SetTimer(hwnd, TIMER_POLL, s.poll_interval_ms, None);
}
}
_ => {
// Transient network / credential-missing errors: exponential backoff.
s.force_notify_auth_error = false;
s.auth_error_paused_polling = false;
s.auth_watch_mode = poller::CredentialWatchMode::ActiveSource;
s.auth_watch_snapshot.clear();
s.session_text = "...".to_string();
s.weekly_text = "...".to_string();
s.retry_count = s.retry_count.saturating_add(1);
let backoff = RETRY_BASE_MS
.saturating_mul(1u32.checked_shl(s.retry_count - 1).unwrap_or(u32::MAX));
let retry_ms = backoff.min(s.poll_interval_ms);
unsafe {
let _ = KillTimer(hwnd, TIMER_RESET_POLL);
SetTimer(hwnd, TIMER_POLL, retry_ms, None);
}
}
}
}
should_notify
};
// Exponential backoff retry: 30s, 60s, 120s, ... up to poll_interval
s.retry_count = s.retry_count.saturating_add(1);
let backoff = RETRY_BASE_MS
.saturating_mul(1u32.checked_shl(s.retry_count - 1).unwrap_or(u32::MAX));
let retry_ms = backoff.min(s.poll_interval_ms);
unsafe {
// Kill the 5-second reset poll so it doesn't bypass backoff
let _ = KillTimer(hwnd, TIMER_RESET_POLL);
SetTimer(hwnd, TIMER_POLL, retry_ms, None);
if notify_auth_error {
let strings = {
let state = lock_state();
state.as_ref().map(|s| s.language.strings())
};
if let Some(strings) = strings {
tray_icon::notify_balloon(
hwnd,
strings.token_expired_title,
strings.token_expired_body,
);
}
}
@ -1807,13 +1872,20 @@ fn schedule_countdown_timer() {
None => return,
};
let hwnd = s.hwnd.to_hwnd();
if !s.last_poll_ok {
unsafe {
let _ = KillTimer(hwnd, TIMER_COUNTDOWN);
let _ = KillTimer(hwnd, TIMER_RESET_POLL);
}
return;
}
let data = match &s.data {
Some(d) => d,
None => return,
};
let hwnd = s.hwnd.to_hwnd();
// If a reset time has passed, poll every 5s to pick up fresh data
if poller::is_past_reset(data) {
unsafe {
@ -1884,20 +1956,20 @@ fn update_display() {
fn position_at_taskbar() {
refresh_dpi();
// Extract everything we need from state, then DROP the lock before making
// any Win32 calls. MoveWindow dispatches WM_PAINT synchronously and our
// wnd_proc also calls lock_state() — Rust's Mutex is not reentrant,
// so holding it across Win32 calls causes a deadlock.
// Drop the app-state lock before any Win32 call that may synchronously
// re-enter our window procedure.
let (hwnd, embedded, tray_offset, taskbar_hwnd) = {
let state = lock_state();
let s = match state.as_ref() {
Some(s) => s,
None => return,
};
// Don't fight the user's drag
if s.dragging {
return;
}
let taskbar_hwnd = match s.taskbar_hwnd {
Some(h) => h,
None => {
@ -1905,6 +1977,7 @@ fn position_at_taskbar() {
return;
}
};
(s.hwnd.to_hwnd(), s.embedded, s.tray_offset, taskbar_hwnd)
};
@ -2043,10 +2116,44 @@ unsafe extern "system" fn wnd_proc(
match timer_id {
TIMER_POLL => {
if !is_quiet_time() {
let sh = SendHwnd::from_hwnd(hwnd);
std::thread::spawn(move || {
do_poll(sh);
});
let auth_watch = {
let state = lock_state();
state
.as_ref()
.map(|s| {
(
s.auth_error_paused_polling,
s.auth_watch_mode,
s.auth_watch_snapshot.clone(),
)
})
};
match auth_watch {
Some((true, watch_mode, previous_snapshot)) => {
let current_snapshot = poller::credential_watch_snapshot(watch_mode);
if current_snapshot != previous_snapshot {
let mut state = lock_state();
if let Some(s) = state.as_mut() {
if s.auth_error_paused_polling && s.auth_watch_mode == watch_mode
{
s.auth_watch_snapshot = current_snapshot;
}
}
drop(state);
let sh = SendHwnd::from_hwnd(hwnd);
std::thread::spawn(move || {
do_poll(sh);
});
}
}
Some((false, _, _)) => {
let sh = SendHwnd::from_hwnd(hwnd);
std::thread::spawn(move || {
do_poll(sh);
});
}
None => {}
}
}
}
TIMER_COUNTDOWN => {
@ -2055,7 +2162,14 @@ unsafe extern "system" fn wnd_proc(
schedule_countdown_timer();
}
TIMER_RESET_POLL => {
if !is_quiet_time() {
let should_poll = {
let state = lock_state();
state
.as_ref()
.map(|s| !s.auth_error_paused_polling)
.unwrap_or(false)
};
if should_poll && !is_quiet_time() {
let sh = SendHwnd::from_hwnd(hwnd);
std::thread::spawn(move || {
do_poll(sh);
@ -2063,7 +2177,6 @@ unsafe extern "system" fn wnd_proc(
}
}
TIMER_QUIET_BOUNDARY => {
// 安靜時刻邊界到了,立即重繪並排程下一個邊界
render_layered();
schedule_quiet_boundary_timer(hwnd);
}
@ -2135,86 +2248,88 @@ unsafe extern "system" fn wnd_proc(
if is_dragging {
let mut pt = POINT::default();
let _ = GetCursorPos(&mut pt);
let move_target = {
let mut state = lock_state();
let s = match state.as_mut() {
Some(s) => s,
None => return LRESULT(0),
};
let mut state = lock_state();
let s = match state.as_mut() {
Some(s) => s,
None => return LRESULT(0),
};
// Moving mouse left = positive delta = larger offset (further left)
let delta = s.drag_start_mouse_x - pt.x;
let mut new_offset = s.drag_start_offset + delta;
// Moving mouse left = positive delta = larger offset (further left)
let delta = s.drag_start_mouse_x - pt.x;
let mut new_offset = s.drag_start_offset + delta;
// Clamp: offset >= 0 (can't go right of default)
if new_offset < 0 {
new_offset = 0;
}
// Clamp: don't go past left edge of taskbar
if let Some(taskbar_hwnd) = s.taskbar_hwnd {
if let Some(taskbar_rect) = native_interop::get_taskbar_rect(taskbar_hwnd) {
let mut tray_left = taskbar_rect.right;
if let Some(tray_hwnd) =
native_interop::find_child_window(taskbar_hwnd, "TrayNotifyWnd")
{
if let Some(tray_rect) = native_interop::get_window_rect_safe(tray_hwnd)
{
tray_left = tray_rect.left;
}
}
let widget_width = total_widget_width();
let max_offset = if s.embedded {
tray_left - taskbar_rect.left - widget_width
} else {
tray_left - taskbar_rect.left - widget_width
};
if new_offset > max_offset {
new_offset = max_offset;
}
// Clamp: offset >= 0 (can't go right of default)
if new_offset < 0 {
new_offset = 0;
}
}
s.tray_offset = new_offset;
let taskbar_hwnd = s.taskbar_hwnd;
let embedded = s.embedded;
let hwnd_val = s.hwnd.to_hwnd();
// Move window directly
let hwnd_val = s.hwnd.to_hwnd();
if let Some(taskbar_hwnd) = s.taskbar_hwnd {
if let Some(taskbar_rect) = native_interop::get_taskbar_rect(taskbar_hwnd) {
let taskbar_height = taskbar_rect.bottom - taskbar_rect.top;
let mut tray_left = taskbar_rect.right;
let anchor_top = taskbar_rect.top;
let anchor_height = taskbar_height;
if let Some(tray_hwnd) =
native_interop::find_child_window(taskbar_hwnd, "TrayNotifyWnd")
{
if let Some(tray_rect) = native_interop::get_window_rect_safe(tray_hwnd)
// Clamp: don't go past left edge of taskbar
if let Some(taskbar_hwnd) = taskbar_hwnd {
if let Some(taskbar_rect) = native_interop::get_taskbar_rect(taskbar_hwnd) {
let mut tray_left = taskbar_rect.right;
if let Some(tray_hwnd) =
native_interop::find_child_window(taskbar_hwnd, "TrayNotifyWnd")
{
tray_left = tray_rect.left;
if let Some(tray_rect) = native_interop::get_window_rect_safe(tray_hwnd)
{
tray_left = tray_rect.left;
}
}
}
let widget_width = total_widget_width();
let widget_height = sc(WIDGET_HEIGHT);
let y = compute_anchor_y(anchor_top, anchor_height, widget_height);
if s.embedded {
let x = tray_left - taskbar_rect.left - widget_width - new_offset;
native_interop::move_window(
hwnd_val,
x,
y - taskbar_rect.top,
widget_width,
widget_height,
);
} else {
let x = tray_left - widget_width - new_offset;
native_interop::move_window(
let widget_width = total_widget_width();
let max_offset = tray_left - taskbar_rect.left - widget_width;
if new_offset > max_offset {
new_offset = max_offset;
}
s.tray_offset = new_offset;
let taskbar_height = taskbar_rect.bottom - taskbar_rect.top;
let anchor_top = taskbar_rect.top;
let anchor_height = taskbar_height;
let widget_height = sc(WIDGET_HEIGHT);
let y = compute_anchor_y(anchor_top, anchor_height, widget_height);
let x = if embedded {
tray_left - taskbar_rect.left - widget_width - new_offset
} else {
tray_left - widget_width - new_offset
};
Some((
hwnd_val,
embedded,
x,
y,
taskbar_rect.top,
widget_width,
widget_height,
);
))
} else {
s.tray_offset = new_offset;
None
}
} else {
s.tray_offset = new_offset;
None
}
};
if let Some((hwnd_val, embedded, x, y, taskbar_top, widget_width, widget_height)) =
move_target
{
if embedded {
native_interop::move_window(
hwnd_val,
x,
y - taskbar_top,
widget_width,
widget_height,
);
} else {
native_interop::move_window(hwnd_val, x, y, widget_width, widget_height);
}
}
}
@ -2254,6 +2369,7 @@ unsafe extern "system" fn wnd_proc(
if let Some(s) = state.as_mut() {
s.session_text = "...".to_string();
s.weekly_text = "...".to_string();
s.force_notify_auth_error = true;
}
}
render_layered();