計與實現(xiàn))
cua 是我最近做的命令行小工具名字來自我敲鍵盤時冒出來的第一感覺——短、響、干脆像有人把抽屜拉開又合上的那一聲。它解決的問題很小但特別煩人代碼里那些反復(fù)出現(xiàn)的片段為什么每次都要重新去翻數(shù)據(jù)庫連接串、正則表達式、部署命令、git 提交信息模板這些內(nèi)容我明明存過卻經(jīng)常要花幾十分鐘重新搜索一遍。于是我用一個周末寫了 cua專門做一件事把常用的文本片段快速存下來再更快地復(fù)制回去。這篇文章記錄需求、設(shè)計、實現(xiàn)和踩坑的完整過程關(guān)鍵代碼和取舍理由都放在下面。如果你也想給自己做一個十分鐘能跑通的效率工具可以拿它當(dāng)參考。1. 為什么我會被一個三字母命令套牢開局痛點與命名由來1.1 這個場景你們多半也遇到過大概在半年多前我接到一個維護老服務(wù)的活兒。那次排查問題需要在服務(wù)器上拼一串參數(shù)復(fù)雜的啟動命令里面包含數(shù)據(jù)庫地址、緩存節(jié)點、日志路徑還有一個帶轉(zhuǎn)義的正則表達式。我先是在搜索引擎里搜了一圈看到好幾篇互相矛盾的文檔又去翻自己本地那個叫 snippets.md 的筆記文件文件已經(jīng)積累了上百條內(nèi)容CtrlF 翻了幾輪才找到一段半年前的記錄復(fù)制粘貼之后發(fā)現(xiàn)里面還混著行號和多余的換行導(dǎo)致命令直接執(zhí)行失敗。那一刻我就意識到問題不在于我懶而在于“存”和“取”這兩個動作被嚴(yán)重割裂。筆記軟件適合寫長文章不適合放零散代碼片段聊天記錄里的代碼可以用但前提是你還能記得是哪一天、哪個群新建文檔就更不現(xiàn)實我總不能為了存一個正則表達式新建一個頁面。我想要的是這樣一類工具操作足夠快快到可以讓我愿意為一條三行代碼付出哪怕五秒鐘的保存時間查找足夠快快到讓我不再依賴瀏覽器的搜索歷史。后來我嘗試過幾個現(xiàn)成的片段管理軟件要么太重要裝客戶端、賬號、同步服務(wù)要么太輕只是把所有的文本塞進一個大文件連最基本的按名稱搜索都做不好。于是我開始認(rèn)真考慮自己寫一個。核心需求其實就三條第一能用命令行直接操作因為寫代碼的時候我已經(jīng)在終端里了第二數(shù)據(jù)必須是我能看懂的文件不能是某個私有格式的數(shù)據(jù)庫第三復(fù)制結(jié)果要進系統(tǒng)剪貼板而不是讓我再手動選中一遍。1.2 cua 這個名字是怎么來的項目一開始的代號是 “snippet-cli”名字太長打起來也累在終端里敲了兩天就覺得煩。有一天我在想這個工具的核心動作其實就是“把片段從口袋里掏出來”那一瞬間腦子里蹦出一個擬聲詞 cua像抽屜開關(guān)的聲音也像按鍵按下的聲音。我試著在終端里敲了三個字母確認(rèn)手感和節(jié)奏都好就決定用它。當(dāng)然為了在跟同事介紹時不至于被追問“cua 到底是什么意思”我后來湊了一個還算能講得通的展開Clipboard Utility for All即面向所有人的剪貼板工具。但說實話這個名字是事后硬湊的真正的原因就是短、好記、不容易撞名。在項目啟動前我特意執(zhí)行了一下which cua確認(rèn)系統(tǒng)里沒有同名命令然后就把這個名字焊死了。這里也提個建議給工具取名一定要先查一次命令是否被占用避免安裝到你電腦上時和系統(tǒng)里的程序沖突。1.3 邊界它不做什么cua 的定位不是筆記軟件也不是密碼管理器更不是 TODO 管理。它的邊界非常清楚只處理純文本片段不做富文本不搞標(biāo)簽系統(tǒng)不為每個片段維護標(biāo)題、作者、創(chuàng)建時間這些元數(shù)據(jù)。原因也很直接任何額外的概念都會增加使用者的決策成本。筆記軟件要你思考該建哪個筆記本、打哪些標(biāo)簽而 cua 把決策壓縮為一步給你想存的內(nèi)容起一個文件名字存進去完事。我見過不少同類項目最后變得難用都是因為功能越加越多支持了圖片、支持了 Markdown 渲染、支持了團隊共享結(jié)果用戶存一個片段要考慮的事情比寫代碼本身還多。所以我在寫 cua 的時候給自己立了一條規(guī)矩任何功能如果不能在我按下回車之后的五秒鐘內(nèi)感受到價值就先不做。后面所有的實現(xiàn)包括存儲、匹配、復(fù)制全都是圍繞這條規(guī)矩展開的。2. 片段即文件cua 的存儲模型與設(shè)計取舍2.1 為什么不選數(shù)據(jù)庫一個“偷懶”的決策最開始我以為該用 SQLite畢竟片段管理天然適合結(jié)構(gòu)化存儲可以做標(biāo)簽、做全文索引、統(tǒng)計使用頻率。但仔細(xì)想了一圈之后我放棄了數(shù)據(jù)庫回到最原始的方式每個片段就是一個獨立的純文本文件。這個決策看起來“偷懶”實際算下來是最省事的方案。原因有幾點。首先是規(guī)模一個普通開發(fā)者的常用片段撐死也就幾百條這個規(guī)模用文件系統(tǒng)完全沒壓力根本不需要數(shù)據(jù)庫的索引能力。其次是可遷移性文本文件放哪兒都認(rèn)得用 U 盤拷走、用專門工具同步、甚至是打包發(fā)到另一臺機器都不會遇到格式問題。第三是生態(tài)文件存好之后我可以用 ripgrep、grep、find、編輯器自帶的全局搜索去翻它們這意味著 cua 哪怕有一天本身掛了我的數(shù)據(jù)也永遠(yuǎn)可以用基礎(chǔ)工具讀到。對比項純文本文件SQLite 數(shù)據(jù)庫初始化成本零一個目錄搞定需要建表、寫連接邏輯備份/版本管理git、壓縮包均可需要導(dǎo)出為其他格式檢索能力依賴文件名和全文掃描自帶索引適合超大片段庫查看便利性任何編輯器都能直接看需要命令行或圖形工具適用規(guī)模幾百條以內(nèi)上萬條、需要復(fù)雜查詢時后來實際跑起來也證明這個選擇帶來的額外紅利是我可以很方便地在片段目錄里執(zhí)行g(shù)it init把整個片段庫納入版本管理。這樣即使某一次批量修改出了問題也能回滾到上一份快照。如果你準(zhǔn)備做類似的工具我的建議是不要急著引入數(shù)據(jù)庫先想想你的數(shù)據(jù)規(guī)模到底有多大。2.2 文件名即標(biāo)簽cua 的片段存儲目錄默認(rèn)是~/.cua/snippets/里面允許再建一層子目錄作為分類。我的實際目錄結(jié)構(gòu)長這樣~/.cua/ snippets/ deploy/ docker-compose-postgres.yml nginx-ssl-conf.txt python/ regex-uuid.txt sqlalchemy-async-session.txt misc/ ssh-tunnel.txt config.toml每個文件的文件名就是它的標(biāo)簽。文件名的格式我強制推薦用 kebab-case也就是全小寫、單詞之間用短橫線連接例如docker-compose-postgres.yml。為什么不用空格或下劃線因為空格在終端世界里會帶來無窮無盡的引號問題下劃線在模糊匹配時又不如短橫線容易拆詞。cua 在add的時候會自動把用戶輸入的名稱做規(guī)范化處理空格、大寫、特殊符號都會被轉(zhuǎn)成短橫線這樣我手動創(chuàng)建的片段無論多隨意最終落到磁盤上的文件名一定符合規(guī)則。分類目錄我控制在兩層以內(nèi)一層是分類名一層是文件名。層級做得越深使用者的心理負(fù)擔(dān)就越重最后的結(jié)果往往是懶得分類、把東西亂丟。如果你只有幾十個片段我建議干脆連分類目錄都省略全部平鋪在 snippets 目錄下靠文件名把含義表達清楚就夠了。2.3 片段內(nèi)容的格式約定片段的正文就是純文本。不過為了在列表展示和實際復(fù)制之間做區(qū)分我約定了一個非常輕量的規(guī)則如果文件的第一行以#開頭那么這一行被視為描述信息在list命令里顯示但不會被復(fù)制到剪貼板。比如一個 shell 配置片段# 生成安全的隨機令牌 openssl rand -base64 32當(dāng)cua list展示時你會看到一行“生成安全的隨機令牌”這時你一眼就知道這段內(nèi)容是干嘛的當(dāng)cua copy執(zhí)行時它只會復(fù)制第二行的實際命令描述行會被自動剝離。如果某個片段本身就是一段以#開頭的代碼比如 Python 注釋、Shell 注釋那它也只會影響列表顯示不影響復(fù)制結(jié)果因為復(fù)制時剝離規(guī)則只判斷第一行。實際上這個小約定是我在寫筆記時順手加上的。早期版本會把描述和正文一起復(fù)制進剪貼板結(jié)果我粘貼到終端里總要多刪一行非常惱火。后來加了剝離邏輯這個問題就徹底消失了。你也可以理解為cua 把每個片段文件都當(dāng)成一個極簡的“標(biāo)題正文”結(jié)構(gòu)標(biāo)題來自文件名描述來自第一行注釋剩下的全是內(nèi)容。3. 從 stdin 到剪貼板的完整鏈路核心命令與關(guān)鍵實現(xiàn)3.1 存儲目錄解析與測試友好性cua 是用 Python 3.9 寫的主要理由是不需要編譯、跨平臺行為一致而且標(biāo)準(zhǔn)庫就能完成大部分工作。依賴只有一個 pyperclip 用來讀寫系統(tǒng)剪貼板列表渲染用的 rich 是可選項。核心代碼的第一步是確定存儲根目錄我讓它優(yōu)先讀取環(huán)境變量CUA_HOME沒有設(shè)置時才落到~/.cuaimport os from pathlib import Path def get_store() - Path: root Path(os.environ.get(CUA_HOME, Path.home() / .cua)) snippets root / snippets snippets.mkdir(parentsTrue, exist_okTrue) return snippets這里特別說一下為什么要做CUA_HOME這個環(huán)境變量。如果所有路徑都寫死成~/.cua測試時會污染真實數(shù)據(jù)而且每次跑測試都要想辦法清理有了環(huán)境變量測試?yán)镏灰阉赶蛞粋€臨時目錄再往臨時目錄里寫文件測試結(jié)束后自動銷毀互不干擾。這也是一個可以復(fù)制到其他小工具里的通用設(shè)計凡是會在磁盤上留下數(shù)據(jù)的程序都應(yīng)該允許用戶通過環(huán)境變量或參數(shù)指定數(shù)據(jù)目錄。3.2 add 命令從管道和剪貼板兩種方式取數(shù)據(jù)cua add的目標(biāo)是讓“保存一個片段”的操作時間壓縮到三秒以內(nèi)。設(shè)計上它支持兩種數(shù)據(jù)來源如果終端有標(biāo)準(zhǔn)輸入正在往管道里傳內(nèi)容就讀取標(biāo)準(zhǔn)輸入否則就讀取系統(tǒng)剪貼板。具體邏輯是這樣的import sys def read_payload(): if not sys.stdin.isatty(): return sys.stdin.read() import pyperclip text pyperclip.paste() if not text.strip(): raise SystemExit(error: stdin is empty and clipboard is empty) return text這個邏輯解決了一個問題在用cat查看某個文件、或者在瀏覽器里復(fù)制了一串代碼之后我可以立刻切回終端執(zhí)行cua add docker-compose-postgres它會把剪貼板里的內(nèi)容變成一個新片段不用再打開編輯器粘貼保存。習(xí)慣之后保存一個片段的成本幾乎可以忽略不計。如果你在編寫類似的工具我建議一定要支持 stdin因為這種無意識的零成本保存才是你愿意長期堅持使用的前提。對于已經(jīng)存在的同名片段默認(rèn)行為是直接覆蓋覆蓋前打印一行警告。這個設(shè)計一開始遭到我自己的懷疑生怕誤刪內(nèi)容但真實使用中發(fā)現(xiàn)同一名稱的片段往往就是同一類內(nèi)容的迭代版本覆蓋帶來的收益大于風(fēng)險。如果你希望嚴(yán)格模式可以加一個全局參數(shù)--no-clobber遇到同名文件時直接報錯退出。3.3 grab 的模糊匹配思路cua 的查找命令有兩個cua list用于瀏覽全部cua grab用于按關(guān)鍵詞快速找到片段。grab 是我用得最多、也是實現(xiàn)時最講究的命令。它的核心思路是把用戶的查詢詞轉(zhuǎn)成一個“按順序匹配字符”的正則表達式比如輸入pgconn它需要能匹配到文件pg-conn-string.txt。實現(xiàn)里我做了一個叫 compile_fuzzy 的函數(shù)它會忽略掉短橫線和下劃線把用戶輸入的每個字符看作必須按順序出現(xiàn)的線索import re def compile_fuzzy(query: str) - re.Pattern: compact_query query.replace(-, ).replace(_, ).lower() parts [] for i, ch in enumerate(compact_query): if i 0: parts.append(r.*?) parts.append(re.escape(ch)) return re.compile(.join(parts), re.IGNORECASE)匹配時不但檢查原始文件名也檢查去掉短橫線之后的緊湊版本所以pgconn能命中pg-conn-string.txtsqlalchemy也能命中帶分類前綴的長文件名。如果你用過編輯器里的模糊查找就會覺得這個體驗很自然不需要記全名不需要管分隔符只要記住幾個關(guān)鍵字母就夠了。很多人以為模糊匹配很難其實在片段文件名這種短文本上一個如此簡單的正則就夠用了完全沒有必要引入復(fù)雜的編輯距離算法。3.4 copy 和 edit高頻操作要快找到片段之后最關(guān)鍵的動作就是復(fù)制。cua copy key會先走與 grab 相同的匹配邏輯然后讀取文件內(nèi)容、剝離描述行、寫入系統(tǒng)剪貼板。這里的核心代碼非常短def cmd_copy(name: str): store get_store() matches search_files(store, name) if not matches: raise SystemExit(ferror: no snippet matched: {name}) snippet load_snippet(store, matches[0]) import pyperclip pyperclip.copy(snippet[body]) print(fcopied {matches[0].name} to clipboard)edit命令則負(fù)責(zé)打開編輯器修改片段內(nèi)容。它會優(yōu)先使用$EDITOR環(huán)境變量指定的編輯器默認(rèn)回退到viimport subprocess def cmd_edit(name: str): store get_store() matches search_files(store, name) if not matches: raise SystemExit(ferror: no snippet matched: {name}) editor os.environ.get(EDITOR, vi) subprocess.run([editor, str(matches[0])])這里有個細(xì)節(jié)編輯完成后我沒有做任何文件變更檢測因為編輯器保存后內(nèi)容自然落在文件里后續(xù)再 grab 或 copy 時就會讀到新內(nèi)容。這種設(shè)計讓 edit 命令變得非常簡單也符合 Unix 工具的哲學(xué)程序只負(fù)責(zé)找到文件并打開保存由編輯器負(fù)責(zé)。如果你要學(xué)習(xí)這個項目的代碼建議從這三條命令開始讀它們各自解決了存取鏈路的一個環(huán)節(jié)。3.5 一個最小測試矩陣為了確保這些命令在改動后不會壞掉我給 cua 寫了一套極簡的測試核心就是利用CUA_HOME臨時目錄。下面這段測試覆蓋了最常用的 add 和 grab 鏈路import importlib.util import io import os import sys import tempfile from pathlib import Path def test_add_and_grab(): with tempfile.TemporaryDirectory() as tmp: os.environ[CUA_HOME] tmp old_stdin sys.stdin sys.stdin io.StringIO(hello world) try: cua importlib.import_module(cua) cua.call([add, hello]) match cua.search_files(cua.get_store(), hello) assert len(list(match)) 1 body cua.load_snippet(cua.get_store(), list(match)[0])[body] assert body hello world finally: sys.stdin old_stdin測試并不復(fù)雜但它能保證最基本的添加、文件名規(guī)范化和搜索邏輯在重構(gòu)后仍然可用。對于個人項目來說這已經(jīng)足夠讓我安心地隨意修改代碼而不用擔(dān)心哪一次順手刪掉了某個功能。如果你嫌寫測試麻煩至少也要保證每個命令在干凈臨時目錄里能打出 help 信息。4. 真實使用中才會撞上的四個坑編碼、遠(yuǎn)程、空格與同步4.1 名稱里的空格終端世界的隱形炸彈第一個坑是在我用了大概一周之后踩中的。某次我想存一個 docker 命令片段順手在終端里執(zhí)行了cua add docker compose up -d。結(jié)果它把“docker compose up -d”這一整串空格都保留成了文件名的一部分于是磁盤上出現(xiàn)了一個名字里帶四個空格的怪異文件。接下來所有跟它相關(guān)的操作都要打引號列表里顯示也對不齊最后我只能手動去目錄里重命名。為了解決這個問題我在 add 命令里加入了強制規(guī)范化無論用戶輸入什么名字都會先轉(zhuǎn)為小寫然后把連續(xù)空格轉(zhuǎn)成短橫線再過濾掉除字母、數(shù)字、短橫線、點號之外的字符。所以上面那條命令實際上會存成docker-compose-up-d.txt而這個文件里存的內(nèi)容是“docker compose up -d”這條命令本身。規(guī)范化的規(guī)則也在 grab 返回結(jié)果時采用保證兩邊邏輯一致。這條經(jīng)驗讓我明白在命令行工具里寧可替用戶做一點看似武斷的決策也不要讓用戶為隨后的引號問題買單。4.2 編碼問題Windows 和舊終端下的中文亂碼第二個坑是編碼。cua 最初在 macOS 上跑得很順但換到一臺 Windows 機器上之后凡是包含中文的片段保存后再讀取全都變成了亂碼。原因很簡單Python 在 Windows 上讀寫文本文件時默認(rèn)編碼可能是系統(tǒng)區(qū)域設(shè)置對應(yīng)的編碼而不是 UTF-8。中文系統(tǒng)下常見的是 GBK用 GBK 寫入、再被其他工具按 UTF-8 讀取自然就亂了。解決辦法是在所有文件讀寫操作里顯式指定編碼為 UTF-8并且在讀取時容忍無法解碼的字節(jié)def read_text(path: Path) - str: return path.read_text(encodingutf-8, errorsreplace) def write_text(path: Path, content: str) - None: path.write_text(content, encodingutf-8)errorsreplace的意思是遇到無法識別的字節(jié)時不要拋出異常而是替換成占位字符這樣至少保證不會因為一個壞字節(jié)導(dǎo)致整個命令崩掉。另外在 Windows 終端里顯示中文時建議設(shè)置環(huán)境變量PYTHONIOENCODINGutf-8讓標(biāo)準(zhǔn)輸出也使用 UTF-8。這個坑對你的用戶來說可能沒有意義但只要你的工具要跨平臺分發(fā)就必須在一開始就統(tǒng)一編碼策略。4.3 遠(yuǎn)程終端里沒有剪貼板必須學(xué)會優(yōu)雅降級第三個坑是遠(yuǎn)程會話。我有相當(dāng)一部分時間是在連接服務(wù)器操作這時候如果執(zhí)行cua copypyperclip 通常會報錯因為遠(yuǎn)程 Linux 環(huán)境里沒有剪貼板協(xié)議也沒有安裝 xclip、xsel 之類的輔助工具。有些情況下即使做了 X11 轉(zhuǎn)發(fā)剪貼板也可能連不上總之遠(yuǎn)程和剪貼板之間經(jīng)常是徹底的失敗。我在實現(xiàn)里加了一個環(huán)境檢測當(dāng)檢測到當(dāng)前會話來自遠(yuǎn)程連接時copy命令自動降級為直接打印內(nèi)容到終端并額外顯示一個提示告訴你這段應(yīng)該手動選擇復(fù)制def cmd_copy(name: str): snippet find_and_load(name) if os.environ.get(SSH_CONNECTION): print(snippet[body]) print(# (remote session: clipboard unavailable, copy this manually)) return import pyperclip pyperclip.copy(snippet[body]) print(fcopied {name})用環(huán)境變量來做判斷算不上什么高深技巧但它很實用本地會話不會誤傷遠(yuǎn)程會話又能正常工作。如果你在 tmux、容器或遠(yuǎn)程開發(fā)環(huán)境里也用這個工具建議你也考慮類似的降級方案。工具本身的能力邊界不是死的在受限環(huán)境里能不能給出一個可用的替代方案往往決定了你是否愿意繼續(xù)使用它。4.4 多臺機器之間的同步文件化紅利與敏感內(nèi)容提醒第四個坑是多設(shè)備同步。因為 cua 的數(shù)據(jù)就是純文本文件我直接把它們交給一個私有 git 倉庫管理。在~/.cua目錄里初始化倉庫每次內(nèi)容變更后手動提交一次。這樣我在辦公室電腦上新增的片段回到家里拉一下最近的變更就能看到遇到誤刪除還能從 git 歷史里恢復(fù)。如果你不想用 git用任何支持增量同步的私有工具數(shù)同步目錄也可以。但這里必須提醒一句cua 里保存的內(nèi)容很可能包含敏感信息比如內(nèi)網(wǎng)數(shù)據(jù)庫地址、帶訪問密鑰的配置、臨時生成的密碼。這些東西一旦被推到公共倉庫就是嚴(yán)重事故。我的做法是在~/.cua下放一個.gitignore把secret-*這類特別命名的文件排除在外另外對少數(shù)真正敏感的片段用 gpg 單獨加密后再保存需要復(fù)制時先手動解密。同步便利和安全永遠(yuǎn)是個權(quán)衡建議你先把“哪些片段可以同步”想清楚再決定是否開啟這項功能。5. 把 cua 變成肌肉記憶工作流整合與進階玩法5.1 在編輯器里直接取片段cua 最舒服的上手方式是在編輯器里直接調(diào)用。比如在 vim 里我經(jīng)常用:r !cua grab db-url把某段配置直接讀進當(dāng)前文件在 neovim 里還可以把這段調(diào)用再包一層快捷鍵。這樣我在寫代碼時不需要離開編輯器也不需要切到終端去復(fù)制粘貼已經(jīng)把 cua 當(dāng)成了輸入法之外的另一種“補全來源”。也許有人會問編輯器不是已經(jīng)有 snippets 插件了嗎確實有但那些插件通常綁定特定語言需要維護復(fù)雜的觸發(fā)詞配置。cua 的優(yōu)勢是完全通用只要內(nèi)容是我曾經(jīng)存過的東西不管它是 shell 命令、SQL 語句還是配置文件我都能用同一套記憶方式把它抓回來。它不是一個代碼補全插件而是一個樸素的文本倉庫正是這種樸素讓它能融入各種工具鏈。5.2 給 fuzzy finder 當(dāng)數(shù)據(jù)源如果你還嫌 cua 的命令行交互不夠直觀可以把它的列表輸出接給一個 fuzzy finder 類的交互式選擇器比如 fzf。我常用的一行組合命令是這樣的cua list --with-description | fzf --preview cua grab {} --bind enter:become(cua copy {})這條命令會先展示所有片段名稱和描述預(yù)覽窗口里顯示選中片段的具體內(nèi)容按下回車就直接復(fù)制到剪貼板。原本需要先猜關(guān)鍵詞再跑 grab 的操作變成了上下移動加回車整個過程基本不需要記憶任何片段名。這也讓我進一步體會到命令行工具之間通過純文本協(xié)作是多么高效。cua 輸出的是人可讀的列表fuzzy finder 負(fù)責(zé)交互選擇兩者都沒有為此專門開發(fā)任何接口。5.3 我打算繼續(xù)加的三件小事用了一段時間之后我給自己列了一個很小的待辦清單都是能明確提升使用體驗的改進方向而不是功能堆疊。第一是模板變量。在片段里保留{{date}}、{{host}}這樣的占位符復(fù)制時用當(dāng)前日期或環(huán)境變量替換。這樣那些“帶日期的提交說明”“帶服務(wù)器名的部署命令”就不用每次手動改一遍。第二是使用頻率統(tǒng)計。每次 copy 成功后在片段文件名旁邊加一個計數(shù)文件列表時按熱度排序這樣最常用的片段永遠(yuǎn)排在最前面。第三是 git 快照自動提交。如果檢測到片段目錄已經(jīng)是一個 git 倉庫就在每次增刪改之后自動打一個提交免去手動提交的負(fù)擔(dān)。這三件事我都刻意控制在很小的范圍內(nèi)。做 cua 這個工具最大的體會就是越是小工具越要克制把一條命令打磨到每天用上幾十次比做一個擁有一百個功能但沒人記得住的軟件有意義得多。如果你也想給自己做一把順手的小工具不要從完美設(shè)計開始從你每天最煩躁的那個操作開始。