diff --git a/README.md b/README.md index ba8a106..88ed39f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ English | [中文](README.zh.md) # 🐾 Paws -[![CI](https://github.com/interesting-vibe-coding/paws/actions/workflows/ci.yml/badge.svg)](https://github.com/interesting-vibe-coding/paws/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Works with Kaku & WezTerm](https://img.shields.io/badge/Works_with-Kaku_%26_WezTerm-blue)](https://wezfurlong.org/wezterm/) [![Made with Lua & Rust](https://img.shields.io/badge/Made_with-Lua_&_Rust-orange)]() [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/interesting-vibe-coding/paws/pulls) [![GitHub Stars](https://img.shields.io/github/stars/interesting-vibe-coding/paws?style=flat&color=yellow)](https://github.com/interesting-vibe-coding/paws/stargazers) +[![CI](https://github.com/interesting-vibe-coding/paws/actions/workflows/ci.yml/badge.svg)](https://github.com/interesting-vibe-coding/paws/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Works with Kaku, WezTerm & iTerm2](https://img.shields.io/badge/Works_with-Kaku_%7C_WezTerm_%7C_iTerm2-blue)](https://github.com/interesting-vibe-coding/paws/blob/main/docs/iterm2-setup.md) [![Made with Lua & Rust](https://img.shields.io/badge/Made_with-Lua_&_Rust-orange)]() [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/interesting-vibe-coding/paws/pulls) [![GitHub Stars](https://img.shields.io/github/stars/interesting-vibe-coding/paws?style=flat&color=yellow)](https://github.com/interesting-vibe-coding/paws/stargazers) Play games while your AI agent works. A status HUD tells you when to come back. @@ -50,9 +50,10 @@ cargo install --git https://github.com/interesting-vibe-coding/paws-games --bin cargo install --git https://github.com/interesting-vibe-coding/paws-games --bin tetris ``` -Then add [`lua/paws.lua`](lua/paws.lua) to your terminal config (before `return config`) and wire hooks for your agent (see [`hooks/`](hooks/) for reference configs). -- **Kaku:** `~/.config/kaku/kaku.lua` — reload with CMD+Shift+R -- **WezTerm:** `~/.config/wezterm/wezterm.lua` — auto-reloads on save +Then wire the terminal integration and hooks for your agent (see [`hooks/`](hooks/) for reference configs). +- **Kaku:** add [`lua/paws.lua`](lua/paws.lua) to `~/.config/kaku/kaku.lua` before `return config` — reload with CMD+Shift+R +- **WezTerm:** add [`lua/paws.lua`](lua/paws.lua) to `~/.config/wezterm/wezterm.lua` — auto-reloads on save +- **iTerm2:** copy [`iterm2/paws.py`](iterm2/paws.py) to `~/.config/iterm2/scripts/AutoLaunch/` and bind 3 keys — see [docs/iterm2-setup.md](docs/iterm2-setup.md) ## Games diff --git a/README.zh.md b/README.zh.md index 954a725..c27ef8b 100644 --- a/README.zh.md +++ b/README.zh.md @@ -4,7 +4,7 @@ # 🐾 Paws -[![CI](https://github.com/interesting-vibe-coding/paws/actions/workflows/ci.yml/badge.svg)](https://github.com/interesting-vibe-coding/paws/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Works with Kaku & WezTerm](https://img.shields.io/badge/Works_with-Kaku_%26_WezTerm-blue)](https://wezfurlong.org/wezterm/) [![Made with Lua & Rust](https://img.shields.io/badge/Made_with-Lua_&_Rust-orange)]() [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/interesting-vibe-coding/paws/pulls) [![GitHub Stars](https://img.shields.io/github/stars/interesting-vibe-coding/paws?style=flat&color=yellow)](https://github.com/interesting-vibe-coding/paws/stargazers) +[![CI](https://github.com/interesting-vibe-coding/paws/actions/workflows/ci.yml/badge.svg)](https://github.com/interesting-vibe-coding/paws/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Works with Kaku, WezTerm & iTerm2](https://img.shields.io/badge/Works_with-Kaku_%7C_WezTerm_%7C_iTerm2-blue)](https://github.com/interesting-vibe-coding/paws/blob/main/docs/iterm2-setup.md) [![Made with Lua & Rust](https://img.shields.io/badge/Made_with-Lua_&_Rust-orange)]() [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/interesting-vibe-coding/paws/pulls) [![GitHub Stars](https://img.shields.io/github/stars/interesting-vibe-coding/paws?style=flat&color=yellow)](https://github.com/interesting-vibe-coding/paws/stargazers) Agent 工作时尽情玩,需要你时一眼就看到。 @@ -50,9 +50,10 @@ cargo install --git https://github.com/interesting-vibe-coding/paws-games --bin cargo install --git https://github.com/interesting-vibe-coding/paws-games --bin tetris ``` -然后将 [`lua/paws.lua`](lua/paws.lua) 添加到终端配置中(`return config` 之前),并为你的 Agent 配置 hooks(参考 [`hooks/`](hooks/) 目录)。 -- **Kaku:** `~/.config/kaku/kaku.lua` — 重载需按 CMD+Shift+R -- **WezTerm:** `~/.config/wezterm/wezterm.lua` — 保存后自动重载 +然后配置终端集成,并为你的 Agent 配置 hooks(参考 [`hooks/`](hooks/) 目录)。 +- **Kaku:** 将 [`lua/paws.lua`](lua/paws.lua) 添加到 `~/.config/kaku/kaku.lua` 的 `return config` 之前 — 重载需按 CMD+Shift+R +- **WezTerm:** 将 [`lua/paws.lua`](lua/paws.lua) 添加到 `~/.config/wezterm/wezterm.lua` — 保存后自动重载 +- **iTerm2:** 将 [`iterm2/paws.py`](iterm2/paws.py) 复制到 `~/.config/iterm2/scripts/AutoLaunch/`,然后绑定 3 个快捷键 — 详见 [docs/iterm2-setup.md](docs/iterm2-setup.md) ## 游戏 diff --git a/docs/iterm2-setup.md b/docs/iterm2-setup.md new file mode 100644 index 0000000..c7dcc04 --- /dev/null +++ b/docs/iterm2-setup.md @@ -0,0 +1,82 @@ +# Paws × iTerm2 setup + +Paws works in iTerm2 via a small Python AutoLaunch script that registers three +keyboard shortcuts. The `paws` binary itself runs identically in any terminal — +this script handles the tab-create/toggle behavior that makes the workflow smooth. + +## What you get + +| Key | Action | +|-----|--------| +| **Cmd+G** | First press: open the game picker. After that: toggle agent ↔ game. | +| **Cmd+Shift+P** | Close the game tab and re-open the picker. | +| **Cmd+H** | Open the Paws repo in your browser. | + +## Install + +### 1. Copy the script + +```bash +mkdir -p ~/.config/iterm2/scripts/AutoLaunch +cp iterm2/paws.py ~/.config/iterm2/scripts/AutoLaunch/paws.py +``` + +iTerm2 automatically runs scripts in `AutoLaunch/` at startup. The script +registers the three RPC functions above and keeps running in the background. + +### 2. Reload scripts + +In iTerm2: **Scripts → AutoLaunch → paws.py** (or restart iTerm2). + +You only need to do this once — after that it auto-starts on every launch. + +### 3. Bind the three keys + +Open **iTerm2 → Settings → Keys → Key Bindings**, then click **+** three times: + +| Keyboard Shortcut | Action | Parameter | +|-------------------|--------|-----------| +| `Cmd+G` | Invoke Script Function | `paws_toggle()` | +| `Cmd+Shift+P` | Invoke Script Function | `paws_picker()` | +| `Cmd+H` | Invoke Script Function | `paws_help()` | + +For each binding: click **+** → set the key combo → choose **"Invoke Script Function"** from the Action drop-down → type the function name exactly as shown above. + +> **Note:** "Invoke Script Function" only appears in the Action list once the +> script is loaded. Reload first (step 2) if you don't see it. + +### 4. Done + +Press **Cmd+G** — the game picker appears in a new tab. The HUD on the top row +shows your agent sessions. Press **Cmd+G** again to toggle back. + +## How it works + +The script (`iterm2/paws.py`) uses the [iTerm2 Python API](https://iterm2.com/python-api/): + +- On `paws_toggle()`: looks for an existing paws tab (by ID, persisted to + `~/.config/paws/iterm2-tab-id`). If none exists, spawns a new tab running + `paws` via a login shell (so `~/.cargo/bin` is on PATH). If you're already on + the paws tab, switches back to the previous tab. +- On `paws_picker()`: closes the existing paws tab and opens a fresh one. +- On `paws_help()`: calls `open` to launch the repo URL. + +## Troubleshooting + +**"Invoke Script Function" not in the Action list** +The script isn't loaded yet. Go to **Scripts → AutoLaunch → paws.py** to +start it, then try the key binding dialog again. + +**`paws: command not found`** +Run `cargo install --path .` from the paws repo, then open a new terminal +tab and try again. + +**State mismatch after closing the game tab manually** +Delete `~/.config/paws/iterm2-tab-id` — Paws will treat the next Cmd+G as +a fresh start. + +## Difference vs WezTerm / Kaku + +iTerm2 setup requires one extra step: wiring keybindings in the GUI (3 clicks +per binding). WezTerm/Kaku uses a Lua config file where Paws ships the bindings +as code. The runtime behavior is identical once set up. diff --git a/iterm2/paws.py b/iterm2/paws.py new file mode 100644 index 0000000..56914bf --- /dev/null +++ b/iterm2/paws.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Paws 🐾 — iTerm2 integration. + +Install +------- + cp iterm2/paws.py ~/.config/iterm2/scripts/AutoLaunch/paws.py + chmod +x ~/.config/iterm2/scripts/AutoLaunch/paws.py + +Then bind three keys in iTerm2 → Settings → Keys → Key Bindings (+): + Cmd+G → Invoke Script Function → paws_toggle() + Cmd+Shift+P → Invoke Script Function → paws_picker() + Cmd+H → Invoke Script Function → paws_help() + +iTerm2 auto-starts this script at launch. State (current paws tab ID) is kept +in ~/.config/paws/iterm2-tab-id so it survives config reloads. +""" + +import iterm2 +import os +import subprocess + +_STATE_FILE = os.path.expanduser("~/.config/paws/iterm2-tab-id") + + +def _read_tab_id() -> str | None: + try: + return open(_STATE_FILE).read().strip() or None + except FileNotFoundError: + return None + + +def _save_tab_id(tab_id: str) -> None: + os.makedirs(os.path.dirname(_STATE_FILE), exist_ok=True) + open(_STATE_FILE, "w").write(tab_id) + + +def _clear_tab_id() -> None: + try: + os.remove(_STATE_FILE) + except FileNotFoundError: + pass + + +async def _find_paws_tab(window, tab_id: str | None): + if tab_id is None: + return None + for tab in window.tabs: + if tab.tab_id == tab_id: + return tab + return None + + +async def _spawn_paws(window) -> None: + shell = os.environ.get("SHELL", "/bin/zsh") + tab = await window.async_create_tab() + # Login shell so ~/.cargo/bin is on PATH + await tab.current_session.async_send_text(f"{shell} -l -c paws\n") + _save_tab_id(tab.tab_id) + + +async def main(connection): + app = await iterm2.async_get_app(connection) + + @iterm2.RPC + async def paws_toggle(window_id=iterm2.Reference("id")): + """Cmd+G — open game picker, or toggle between agent and game.""" + window = app.get_window_by_id(window_id) + if not window: + return + + paws_tab = await _find_paws_tab(window, _read_tab_id()) + + if paws_tab is None: + await _spawn_paws(window) + elif window.current_tab.tab_id == paws_tab.tab_id: + # On paws tab → switch back to the previous tab + tabs = window.tabs + idx = next(i for i, t in enumerate(tabs) if t.tab_id == paws_tab.tab_id) + prev_idx = idx - 1 if idx > 0 else (1 if len(tabs) > 1 else None) + if prev_idx is not None: + await tabs[prev_idx].async_select() + else: + await paws_tab.async_select() + + await paws_toggle.async_register(connection, paws_toggle) + + @iterm2.RPC + async def paws_picker(window_id=iterm2.Reference("id")): + """Cmd+Shift+P — close the game tab and reopen the picker.""" + window = app.get_window_by_id(window_id) + if not window: + return + + paws_tab = await _find_paws_tab(window, _read_tab_id()) + if paws_tab: + await paws_tab.async_close(force=True) + _clear_tab_id() + await _spawn_paws(window) + + await paws_picker.async_register(connection, paws_picker) + + @iterm2.RPC + async def paws_help(window_id=iterm2.Reference("id")): + """Cmd+H — open the Paws repo in your browser.""" + subprocess.run(["open", "https://github.com/interesting-vibe-coding/paws"]) + + await paws_help.async_register(connection, paws_help) + + +iterm2.run_forever(main) diff --git a/skills/paws-install/SKILL.md b/skills/paws-install/SKILL.md index 56d5bbe..815f903 100644 --- a/skills/paws-install/SKILL.md +++ b/skills/paws-install/SKILL.md @@ -11,12 +11,13 @@ You are installing Paws for the user. Work from a local clone of this repo ## 0. Preconditions -- Confirm the terminal is **Kaku** (`which kaku`) or **WezTerm** (`which wezterm`). - Both are supported — the same `lua/paws.lua` works in both without modification. - If neither is installed, ask the user to install one first and stop. -- Determine the terminal type — it affects the config path in Step 2: - - Kaku: `~/.config/kaku/kaku.lua` - - WezTerm: `~/.config/wezterm/wezterm.lua` +- Confirm the terminal is **Kaku** (`which kaku`), **WezTerm** (`which wezterm`), + or **iTerm2** (`ls /Applications/iTerm.app`). All three are supported. + If none is installed, ask the user to install one first and stop. +- Determine the terminal type — it affects Step 2: + - Kaku: `~/.config/kaku/kaku.lua` (Lua config) + - WezTerm: `~/.config/wezterm/wezterm.lua` (Lua config) + - iTerm2: Python script + manual key bindings (see Step 2b) - Note the repo root (absolute path) — you'll need it for hook paths. **All hook paths in config files must be absolute** — `~` is not expanded by any of the three agents. @@ -48,7 +49,7 @@ Any games not installed now can be installed later directly from the in-app game picker (uninstalled entries show "⤓ install" and run the install command on Enter). -## 2. Merge the Lua into the terminal config +## 2a. Merge the Lua into the terminal config (Kaku / WezTerm) The terminal config returns a `config` table at the end. The snippet to insert is `lua/paws.lua` from this repo — it works identically in Kaku and WezTerm. @@ -65,6 +66,24 @@ Steps: add `config.keys = config.keys or {}` at the top of the inserted block. - Syntax-check afterward: `luac -p ` (if `luac` exists). +## 2b. Install the Python script (iTerm2 only) + +Skip this step if the user is on Kaku or WezTerm. + +```bash +mkdir -p ~/.config/iterm2/scripts/AutoLaunch +cp /iterm2/paws.py ~/.config/iterm2/scripts/AutoLaunch/paws.py +``` + +Then tell the user to: +1. In iTerm2: **Scripts → AutoLaunch → paws.py** to load the script. +2. Open **Settings → Keys → Key Bindings** and add three bindings: + - `Cmd+G` → Invoke Script Function → `paws_toggle()` + - `Cmd+Shift+P` → Invoke Script Function → `paws_picker()` + - `Cmd+H` → Invoke Script Function → `paws_help()` + +Full details: [docs/iterm2-setup.md](../../docs/iterm2-setup.md) + ## 3. Wire the agent's state signals (for the status HUD) Paws ships two hook scripts that write session state to `/tmp/paws-sessions/`