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
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ The tool can be invoked using any of these commands:
- cmonitor (short)
- ccmonitor (short alternative)
- ccm (shortest)
- claude-monitor-tray (tray application)
- ccmt (tray application short)

#### Save Flags Feature

Expand Down Expand Up @@ -269,6 +271,10 @@ cmonitor # Short alias
ccmonitor # Short alternative
ccm # Shortest alias

# System tray application
claude-monitor-tray # Tray app
ccmt # Tray app short alias

# Exit the monitor
# Press Ctrl+C to gracefully exit
```
Expand Down Expand Up @@ -848,6 +854,71 @@ claude-monitor --plan pro
claude-monitor --plan max20 --reset-hour 6
```

### 🖥️ System Tray Application

A **system tray application** that lets you monitor your Claude Code token usage directly from your desktop, without needing a terminal window open.

![System Tray Icon](static/tray.png)

![Claude Monitor Tray App](static/ccmt.png)

#### Tray Features

- Real-time token usage monitoring in the system tray
- Claude Code style icon with color-coded status (green/yellow/red)
- Click to view detailed statistics window
- Configurable warning and critical thresholds
- Autostart support
- Settings dialog for customization

#### Running the Tray Application

```bash
# Launch the tray application
claude-monitor-tray

# Or use the short alias
ccmt
```

The tray icon will appear in your system tray. Click on it to view detailed statistics, or right-click for the menu.

#### Tray Icon States

| Color | Status | Description |
|-------|--------|-------------|
| Green | Normal | Usage below warning threshold |
| Yellow | Warning | Usage between warning and critical thresholds |
| Red | Critical | Usage above critical threshold |
| Gray | Loading/Error | Data loading or error state |

#### Tray Configuration

The tray app stores its settings in `~/.config/claude-monitor-tray/settings.json`.

Available settings:
- **Plan**: pro, max5, max20, or custom
- **Refresh rate**: How often to update (in seconds)
- **Warning threshold**: Percentage for warning state (default: 70%)
- **Critical threshold**: Percentage for critical state (default: 90%)
- **Autostart**: Launch on system startup

#### Installing with Tray Support

```bash
# With uv (recommended)
uv tool install claude-monitor[tray]

# With pip
pip install claude-monitor[tray]

# From source
pip install -e ".[tray]"
```

The tray application requires PyQt6 (included with `[tray]` extra).

---

## 🔧 Development Installation

Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ test = [
"pytest-asyncio>=0.24.0",
"pytest-benchmark>=4.0.0"
]
tray = ["PyQt6>=6.4.0"]


[project.urls]
Expand All @@ -88,6 +89,8 @@ claude-code-monitor = "claude_monitor.__main__:main"
cmonitor = "claude_monitor.__main__:main"
ccmonitor = "claude_monitor.__main__:main"
ccm = "claude_monitor.__main__:main"
claude-monitor-tray = "claude_monitor.tray.__main__:main"
ccmt = "claude_monitor.tray.__main__:main"

[tool.setuptools.packages.find]
where = ["src"]
Expand Down
41 changes: 41 additions & 0 deletions src/claude_monitor/tray/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""System tray component for Claude Code Usage Monitor.

This package provides a PyQt6-based system tray application for monitoring
Claude Code token usage in real-time.
"""

from typing import Tuple

PYQT_AVAILABLE = False
PYQT_ERROR: str = ""


def check_dependencies() -> Tuple[bool, str]:
"""Check if PyQt6 is available.

Returns:
Tuple of (is_available, error_message)
"""
global PYQT_AVAILABLE, PYQT_ERROR

try:
from PyQt6.QtWidgets import QApplication # noqa: F401

PYQT_AVAILABLE = True
PYQT_ERROR = ""
return True, ""
except ImportError as e:
PYQT_AVAILABLE = False
PYQT_ERROR = str(e)
return False, (
"PyQt6 is required for the system tray application.\n"
"Install it with: pip install 'claude-monitor[tray]'\n"
f"Error: {e}"
)


__all__ = [
"check_dependencies",
"PYQT_AVAILABLE",
"PYQT_ERROR",
]
127 changes: 127 additions & 0 deletions src/claude_monitor/tray/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Entry point for claude-monitor-tray command."""

import argparse
import logging
import sys
from pathlib import Path


def setup_logging() -> None:
"""Setup logging for the tray application."""
log_dir = Path.home() / ".claude-monitor"
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / "tray.log"

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler(log_file),
logging.StreamHandler(),
],
)


def parse_args() -> argparse.Namespace:
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
prog="claude-monitor-tray",
description="System tray monitor for Claude Code usage",
)
parser.add_argument(
"--plan",
choices=["pro", "max5", "max20", "custom"],
default=None,
help="Plan type (pro, max5, max20, custom)",
)
parser.add_argument(
"--refresh-rate",
type=int,
default=None,
help="Refresh rate in seconds (default: 60)",
)
return parser.parse_args()


def kill_existing_instances() -> None:
"""Kill any existing tray app instances."""
import os
import signal
import subprocess

try:
# Find and kill existing instances (excluding current process)
current_pid = os.getpid()
result = subprocess.run(
["pgrep", "-f", "claude-monitor-tray"],
capture_output=True,
text=True,
)
if result.stdout:
for pid_str in result.stdout.strip().split("\n"):
if pid_str:
pid = int(pid_str)
if pid != current_pid:
os.kill(pid, signal.SIGTERM)
except Exception:
pass # Ignore errors


def main() -> int:
"""Main entry point for the tray application."""
# Kill existing instances first
kill_existing_instances()

# Parse args first
args = parse_args()

# Check dependencies
from claude_monitor.tray import check_dependencies

available, error_message = check_dependencies()
if not available:
print(error_message, file=sys.stderr)
return 1

# Setup logging
setup_logging()
logger = logging.getLogger(__name__)

try:
logger.info("Starting Claude Monitor Tray application")

# Load and update settings from CLI args
from claude_monitor.tray.settings import TraySettingsManager

settings_manager = TraySettingsManager()
settings = settings_manager.load()

# Override from CLI args
if args.plan:
settings.plan = args.plan
settings_manager.save(settings)
logger.info(f"Plan set to: {args.plan}")

if args.refresh_rate:
settings.refresh_rate = args.refresh_rate
settings_manager.save(settings)

# Import and run app
from claude_monitor.tray.app import TrayApplication

app = TrayApplication(sys.argv)
app.start()

return app.exec()

except KeyboardInterrupt:
logger.info("Interrupted by user")
return 0
except Exception as e:
logger.exception(f"Fatal error: {e}")
print(f"Error: {e}", file=sys.stderr)
return 1


if __name__ == "__main__":
sys.exit(main())
Loading