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
61 changes: 54 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🚀 macOS Development Setup
# 🚀 macOS & Windows Development Setup

Complete, automated setup for macOS development environments. Get up and running with essential dev tools, a beautiful terminal, tmux workflow, and a live Claude Code monitoring dashboard.
Complete, automated setup for macOS and Windows development environments. Get up and running with essential dev tools, a beautiful terminal, tmux workflow, and a live Claude Code monitoring dashboard.

## ✨ What This Does

Expand Down Expand Up @@ -52,7 +52,42 @@ This launches an interactive menu:
6. **Verify Setup** - Check what's already installed
7. **Exit**

## 📋 Prerequisites
## 🪟 Windows Setup

### One-Command Setup

```powershell
powershell -ExecutionPolicy Bypass -File setup-windows.ps1
```

Same interactive menu, adapted for Windows:

1. **Full Setup** - Dev tools + Terminal + Multiplexer
2. **Dev Environment Only** - Git, Python (pyenv-win), Node.js, utilities via winget + scoop
3. **Terminal Only** - Oh My Posh + PSReadLine + modern CLI tools + Catppuccin theme
4. **Multiplexer Only** - Windows Terminal panes + optional WSL2/tmux
5. **Claude Monitor** - Live dashboard for Claude Code instances
6. **Verify Setup** - Check what's already installed

### Windows Equivalents

| macOS | Windows |
|-------|---------|
| Homebrew | winget + scoop |
| Oh My Zsh + Powerlevel10k | Oh My Posh + Catppuccin theme |
| zsh-autosuggestions | PSReadLine PredictiveIntelliSense |
| zsh-syntax-highlighting | PSReadLine inline predictions |
| tmux | Windows Terminal panes + optional WSL/tmux |
| .zshrc | PowerShell $PROFILE |

### Windows Prerequisites

- Windows 10 or later
- PowerShell 5.1+ (pre-installed)
- Internet connection
- winget (pre-installed on Windows 10/11)

## 📋 macOS Prerequisites

- macOS 11.0 or later
- Terminal with zsh (default on modern macOS)
Expand All @@ -63,15 +98,26 @@ This launches an interactive menu:

```
MacDev/
├── setup-macos.sh # Main entry point (interactive menu)
├── scripts/ # Core setup scripts
├── setup-macos.sh # macOS entry point (interactive menu)
├── setup-windows.ps1 # Windows entry point (interactive menu)
├── scripts/ # macOS/Linux setup scripts
│ ├── setup-dev-env.sh # Development environment installer
│ ├── pimp-my-terminal.sh # Terminal customization installer
│ ├── setup-tmux.sh # tmux + theme + plugins installer
│ ├── setup-claude-monitor.sh # Claude Code Monitor installer
│ ├── claude-monitor.py # Live dashboard (rich TUI)
│ ├── claude-monitor.py # Live dashboard (rich TUI, cross-platform)
│ └── verify-setup.sh # Verification script
├── configs/ # Configuration files
├── windows/ # Windows setup scripts (PowerShell)
│ ├── setup-dev-env.ps1 # Dev tools via winget + scoop
│ ├── pimp-my-terminal.ps1 # Oh My Posh + PSReadLine + CLI tools
│ ├── setup-tmux.ps1 # Windows Terminal + optional WSL/tmux
│ ├── setup-claude-monitor.ps1 # Claude Monitor installer
│ ├── dev-session.ps1 # Multi-pane layout via wt CLI
│ ├── verify-setup.ps1 # Verification script
│ └── configs/ # Windows-specific configs
│ ├── oh-my-posh-theme.json # Catppuccin Mocha for Oh My Posh
│ └── windows-terminal-settings.json # Color scheme for Windows Terminal
├── configs/ # macOS configuration files
│ ├── tmux.conf # tmux config (Catppuccin Mocha theme)
│ └── dev-session.sh # Dev session layout launcher
├── docs/ # Documentation
Expand Down Expand Up @@ -271,6 +317,7 @@ This setup installs and configures these open-source projects:
- [tmux](https://github.com/tmux/tmux) · [Catppuccin](https://github.com/catppuccin/tmux) · [TPM](https://github.com/tmux-plugins/tpm)
- [eza](https://github.com/eza-community/eza) · [bat](https://github.com/sharkdp/bat) · [fzf](https://github.com/junegunn/fzf) · [fd](https://github.com/sharkdp/fd)
- [rich](https://github.com/Textualize/rich) · [uv](https://github.com/astral-sh/uv)
- [Oh My Posh](https://ohmyposh.dev/) · [Scoop](https://scoop.sh/) · [Windows Terminal](https://github.com/microsoft/terminal)

---

Expand Down
110 changes: 101 additions & 9 deletions scripts/claude-monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ class ClaudeData:
HISTORY_PATH = Path.home() / ".claude" / "history.jsonl"

def get_processes(self) -> list[dict]:
"""Get running Claude Code CLI instances via ps + lsof."""
"""Get running Claude Code CLI instances via ps (Unix) or wmic (Windows)."""
if sys.platform == "win32":
return self._get_processes_windows()
return self._get_processes_unix()

def _get_processes_unix(self) -> list[dict]:
"""Get processes on macOS/Linux via ps."""
try:
result = subprocess.run(
["ps", "-eo", "pid,etime,pcpu,pmem,command"],
Expand All @@ -68,7 +74,6 @@ def get_processes(self) -> list[dict]:
cpu = parts[2]
mem = parts[3]

# Get working directory via lsof
project = self._get_project(pid)

instances.append({
Expand All @@ -79,12 +84,97 @@ def get_processes(self) -> list[dict]:
"project": project,
})

# Sort by CPU descending
instances.sort(key=lambda x: float(x["cpu"]), reverse=True)
return instances

def _get_processes_windows(self) -> list[dict]:
"""Get processes on Windows via wmic/powershell."""
try:
# Use PowerShell to get Claude processes with details
result = subprocess.run(
["powershell", "-NoProfile", "-Command",
"Get-Process -Name 'claude' -ErrorAction SilentlyContinue | "
"Select-Object Id, CPU, WorkingSet64, StartTime, Path | "
"ConvertTo-Json"],
capture_output=True, text=True, timeout=10
)
except (subprocess.TimeoutExpired, FileNotFoundError):
return []

if not result.stdout.strip():
return []

instances = []
try:
data = json.loads(result.stdout)
# Ensure it's a list (single process returns a dict)
if isinstance(data, dict):
data = [data]

for proc in data:
pid = str(proc.get("Id", ""))
cpu = f"{proc.get('CPU', 0):.1f}"
mem_bytes = proc.get("WorkingSet64", 0)
mem_mb = mem_bytes / (1024 * 1024)

# Calculate elapsed time
start_time = proc.get("StartTime", {})
etime = "?"
if start_time:
try:
# PowerShell returns DateTime as a string or object
if isinstance(start_time, str):
from datetime import datetime as dt
start = dt.fromisoformat(start_time)
else:
# Handle .NET DateTime ticks
ticks = start_time.get("/Date(", start_time)
if isinstance(ticks, (int, float)):
start = datetime.fromtimestamp(ticks / 1000)
else:
start = datetime.now()
delta = datetime.now() - start
total_min = int(delta.total_seconds() / 60)
if total_min >= 1440:
etime = f"{total_min // 1440}d {(total_min % 1440) // 60}h"
elif total_min >= 60:
etime = f"{total_min // 60}h {total_min % 60}m"
else:
etime = f"{total_min}m"
except (ValueError, TypeError, KeyError):
etime = "?"

project = self._get_project(pid)

instances.append({
"pid": pid,
"etime": etime,
"cpu": cpu,
"mem": f"{mem_mb:.0f}",
"project": project,
})
except (json.JSONDecodeError, KeyError):
pass

instances.sort(key=lambda x: float(x["cpu"]), reverse=True)
return instances

def _get_project(self, pid: str) -> str:
"""Get project name from process working directory."""
# On Windows, use PowerShell to get the working directory
if sys.platform == "win32":
try:
result = subprocess.run(
["powershell", "-NoProfile", "-Command",
f"(Get-Process -Id {pid} -ErrorAction SilentlyContinue).Path | Split-Path -Parent"],
capture_output=True, text=True, timeout=5
)
if result.stdout.strip():
return Path(result.stdout.strip()).name
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
return "unknown"

# On Linux, /proc/<pid>/cwd is a symlink to the working directory
if sys.platform == "linux":
try:
Expand Down Expand Up @@ -166,7 +256,9 @@ def get_today_stats(self) -> dict:
ts = entry.get("timestamp")
if ts is None:
continue
entry_date = datetime.fromtimestamp(ts / 1000).date().isoformat()
# Auto-detect ms vs seconds: ms timestamps are > 1e11
entry_dt = datetime.fromtimestamp(ts / 1000 if ts > 1e11 else ts)
entry_date = entry_dt.date().isoformat()
if entry_date == today_str:
messages += 1
sid = entry.get("sessionId")
Expand Down Expand Up @@ -263,7 +355,7 @@ def build_compact_dashboard(data: ClaudeData) -> Panel:
for p in processes:
proj = p["project"][:12]
cpu_val = float(p["cpu"])
cpu_style = RED if cpu_val > 50 else PEACH if cpu_val > 10 else SUBTEXT
cpu_style = RED if cpu_val > 500 else PEACH if cpu_val > 100 else SUBTEXT
item = Text(proj, style=PINK)
item.append(f" {p['cpu']}%", style=cpu_style)
item.append(f" {p['etime']}", style=SUBTEXT)
Expand All @@ -277,7 +369,7 @@ def build_compact_dashboard(data: ClaudeData) -> Panel:
parts.append(Text("○ No active instances", style=SUBTEXT))

# === TODAY: inline with bars ===
today_date = date.today().strftime("%b %-d")
today_date = date.today().strftime("%b %d")
msg_line = Text(f"TODAY {today_date} ", style=f"bold {BLUE}")
msg_line.append("Msgs ")
msg_line.append_text(make_bar(today["messages"], max(today["messages"], 200), width=8))
Expand Down Expand Up @@ -379,7 +471,7 @@ def build_dashboard(data: ClaudeData, compact: bool = False) -> Panel:
if len(proj) > 12:
proj = proj[:11] + "…"
cpu_val = float(p["cpu"])
cpu_style = RED if cpu_val > 50 else PEACH if cpu_val > 10 else SUBTEXT
cpu_style = RED if cpu_val > 500 else PEACH if cpu_val > 100 else SUBTEXT
proc_table.add_row(
proj,
f"PID {p['pid']}",
Expand All @@ -390,7 +482,7 @@ def build_dashboard(data: ClaudeData, compact: bool = False) -> Panel:
parts.append(Text(""))

# === Today Stats ===
today_date = date.today().strftime("%b %-d")
today_date = date.today().strftime("%b %d")
parts.append(Text(f" TODAY {today_date}", style=f"bold {BLUE}"))

msg_max = max(today["messages"], 200)
Expand Down Expand Up @@ -442,7 +534,7 @@ def build_dashboard(data: ClaudeData, compact: bool = False) -> Panel:
first_date = stats.get("firstSessionDate", "")
if first_date:
try:
since = datetime.fromisoformat(first_date.replace("Z", "+00:00")).strftime("%b %-d, %Y")
since = datetime.fromisoformat(first_date.replace("Z", "+00:00")).strftime("%b %d, %Y")
except (ValueError, AttributeError):
since = "?"
else:
Expand Down
Loading