# 開發過程摘要

記錄這個專案的演進脈絡、遇到的問題、以及為什麼做出這些選擇。

## 需求演進

| 階段 | 需求 | 產出 |
|------|------|------|
| 1 | 抓台銀 USD / JPY / EUR 即期賣出 | Python CLI (`bot_rates.py`) |
| 2 | 要給別人用、Windows、GUI、雙擊即跑、不要裝東西 | Python tkinter + PyInstaller 方案 |
| 3 | 太麻煩，改用 Rust，使用者無 Windows 機器 | Rust + `eframe` + 交叉編譯 |
| 4 | 也給我一份 macOS 能跑的 | `BotRates.app` bundle |
| 5 | 壓縮 exe 方便傳送 | `bot_rates.zip`（5.4 MB → 2.7 MB） |
| 6 | 加上金額輸入 / 即時換算 | 三欄布局：幣別 / 輸入 / TWD |
| 7 | 換算結果要能直接複製 | 「複製」按鈕 + 可選取文字 |

## 階段 1：Python CLI

直接用 `urllib` 抓台銀 CSV，起初用 `csv.DictReader`，但踩到一個坑——CSV 有**重複欄名**（買入、賣出區段都用 `匯率 / 現金 / 即期`），`DictReader` 只會保留最後一個同名欄位，所以找不到「本行即期賣出」。

**解法**：改用位置索引，`即期賣出` 在 col 13。

## 階段 2：Python GUI（最終未採用）

計畫：`tkinter` + ttk 美化 + `PyInstaller --onefile --windowed` 打包成 exe。寫了 `bot_rates_gui.py` 和 `build.bat`。

**問題**：使用者手上沒有 Windows 機器，PyInstaller 必須在目標 OS 上執行，無法從 macOS 直接產出 Windows exe。只能給使用者 `build.bat` 要他在 Windows 上自己打包——違背「不要裝東西」的初衷。

## 階段 3：改用 Rust

Rust 可以從 macOS 乾淨地交叉編譯到 Windows，單一靜態執行檔，零 runtime 依賴，剛好解決 Python 方案的痛點。

### 技術選型

| 選擇 | 理由 |
|------|------|
| `eframe` / `egui` | Immediate mode GUI，使用 OpenGL（Windows 內建），不需要 GTK / Qt 這類系統依賴。交叉編譯幾乎沒坑。 |
| `ureq` + `rustls` | `reqwest` 預設用 `native-tls` 會綁 OpenSSL 或 Windows SChannel，交叉編譯會有系統 library 問題。`ureq` 搭 `rustls` 是純 Rust，無系統依賴。 |
| `x86_64-pc-windows-gnu` | 用 mingw-w64 當 linker，比 `-msvc` 少了需要 Windows SDK 的麻煩。`brew install mingw-w64` 就好。 |

### 踩過的坑

**LLVM 在 release 編譯時 SIGSEGV**

```
error: rustc interrupted by SIGSEGV, printing backtrace
llvm::DeadArgumentEliminationPass::run
```

第一次 release 編譯時，`Cargo.toml` 用了 `opt-level = "z"` + `lto = true`，LLVM 的 DeadArgumentElimination pass 在處理 `eframe` 依賴時堆疊用光。

**解法**：
1. 把 `opt-level` 從 `"z"` 改成 `3`、`lto` 從 `true` 改成 `"thin"`（降低 pass 壓力）
2. 加上 `RUST_MIN_STACK=16777216`（把執行緒堆疊從預設 2MB 拉到 16MB）

兩個同時做下去就過了。

**CJK 字型**

egui 預設字型不含中文字，幣別名稱會顯示成「豆腐」。

**解法**：不 bundle 字型（會讓 exe 大 5-15MB），改在執行期讀取作業系統字型。
- Windows：`C:\Windows\Fonts\msjh.ttc`（微軟正黑體）
- macOS：`/System/Library/Fonts/PingFang.ttc`

fallback 鏈有多個候選（msjhl / msyh / simhei），找到第一個能讀的就用。

## 階段 4：macOS `.app` bundle

`cargo build --release` 只產出裸執行檔，從 Finder 雙擊會跳 terminal 然後才開 GUI，體驗差。

**解法**：手動組 `.app` bundle——其實就是特定目錄結構加一個 `Info.plist`：

```
BotRates.app/
├── Contents/
│   ├── Info.plist           # 告訴 Finder 這是 app
│   └── MacOS/BotRates       # 實際執行檔
```

寫 plist 時標示 `CFBundlePackageType = APPL` 和 `CFBundleExecutable = BotRates` 即可。另外跑 `xattr -cr` 清掉 quarantine 屬性避免 Gatekeeper 擋下。

## 階段 5：壓縮

`bot_rates.exe` 5.4 MB，`zip -9` 後 2.7 MB（壓縮率 50%）。PE 檔因為有大量重複的 LLVM / rustc 樣板程式碼壓縮效果很好。

## 階段 6：金額換算

重新設計每一列為三欄 horizontal layout：

```
[幣別+匯率 寬110]  [輸入 寬120]  [ → ]  [TWD 靠右]
```

狀態放在 `App.amounts: HashMap<String, String>`（幣別 → 輸入字串）。為什麼存字串而不是 `f64`？因為使用者輸入過程中可能有中間態（例如 `"100."`、空字串、`"1,000"`），存字串才能保留原樣。

輸入解析用 `parse_amount`，忽略逗號和空白再 parse。

TWD 顯示用 `format_twd`，手刻千分位 separator（沒引入 `num-format` crate 以減少 binary size）。

## 階段 7：複製功能

兩種複製方式並存：

1. **按鈕複製**（主要 UX）：按「複製」→ `ui.output_mut(|o| o.copied_text = raw)`，複製純數字 `3162.00`（不含 `NT$`、不含逗號）。原因：這樣貼到 Excel 會被識別成數字而不是文字。
2. **選取複製**：`egui::Label::new(...).selectable(true)`，使用者可以用滑鼠拉選 TWD 文字後 `Cmd/Ctrl+C`。

按下按鈕後短暫顯示「已複製」（1.2 秒），用 `Option<(String, Instant)>` 追蹤狀態 + `ctx.request_repaint_after` 觸發回復重繪。

## 最終成果

| 項目 | 數值 |
|------|------|
| Windows exe 大小 | 5.4 MB（zip 後 2.7 MB） |
| macOS 執行檔大小 | 5.2 MB |
| 執行期依賴 | 無（兩平台皆零依賴） |
| 首次冷啟動 | < 1 秒 |
| 抓匯率耗時 | 網路 + 解析 < 500ms |
| 總原始碼行數 | ~310 行 Rust |

## 如果要擴充

- **加更多幣別**：改 `TARGETS` 即可，其他地方都自動處理。
- **換成即期買入 / 現金匯率**：改 `fetch_rates` 裡的 col index（3 買入 / 13 賣出 / 2 現金買入 / 12 現金賣出）。
- **雙向換算**（TWD → 外幣）：在每列加第二個輸入框，狀態多存一組字串；注意避免無限循環更新。
- **歷史紀錄 / 圖表**：台銀有 `flcsv/1/month` 等端點可以抓歷史，但就不是「即期」而是均價了。
