Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[target.x86_64-pc-windows-gnu]
linker = "C:/msys64/mingw64/bin/gcc.exe"
ar = "C:/msys64/mingw64/bin/ar.exe"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ AGENTS.md

# Local WinGet manifest generation output
/manifests/

# Cursor IDE agent rules - hard-linked, not repo content
.cursor/
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod localization;
mod models;
mod native_interop;
mod poller;
mod spend_pace;
mod theme;
mod tray_icon;
mod updater;
Expand Down
34 changes: 34 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::time::SystemTime;
pub struct UsageSection {
pub percentage: f64,
pub resets_at: Option<SystemTime>,
pub has_bucket: bool,
}

#[derive(Clone, Debug, Default)]
Expand All @@ -12,9 +13,42 @@ pub struct UsageData {
pub weekly: UsageSection,
}

#[derive(Clone, Debug, Default)]
pub struct AccountUsage {
pub credit_pct: f64,
pub credit_expiry: Option<SystemTime>,
pub spend_used: f64,
pub spend_limit: f64,
}

#[derive(Clone, Debug, Default)]
pub struct SpendPaceSlots {
pub month_actual: f64,
pub month_cap: f64,
pub month_expected: f64,
pub month_level: u8,
pub week_actual: f64,
pub week_cap: f64,
pub week_expected: f64,
pub week_level: u8,
pub day_actual: f64,
pub day_cap: f64,
pub day_expected: f64,
pub day_level: u8,
}

#[derive(Clone, Debug)]
pub struct SpendPaceView {
pub credit_pct: f64,
pub credit_expiry: Option<SystemTime>,
pub slots: SpendPaceSlots,
}

#[derive(Clone, Debug, Default)]
pub struct AppUsageData {
pub claude_code: Option<UsageData>,
pub codex: Option<UsageData>,
pub antigravity: Option<UsageData>,
pub account: Option<AccountUsage>,
pub spend_pace: Option<SpendPaceView>,
}
92 changes: 92 additions & 0 deletions src/native_interop.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
use windows::core::PCWSTR;
use windows::Win32::Foundation::{BOOL, HWND, LPARAM, RECT};
use windows::Win32::Globalization::GetLocaleInfoW;
use windows::Win32::UI::Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK};
use windows::Win32::UI::Shell::{SHAppBarMessage, ABM_GETTASKBARPOS, APPBARDATA};
use windows::Win32::UI::WindowsAndMessaging::*;

const LOCALE_USER_DEFAULT: u32 = 0x0400;
// Short date format pattern (e.g. "M/d/yyyy")
const LOCALE_SSHORTDATE: u32 = 0x001F;

// Window style constants
pub const WS_POPUP_STYLE: u32 = 0x80000000;
pub const WS_CHILD_STYLE: u32 = 0x40000000;
Expand All @@ -18,6 +23,7 @@ pub const TIMER_POLL: usize = 1;
pub const TIMER_COUNTDOWN: usize = 2;
pub const TIMER_RESET_POLL: usize = 3;
pub const TIMER_UPDATE_CHECK: usize = 4;
pub const TIMER_DRAG: usize = 5;

// Custom messages
pub const WM_APP: u32 = 0x8000;
Expand Down Expand Up @@ -134,13 +140,64 @@ pub fn embed_in_taskbar(hwnd: HWND, taskbar_hwnd: HWND) {
}
}

/// Detach our window from the taskbar, restoring popup style and topmost z-order
pub fn detach_from_taskbar(hwnd: HWND) {
unsafe {
let style = GetWindowLongW(hwnd, GWL_STYLE) as u32;
let new_style = (style & !(WS_CHILD_STYLE | WS_CLIPSIBLINGS_STYLE)) | WS_POPUP_STYLE;
let _ = SetWindowLongW(hwnd, GWL_STYLE, new_style as i32);
let _ = SetParent(hwnd, None);
let _ = SetWindowPos(
hwnd,
HWND_TOPMOST,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
);
}
}

/// Re-assert HWND_TOPMOST so the window sits above Shell_TrayWnd (which is also topmost).
/// MoveWindow preserves z-order but doesn't lift us to the front of the topmost band.
pub fn raise_above_taskbar(hwnd: HWND) {
unsafe {
let _ = SetWindowPos(
hwnd,
HWND_TOPMOST,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
);
}
}

/// Move the window
pub fn move_window(hwnd: HWND, x: i32, y: i32, w: i32, h: i32) {
unsafe {
let _ = MoveWindow(hwnd, x, y, w, h, true);
}
}

/// Move the window asynchronously — posts a move request to the owning thread's queue
/// instead of blocking cross-process. Required for WS_CHILD windows embedded in Explorer.
pub fn move_window_async(hwnd: HWND, x: i32, y: i32, w: i32, h: i32) {
unsafe {
let _ = SetWindowPos(
hwnd,
HWND::default(),
x,
y,
w,
h,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS,
);
}
}

/// Set up a WinEvent hook for tray location changes
pub fn set_tray_event_hook(
thread_id: u32,
Expand Down Expand Up @@ -181,6 +238,41 @@ pub fn wide_str(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}

/// Format a month/day pair respecting the Windows system locale
/// (separator, and whether day or month comes first).
/// Returns e.g. "9/15" (en-US), "15/9" (en-GB), "15.9" (de-DE).
pub fn format_month_day_locale(month: u8, day: u8) -> String {
if let Some(pattern) = locale_short_date_pattern() {
let lower = pattern.to_lowercase();
// Find the separator: first non-alphabetic, non-quote character
let sep = lower
.chars()
.find(|c| !c.is_alphabetic() && *c != '\'')
.unwrap_or('/');
// day-first when 'd' appears before 'm' in the pattern (e.g. "dd/MM/yyyy")
let d_pos = lower.find('d');
let m_pos = lower.find('m');
return match (d_pos, m_pos) {
(Some(d), Some(m)) if d < m => format!("{}{}{}", day, sep, month),
(Some(_), Some(_)) => format!("{}{}{}", month, sep, day),
_ => format!("{}/{}", month, day), // malformed pattern — safe fallback
};
}
format!("{}/{}", month, day)
}

fn locale_short_date_pattern() -> Option<String> {
unsafe {
let mut buf = [0u16; 256];
let len = GetLocaleInfoW(LOCALE_USER_DEFAULT, LOCALE_SSHORTDATE, Some(&mut buf));
if len > 1 && (len as usize) <= buf.len() {
Some(String::from_utf16_lossy(&buf[..len as usize - 1]).to_string())
} else {
None
}
}
}

/// COLORREF wrapper (RGB packed into u32)
pub fn colorref(r: u8, g: u8, b: u8) -> u32 {
r as u32 | (g as u32) << 8 | (b as u32) << 16
Expand Down
Loading