Metadata-Version: 2.4
Name: tokyo-train-board
Version: 0.1.0
Summary: Terminal split-flap (Solari) departure board for JR / Tokyo Metro / Toei lines.
Author: jrboard contributors
License: MIT
Project-URL: Homepage, https://github.com/JaeggerJose/tokyo-train-board
Project-URL: Repository, https://github.com/JaeggerJose/tokyo-train-board
Project-URL: Issues, https://github.com/JaeggerJose/tokyo-train-board/issues
Keywords: terminal,tui,ansi,split-flap,jr,tokyo-metro,timetable
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Terminals
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Provides-Extra: live
Requires-Dist: requests; extra == "live"

# 🚉 JR / Tokyo Metro Split-Flap Board

[English](README.en.md) · **繁體中文** · [日本語](README.ja.md)

終端機裡的 JR／東京地鐵 **翻牌式（split-flap / Solari board）時刻表站牌**模擬器。

每次刷新會先像老火車站、航站樓的機械翻牌那樣亂數翻滾，再逐格鎖定成真實的到站資訊。
20 條路線、資料驅動、可接 ODPT 即時資料，並提供可嵌入 Claude Code 狀態列的單行跑馬燈模式。

![demo](demo.gif)

```text
+----------------------------------------------------------+
|   JY   山手線  Yamanote Line                             |
|   JY 17                     新  宿                       |
|                  しんじゅく   Shinjuku                   |
|  ◀ 新大久保                ■                   代々木 ▶  |
|  < Shin-Okubo           Shinjuku               Yoyogi >  |
+----------------------------------------------------------+
| 時刻   | 種別       | 行先 (方面)                | 番線  |
|----------------------------------------------------------|
| 15:45  | 各駅停車   | 品川・渋谷方面             | 1     |
| 15:45  | 各駅停車   | 上野・池袋方面             | 2     |
| 15:50  | 各駅停車   | 品川・渋谷方面             | 1     |
| ...    | ...        | ...                        | ...   |
|----------------------------------------------------------|
| Yamanote Line                                src: STATIC |
+----------------------------------------------------------+
```

> 終端機實際輸出帶有各線的官方代表色（山手線綠、銀座線橘、丸ノ内線紅⋯⋯）。

---

## ✨ 功能

- **真實資料**：可接 [ODPT](https://www.odpt.org/)（公共交通開放資料平台）即時時刻表；未設定金鑰時自動回退到內建的真實靜態時刻表。站牌右下角誠實標示資料來源 `src: ODPT` / `src: STATIC`。
- **20 條路線可切換**，全部資料驅動（見下方路線表）。
- **翻牌動畫**：經典 split-flap / Solari board 效果，速度可調。
- **單行跑馬燈模式**：站名釘住、班次捲動，可嵌入 Claude Code 狀態列。
- **CJK 對齊**：以 `east_asian_width` 處理日文全形字佔 2 格的問題，每行精準對齊到固定視覺寬度。

---

## 🚀 快速開始

```bash
# （選用）只有要接 ODPT 即時資料時才需要
pip install requests

# 列出全部 20 條路線與站點
python3 main.py --list

# 全站牌 + 翻牌動畫（預設山手線新宿，每 10 秒刷新；Ctrl-C 結束）
python3 main.py
```

`--station` 接受**英文名、站號或站 id**，大小寫不拘（例：`--station shinjuku`、`--station 17`、`--station JY17`）。

```bash
# 指定路線／站點，渲染一次就結束
python3 main.py --once --line ginza        --station ginza
python3 main.py --once --line keihintohoku --station tokyo --no-flap

# 翻牌速度微調
python3 main.py --line yamanote --station shinjuku --flap-delay 0.15   # 更慢、更機械感
python3 main.py --line yamanote --station shinjuku --flap-steps 8      # 更少格、更俐落

# 單行跑馬燈
python3 main.py --mode statusline --line oedo --station tochomae --columns 70
python3 main.py --mode statusline --line marunouchi --station tokyo --columns 70 --scroll-all
```

---

## 🧩 更多模式：TUI、番茄鐘、通勤守門員、行事曆

除了站牌與跑馬燈，同一支 CLI 還提供四種附加模式（所有舊旗標行為不變，這些都是選用）。

```bash
# 互動式 curses 瀏覽器：左邊是「依城市分組」的路線清單（各線官方色，
# j/k 移動、/ 模糊搜尋、h/l 切換站點、f 收藏、q 離開），右邊是即時彩色站牌
# （切換線/站時會播翻牌動畫）
python3 main.py --tui

# 番茄鐘＝一趟電車旅程：把專注計時畫成從起點到終點的乘車。會在路線上
# 自動挑兩站（或用 --from/--to 指定），先播翻牌動畫，之後每秒重繪，
# 直到「とうちゃく」（抵達）。
python3 main.py --pomodoro 25 --line yamanote
python3 main.py --pomodoro 25 --line yamanote --from shinjuku --to tokyo
python3 main.py --pomodoro 1 --line yamanote --once   # 只畫一格就結束

# 通勤守門員：「我幾點要出門才趕得上下一班車？」需要在設定檔填
# [commute] home/work（見下）。早上 → 家→公司，下午／晚上 → 公司→家。
python3 main.py --commute                       # 完整站牌
python3 main.py --commute --mode statusline     # 精簡單行

# 行事曆來源：用本機 .ics 檔當作發車來源（標籤 AGENDA），把接下來的
# 會議當成電車顯示，而非時刻表。
python3 main.py --feed-ics ~/cal.ics --once
python3 main.py --feed-ics ~/cal.ics --mode statusline --columns 70
```

### 設定檔

設定讀自 `~/.config/jrboard/config.toml`（會尊重 `XDG_CONFIG_HOME`）。檔案缺失或格式錯誤會直接忽略——一律套用預設值，而 CLI 旗標永遠覆寫設定檔。

```toml
[board]
line = "oedo"
station = "tochomae"
columns = 50
width = 60
flap_steps = 22
flap_delay = 0.08

[commute]
home = ["yamanote", "shinjuku"]
work = ["yamanote", "tokyo"]
leave_buffer_min = 7      # 走到車站的緩衝分鐘數
```

在 TUI 中切換的收藏會存到 `~/.config/jrboard/favorites.txt`（每行一組 `line_key,station_key`）。

### 安裝／進入點

```bash
pip install -e .        # 安裝 `jrboard` 命令列腳本
jrboard --list          # 與 `python3 main.py` 相同的 CLI
jrboard --tui
```

---

## 🚇 路線一覽（20 條）

| 代碼 | `--line` key | 路線 | 站數 | 範例站 |
|:----:|------|------|:----:|------|
| JY | `yamanote` | 山手線（環狀）| 30 | `shinjuku` |
| JC | `chuo` | 中央線快速 | 24 | `tokyo` |
| JB | `sobu` | 中央・総武線各駅停車 | 39 | `akihabara` |
| JK | `keihintohoku` | 京浜東北・根岸線 | 47 | `tokyo` |
| JA | `saikyo` | 埼京線 | 19 | `osaki` |
| JS | `shonanshinjuku` | 湘南新宿ライン | 19 | `shinjuku` |
| JO | `yokosuka` | 横須賀線 | 19 | `yokohama` |
| G | `ginza` | 東京メトロ銀座線 | 19 | `ginza` |
| M | `marunouchi` | 東京メトロ丸ノ内線 | 25 | `tokyo` |
| H | `hibiya` | 東京メトロ日比谷線 | 22 | `naka-meguro` |
| T | `tozai` | 東京メトロ東西線 | 23 | `otemachi` |
| C | `chiyoda` | 東京メトロ千代田線 | 19 | `yoyogi-uehara` |
| Y | `yurakucho` | 東京メトロ有楽町線 | 24 | `wakoshi` |
| Z | `hanzomon` | 東京メトロ半蔵門線 | 14 | `shibuya` |
| N | `namboku` | 東京メトロ南北線 | 19 | `meguro` |
| F | `fukutoshin` | 東京メトロ副都心線 | 16 | `wakoshi` |
| A | `asakusa` | 都営浅草線 | 20 | `asakusa` |
| I | `mita` | 都営三田線 | 27 | `meguro` |
| S | `shinjuku` | 都営新宿線 | 21 | `shinjuku` |
| E | `oedo` | 都営大江戸線 | 39 | `tochomae` |

> `--line shinjuku` 指的是**都營新宿線**（地鐵）；JR 線各有獨立 key（`chuo`/`sobu`/…）。

### 🌏 其他城市（京都／大阪／札幌／小樽）

用 `--city` 篩選城市：`python3 main.py --list --city Osaka`；`--rotate --city Osaka` 只在大阪境內隨機巡迴。

| key | 城市 | 路線 | 站數 |
|------|:----:|------|:----:|
| `osaka-loop` | 大阪 | JR 大阪環狀線（環狀）| 19 |
| `osaka-midosuji` | 大阪 | 御堂筋線 | 20 |
| `osaka-tanimachi` | 大阪 | 谷町線 | 26 |
| `kyoto-karasuma` | 京都 | 地下鐵烏丸線 | 15 |
| `kyoto-tozai` | 京都 | 地下鐵東西線 | 17 |
| `kyoto-randen` | 京都 | 嵐電 嵐山本線（路面電車）| 13 |
| `kyoto-sagano` | 京都 | JR 嵯峨野線（山陰本線）| 15 |
| `kyoto-keihan` | 京都 | 京阪本線 | 42 |
| `sapporo-namboku` | 札幌 | 南北線 | 16 |
| `sapporo-tozai` | 札幌 | 東西線 | 19 |
| `sapporo-toho` | 札幌 | 東豊線 | 14 |
| `otaru-hakodate` | 小樽 | JR 函館本線（小樽—札幌）| 15 |

---

## 🛰️ 接 ODPT 即時資料

1. 到 <https://developer.odpt.org/> 免費申請 consumer key。
2. 設環境變數後執行：

```bash
export ODPT_KEY="你的金鑰"
python3 main.py --line yamanote --station shinjuku   # 站牌右下角會顯示 src: ODPT
```

抓不到（無金鑰、API 錯誤、空資料）時會自動回退靜態時刻表並在 stderr 記錄原因，站牌顯示 `src: STATIC`。

> 🔐 金鑰請勿寫進程式或 commit，用環境變數即可（`.env` 已被 `.gitignore` 忽略）。

---

## 📟 接進 Claude Code 狀態列（statusLine）

多行站牌**不適合**狀態列（會佔 16 行），請用單行 `statusline` 模式。它會把站名釘在最左、班次像燈條捲過去：

```text
[JY] 17 新宿 ▸ 15:45 品川・渋谷方面  15:45 上野・池袋方面  15:50 …
```

在 `~/.claude/settings.json`：

```json
{
  "statusLine": {
    "type": "command",
    "command": "python3 /Users/minghsuan/Downloads/JR-timetable/main.py --mode statusline --line yamanote --station shinjuku --columns 80"
  }
}
```

兩個狀態列的本質限制（已在設計內處理）：

1. **非 TTY**：statusLine 指令沒有終端寬度，**必須用 `--columns N` 明確給寬度**，跑馬燈才會捲動。
2. **非計時器驅動**：Claude Code 只在 render（有活動）時才重跑指令，所以跑馬燈是「每次刷新前進一格」而非閒置時平滑流動。捲動偏移量由當前時間推導，因此每次刷新位置都不同。

`--scroll-all` 可改成整行一起捲動（連站名一起跑）。

### 用 csl 主題（推薦，會動的跑馬燈）

若你用 [`csl`](https://) 狀態列主題管理器，本專案附了一個現成主題 `integrations/csl/jr-board.sh`（＋ manifest）。它覆寫 `render()` 呼叫上面的跑馬燈，並靠 `settings.json` 的 `refreshInterval: 1` 達成**每秒前進一格的真實捲動**：

```bash
# 安裝主題到 user-tier 後啟用
cp integrations/csl/jr-board.* ~/.config/csl/themes/
csl preview jr-board     # 先試跑一次
csl set jr-board         # 啟用（自動改寫 settings.json 並備份）
csl set bastille-day     # 隨時切回原本的主題
```

在 `jr-board.sh` 頂部可調 `JR_LINE` / `JR_STATION` / `JR_COLUMNS`（越窄越會捲）/ `JR_SCROLL_ALL`。

---

## 🎞️ 翻牌動畫調校

| 旗標 | 預設 | 效果 |
|------|:----:|------|
| `--no-flap` | — | 跳過動畫，直接畫出結果 |
| `--flap-steps N` | 22 | 從全亂到解出的影格數；越大越漸進 |
| `--flap-delay S` | 0.08 | 每影格停留秒數；越大越慢 |

預設整段動畫約 2 秒。動畫「以什麼順序鎖定」由 `jrboard/flap.py` 的 `lock_threshold()` 決定（目前是由左往右擦除＋微抖動），想改成隨機落定或 ease-out 收尾，調這個函式即可，其他模組不用動。

---

## 🏗️ 架構

**資料驅動**：引擎與路線無關，**新增路線 = 在 `jrboard/data/` 丟一個 `<key>.json`，不必改任何程式**。

| 模組 | 職責 |
|------|------|
| `jrboard/width.py` | CJK 全形字 / ANSI 視覺寬度計算與對齊 |
| `jrboard/model.py` | `Line` / `Station` 資料模型，載入 `data/*.json` |
| `jrboard/sources.py` | 時刻表來源（ODPT + 靜態備援，repository pattern）|
| `jrboard/flap.py` | split-flap 翻牌動畫引擎（純函式、可測）|
| `jrboard/render.py` | 站牌 + 時刻表 ANSI 渲染 |
| `jrboard/statusline.py` | 單行跑馬燈 |
| `jrboard/cli.py` | argparse CLI 與刷新排程 |
| `jrboard/data/*.json` | 各路線的站點 + 時刻表資料 |

### 新增一條路線

在 `jrboard/data/` 放一個 `<key>.json`，照既有檔的結構填：

```jsonc
{
  "key": "tokaido",
  "name_jp": "東海道線", "name_en": "Tokaido Line",
  "symbol": "JT",
  "color": { "name": "...", "ansi_fg": "[38;5;208m",
             "ansi_bg": "[48;5;208m[38;5;232m", "hex": "#F68B1E" },
  "operator": "JR-East",
  "odpt_railway": "odpt.Railway:JR-East.Tokaido",
  "loop": false,
  "stations": [
    { "id": "JT01", "number": "01", "name_jp": "東京", "kana": "とうきょう",
      "name_en": "Tokyo", "odpt_station": "odpt.Station:JR-East.Tokaido.Tokyo" }
  ],
  "timetable": {
    "first_train": "04:30", "last_train": "00:30",
    "headway_min": { "weekday": { "7": 5, "8": 4, "...": 0 }, "holiday": { "...": 0 } },
    "directions": [
      { "id": "down", "name_jp": "熱海方面", "via_jp": "横浜方面", "track": "1" },
      { "id": "up",   "name_jp": "東京方面", "via_jp": "品川方面", "track": "2" }
    ]
  }
}
```

存檔後 `python3 main.py --list` 立刻就會出現這條線。

---

## ✅ 測試

```bash
python3 -m pytest tests -q      # 47 tests
```

涵蓋：視覺寬度（CJK＋ANSI）、資料模型（載入／找站／環狀鄰站）、時刻表來源（靜態產生與 ODPT 備援切換）、翻牌（寬度保持、最後一幀完全解出）、狀態列（站名釘住、跑馬燈隨時間前進）。
