跳至主要內容
Gray Tsao
回文章列表
發佈於
分鐘
6

Jot:三秒鐘寫下一個念頭,從任何地方

一個住在 macOS 選單列的快速筆記工具:按 ⇧⌘J、打字、按 Return,筆記存好、面板消失。沒有帳號、沒有網路、沒有「要放哪本筆記本」——順便講清楚中文搜尋為什麼要換分詞器,以及三個要真的打開 app 才看得到的鍵盤 bug。

筆記 app 輸的是摩擦,不是功能

一個念頭出現的時候,通常正在做別的事:在讀一份 PDF、在回一封信、在等一個 build 跑完。

大多數筆記 app 在這一刻要求兩件事:切過去,然後決定這則筆記屬於哪本筆記本、哪個資料夾、哪個標籤。這兩個決定各自只要兩秒,但加起來剛好長到讓人想「等一下再記」。等一下,通常就是沒有。

Jot 只做一件事:把這兩個決定從「寫下來」的路徑上拿掉。

快捷鍵、打字、Return

Jot 住在 macOS 的選單列。在任何 app 裡按 ⇧⌘J,一個浮動面板會出現在螢幕偏上方;打字,按 Return,筆記存好、面板消失。整個過程不切換 app,也沒有人問你要放哪裡。

還有第二個入口:點選單列的圖示,會開一個錨在圖示下方的 popover。兩個入口共用同一份輸入、同一份草稿。

鍵盤就這幾個:

按鍵 做什麼
⇧⌘J 從任何 app 叫出擷取面板(可改)
Return 存檔並關閉
⇧Return 換行
Esc 關閉,草稿留著
⌘F 開回顧視窗並開始搜尋
⌘⌫ 把正在編輯的筆記移到垃圾桶
⌘Z 復原——包括刪除

Esc 跟點到外面都不會丟掉打到一半的字,重開 app 也還在。整個 app 沒有任何確認對話框,因為沒有一個動作是回不了頭的:刪除進垃圾桶,垃圾桶在你選的天數之後自己清空,而 ⌘Z 可以馬上把剛刪掉的拿回來。

日誌,不是收件匣

「決定放哪裡」並沒有消失,只是搬到另一個你進去頻率低很多的模式。

回顧視窗是兩欄。左欄列出有筆記的日子,最新的在最上面;右欄是那一天的串流,最早的在最上面,打開時捲到最底——也就是最新那一則的位置。兩欄方向刻意相反:索引要把最常伸手拿的放在手邊,串流則往下流。

要分類的話,在內文打 #工作 就好。標籤在寫入時解析,點一下等於精確的標籤搜尋。從來不是必填,也沒有下拉選單。

中文搜尋為什麼會找不到東西

這是整個專案花最多心思的一段。

SQLite 的 FTS5 預設用 unicode61 分詞器。它切英文切得很好——照空白跟標點切——但它不會切中文。一句中文會被當成一個 token 整個塞進索引,於是搜「租約」只會在某一則筆記剛好整句就是「租約」的時候找到東西。

Jot 改用 trigram 分詞器:索引每三個字元的滑動窗格,任何文字系統都能做真正的子字串搜尋。

代價是三個字元以下的查詢永遠比不到——而兩個字的中文詞,大概是最常被搜的東西。所以短於三個字元的查詢不走索引,退回 LIKE 全掃。在個人筆記的規模下這個掃描感覺不到,而它是「租約」找得到跟找不到的差別。

查詢還有一層分類:整段輸入剛好就是一個標籤時(#work),做的是精確的標籤查詢,這樣 #work 不會把 #workflow 也拖進來;#work meeting 則是普通的文字搜尋。文字查詢會整串包成 FTS5 的 phrase,使用者打的標點跟運算子不會變成查詢語法,也不會變成語法錯誤。

標籤解析另外有幾條規則,為了不讓一般文字長出意外的標籤:

  • # 必須在開頭、或接在空白之後,所以 https://x#anchorC# 不會變成標籤
  • 至少要含一個字母,所以「issue #1」還是一般文字
  • 「字母」是任何文字系統的字母,所以 #工作 是標籤

三個要真的打開 app 才看得到的 bug

單元測試目前 60 個,涵蓋 repository、標籤解析、查詢分類、匯出跟在地化——全是不碰 UI 的純邏輯。真正把 app 跑起來、用腳本按快捷鍵、打字、按 Return 之後,抓到三個測試永遠不會抓到的 bug:

  • 快速打字會掉字。 一個按鍵落在 SwiftUI 重繪還在進行的當下,會被過期的 binding 值蓋掉;打 jot e2e check #testtag,存下來的是 ez。修法是 text view 只接受不是自己發出的寫入。
  • ⌘A、⌘C、⌘V、⌘X、⌘Z 在擷取欄位裡沒反應。 AppKit 把這些按鍵透過主選單分派,而一個 LSUIElement(只出現在選單列)的 app 預設沒有主選單——貼上,一個連結進到筆記工具最常見的方式,是壞的。現在裝了一個看不見、但帶著 key equivalent 的選單。
  • 別的 app 在前景時,面板永遠拿不到鍵盤。 .nonactivatingPanel 這個 flag 會讓 app 留在背景,而背景 app 的面板不會成為 key window,按鍵繼續送給原本在前面的 app。順序也要對:先把面板 order front、再 activate;反過來的話,焦點會交給這個 app 裡已經在最前面的視窗。

第四個不算 bug,但每次啟動都會煩:macOS 會在 com.apple.provenance 記下每個檔案從哪裡來,一個帶著 Documents 資料夾來源的 app,每次啟動都會跳出「想要存取你的 Documents 資料夾」。build script 在簽章之前把這個屬性剝掉。

幾個刻意的選擇

  • 全域快捷鍵用 Carbon 的 RegisterEventHotKey,不用 NSEvent 的全域監聽。 後者需要「輔助使用」權限——對一個筆記 app 來說是一個嚇人的系統提示。前者不用。
  • 快捷鍵從一份預先檢查過的清單裡選,不是現場錄。 錄製器要有衝突偵測才值得存在,而那比看起來大得多。清單刻意排除了 ⌘Space、⌃Space、⌥⌘Space 這幾個 macOS 自己保留的組合。
  • 資料庫放在 Application Support,不放 Documents 或任何雲端同步的資料夾。 SQLite 的 write-ahead log 跟檔案層級的同步引擎會安靜地互相弄壞對方,這樣丟掉的筆記本救不回來。要拿走就用匯出:Markdown 依天分組,或帶著已解析標籤的 JSON。
  • 完全不連網。 沒有帳號、沒有同步、沒有任何網路請求。
  • 不需要 Xcode。 Swift 6 加 Command Line Tools 就能建置:SwiftPM 產出二進位,build.sh 包成 .app 並做 ad-hoc 簽章。唯一的坑是 Command Line Tools 附的 Testing.framework 不在 SwiftPM 的搜尋路徑上,swift test 會說找不到模組,得用 test.sh 把路徑指過去。

現在還不行的事

  • 沒有連結標題、圖片或附件。schema 跟 migration 已經留好位置。
  • 沒有簽章、公證跟自動更新。所以請自己 build——本機建的 app 沒有 quarantine 旗標,Gatekeeper 不會管它;下載來的就會。
  • 沒有同步。匯出的存在,就是為了讓這不是一個陷阱。
  • UI 沒有自動化測試。上面那三個 bug 全靠手動驅動 app 抓出來;Scripts/ 裡有一支 CGEvent 點擊器,因為合成點擊點不到 SwiftUI 的 view。

v1,我每天在用;還沒打包給其他人。

拿去用

https://github.com/Grayidea-bit/Jot(MIT)

git clone git@github.com:Grayidea-bit/Jot.git
cd Jot
./build.sh
cp -R build/Jot.app /Applications/
open /Applications/Jot.app

需要 macOS 14 以上。第一次啟動會開回顧視窗的空狀態,因為這個 app 其他的一切,都藏在一個你還沒看過的快捷鍵跟選單列圖示後面。

04聯絡
所在地
新北,台灣 · UTC+8
狀態
2026年6月畢業 · 軟體工程職缺洽談中

說點具體的

寄信最快。我會在一個工作天內回覆,通常帶著比答案更多的問題。

或直接在這裡留言
Gray Tsao© 2026 · 最後更新 2026年9月