Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ It lands in your Applications folder automatically. Right-click **Open** once to
- **14 Visual Themes:** Switch between panel styles including Classic, Matrix, Windows 95, Newspaper, Cloud Observation, Midnight Aquarium, Prism Arcade, Black Hole, World Cup 2026, Lepidoptera (blueprint), Migration, Stained Glass, Origami, and Catppuccin (official palette, all four flavors).
- **Place the Panel Anywhere:** The panel is no longer pinned under the menu bar icon. Drag it from any empty spot to wherever you want it, and it reopens there next time. It stays put when another app takes focus — a second click on the menu bar icon, or Escape, closes it.
- **Drag to Reorder:** Grab any quota card and drag it up or down to swap the order — the arrangement is shared across every theme with quota cards (all except World Cup 2026) and survives restarts.
- **Spirit Companions:** A small animated white silhouette lives beside your usage percentages — a phoenix for Claude, a dragon for Codex, a lion for Antigravity. Each accelerates dynamically as its own tool's token burn rate climbs.
- **Automatic Localization:** UI text is available in Traditional Chinese, Simplified Chinese, English, Japanese, and Korean, automatically matching your system settings.

## Privacy & Data Sources
Expand Down
Binary file removed assets/critters/dragon/1.png
Binary file not shown.
Binary file removed assets/critters/dragon/2.png
Binary file not shown.
Binary file removed assets/critters/dragon/3.png
Binary file not shown.
Binary file removed assets/critters/dragon/4.png
Binary file not shown.
Binary file removed assets/critters/dragon/5.png
Binary file not shown.
Binary file removed assets/critters/lion/1.png
Binary file not shown.
Binary file removed assets/critters/lion/2.png
Binary file not shown.
Binary file removed assets/critters/lion/3.png
Binary file not shown.
Binary file removed assets/critters/lion/4.png
Binary file not shown.
Binary file removed assets/critters/lion/5.png
Binary file not shown.
Binary file removed assets/critters/phoenix/1.png
Binary file not shown.
Binary file removed assets/critters/phoenix/2.png
Binary file not shown.
Binary file removed assets/critters/phoenix/3.png
Binary file not shown.
Binary file removed assets/critters/phoenix/4.png
Binary file not shown.
Binary file removed assets/critters/phoenix/5.png
Binary file not shown.
Binary file added assets/grok_mono_menubar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@ source .venv/bin/activate
python3 main.py
```

- **Menu bar format:** `🐾 37%`. If Codex usage is also detected, a Codex suffix is appended: `🐾 37% · 📜 10%`.

<img src="menubar.png" alt="menu bar display" width="240">

- **Click the icon to expand the popover.** It has four sections:
Expand Down
1 change: 0 additions & 1 deletion docs/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ Applicationsフォルダに自動でインストールされます。Gatekeeper
- **14種類のビジュアルテーマ:** Classic、Matrix、Windows 95、Newspaper、Cloud Observation、Midnight Aquarium、Prism Arcade、Black Hole、World Cup 2026、Lepidoptera(blueprint)、渡り鳥(Migration)、ステンドグラス、折り紙、Catppuccin(公式パレット、4種のflavorすべてに対応)を含むパネルスタイルを切り替えられます。
- **パネルを自由に配置:** パネルはメニューバーアイコンの下に固定されなくなりました。空白部分をドラッグして好きな場所に移動でき、次回開いたときもその位置を保持します。他のアプリにフォーカスが移っても消えず、メニューバーアイコンをもう一度クリックするかEscキーを押すと閉じます。
- **ドラッグで並べ替え:** 任意のクォータカードをつかんで上下にドラッグすると順序を入れ替えられます。並び順はクォータカードを含むすべてのテーマ(World Cup 2026 を除く)で共有され、再起動後も維持されます。
- **スピリットコンパニオン:** 使用率のそばに小さなアニメーション付きの白いシルエットが現れます。Claudeには不死鳥、Codexにはドラゴン、Antigravityにはライオンです。それぞれのツールのtoken消費率が上がると動きも動的に速くなります。
- **自動ローカライズ:** UIテキストは繁体字中国語、簡体字中国語、英語、日本語、韓国語で利用でき、システム設定に自動的に合わせます。

## プライバシーとデータソース
Expand Down
1 change: 0 additions & 1 deletion docs/README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ Applications 폴더에 자동으로 설치됩니다. Gatekeeper를 통과하려
- **14가지 시각 테마:** Classic, Matrix, Windows 95, Newspaper, Cloud Observation, Midnight Aquarium, Prism Arcade, Black Hole, World Cup 2026, Lepidoptera(blueprint), 철새 이동(Migration), 스테인드글라스, 종이접기, Catppuccin(공식 팔레트, 4가지 flavor 모두 지원)을 포함한 패널 스타일을 전환할 수 있습니다.
- **패널 자유 배치:** 패널이 더 이상 메뉴 막대 아이콘 아래에 고정되지 않습니다. 빈 공간을 드래그해 원하는 위치로 이동할 수 있으며, 다음에 열 때도 그 위치가 유지됩니다. 다른 앱으로 포커스가 이동해도 사라지지 않으며, 메뉴 막대 아이콘을 다시 클릭하거나 Esc 키를 눌러야 닫힙니다.
- **드래그로 순서 변경:** 아무 할당량 카드나 잡고 위아래로 드래그하면 순서를 바꿀 수 있습니다. 배치는 할당량 카드가 있는 모든 테마(World Cup 2026 제외)에서 공유되며 다시 시작해도 유지됩니다.
- **영적 동반자:** 작은 흰색 애니메이션 실루엣이 사용률 옆에 표시됩니다. Claude에는 불사조, Codex에는 용, Antigravity에는 사자가 함께하며 각자 해당 도구의 token 소모 속도가 올라갈수록 동작도 더 빨라집니다.
- **자동 현지화:** UI 텍스트는 번체 중국어, 간체 중국어, 영어, 일본어, 한국어로 제공되며 시스템 설정에 맞춰 자동으로 전환됩니다.

## 개인정보 보호와 데이터 소스
Expand Down
1 change: 0 additions & 1 deletion docs/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ brew install --cask aqua5230/usage/usage
- **14 个视觉主题:** 可切换面板风格,包括 Classic、Matrix、Windows 95、Newspaper、Cloud Observation、Midnight Aquarium、Prism Arcade、Black Hole、World Cup 2026、Lepidoptera(蓝图)、候鸟迁徙(Migration)、彩绘玻璃、折纸和 Catppuccin(官方配色,四款 flavor 全支持)。
- **面板自由摆放:** 面板不再固定在菜单栏图标下方。在任何空白处按住即可拖动到你想要的位置,下次打开仍保留在原位。切换到其他 App 时也不会消失,再次点击菜单栏图标或按 Esc 键才会关闭。
- **拖拽排序:** 按住任意配额卡上下拖拽即可交换顺序——这一排列在所有包含配额卡的主题间共享(除 World Cup 2026 之外),并在重启后保留。
- **灵伴:** 一个小型动态白色剪影会出现在使用百分比旁边:Claude 是凤凰,Codex 是龙,Antigravity 是狮子。每个伙伴都会随各自工具的 token 消耗速率上升而动态加速。
- **自动本地化:** 界面文本提供繁体中文、简体中文、英语、日语和韩语,并自动匹配系统设置。

## 隐私与数据来源
Expand Down
1 change: 0 additions & 1 deletion docs/README.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ brew install --cask aqua5230/usage/usage
- **14 款視覺面板:** 可在 Classic、Matrix、Windows 95、復古報紙(Newspaper)、雲圖觀測(Cloud Observation)、深夜水族箱(Midnight Aquarium)、Prism Arcade、黑洞(Black Hole)、World Cup 2026、藍曬圖(Lepidoptera)、候鳥遷徙(Migration)、彩繪玻璃(Stained Glass)、摺紙(Origami)與 Catppuccin(官方配色,四款 flavor 全支援)之間切換。
- **面板自由擺放:** 面板不再釘在選單列圖示下方。在任何空白處按住就能拖到你想要的位置,下次打開還在原地。點到別的 App 也不會消失,要再點一次選單列圖示或按 Esc 才關。
- **拖曳排序:** 按住任何一張額度卡上下拖曳就能交換順序,排法在所有包含額度卡的主題間共用(除 World Cup 2026 之外),重開也會記住。
- **神獸夥伴:** 百分比旁常駐一隻小型白色動畫神獸(Claude 是鳳凰,Codex 是飛龍,Antigravity 是獅子),各自跟著自家工具的 token 燃燒率動態加速。
- **自動多語言 (i18n):** 介面支援繁中、簡中、英、日、韓,自動跟隨系統語言設定。

## 隱私與資料來源
Expand Down
111 changes: 2 additions & 109 deletions menubar/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@
flush_caches_on_terminate as flush_history_cache,
)
from menubar import actions as menubar_actions
from menubar import critter_frames
from menubar import menu as menubar_menu
from menubar import notify as menubar_notify
from menubar import refresh as menubar_refresh
Expand Down Expand Up @@ -282,17 +281,6 @@ class AppDelegate(NSObject):
_pre_talent_panel_id = objc.ivar()
_discussion_window_controller = objc.ivar()
_usage_client = objc.ivar()
critter_timer = objc.ivar()
critter_frame = objc.ivar()
critter_interval = objc.ivar()
critter_group = objc.ivar()
critter_last_advanced_at = objc.ivar()
dragon_frame = objc.ivar()
dragon_group = objc.ivar()
dragon_last_advanced_at = objc.ivar()
lion_frame = objc.ivar()
lion_group = objc.ivar()
lion_last_advanced_at = objc.ivar()
language = objc.ivar()

def initWithMock_interval_(self, mock: bool, interval: int) -> AppDelegate:
Expand Down Expand Up @@ -333,19 +321,8 @@ def initWithMock_interval_(self, mock: bool, interval: int) -> AppDelegate:
self._pre_talent_panel_id = None
self._discussion_window_controller = None
self._usage_client = ClaudeUsageClient(mock=mock)
self.critter_timer = None
self.critter_frame = 0
self.critter_interval = 0.0
self.critter_group = 0
self.critter_last_advanced_at = 0.0
self.dragon_frame = 0
self.dragon_group = 0
self.dragon_last_advanced_at = 0.0
self.lion_frame = 0
self.lion_group = 0
self.lion_last_advanced_at = 0.0
self._menubar_text_cache: dict[str, Any] = {}
self._last_button_title_key: tuple[str, int | None, int | None, int | None] | None = None
self._last_button_title_key: tuple[str] | None = None
self._last_plain_title_key: tuple[str] | None = None
return self

Expand All @@ -357,7 +334,7 @@ def applicationDidFinishLaunching_(self, notification: Any) -> None:
# Do not change this string; it is the stable identity for saved menu bar position.
self.status_item.setAutosaveName_("usage")
button = self.status_item.button()
button.setTitle_("🐾 ...")
button.setTitle_("...")
button.setTarget_(self)
button.setAction_("togglePopover:")

Expand Down Expand Up @@ -489,7 +466,6 @@ def applicationWillTerminate_(self, notification: Any) -> None:
self._stop_poll_timer()
cleanup_fsevents(self._fs_stream)
self._fs_stream = None
self._stop_critter_timer()
asyncio.run(self._usage_client.aclose())
if self._file_event_refresh_timer is not None:
self._file_event_refresh_timer.invalidate()
Expand Down Expand Up @@ -790,74 +766,6 @@ def _resync_popover_after_menu(self) -> None:
_popover_size(self.latest_state, self.active_panel), restored=True
)

def animateCritters_(self, timer: Any) -> None:
now = time.monotonic()
intervals = self._critter_intervals()
previous = (
float(self.critter_last_advanced_at),
float(self.dragon_last_advanced_at),
float(self.lion_last_advanced_at),
)
tick = menubar_state.critter_animation_tick(now, intervals, previous)
if tick.timer_interval <= 0:
self._stop_critter_timer()
return
if tick.advance[0]:
self.critter_frame = (
int(self.critter_frame) + 1
) % len(critter_frames.PHOENIX_FRAMES)
self.critter_last_advanced_at = previous[0] + intervals[0]
if tick.advance[1]:
self.dragon_frame = (
int(self.dragon_frame) + 1
) % len(critter_frames.DRAGON_FRAMES)
self.dragon_last_advanced_at = previous[1] + intervals[1]
if tick.advance[2]:
self.lion_frame = (
int(self.lion_frame) + 1
) % len(critter_frames.LION_FRAMES)
self.lion_last_advanced_at = previous[2] + intervals[2]
if any(tick.advance):
menubar_title._set_button_title(self, self.latest_state)

def _critter_intervals(self) -> tuple[float, float, float]:
return (
critter_frames.group_to_interval(int(self.critter_group)),
critter_frames.group_to_interval(int(self.dragon_group)),
critter_frames.group_to_interval(int(self.lion_group)),
)

def _sync_critter_timer(self) -> None:
intervals = self._critter_intervals()
interval = min((value for value in intervals if value > 0), default=0.0)
now = time.monotonic()
self.critter_last_advanced_at = now
self.dragon_last_advanced_at = now
self.lion_last_advanced_at = now
if intervals[0] <= 0:
self.critter_frame = 0
if intervals[1] <= 0:
self.dragon_frame = 0
if intervals[2] <= 0:
self.lion_frame = 0
if interval <= 0:
self._stop_critter_timer()
return
if self.critter_timer is not None and self.critter_interval == interval:
return
self._stop_critter_timer()
self.critter_interval = interval
scheduled_timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_
self.critter_timer = scheduled_timer(interval, self, "animateCritters:", None, True)
NSRunLoop.currentRunLoop().addTimer_forMode_(self.critter_timer, NSRunLoopCommonModes)

def _stop_critter_timer(self) -> None:
timer = self.critter_timer
self.critter_timer = None
self.critter_interval = 0.0
if timer is not None:
timer.invalidate()

def togglePopover_(self, sender: Any) -> None:
if self._panel_window_is_visible():
self.popover.close()
Expand Down Expand Up @@ -944,21 +852,6 @@ def _applyRefreshResult_(self, result: dict[str, Any]) -> None:
self.codex_5h_pct = codex_5h_pct
self.codex_model = codex_model
self.latest_state = state
animation_groups = tuple(
int(group)
for group in result.get(
"animation_groups",
(self.critter_group, self.dragon_group, self.lion_group),
)
)
previous_groups = (
int(self.critter_group),
int(self.dragon_group),
int(self.lion_group),
)
self.critter_group, self.dragon_group, self.lion_group = animation_groups
if animation_groups != previous_groups:
self._sync_critter_timer()
self._process_quota_notifications(state)
if self._panel_window_is_visible():
self.popover_controller.setState_(self.latest_state)
Expand Down
41 changes: 15 additions & 26 deletions menubar/chrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
_CODEX_MENUBAR_ICON_LOADED = False
_AGY_MENUBAR_ICON: Any = None
_AGY_MENUBAR_ICON_LOADED = False
_CRITTER_IMAGE_CACHE: dict[str, Any] = {}
_CRITTER_ATTACHMENT_STRING_CACHE: dict[str, Any] = {}
_GROK_MENUBAR_ICON: Any = None
_GROK_MENUBAR_ICON_LOADED = False


class _NoopAlert:
Expand Down Expand Up @@ -107,37 +107,26 @@ def _agy_menubar_icon() -> Any:
return _AGY_MENUBAR_ICON


def _grok_menubar_icon() -> Any:
global _GROK_MENUBAR_ICON, _GROK_MENUBAR_ICON_LOADED
if not _GROK_MENUBAR_ICON_LOADED:
_GROK_MENUBAR_ICON_LOADED = True
try:
_GROK_MENUBAR_ICON = _load_menubar_brand_icon("grok_mono_menubar.png")
except Exception:
_GROK_MENUBAR_ICON = None
if os.environ.get("USAGE_DEBUG") == "1":
logger.warning("load Grok menubar icon failed", exc_info=True)
return _GROK_MENUBAR_ICON


def _menubar_icon_attachment_string(image: Any) -> Any:
attachment = NSTextAttachment.alloc().init()
attachment.setImage_(image)
attachment.setBounds_(NSMakeRect(0, -3.5, 16, 16))
return NSAttributedString.attributedStringWithAttachment_(attachment)


def _critter_icon_attachment_string(path: str, image: Any) -> Any:
cached = _CRITTER_ATTACHMENT_STRING_CACHE.get(path)
if cached is not None:
return cached
attachment = NSTextAttachment.alloc().init()
attachment.setImage_(image)
attachment.setBounds_(NSMakeRect(0, -4.0, 18, 18))
attributed = NSAttributedString.attributedStringWithAttachment_(attachment)
_CRITTER_ATTACHMENT_STRING_CACHE[path] = attributed
return attributed


def _critter_frame_image(path: str) -> Any:
cached = _CRITTER_IMAGE_CACHE.get(path)
if cached is not None:
return cached
image = NSImage.alloc().initWithContentsOfFile_(resolve_resource(path))
if image is not None:
image.setTemplate_(True)
image.setSize_(NSMakeSize(18, 18))
_CRITTER_IMAGE_CACHE[path] = image
return image


def _make_alert() -> Any:
try:
alert = NSAlert.alloc().init()
Expand Down
26 changes: 0 additions & 26 deletions menubar/critter_frames.py

This file was deleted.

26 changes: 0 additions & 26 deletions menubar/refresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ class RefreshSources:
agy_projection: menubar_agy.AgyQuotaProjection
grok_result: menubar_grok.GrokRefreshResult
grok_projection: menubar_grok.GrokQuotaProjection
animation_groups: tuple[int, int, int]
debug_timing: bool

def measure(self, stage: str, started_at: float) -> None:
Expand All @@ -86,11 +85,6 @@ def measure(self, stage: str, started_at: float) -> None:

def load_sources(app: _RefreshApp) -> RefreshSources:
debug_timing = os.environ.get("USAGE_DEBUG") == "1"
animation_groups = (
int(getattr(app, "critter_group", 0)),
int(getattr(app, "dragon_group", 0)),
int(getattr(app, "lion_group", 0)),
)
started_at = time.monotonic() if debug_timing else 0.0
codex_result = app._load_codex_refresh_result()
history_scan = codex_result.get("_history_scan")
Expand Down Expand Up @@ -118,7 +112,6 @@ def load_sources(app: _RefreshApp) -> RefreshSources:
agy_projection=agy_projection,
grok_result=grok_result,
grok_projection=grok_projection,
animation_groups=animation_groups,
debug_timing=debug_timing,
)

Expand All @@ -129,7 +122,6 @@ def build_result(app: _RefreshApp, sources: RefreshSources) -> dict[str, Any]:
agy_projection = sources.agy_projection
grok_result = sources.grok_result
grok_projection = sources.grok_projection
animation_groups = sources.animation_groups
fallback_state = getattr(app, "latest_state", menubar_state._empty_state(app.language))
project_rows = list(fallback_state.projects)
project_rows_yesterday = list(fallback_state.projects_yesterday)
Expand Down Expand Up @@ -198,23 +190,6 @@ def build_result(app: _RefreshApp, sources: RefreshSources) -> dict[str, Any]:
get_service_status(CODEX_STATUS),
)
group = int(app.tracker.group())
try:
codex_group = int(app.codex_tracker.group())
except Exception:
codex_group = 0
if os.environ.get("USAGE_DEBUG") == "1":
logger.warning("Codex animation group load failed", exc_info=True)
try:
agy_group = int(app.agy_tracker.group())
except Exception:
agy_group = 0
if os.environ.get("USAGE_DEBUG") == "1":
logger.warning("Antigravity animation group load failed", exc_info=True)
animation_groups = (
group,
codex_group,
agy_group,
)
state = menubar_state.build_popover_state(
outcome=outcome,
codex_rows=codex_rows,
Expand Down Expand Up @@ -305,5 +280,4 @@ def build_result(app: _RefreshApp, sources: RefreshSources) -> dict[str, Any]:
"state": state,
"codex_5h_pct": codex_5h_pct,
"codex_model": codex_model,
"animation_groups": animation_groups,
}
Loading
Loading