# 開發過程摘要 記錄這個專案的演進脈絡、遇到的問題、以及為什麼做出這些選擇。 ## 需求演進 | 階段 | 需求 | 產出 | |------|------|------| | 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`(幣別 → 輸入字串)。為什麼存字串而不是 `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` 等端點可以抓歷史,但就不是「即期」而是均價了。