<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Dotfile on Tarragon</title><link>https://tarrragon.github.io/blog/tags/dotfile/</link><description>Recent content in Dotfile on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 02 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/dotfile/index.xml" rel="self" type="application/rss+xml"/><item><title>安裝過程用到的基礎操作</title><link>https://tarrragon.github.io/blog/linux/install/basic-operations/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/basic-operations/</guid><description>&lt;p>這篇是「Linux 安裝與機器初始化」系列的基礎操作篇，系列用一套最小化的 Arch Linux 安裝當貫穿例子。在安裝、補工具、設定 SSH 的過程中，會用到一小撮基礎操作——成為 root、用 nano 改設定檔、shell 的幾個符號。Linux 的指令入門教學網路上已經很豐富，這篇不重複那些，只挑「這個系列實際用到、而且沒太多 Linux 實作經驗的人容易卡住」的那幾個介紹清楚，讓你照著操作時不會被一個沒見過的指令擋在半路。已經熟的，直接跳到 &lt;a href="../install-option-decisions/">安裝選項判讀&lt;/a>。&lt;/p>
&lt;h2 id="成為-rootsu--">成為 root：su -&lt;/h2>
&lt;p>&lt;code>su -&lt;/code> 讓你從一般使用者切換成 root（系統管理員），整個 session 都以 root 身分操作。這個系列用到它，是因為在還沒有 &lt;code>sudo&lt;/code> 的最小系統上，要裝 sudo、改系統設定，得先成為 root——而成為 root 的方式就是 &lt;code>su -&lt;/code>，輸入 root 密碼後進入 root 的 shell。&lt;/p>
&lt;p>那個 &lt;code>-&lt;/code> 決定你載入哪一套環境。&lt;code>su -&lt;/code> 啟動一個 login shell（模擬從頭登入、會跑完整的登入環境初始化），載入 root 自己的完整環境——把 &lt;code>PATH&lt;/code>（shell 搜尋指令的目錄清單）換成 root 的（會包含 &lt;code>/usr/sbin&lt;/code> 這類放管理工具的目錄）、工作目錄切到 root 的家。&lt;code>su&lt;/code>（不加 &lt;code>-&lt;/code>）則不啟動 login shell，環境多半沿用你呼叫時的、可能少掉那些管理工具目錄，於是有些管理指令會因為不在 &lt;code>PATH&lt;/code> 裡而「找不到」，明明你已經是 root。所以要做系統管理，習慣用 &lt;code>su -&lt;/code>。&lt;/p>
&lt;p>&lt;code>su -&lt;/code> 跟 &lt;code>sudo&lt;/code> 解決的是不同情境。&lt;code>sudo&lt;/code> 是「以 root 身分跑單一一條指令」，跑完就回到你自己；&lt;code>su -&lt;/code> 是「整段都當 root」。這個系列先用 &lt;code>su -&lt;/code> 是因為 sudo 還沒裝——一旦 sudo 裝好、wheel 群組（慣例上被授權可用 sudo 的群組）的授權設好，後面就改用 &lt;code>sudo &amp;lt;指令&amp;gt;&lt;/code>，不再整段切 root。做完 root 的事，打 &lt;code>exit&lt;/code> 回到原本的使用者。&lt;/p>
&lt;h2 id="用-nano-改設定檔">用 nano 改設定檔&lt;/h2>
&lt;p>nano 是一個對照 vi 更直覺的文字編輯器，安裝過程改 &lt;code>locale.gen&lt;/code>、&lt;code>hostname&lt;/code>、&lt;code>sudoers&lt;/code> 這些設定檔時會用到它。它的好處是所有快捷鍵都列在畫面最下面兩行，不必背。&lt;/p>
&lt;p>那兩行裡的 &lt;code>^&lt;/code> 符號代表 Ctrl 鍵。&lt;code>^O&lt;/code> 就是 Ctrl+O、&lt;code>^X&lt;/code> 就是 Ctrl+X。這個系列用到的幾個：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>按鍵&lt;/th>
 &lt;th>畫面標示&lt;/th>
 &lt;th>作用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Ctrl+O&lt;/td>
 &lt;td>&lt;code>^O Write Out&lt;/code>&lt;/td>
 &lt;td>存檔（會問檔名，按 Enter 確認）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ctrl+X&lt;/td>
 &lt;td>&lt;code>^X Exit&lt;/code>&lt;/td>
 &lt;td>離開（有未存變更會問要不要存）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ctrl+W&lt;/td>
 &lt;td>&lt;code>^W Where Is&lt;/code>&lt;/td>
 &lt;td>搜尋——在長檔案裡跳到某個字串&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ctrl+K&lt;/td>
 &lt;td>&lt;code>^K Cut&lt;/code>&lt;/td>
 &lt;td>剪掉游標所在的整行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ctrl+U&lt;/td>
 &lt;td>&lt;code>^U Paste&lt;/code>&lt;/td>
 &lt;td>把剪掉的內容貼回來&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把這幾個串起來就是一次典型的設定檔編輯。以這個系列裡「解開 &lt;code>locale.gen&lt;/code> 某一行的註解」為例：按 Ctrl+W 搜 &lt;code>en_US.UTF-8 UTF-8&lt;/code> 跳到那行、用 Backspace 刪掉行首的 &lt;code>#&lt;/code>、按 Ctrl+O 再 Enter 存檔、按 Ctrl+X 離開。改 &lt;code>hostname&lt;/code> 則是 Ctrl+K 剪掉預設那行、打上新主機名、Ctrl+O、Ctrl+X。Ctrl+K 加 Ctrl+U 合起來就是「剪下再貼上」，是搬移整行的常見組合。&lt;/p>
&lt;h2 id="檔名與指令的大小寫">檔名與指令的大小寫&lt;/h2>
&lt;p>Linux 把大小寫當成不同的字元，這對檔名跟指令都成立。&lt;code>Setup&lt;/code> 跟 &lt;code>setup&lt;/code> 是兩個不同的東西、&lt;code>Documents&lt;/code> 跟 &lt;code>documents&lt;/code> 是兩個不同的資料夾、打 &lt;code>Sudo&lt;/code> 不會執行到 &lt;code>sudo&lt;/code>。這條規則貫穿整個系列：執行 archboot 的安裝程式是 &lt;code>setup&lt;/code>（全小寫），啟動 Hyprland 桌面的指令是 &lt;code>Hyprland&lt;/code>（首字母大寫），兩者差一個字母的大小寫就是不同的目標。&lt;/p>
&lt;p>這對從 macOS 或 Windows 過來的人尤其常見，因為那兩個系統的預設檔案系統不分大小寫——在 Mac 上 &lt;code>File.txt&lt;/code> 跟 &lt;code>file.txt&lt;/code> 指向同一個檔，到了 Linux 就是兩個檔。同一個專案在 Mac 上跑得好好的，搬到 Linux 卻出現「檔案找不到」，常常就是某處大小寫對不上、而 Mac 的不分大小寫把問題藏了起來。&lt;/p>
&lt;p>判讀方式很簡單：在 Linux 上，指令、檔名、路徑一律照原樣的大小寫打。錯誤訊息 &lt;code>command not found&lt;/code> 或 &lt;code>No such file or directory&lt;/code> 在你確定東西明明就在時，先懷疑大小寫。&lt;/p></description><content:encoded><![CDATA[<p>這篇是「Linux 安裝與機器初始化」系列的基礎操作篇，系列用一套最小化的 Arch Linux 安裝當貫穿例子。在安裝、補工具、設定 SSH 的過程中，會用到一小撮基礎操作——成為 root、用 nano 改設定檔、shell 的幾個符號。Linux 的指令入門教學網路上已經很豐富，這篇不重複那些，只挑「這個系列實際用到、而且沒太多 Linux 實作經驗的人容易卡住」的那幾個介紹清楚，讓你照著操作時不會被一個沒見過的指令擋在半路。已經熟的，直接跳到 <a href="../install-option-decisions/">安裝選項判讀</a>。</p>
<h2 id="成為-rootsu--">成為 root：su -</h2>
<p><code>su -</code> 讓你從一般使用者切換成 root（系統管理員），整個 session 都以 root 身分操作。這個系列用到它，是因為在還沒有 <code>sudo</code> 的最小系統上，要裝 sudo、改系統設定，得先成為 root——而成為 root 的方式就是 <code>su -</code>，輸入 root 密碼後進入 root 的 shell。</p>
<p>那個 <code>-</code> 決定你載入哪一套環境。<code>su -</code> 啟動一個 login shell（模擬從頭登入、會跑完整的登入環境初始化），載入 root 自己的完整環境——把 <code>PATH</code>（shell 搜尋指令的目錄清單）換成 root 的（會包含 <code>/usr/sbin</code> 這類放管理工具的目錄）、工作目錄切到 root 的家。<code>su</code>（不加 <code>-</code>）則不啟動 login shell，環境多半沿用你呼叫時的、可能少掉那些管理工具目錄，於是有些管理指令會因為不在 <code>PATH</code> 裡而「找不到」，明明你已經是 root。所以要做系統管理，習慣用 <code>su -</code>。</p>
<p><code>su -</code> 跟 <code>sudo</code> 解決的是不同情境。<code>sudo</code> 是「以 root 身分跑單一一條指令」，跑完就回到你自己；<code>su -</code> 是「整段都當 root」。這個系列先用 <code>su -</code> 是因為 sudo 還沒裝——一旦 sudo 裝好、wheel 群組（慣例上被授權可用 sudo 的群組）的授權設好，後面就改用 <code>sudo &lt;指令&gt;</code>，不再整段切 root。做完 root 的事，打 <code>exit</code> 回到原本的使用者。</p>
<h2 id="用-nano-改設定檔">用 nano 改設定檔</h2>
<p>nano 是一個對照 vi 更直覺的文字編輯器，安裝過程改 <code>locale.gen</code>、<code>hostname</code>、<code>sudoers</code> 這些設定檔時會用到它。它的好處是所有快捷鍵都列在畫面最下面兩行，不必背。</p>
<p>那兩行裡的 <code>^</code> 符號代表 Ctrl 鍵。<code>^O</code> 就是 Ctrl+O、<code>^X</code> 就是 Ctrl+X。這個系列用到的幾個：</p>
<table>
  <thead>
      <tr>
          <th>按鍵</th>
          <th>畫面標示</th>
          <th>作用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ctrl+O</td>
          <td><code>^O Write Out</code></td>
          <td>存檔（會問檔名，按 Enter 確認）</td>
      </tr>
      <tr>
          <td>Ctrl+X</td>
          <td><code>^X Exit</code></td>
          <td>離開（有未存變更會問要不要存）</td>
      </tr>
      <tr>
          <td>Ctrl+W</td>
          <td><code>^W Where Is</code></td>
          <td>搜尋——在長檔案裡跳到某個字串</td>
      </tr>
      <tr>
          <td>Ctrl+K</td>
          <td><code>^K Cut</code></td>
          <td>剪掉游標所在的整行</td>
      </tr>
      <tr>
          <td>Ctrl+U</td>
          <td><code>^U Paste</code></td>
          <td>把剪掉的內容貼回來</td>
      </tr>
  </tbody>
</table>
<p>把這幾個串起來就是一次典型的設定檔編輯。以這個系列裡「解開 <code>locale.gen</code> 某一行的註解」為例：按 Ctrl+W 搜 <code>en_US.UTF-8 UTF-8</code> 跳到那行、用 Backspace 刪掉行首的 <code>#</code>、按 Ctrl+O 再 Enter 存檔、按 Ctrl+X 離開。改 <code>hostname</code> 則是 Ctrl+K 剪掉預設那行、打上新主機名、Ctrl+O、Ctrl+X。Ctrl+K 加 Ctrl+U 合起來就是「剪下再貼上」，是搬移整行的常見組合。</p>
<h2 id="檔名與指令的大小寫">檔名與指令的大小寫</h2>
<p>Linux 把大小寫當成不同的字元，這對檔名跟指令都成立。<code>Setup</code> 跟 <code>setup</code> 是兩個不同的東西、<code>Documents</code> 跟 <code>documents</code> 是兩個不同的資料夾、打 <code>Sudo</code> 不會執行到 <code>sudo</code>。這條規則貫穿整個系列：執行 archboot 的安裝程式是 <code>setup</code>（全小寫），啟動 Hyprland 桌面的指令是 <code>Hyprland</code>（首字母大寫），兩者差一個字母的大小寫就是不同的目標。</p>
<p>這對從 macOS 或 Windows 過來的人尤其常見，因為那兩個系統的預設檔案系統不分大小寫——在 Mac 上 <code>File.txt</code> 跟 <code>file.txt</code> 指向同一個檔，到了 Linux 就是兩個檔。同一個專案在 Mac 上跑得好好的，搬到 Linux 卻出現「檔案找不到」，常常就是某處大小寫對不上、而 Mac 的不分大小寫把問題藏了起來。</p>
<p>判讀方式很簡單：在 Linux 上，指令、檔名、路徑一律照原樣的大小寫打。錯誤訊息 <code>command not found</code> 或 <code>No such file or directory</code> 在你確定東西明明就在時，先懷疑大小寫。</p>
<h2 id="shell-的幾個符號">shell 的幾個符號</h2>
<p>這個系列的指令裡出現了幾個 shell 符號，它們是 shell 本身的語法、不是某個程式的參數，認得它們才讀得懂那些指令在做什麼。</p>
<p><code>&gt;</code> 跟 <code>&gt;&gt;</code> 是重導向，把本來會印到畫面的輸出改寫進檔案。<code>&gt;</code> 覆蓋、<code>&gt;&gt;</code> 追加。系列裡 <code>echo '%wheel ALL=(ALL:ALL) ALL' &gt; /etc/sudoers.d/10-wheel</code> 就是把那行文字寫進一個新檔，而設 <code>authorized_keys</code> 時用 <code>&gt;&gt;</code> 是為了追加、不洗掉既有的 key。</p>
<p><code>|</code>（管線）把左邊指令的輸出，直接餵給右邊指令當輸入。傳 dotfile 進 VM 的 <code>tar czf - . | ssh host 'tar xzf -'</code> 就是把 tar 打包的資料流，不落地直接透過 ssh 送到對面解開。</p>
<p><code>&amp;&amp;</code> 串接兩條指令，而且只有左邊成功才跑右邊。<code>cd ~/dotfiles &amp;&amp; ./scripts/install.sh</code> 的意思是「先切到目錄，切成功了才跑腳本」——如果目錄不存在、<code>cd</code> 失敗，後面的腳本就不會在錯的目錄下執行。</p>
<p><code>$(...)</code> 是命令替換，把括號裡指令的輸出，當成值填進當下這條指令。<code>chsh -s &quot;$(command -v zsh)&quot;</code> 會先跑 <code>command -v zsh</code> 得到 <code>/usr/bin/zsh</code>，再把這個路徑填給 <code>chsh</code>。理解這個語法，也才看得懂 <a href="../minimal-install-verify/">工具驗證篇</a> 講的 <code>which</code> 地雷：當 <code>which</code> 不存在、<code>$(which zsh)</code> 算出空字串，整條指令就拿到一個空值。</p>
<p>跟重導向相關的還有一個經典陷阱：<code>sudo echo '內容' &gt; /etc/某個-root-檔</code> 會失敗。原因是重導向 <code>&gt;</code> 由你的 shell 以你的身分執行，不是被 <code>sudo</code> 提權的那條指令——被提權的只有 <code>echo</code>，真正寫檔的還是你，而你沒權限寫那個 root 檔。解法是 <code>echo '內容' | sudo tee /etc/某個-root-檔</code>：<code>tee</code> 把流進它的 stdin 寫進檔案，而 <code>tee</code> 才是被 <code>sudo</code> 提權的那支，所以寫得進去。這個系列補 <code>sudoers</code> 檔時用的就是這個寫法。</p>
<p>權限對某些檔是硬要求，所以系列裡出現了 <code>chmod</code>。<code>chmod</code> 改檔案權限，那串數字是八進位的權限碼，三位分別代表擁有者/群組/其他人；每一位是讀（4）+ 寫（2）+ 執行（1）的和，所以 <code>7</code> 是全權、<code>6</code> 是可讀寫、<code>4</code> 是只讀、<code>0</code> 是無權。系列裡的 <code>chmod 440</code>（擁有者與群組可讀、其他人無權，給 sudoers 檔）、<code>chmod 600</code>（只有擁有者可讀寫，給私鑰）、<code>chmod 700</code>（只有擁有者全權，給 <code>.ssh</code> 目錄）就是這樣算出來的。這些檔對權限有要求——sudoers 必須是 440、私鑰必須只有自己讀得到，否則對應的工具會拒絕使用它們，所以那幾個 <code>chmod</code> 不是裝飾、是讓 sudo 跟 ssh 願意接受那些檔的條件。</p>
<h2 id="下一步">下一步</h2>
<p>認得這些基礎操作之後，就可以從 <a href="../install-option-decisions/">安裝選項判讀</a> 開始走完整的安裝流程，過程中再遇到這幾個操作就不會卡。</p>
]]></content:encoded></item><item><title>Linux 桌面的故障隔離模型</title><link>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/fault-isolation-model/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/fault-isolation-model/</guid><description>&lt;p>Linux 桌面環境的故障隔離建立在一個結構性的設計決策上：&lt;strong>顯示合成器（compositor）是 userspace process，不是 kernel 的一部分&lt;/strong>。這意味著 Hyprland 掛了只是桌面消失，作業系統核心還在正常運作。&lt;/p>
&lt;p>本文用「桌面環境」泛指使用者看到的圖形介面整體。技術上，Hyprland 是 Wayland compositor——負責視窗合成和輸入管理，不含完整桌面環境（DE）的套件管理、設定面板等元件。GNOME、KDE Plasma 才是完整的 DE。但在故障隔離的討論中，關鍵區分是 kernel space vs userspace，compositor 和 DE 都在 userspace 這一側。&lt;/p>
&lt;h2 id="kernel-與-userspace-的隔離邊界">Kernel 與 Userspace 的隔離邊界&lt;/h2>
&lt;p>作業系統分成兩個執行空間。Kernel space 負責硬體驅動、記憶體管理、process 排程這些基礎設施。Userspace 跑所有應用程式，包括桌面環境本身。&lt;/p>
&lt;p>兩個空間之間有硬體層級的隔離——CPU 的保護環機制（ring 0 是核心層級、ring 3 是應用程式層級，硬體強制限制 ring 3 的程式碼存取 ring 0 的記憶體）。Userspace 的 process 不管怎麼崩潰，都不會直接影響 kernel。Kernel 會清理掉崩潰的 process、回收它佔用的記憶體，然後繼續運作。&lt;/p>
&lt;p>這個隔離機制解釋了一個關鍵差異：為什麼 Linux 上一個 app crash 通常只是那個視窗消失，而不會拖垮整台機器。&lt;/p>
&lt;h2 id="為什麼-windows-會藍屏">為什麼 Windows 會藍屏&lt;/h2>
&lt;p>Windows 的藍屏（Blue Screen of Death, BSOD）是 kernel panic 的表現——作業系統核心本身遇到無法恢復的錯誤，只能停機。&lt;/p>
&lt;p>Windows 藍屏頻率較高的結構性原因在於&lt;strong>顯示驅動的執行位置&lt;/strong>。Windows 把 GPU 驅動放在 kernel mode（WDDM 架構），NVIDIA 或 AMD 的驅動程式碼直接跑在核心空間。驅動有 bug 時，錯誤發生在 kernel space，清理掉再繼續的選項不存在——繼續執行可能造成資料損壞，只能停機。&lt;/p>
&lt;p>藍屏頻率高是架構選擇的代價。把驅動放在 kernel mode 可以減少 context switch 的效能開銷，GPU 效能更好。代價是驅動 bug 的爆炸半徑從「app crash」升級成「整台停機」。Windows 10/11 已加入 TDR（Timeout Detection and Recovery）機制——GPU driver hang 時系統嘗試 reset driver 而非直接藍屏，大幅降低了 GPU 導致的 BSOD 頻率。但架構上 driver 仍在 kernel mode，藍屏的可能性仍然存在。&lt;/p>
&lt;h2 id="linux-桌面的架構差異">Linux 桌面的架構差異&lt;/h2>
&lt;p>Linux 桌面環境的顯示合成器（Hyprland、Sway、KDE Plasma 的 KWin）跑在 userspace。它們透過 DRM/KMS（Direct Rendering Manager / Kernel Mode Setting，Linux 的顯示子系統介面）跟 kernel 的 GPU 驅動溝通，但合成器本身的程式碼不在 kernel space 裡。&lt;/p>
&lt;p>這個架構選擇的效果：&lt;/p>
&lt;p>&lt;strong>Compositor crash&lt;/strong>。Hyprland 如果遇到 segfault 或其他 fatal error，kernel 終止這個 userspace process。所有由它管理的視窗消失，螢幕回到 TTY 登入畫面或黑屏。但 kernel 還在跑——其他 TTY 可以登入，SSH 可以連線，背景的 service 繼續運作。&lt;/p>
&lt;p>&lt;strong>GPU driver bug&lt;/strong>。Linux 的 GPU 驅動分兩層：kernel module（可動態載入的核心擴充模組，如 &lt;code>nvidia.ko&lt;/code>、&lt;code>amdgpu.ko&lt;/code>）負責硬體操作，userspace 的 Mesa / NVIDIA userspace driver 負責 OpenGL/Vulkan 實作。Kernel module 出問題理論上可以 kernel panic，但實際行為取決於驅動。AMD 的開源 &lt;code>amdgpu&lt;/code> 通常會嘗試 reset GPU 而非直接 panic，常見的表現是畫面凍結幾秒後恢復。NVIDIA 的閉源 &lt;code>nvidia.ko&lt;/code> 是隔離模型的主要例外——kernel 社群無法審查或修復其程式碼，hang 時恢復能力遠弱於 amdgpu，經常拖垮整個 session 且 TTY 切換也受影響。這是後續&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">故障場景&lt;/a>中 NVIDIA 相關 caveat 的根源。&lt;/p></description><content:encoded><![CDATA[<p>Linux 桌面環境的故障隔離建立在一個結構性的設計決策上：<strong>顯示合成器（compositor）是 userspace process，不是 kernel 的一部分</strong>。這意味著 Hyprland 掛了只是桌面消失，作業系統核心還在正常運作。</p>
<p>本文用「桌面環境」泛指使用者看到的圖形介面整體。技術上，Hyprland 是 Wayland compositor——負責視窗合成和輸入管理，不含完整桌面環境（DE）的套件管理、設定面板等元件。GNOME、KDE Plasma 才是完整的 DE。但在故障隔離的討論中，關鍵區分是 kernel space vs userspace，compositor 和 DE 都在 userspace 這一側。</p>
<h2 id="kernel-與-userspace-的隔離邊界">Kernel 與 Userspace 的隔離邊界</h2>
<p>作業系統分成兩個執行空間。Kernel space 負責硬體驅動、記憶體管理、process 排程這些基礎設施。Userspace 跑所有應用程式，包括桌面環境本身。</p>
<p>兩個空間之間有硬體層級的隔離——CPU 的保護環機制（ring 0 是核心層級、ring 3 是應用程式層級，硬體強制限制 ring 3 的程式碼存取 ring 0 的記憶體）。Userspace 的 process 不管怎麼崩潰，都不會直接影響 kernel。Kernel 會清理掉崩潰的 process、回收它佔用的記憶體，然後繼續運作。</p>
<p>這個隔離機制解釋了一個關鍵差異：為什麼 Linux 上一個 app crash 通常只是那個視窗消失，而不會拖垮整台機器。</p>
<h2 id="為什麼-windows-會藍屏">為什麼 Windows 會藍屏</h2>
<p>Windows 的藍屏（Blue Screen of Death, BSOD）是 kernel panic 的表現——作業系統核心本身遇到無法恢復的錯誤，只能停機。</p>
<p>Windows 藍屏頻率較高的結構性原因在於<strong>顯示驅動的執行位置</strong>。Windows 把 GPU 驅動放在 kernel mode（WDDM 架構），NVIDIA 或 AMD 的驅動程式碼直接跑在核心空間。驅動有 bug 時，錯誤發生在 kernel space，清理掉再繼續的選項不存在——繼續執行可能造成資料損壞，只能停機。</p>
<p>藍屏頻率高是架構選擇的代價。把驅動放在 kernel mode 可以減少 context switch 的效能開銷，GPU 效能更好。代價是驅動 bug 的爆炸半徑從「app crash」升級成「整台停機」。Windows 10/11 已加入 TDR（Timeout Detection and Recovery）機制——GPU driver hang 時系統嘗試 reset driver 而非直接藍屏，大幅降低了 GPU 導致的 BSOD 頻率。但架構上 driver 仍在 kernel mode，藍屏的可能性仍然存在。</p>
<h2 id="linux-桌面的架構差異">Linux 桌面的架構差異</h2>
<p>Linux 桌面環境的顯示合成器（Hyprland、Sway、KDE Plasma 的 KWin）跑在 userspace。它們透過 DRM/KMS（Direct Rendering Manager / Kernel Mode Setting，Linux 的顯示子系統介面）跟 kernel 的 GPU 驅動溝通，但合成器本身的程式碼不在 kernel space 裡。</p>
<p>這個架構選擇的效果：</p>
<p><strong>Compositor crash</strong>。Hyprland 如果遇到 segfault 或其他 fatal error，kernel 終止這個 userspace process。所有由它管理的視窗消失，螢幕回到 TTY 登入畫面或黑屏。但 kernel 還在跑——其他 TTY 可以登入，SSH 可以連線，背景的 service 繼續運作。</p>
<p><strong>GPU driver bug</strong>。Linux 的 GPU 驅動分兩層：kernel module（可動態載入的核心擴充模組，如 <code>nvidia.ko</code>、<code>amdgpu.ko</code>）負責硬體操作，userspace 的 Mesa / NVIDIA userspace driver 負責 OpenGL/Vulkan 實作。Kernel module 出問題理論上可以 kernel panic，但實際行為取決於驅動。AMD 的開源 <code>amdgpu</code> 通常會嘗試 reset GPU 而非直接 panic，常見的表現是畫面凍結幾秒後恢復。NVIDIA 的閉源 <code>nvidia.ko</code> 是隔離模型的主要例外——kernel 社群無法審查或修復其程式碼，hang 時恢復能力遠弱於 amdgpu，經常拖垮整個 session 且 TTY 切換也受影響。這是後續<a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">故障場景</a>中 NVIDIA 相關 caveat 的根源。</p>
<p><strong>應用程式 crash</strong>。Firefox、VS Code、任何 GUI 程式崩潰，只有那個視窗消失。Compositor 繼續管理剩下的視窗，桌面環境不受影響。</p>
<h2 id="ttykernel-存活時的首選救生通道">TTY：kernel 存活時的首選救生通道</h2>
<p>TTY（TeleTYpewriter）是 Linux 核心直接提供的純文字終端機介面，獨立於任何桌面環境。systemd 預設配置下有 6 個 virtual console（TTY1-TTY6）。Wayland compositor（如 Hyprland）通常佔用 TTY1，其餘可用。</p>
<p>切換方式：<code>Ctrl+Alt+F2</code>（切到 TTY2）到 <code>Ctrl+Alt+F6</code>（切到 TTY6）。</p>
<p>TTY 的重要性在於它<strong>不依賴 compositor</strong>。Hyprland 掛了、compositor crash 導致桌面消失——只要 kernel 還活著、GPU driver 仍能處理 VT switch，TTY 就能切過去登入操作：</p>
<ul>
<li>用 <code>htop</code> 或 <code>ps</code> 查看哪個 process 出問題</li>
<li><code>kill</code> 有問題的 process</li>
<li>用 <code>vim</code> 或 <code>nano</code> 修改配置檔</li>
<li>重新啟動 Hyprland（<code>Hyprland</code> 指令）</li>
<li>如果需要，正常 <code>reboot</code></li>
</ul>
<p>TTY 切換失效的情境有兩種：kernel panic（極罕見）和 GPU 完全 hang 導致 VT switch 本身卡住（NVIDIA 閉源驅動在 Wayland 上較常見，需確保 <code>nvidia_drm.modeset=1</code>）。後者的替代手段是 SSH 遠端登入或 Magic SysRq 鍵（見<a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景</a>的場景三）。</p>
<h2 id="記憶體耗盡oom的處理機制">記憶體耗盡（OOM）的處理機制</h2>
<p>Linux kernel 有 OOM Killer（Out of Memory Killer）機制——當記憶體和 swap 都用完、kernel 無法再分配新頁面時，自動挑選佔用記憶體最多、重要性最低的 process 強制終止，釋放記憶體讓系統繼續運作。</p>
<p>OOM Killer 的行為有時超出使用者的預期——它可能直接終止 Hyprland（因為 compositor 通常佔用不少記憶體），導致桌面突然消失。但關鍵是：<strong>系統沒有崩潰</strong>。Kernel 還在、TTY 還在、SSH 還在。</p>
<p>預防 OOM 的常見做法：</p>
<ul>
<li>設定 swap（即使用 SSD，2-4GB 的 swap 也能在記憶體壓力大時提供緩衝）</li>
<li>啟用 <code>systemd-oomd</code>（userspace 的 OOM 管理，比 kernel OOM Killer 更早介入、更可控）</li>
<li>監控記憶體用量（<code>btop</code> 或 <code>htop</code> 可以看即時狀態）</li>
</ul>
<h2 id="故障層級速查">故障層級速查</h2>
<table>
  <thead>
      <tr>
          <th>故障層級</th>
          <th>症狀</th>
          <th>系統影響</th>
          <th>恢復手段</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>應用程式 crash</td>
          <td>單一視窗消失</td>
          <td>無</td>
          <td>重開該程式</td>
      </tr>
      <tr>
          <td>工具 crash（waybar 等）</td>
          <td>狀態列 / 通知 / 啟動器消失</td>
          <td>無</td>
          <td>重啟該工具</td>
      </tr>
      <tr>
          <td>Compositor crash</td>
          <td>所有視窗消失、黑屏</td>
          <td>桌面環境不可用</td>
          <td>TTY 登入、重啟 compositor</td>
      </tr>
      <tr>
          <td>GPU driver hang</td>
          <td>畫面凍結</td>
          <td>桌面環境不可用</td>
          <td>TTY 或 SSH、kill compositor</td>
      </tr>
      <tr>
          <td>OOM</td>
          <td>系統極慢或桌面被殺</td>
          <td>部分 process 被終止</td>
          <td>TTY 登入、清理 process</td>
      </tr>
      <tr>
          <td>Kernel panic</td>
          <td>完全停機</td>
          <td>全機不可用</td>
          <td>只能重開機</td>
      </tr>
  </tbody>
</table>
<p>前五個層級都有恢復手段，只有 kernel panic 需要重開機。日常使用中遇到的故障多數落在前三層。</p>
]]></content:encoded></item><item><title>Devcontainer 與 Nix：容器化和宣告式的開發環境</title><link>https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/devcontainer-nix/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/devcontainer-nix/</guid><description>&lt;p>個人 dotfile 管理解決的是「一個人的環境可重現性」。當同樣的需求擴展到團隊——新人 onboarding 要多久能開始寫 code、團隊成員的開發環境差異造成「在我電腦上能跑」的問題、CI 環境跟本機環境不一致——就進入了「團隊開發環境標準化」的範疇。&lt;/p>
&lt;h2 id="個人-dotfile-跟團隊環境的邊界">個人 Dotfile 跟團隊環境的邊界&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>個人 Dotfile&lt;/th>
 &lt;th>團隊環境標準化&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>管理對象&lt;/td>
 &lt;td>個人偏好（alias、keybind、配色）&lt;/td>
 &lt;td>專案依賴（runtime 版本、系統套件、服務容器）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>目標&lt;/td>
 &lt;td>個人效率和舒適度&lt;/td>
 &lt;td>環境一致性和 onboarding 速度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>儲存位置&lt;/td>
 &lt;td>個人 dotfile repo&lt;/td>
 &lt;td>專案 repo 內（.devcontainer/、flake.nix）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>強制程度&lt;/td>
 &lt;td>完全個人自由&lt;/td>
 &lt;td>團隊約定或強制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>變動頻率&lt;/td>
 &lt;td>高（個人隨時調整）&lt;/td>
 &lt;td>低（跟專案版本走）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩者共用同一個核心思想（環境 as code、版控、可重現），但管理的對象和約定的範圍不同。個人 dotfile 是「我喜歡怎麼工作」，團隊環境是「這個專案需要什麼才能跑」。&lt;/p>
&lt;h2 id="devcontainer容器化的開發環境">Devcontainer：容器化的開發環境&lt;/h2>
&lt;p>Devcontainer 是微軟提出的開放規格（devcontainers.org），定義了怎麼用 Docker 容器作為開發環境。VS Code、GitHub Codespaces、JetBrains 都支援。&lt;/p>
&lt;h3 id="核心概念">核心概念&lt;/h3>
&lt;p>專案 repo 裡放一個 &lt;code>.devcontainer/devcontainer.json&lt;/code>，描述這個專案的開發環境需要什麼：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;My Project&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;image&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;mcr.microsoft.com/devcontainers/base:ubuntu&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;features&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;ghcr.io/devcontainers/features/go:1&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1.22&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;ghcr.io/devcontainers/features/node:1&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;20&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;postCreateCommand&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;go mod download &amp;amp;&amp;amp; npm install&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;customizations&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;vscode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;extensions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;golang.go&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;esbenp.prettier-vscode&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;forwardPorts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">8080&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3000&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>打開專案時，IDE 自動啟動這個容器、在裡面安裝指定版本的 Go 和 Node、跑 dependency install、裝 VS Code extension。新人 clone repo → 打開 → 等容器建好 → 直接開始寫 code。&lt;/p>
&lt;h3 id="跟個人-dotfile-的互動">跟個人 Dotfile 的互動&lt;/h3>
&lt;p>Devcontainer 管的是「專案需要什麼」，但你在容器裡工作時還是會想要自己的 shell alias、Git 設定、editor keybind。兩者的整合方式：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>dotfiles repo 自動部署&lt;/strong>：devcontainer.json 支援 &lt;code>&amp;quot;dotfiles.repository&amp;quot;&lt;/code> 欄位，容器啟動時自動 clone 你的 dotfile repo 並執行 install script&lt;/li>
&lt;li>&lt;strong>個人 vs 團隊設定分離&lt;/strong>：&lt;code>.devcontainer/&lt;/code> 裡放團隊共用的環境定義，個人偏好透過 dotfiles 機制注入，不互相干擾&lt;/li>
&lt;/ul>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;dotfiles.repository&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://github.com/you/dotfiles&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;dotfiles.installCommand&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;scripts/install.sh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;dotfiles.targetPath&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;~/dotfiles&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這是個人 dotfile 和團隊環境標準化最乾淨的接合點——團隊定義「環境長什麼樣」，個人 dotfile 定義「在這個環境裡我怎麼操作」。&lt;/p>
&lt;h3 id="devcontainer-的限制">Devcontainer 的限制&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Docker 是前提&lt;/strong>：團隊每個人的機器都要裝 Docker，macOS 上要跑 Docker Desktop 或 OrbStack&lt;/li>
&lt;li>&lt;strong>GUI 應用不適合&lt;/strong>：devcontainer 定位是 headless 開發環境，不處理圖形桌面&lt;/li>
&lt;li>&lt;strong>效能折扣&lt;/strong>：檔案系統操作在 macOS 上的 Docker volume 有效能折扣（Linux 上幾乎沒差）&lt;/li>
&lt;li>&lt;strong>離線環境&lt;/strong>：建容器需要拉 image 和 feature，斷網環境要另外處理（見 &lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/" data-link-title="斷網環境的 infra：沒有網路時怎麼做" data-link-desc="實體隔離或無法連網的環境裡，IaC、套件管理、容器映像、監控、CI/CD 怎麼運作 — 原則不變、工具路徑全部要換">Infra 斷網模組&lt;/a>）&lt;/li>
&lt;/ul>
&lt;h2 id="nix宣告式的環境管理">Nix：宣告式的環境管理&lt;/h2>
&lt;p>Nix 是另一條技術路線，用宣告式的方式描述整個開發環境，不依賴 Docker。&lt;/p>
&lt;h3 id="核心概念-1">核心概念&lt;/h3>
&lt;p>Nix 的 &lt;code>flake.nix&lt;/code>（或 &lt;code>shell.nix&lt;/code>）宣告了開發環境需要哪些套件，&lt;code>nix develop&lt;/code> 進入這個環境：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nix" data-lang="nix">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># flake.nix&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="n">inputs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">nixpkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;github:NixOS/nixpkgs/nixos-unstable&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="n">outputs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">nixpkgs&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">let&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="n">pkgs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">nixpkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">legacyPackages&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">x86_64-linux&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">in&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">devShells&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">default&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkShell&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="n">packages&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">pkgs&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="n">go_1_22&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="n">nodejs_20&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="n">postgresql_16&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="n">redis&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="n">shellHook&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s1"> echo &amp;#34;Dev environment ready&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="s1"> &amp;#39;&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跟 Devcontainer 的差異：Nix 不用容器，直接在宿主機上建立隔離的環境（透過 Nix store 的路徑隔離）。優點是沒有 Docker 的效能折扣和額外層級；缺點是 Nix 的學習曲線陡峭、語法不直覺。&lt;/p></description><content:encoded><![CDATA[<p>個人 dotfile 管理解決的是「一個人的環境可重現性」。當同樣的需求擴展到團隊——新人 onboarding 要多久能開始寫 code、團隊成員的開發環境差異造成「在我電腦上能跑」的問題、CI 環境跟本機環境不一致——就進入了「團隊開發環境標準化」的範疇。</p>
<h2 id="個人-dotfile-跟團隊環境的邊界">個人 Dotfile 跟團隊環境的邊界</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>個人 Dotfile</th>
          <th>團隊環境標準化</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>管理對象</td>
          <td>個人偏好（alias、keybind、配色）</td>
          <td>專案依賴（runtime 版本、系統套件、服務容器）</td>
      </tr>
      <tr>
          <td>目標</td>
          <td>個人效率和舒適度</td>
          <td>環境一致性和 onboarding 速度</td>
      </tr>
      <tr>
          <td>儲存位置</td>
          <td>個人 dotfile repo</td>
          <td>專案 repo 內（.devcontainer/、flake.nix）</td>
      </tr>
      <tr>
          <td>強制程度</td>
          <td>完全個人自由</td>
          <td>團隊約定或強制</td>
      </tr>
      <tr>
          <td>變動頻率</td>
          <td>高（個人隨時調整）</td>
          <td>低（跟專案版本走）</td>
      </tr>
  </tbody>
</table>
<p>兩者共用同一個核心思想（環境 as code、版控、可重現），但管理的對象和約定的範圍不同。個人 dotfile 是「我喜歡怎麼工作」，團隊環境是「這個專案需要什麼才能跑」。</p>
<h2 id="devcontainer容器化的開發環境">Devcontainer：容器化的開發環境</h2>
<p>Devcontainer 是微軟提出的開放規格（devcontainers.org），定義了怎麼用 Docker 容器作為開發環境。VS Code、GitHub Codespaces、JetBrains 都支援。</p>
<h3 id="核心概念">核心概念</h3>
<p>專案 repo 裡放一個 <code>.devcontainer/devcontainer.json</code>，描述這個專案的開發環境需要什麼：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;My Project&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nt">&#34;image&#34;</span><span class="p">:</span> <span class="s2">&#34;mcr.microsoft.com/devcontainers/base:ubuntu&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nt">&#34;features&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="nt">&#34;ghcr.io/devcontainers/features/go:1&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="nt">&#34;version&#34;</span><span class="p">:</span> <span class="s2">&#34;1.22&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nt">&#34;ghcr.io/devcontainers/features/node:1&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="nt">&#34;version&#34;</span><span class="p">:</span> <span class="s2">&#34;20&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nt">&#34;postCreateCommand&#34;</span><span class="p">:</span> <span class="s2">&#34;go mod download &amp;&amp; npm install&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nt">&#34;customizations&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="nt">&#34;vscode&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="nt">&#34;extensions&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">                <span class="s2">&#34;golang.go&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">                <span class="s2">&#34;esbenp.prettier-vscode&#34;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">            <span class="p">]</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="nt">&#34;forwardPorts&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">8080</span><span class="p">,</span> <span class="mi">3000</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>打開專案時，IDE 自動啟動這個容器、在裡面安裝指定版本的 Go 和 Node、跑 dependency install、裝 VS Code extension。新人 clone repo → 打開 → 等容器建好 → 直接開始寫 code。</p>
<h3 id="跟個人-dotfile-的互動">跟個人 Dotfile 的互動</h3>
<p>Devcontainer 管的是「專案需要什麼」，但你在容器裡工作時還是會想要自己的 shell alias、Git 設定、editor keybind。兩者的整合方式：</p>
<ul>
<li><strong>dotfiles repo 自動部署</strong>：devcontainer.json 支援 <code>&quot;dotfiles.repository&quot;</code> 欄位，容器啟動時自動 clone 你的 dotfile repo 並執行 install script</li>
<li><strong>個人 vs 團隊設定分離</strong>：<code>.devcontainer/</code> 裡放團隊共用的環境定義，個人偏好透過 dotfiles 機制注入，不互相干擾</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nt">&#34;dotfiles.repository&#34;</span><span class="p">:</span> <span class="s2">&#34;https://github.com/you/dotfiles&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nt">&#34;dotfiles.installCommand&#34;</span><span class="p">:</span> <span class="s2">&#34;scripts/install.sh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nt">&#34;dotfiles.targetPath&#34;</span><span class="p">:</span> <span class="s2">&#34;~/dotfiles&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這是個人 dotfile 和團隊環境標準化最乾淨的接合點——團隊定義「環境長什麼樣」，個人 dotfile 定義「在這個環境裡我怎麼操作」。</p>
<h3 id="devcontainer-的限制">Devcontainer 的限制</h3>
<ul>
<li><strong>Docker 是前提</strong>：團隊每個人的機器都要裝 Docker，macOS 上要跑 Docker Desktop 或 OrbStack</li>
<li><strong>GUI 應用不適合</strong>：devcontainer 定位是 headless 開發環境，不處理圖形桌面</li>
<li><strong>效能折扣</strong>：檔案系統操作在 macOS 上的 Docker volume 有效能折扣（Linux 上幾乎沒差）</li>
<li><strong>離線環境</strong>：建容器需要拉 image 和 feature，斷網環境要另外處理（見 <a href="/blog/infra/air-gapped/" data-link-title="斷網環境的 infra：沒有網路時怎麼做" data-link-desc="實體隔離或無法連網的環境裡，IaC、套件管理、容器映像、監控、CI/CD 怎麼運作 — 原則不變、工具路徑全部要換">Infra 斷網模組</a>）</li>
</ul>
<h2 id="nix宣告式的環境管理">Nix：宣告式的環境管理</h2>
<p>Nix 是另一條技術路線，用宣告式的方式描述整個開發環境，不依賴 Docker。</p>
<h3 id="核心概念-1">核心概念</h3>
<p>Nix 的 <code>flake.nix</code>（或 <code>shell.nix</code>）宣告了開發環境需要哪些套件，<code>nix develop</code> 進入這個環境：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># flake.nix</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="n">inputs</span><span class="o">.</span><span class="n">nixpkgs</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="s2">&#34;github:NixOS/nixpkgs/nixos-unstable&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="n">outputs</span> <span class="o">=</span> <span class="p">{</span> <span class="n">nixpkgs</span><span class="o">,</span> <span class="o">...</span> <span class="p">}:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">let</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="n">pkgs</span> <span class="o">=</span> <span class="n">nixpkgs</span><span class="o">.</span><span class="n">legacyPackages</span><span class="o">.</span><span class="n">x86_64-linux</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">in</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="n">devShells</span><span class="o">.</span><span class="n">default</span> <span class="o">=</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">mkShell</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">packages</span> <span class="o">=</span> <span class="k">with</span> <span class="n">pkgs</span><span class="p">;</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">          <span class="n">go_1_22</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">          <span class="n">nodejs_20</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">          <span class="n">postgresql_16</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">          <span class="n">redis</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="p">];</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="n">shellHook</span> <span class="o">=</span> <span class="s1">&#39;&#39;
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s1">          echo &#34;Dev environment ready&#34;
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s1">        &#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">      <span class="p">};</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>跟 Devcontainer 的差異：Nix 不用容器，直接在宿主機上建立隔離的環境（透過 Nix store 的路徑隔離）。優點是沒有 Docker 的效能折扣和額外層級；缺點是 Nix 的學習曲線陡峭、語法不直覺。</p>
<h3 id="home-managernix-管理-dotfile">Home Manager：Nix 管理 Dotfile</h3>
<p>Nix 生態裡的 Home Manager 可以用 Nix 語言宣告式地管理整個家目錄的配置——等於用 Nix 取代 stow/chezmoi 做 dotfile 管理：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># home.nix</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">{</span> <span class="n">config</span><span class="o">,</span> <span class="n">pkgs</span><span class="o">,</span> <span class="o">...</span> <span class="p">}:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="n">programs</span><span class="o">.</span><span class="n">git</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">userName</span> <span class="o">=</span> <span class="s2">&#34;Your Name&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">userEmail</span> <span class="o">=</span> <span class="s2">&#34;you@example.com&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">extraConfig</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="n">init</span><span class="o">.</span><span class="n">defaultBranch</span> <span class="o">=</span> <span class="s2">&#34;main&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="n">pull</span><span class="o">.</span><span class="n">rebase</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="n">programs</span><span class="o">.</span><span class="n">zsh</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">shellAliases</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">      <span class="n">ll</span> <span class="o">=</span> <span class="s2">&#34;ls -alF&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="n">gs</span> <span class="o">=</span> <span class="s2">&#34;git status&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="n">programs</span><span class="o">.</span><span class="n">neovim</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="n">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="n">defaultEditor</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Home Manager 把「安裝軟體」和「寫配置」統一成一份宣告——改完 <code>home-manager switch</code> 就同時更新套件和配置。這是 dotfile 管理的極致形式，但代價是整個技術棧鎖定在 Nix 生態裡。</p>
]]></content:encoded></item><item><title>Hyprland 安裝與環境建置</title><link>https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/hyprland-installation/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/hyprland-installation/</guid><description>&lt;p>Hyprland 的安裝分三層：compositor（負責視窗管理與畫面合成的核心程式，詳見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/04-window-management/wayland-explainer/" data-link-title="Wayland 顯示協議：為什麼 Hyprland 不跑在 X11 上" data-link-desc="想理解 Hyprland 底層的圖形架構、Wayland 跟 X11 的差異、XWayland 相容層、以及 2026 年 Wayland 已經是主流這件事時回來讀">Wayland 顯示協議&lt;/a>）本身、GPU 驅動、桌面配套工具。Compositor 只管視窗排列和畫面輸出，其餘功能（status bar、launcher、通知、音訊、藍牙、網路）都需要另外裝。本篇以 Arch Linux 為主要說明對象，其他發行版在最後簡述。&lt;/p>
&lt;h2 id="核心套件">核心套件&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">sudo pacman -S hyprland xorg-xwayland xdg-desktop-portal-hyprland&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>套件&lt;/th>
 &lt;th>角色&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>hyprland&lt;/code>&lt;/td>
 &lt;td>Compositor 本體，在 Arch 官方 &lt;code>extra&lt;/code> repo，不需要 AUR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>xorg-xwayland&lt;/code>&lt;/td>
 &lt;td>X11 相容層，讓舊 X11 應用程式在 Wayland 環境裡正常運行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>xdg-desktop-portal-hyprland&lt;/code>&lt;/td>
 &lt;td>Hyprland 專屬的 portal backend，處理 screen sharing 和檔案對話框&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>[VM 可測試]&lt;/strong> 套件安裝本身在 VM 和實機上完全相同。&lt;/p>&lt;/blockquote>
&lt;h2 id="gpu-驅動">GPU 驅動&lt;/h2>
&lt;p>GPU 驅動決定 Hyprland 的渲染是走硬體加速還是軟體 fallback。AMD 和 Intel 開箱即用，NVIDIA 需要額外配置。&lt;/p>
&lt;h3 id="amd推薦開箱即用">AMD（推薦，開箱即用）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">sudo pacman -S mesa vulkan-radeon libva-mesa-driver mesa-vdpau&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>mkinitcpio MODULES 加入：&lt;code>amdgpu&lt;/code>&lt;/p>
&lt;p>環境變數（放在 Hyprland 配置裡）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">env&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;LIBVA_DRIVER_NAME, radeonsi&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;VDPAU_DRIVER, radeonsi&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;AMD_VULKAN_ICD, RADV&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>AMD 是 Hyprland / Wayland 生態支援最好的 GPU。如果是新購硬體、有選擇的餘地，AMD 是最省事的選項。&lt;/p>
&lt;h3 id="intel開箱即用">Intel（開箱即用）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">sudo pacman -S mesa vulkan-intel intel-media-driver&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>mkinitcpio MODULES 加入：&lt;code>i915&lt;/code>&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">env&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;LIBVA_DRIVER_NAME, iHD&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;VDPAU_DRIVER, va_gl&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Intel 內顯在 Wayland 上表現穩定，適合筆電和輕度桌面使用。&lt;/p>
&lt;h3 id="nvidia需要額外配置非官方支援">NVIDIA（需要額外配置，非官方支援）&lt;/h3>
&lt;blockquote>
&lt;p>&lt;strong>[需實機測試]&lt;/strong> NVIDIA 的所有設定在 VM 中無意義——VM 使用 virtio-gpu 或軟體渲染，不走 NVIDIA 驅動。以下步驟只在實體機有 NVIDIA 顯卡時才需要。&lt;/p>&lt;/blockquote>
&lt;p>Hyprland 官方不支援 NVIDIA，但社群有成熟的 workaround。最低版本需求：&lt;/p>
&lt;ul>
&lt;li>NVIDIA driver &amp;gt;= 555（555 引入完整 GBM 支援和 explicit sync，是 Wayland 硬體渲染的最低依賴。535 系列有 explicit sync 問題，Wayland 下容易 flicker。590+ 修復多數 HDR 和多螢幕邊界問題）&lt;/li>
&lt;li>xorg-xwayland &amp;gt;= 24.1&lt;/li>
&lt;li>wayland-protocols &amp;gt;= 1.34&lt;/li>
&lt;/ul>
&lt;p>安裝：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">sudo pacman -S nvidia-dkms nvidia-utils libva-nvidia-driver
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 32-bit 支援（遊戲可能需要）：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">sudo pacman -S lib32-nvidia-utils&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>mkinitcpio MODULES（依硬體配置選擇）：&lt;/p>
&lt;p>純 NVIDIA 桌機：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">MODULES=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Intel + NVIDIA hybrid 筆電（Intel 排在 NVIDIA 前面）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">MODULES=(i915 nvidia nvidia_modeset nvidia_uvm nvidia_drm)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>更新 initramfs：&lt;/p></description><content:encoded><![CDATA[<p>Hyprland 的安裝分三層：compositor（負責視窗管理與畫面合成的核心程式，詳見 <a href="/blog/linux/dotfile/04-window-management/wayland-explainer/" data-link-title="Wayland 顯示協議：為什麼 Hyprland 不跑在 X11 上" data-link-desc="想理解 Hyprland 底層的圖形架構、Wayland 跟 X11 的差異、XWayland 相容層、以及 2026 年 Wayland 已經是主流這件事時回來讀">Wayland 顯示協議</a>）本身、GPU 驅動、桌面配套工具。Compositor 只管視窗排列和畫面輸出，其餘功能（status bar、launcher、通知、音訊、藍牙、網路）都需要另外裝。本篇以 Arch Linux 為主要說明對象，其他發行版在最後簡述。</p>
<h2 id="核心套件">核心套件</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo pacman -S hyprland xorg-xwayland xdg-desktop-portal-hyprland</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>套件</th>
          <th>角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>hyprland</code></td>
          <td>Compositor 本體，在 Arch 官方 <code>extra</code> repo，不需要 AUR</td>
      </tr>
      <tr>
          <td><code>xorg-xwayland</code></td>
          <td>X11 相容層，讓舊 X11 應用程式在 Wayland 環境裡正常運行</td>
      </tr>
      <tr>
          <td><code>xdg-desktop-portal-hyprland</code></td>
          <td>Hyprland 專屬的 portal backend，處理 screen sharing 和檔案對話框</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><strong>[VM 可測試]</strong> 套件安裝本身在 VM 和實機上完全相同。</p></blockquote>
<h2 id="gpu-驅動">GPU 驅動</h2>
<p>GPU 驅動決定 Hyprland 的渲染是走硬體加速還是軟體 fallback。AMD 和 Intel 開箱即用，NVIDIA 需要額外配置。</p>
<h3 id="amd推薦開箱即用">AMD（推薦，開箱即用）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo pacman -S mesa vulkan-radeon libva-mesa-driver mesa-vdpau</span></span></code></pre></div><p>mkinitcpio MODULES 加入：<code>amdgpu</code></p>
<p>環境變數（放在 Hyprland 配置裡）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">env</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s2">&#34;LIBVA_DRIVER_NAME, radeonsi&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s2">&#34;VDPAU_DRIVER, radeonsi&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s2">&#34;AMD_VULKAN_ICD, RADV&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>AMD 是 Hyprland / Wayland 生態支援最好的 GPU。如果是新購硬體、有選擇的餘地，AMD 是最省事的選項。</p>
<h3 id="intel開箱即用">Intel（開箱即用）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo pacman -S mesa vulkan-intel intel-media-driver</span></span></code></pre></div><p>mkinitcpio MODULES 加入：<code>i915</code></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">env</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s2">&#34;LIBVA_DRIVER_NAME, iHD&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s2">&#34;VDPAU_DRIVER, va_gl&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Intel 內顯在 Wayland 上表現穩定，適合筆電和輕度桌面使用。</p>
<h3 id="nvidia需要額外配置非官方支援">NVIDIA（需要額外配置，非官方支援）</h3>
<blockquote>
<p><strong>[需實機測試]</strong> NVIDIA 的所有設定在 VM 中無意義——VM 使用 virtio-gpu 或軟體渲染，不走 NVIDIA 驅動。以下步驟只在實體機有 NVIDIA 顯卡時才需要。</p></blockquote>
<p>Hyprland 官方不支援 NVIDIA，但社群有成熟的 workaround。最低版本需求：</p>
<ul>
<li>NVIDIA driver &gt;= 555（555 引入完整 GBM 支援和 explicit sync，是 Wayland 硬體渲染的最低依賴。535 系列有 explicit sync 問題，Wayland 下容易 flicker。590+ 修復多數 HDR 和多螢幕邊界問題）</li>
<li>xorg-xwayland &gt;= 24.1</li>
<li>wayland-protocols &gt;= 1.34</li>
</ul>
<p>安裝：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo pacman -S nvidia-dkms nvidia-utils libva-nvidia-driver
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 32-bit 支援（遊戲可能需要）：</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo pacman -S lib32-nvidia-utils</span></span></code></pre></div><p>mkinitcpio MODULES（依硬體配置選擇）：</p>
<p>純 NVIDIA 桌機：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">MODULES=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)</span></span></code></pre></div><p>Intel + NVIDIA hybrid 筆電（Intel 排在 NVIDIA 前面）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">MODULES=(i915 nvidia nvidia_modeset nvidia_uvm nvidia_drm)</span></span></code></pre></div><p>更新 initramfs：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo mkinitcpio -P</span></span></code></pre></div><p>Modprobe 設定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl"># /etc/modprobe.d/nvidia.conf
</span></span><span class="line"><span class="ln">2</span><span class="cl">options nvidia_drm modeset=1</span></span></code></pre></div><p>GRUB kernel parameter：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl"># /etc/default/grub
</span></span><span class="line"><span class="ln">2</span><span class="cl">GRUB_CMDLINE_LINUX_DEFAULT=&#34;... nvidia_drm.modeset=1 nvidia_drm.fbdev=1&#34;</span></span></code></pre></div><p>更新 GRUB：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo grub-mkconfig -o /boot/grub/grub.cfg</span></span></code></pre></div><p>Hyprland 配置裡的 NVIDIA 環境變數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">env</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s2">&#34;LIBVA_DRIVER_NAME, nvidia&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s2">&#34;__GLX_VENDOR_LIBRARY_NAME, nvidia&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s2">&#34;GBM_BACKEND, nvidia-drm&#34;</span><span class="p">,</span>         <span class="c1">-- 如果 Firefox crash，移除這行</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s2">&#34;NVD_BACKEND, direct&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Cursor 修正（NVIDIA 常見的硬體 cursor 問題）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">cursor</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">no_hardware_cursors</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">allow_dumb_copy</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>Suspend/resume 支援：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl <span class="nb">enable</span> nvidia-suspend.service nvidia-hibernate.service nvidia-resume.service</span></span></code></pre></div><h3 id="檢查-gpu-驅動狀態">檢查 GPU 驅動狀態</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">lspci -k <span class="p">|</span> grep -A <span class="m">3</span> VGA        <span class="c1"># 查看使用中的驅動</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">glxinfo <span class="p">|</span> grep <span class="s2">&#34;OpenGL renderer&#34;</span> <span class="c1"># 查看活躍的 renderer</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">vulkaninfo --summary             <span class="c1"># Vulkan 裝置資訊</span></span></span></code></pre></div><p>GPU 驅動相關的故障診斷（driver hang、畫面凍結、suspend/resume 後異常）見<a href="/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">桌面環境維護與故障排除</a>。</p>
<h2 id="桌面配套套件">桌面配套套件</h2>
<p>Hyprland 只管視窗，以下是一個可用桌面需要的配套工具：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">sudo pacman -S <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="se"></span>  kitty waybar wofi mako <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  polkit-gnome grim slurp wl-clipboard cliphist <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  hyprpaper hyprlock hypridle <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  pipewire pipewire-alsa pipewire-jack pipewire-pulse wireplumber <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  pamixer pavucontrol brightnessctl <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  bluez bluez-utils blueman <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  networkmanager network-manager-applet <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  thunar gvfs <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  qt5-wayland qt6-wayland <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  ttf-meslo-nerd noto-fonts noto-fonts-cjk</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>類別</th>
          <th>套件</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>終端機</td>
          <td><code>kitty</code></td>
          <td>Hyprland 預設配置使用的 terminal emulator</td>
      </tr>
      <tr>
          <td>狀態列</td>
          <td><code>waybar</code></td>
          <td>JSON config + CSS styling 的 status bar</td>
      </tr>
      <tr>
          <td>啟動器</td>
          <td><code>wofi</code></td>
          <td>Wayland 原生 app launcher</td>
      </tr>
      <tr>
          <td>通知</td>
          <td><code>mako</code></td>
          <td>輕量 notification daemon</td>
      </tr>
      <tr>
          <td>認證代理</td>
          <td><code>polkit-gnome</code></td>
          <td>GUI 權限提升（sudo 對話框）</td>
      </tr>
      <tr>
          <td>截圖</td>
          <td><code>grim</code> + <code>slurp</code></td>
          <td>截圖工具 + 選區工具</td>
      </tr>
      <tr>
          <td>剪貼簿</td>
          <td><code>wl-clipboard</code> + <code>cliphist</code></td>
          <td>剪貼簿存取 + 歷史記錄</td>
      </tr>
      <tr>
          <td>桌布</td>
          <td><code>hyprpaper</code></td>
          <td>靜態桌布</td>
      </tr>
      <tr>
          <td>鎖屏</td>
          <td><code>hyprlock</code></td>
          <td>Hyprland 配套鎖屏</td>
      </tr>
      <tr>
          <td>Idle</td>
          <td><code>hypridle</code></td>
          <td>閒置偵測（觸發鎖屏/休眠）</td>
      </tr>
      <tr>
          <td>音訊</td>
          <td><code>pipewire</code> + <code>wireplumber</code> + <code>pipewire-pulse</code></td>
          <td>音訊 server（取代 PulseAudio）</td>
      </tr>
      <tr>
          <td>音量控制</td>
          <td><code>pamixer</code>（CLI）+ <code>pavucontrol</code>（GUI）</td>
          <td>音量調整</td>
      </tr>
      <tr>
          <td>亮度</td>
          <td><code>brightnessctl</code></td>
          <td>螢幕亮度（筆電）</td>
      </tr>
      <tr>
          <td>藍牙</td>
          <td><code>bluez</code> + <code>bluez-utils</code> + <code>blueman</code></td>
          <td>藍牙協定 + GUI 管理</td>
      </tr>
      <tr>
          <td>網路</td>
          <td><code>networkmanager</code> + <code>nm-applet</code></td>
          <td>網路管理 + system tray 圖示</td>
      </tr>
      <tr>
          <td>檔案管理</td>
          <td><code>thunar</code> + <code>gvfs</code></td>
          <td>圖形檔案管理器 + 回收筒/遠端掛載</td>
      </tr>
      <tr>
          <td>Qt 相容</td>
          <td><code>qt5-wayland</code> + <code>qt6-wayland</code></td>
          <td>Qt 應用在 Wayland 下正常運行</td>
      </tr>
      <tr>
          <td>字型</td>
          <td><code>ttf-meslo-nerd</code> + <code>noto-fonts-cjk</code></td>
          <td>Nerd Font（waybar icon 需要）+ CJK 字型</td>
      </tr>
  </tbody>
</table>
<p>AUR 補充套件（用 <code>yay</code> 或 <code>paru</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">yay -S grimblast-git   <span class="c1"># Hyprland 截圖 wrapper</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">yay -S swww            <span class="c1"># 動態桌布（hyprpaper 的替代）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">yay -S rofi-wayland    <span class="c1"># 功能比 wofi 更豐富的 launcher</span></span></span></code></pre></div><blockquote>
<p><strong>[已驗證]</strong> 套件安裝在 VM 中完全可執行。waybar、wofi、mako、hyprlock 在 virtio-gpu-gl-pci 加速下正常 render（Hyprland 0.55 + UTM 4.7.5 實測）。</p>
<p><strong>[需實機測試]</strong> 藍牙（<code>bluez</code>）、亮度（<code>brightnessctl</code>）、音訊裝置偵測（<code>pipewire</code>）需要實際硬體。</p></blockquote>
<h2 id="登入管理器">登入管理器</h2>
<p>Hyprland 預設不含登入管理器。三個選項：</p>
<h3 id="從-tty-直接啟動最簡單">從 TTY 直接啟動（最簡單）</h3>
<p>登入文字介面後直接執行 <code>Hyprland</code>。建議用 wrapper script 設定必要的環境變數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="cp">#!/bin/sh
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="cp"></span><span class="c1"># ~/start-hyprland.sh</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">export</span> <span class="nv">XDG_SESSION_TYPE</span><span class="o">=</span>wayland
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">export</span> <span class="nv">XDG_CURRENT_DESKTOP</span><span class="o">=</span>Hyprland
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">export</span> <span class="nv">XDG_SESSION_DESKTOP</span><span class="o">=</span>Hyprland
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">exec</span> Hyprland</span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">chmod +x ~/start-hyprland.sh
</span></span><span class="line"><span class="ln">2</span><span class="cl">~/start-hyprland.sh</span></span></code></pre></div><h3 id="greetd--tuigreet推薦">greetd + tuigreet（推薦）</h3>
<p>greetd 是一個輕量的登入管理器，tuigreet 是它的 TUI 前端：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo pacman -S greetd greetd-tuigreet</span></span></code></pre></div><p>設定 <code>/etc/greetd/config.toml</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span><span class="nx">terminal</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">vt</span> <span class="p">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">[</span><span class="nx">default_session</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">command</span> <span class="p">=</span> <span class="s2">&#34;tuigreet --cmd Hyprland&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">user</span> <span class="p">=</span> <span class="s2">&#34;greeter&#34;</span></span></span></code></pre></div><p>自動登入（可選）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span><span class="nx">initial_session</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">command</span> <span class="p">=</span> <span class="s2">&#34;Hyprland&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">user</span> <span class="p">=</span> <span class="s2">&#34;yourusername&#34;</span></span></span></code></pre></div><p>啟用服務：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl <span class="nb">enable</span> greetd</span></span></code></pre></div><h3 id="sddm">SDDM</h3>
<p>如果已經有 KDE 或偏好圖形化登入介面：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo pacman -S sddm
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo systemctl <span class="nb">enable</span> sddm</span></span></code></pre></div><p>SDDM 會自動偵測到 Hyprland 並在登入畫面顯示它作為 session 選項。</p>
<blockquote>
<p><strong>[VM 可測試]</strong> 登入管理器的設定在 VM 中可完整測試。</p></blockquote>
<h2 id="首次啟動常見問題">首次啟動常見問題</h2>
<table>
  <thead>
      <tr>
          <th>症狀</th>
          <th>原因</th>
          <th>修正</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>黑屏、沒有 cursor</td>
          <td>缺少 polkit agent 或 seatd service</td>
          <td><code>sudo systemctl enable --now seatd</code> 或安裝 <code>polkit-gnome</code></td>
      </tr>
      <tr>
          <td>開不了 terminal</td>
          <td>預設 keybind 用的是 kitty，但 kitty 沒裝</td>
          <td><code>sudo pacman -S kitty</code> 或改 keybind 指向已安裝的 terminal</td>
      </tr>
      <tr>
          <td>Cursor 不見（NVIDIA）</td>
          <td>硬體 cursor 問題</td>
          <td>設定 <code>cursor { no_hardware_cursors = true }</code></td>
      </tr>
      <tr>
          <td>Portal 衝突，screen share 失敗</td>
          <td>同時裝了多個 portal backend</td>
          <td>移除 <code>xdg-desktop-portal-gnome</code> 和 <code>xdg-desktop-portal-gtk</code></td>
      </tr>
      <tr>
          <td>沒有音訊</td>
          <td>PipeWire 服務未啟動</td>
          <td><code>systemctl --user enable --now pipewire wireplumber</code></td>
      </tr>
      <tr>
          <td>Config 報錯但不影響使用</td>
          <td>自動產生的預設 config 語法不完整</td>
          <td>emergency keybind 仍可用：SUPER+Q 開 terminal、SUPER+M 離開</td>
      </tr>
  </tbody>
</table>
<h2 id="配置格式lua-取代-hyprlangv055">配置格式：Lua 取代 hyprlang（v0.55+）</h2>
<p>Hyprland v0.55（2026 年 4 月）起，配置格式從 hyprlang（<code>.conf</code>）遷移到 <strong>Lua</strong>（<code>.lua</code>）。舊的 <code>hyprland.conf</code> 在沒有 <code>.lua</code> 檔案時仍然可用，但 hyprlang 已被標記為 deprecated，預計在後續版本移除。</p>
<table>
  <thead>
      <tr>
          <th>格式</th>
          <th>檔案</th>
          <th>狀態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Lua（新）</td>
          <td><code>~/.config/hypr/hyprland.lua</code></td>
          <td>推薦</td>
      </tr>
      <tr>
          <td>hyprlang（舊）</td>
          <td><code>~/.config/hypr/hyprland.conf</code></td>
          <td>deprecated</td>
      </tr>
  </tbody>
</table>
<p>遷移工具：<code>hyprlang2lua</code>（Go，有瀏覽器版）、<code>hyprconf2lua</code>（Python pip）、<code>hypr-migrate</code>。</p>
<p>本系列後續文章（<a href="/blog/linux/dotfile/05-hyprland-config/hyprland-core-config/" data-link-title="Hyprland 核心配置" data-link-desc="Hyprland 的配置檔該怎麼組織、monitor 怎麼設定、keybind 怎麼設計、輸入裝置和環境變數怎麼配時回來讀">Hyprland 核心配置</a>、<a href="/blog/linux/dotfile/05-hyprland-config/workspace-rules-appearance/" data-link-title="Workspace、Window Rules 與外觀" data-link-desc="Hyprland 的 workspace 綁定螢幕、window rules 設定浮動例外、外觀動畫調教、layout 選型、autostart 和 plugin 管理時回來讀">Workspace 與外觀</a>、<a href="/blog/linux/dotfile/06-rice-design/caelestia-overview/" data-link-title="Caelestia 總覽：預組裝的 Hyprland 桌面 Shell" data-link-desc="考慮用 Caelestia 取代手動拼裝 waybar&#43;wofi&#43;mako、或評估預組裝桌面 shell 的 trade-off 時回來讀">Caelestia</a>）都使用 Lua 格式。如果在網路上看到 <code>.conf</code> 格式的教學，多數仍然可用，但建議儘早遷移到 Lua。</p>
<p>拆分配置用 Lua 原生的 <code>require()</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- hyprland.lua</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;keybinds&#34;</span><span class="p">)</span>    <span class="c1">-- 載入同目錄下的 keybinds.lua</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;rules&#34;</span><span class="p">)</span>       <span class="c1">-- 載入同目錄下的 rules.lua</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;appearance&#34;</span><span class="p">)</span></span></span></code></pre></div><p>詳細的配置語法和設定邏輯在<a href="/blog/linux/dotfile/05-hyprland-config/hyprland-core-config/" data-link-title="Hyprland 核心配置" data-link-desc="Hyprland 的配置檔該怎麼組織、monitor 怎麼設定、keybind 怎麼設計、輸入裝置和環境變數怎麼配時回來讀">核心配置</a>和 <a href="/blog/linux/dotfile/05-hyprland-config/workspace-rules-appearance/" data-link-title="Workspace、Window Rules 與外觀" data-link-desc="Hyprland 的 workspace 綁定螢幕、window rules 設定浮動例外、外觀動畫調教、layout 選型、autostart 和 plugin 管理時回來讀">Workspace、Window Rules 與外觀</a>。</p>
<h2 id="其他發行版">其他發行版</h2>
<h3 id="fedora">Fedora</h3>
<p>Fedora 39 起即在官方 repo。安裝：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo dnf install hyprland</span></span></code></pre></div><p>JaKooLit 提供自動化安裝腳本（<code>github.com/JaKooLit/Fedora-Hyprland</code>），Fedora 42+ 運作良好。</p>
<h3 id="nixos">NixOS</h3>
<p>NixOS 有官方 module，在 <code>configuration.nix</code> 加入：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">programs</span><span class="o">.</span><span class="n">hyprland</span><span class="o">.</span><span class="n">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span></span></span></code></pre></div><p>Home Manager 也有對應 module，可以宣告式管理 Hyprland 配置。</p>
<h3 id="ubuntu">Ubuntu</h3>
<p>不推薦。Ubuntu 的 point-release 模型跟 Hyprland 的 bleeding-edge 更新節奏衝突。沒有官方 PPA，從 source 編譯可行但維護成本高。如果一定要用 Debian 系，Arch 的 rolling release 或 Fedora 的半年週期更適合 Hyprland。</p>
<h2 id="安裝後的下一步">安裝後的下一步</h2>
<p>安裝完成後，VM 環境的額外設定（環境變數、效能調整、測試矩陣）見 <a href="/blog/linux/dotfile/05-hyprland-config/hyprland-vm-setup/" data-link-title="Hyprland VM 環境設定與測試矩陣" data-link-desc="要在 VM 裡測試 Hyprland 配置、或判斷某個設定該在 VM 還是實機驗證時回來讀">VM 環境設定與測試矩陣</a>。配置檔的組織方式和 keybind 設計見 <a href="/blog/linux/dotfile/05-hyprland-config/hyprland-core-config/" data-link-title="Hyprland 核心配置" data-link-desc="Hyprland 的配置檔該怎麼組織、monitor 怎麼設定、keybind 怎麼設計、輸入裝置和環境變數怎麼配時回來讀">Hyprland 核心配置</a>。</p>
]]></content:encoded></item><item><title>Lua 腳本語言</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/lua-scripting-language/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/lua-scripting-language/</guid><description>&lt;p>Lua 是一個輕量級腳本語言，1993 年在巴西開發，名字是葡萄牙語的「月亮」。整個直譯器約 300KB，設計目標是&lt;strong>嵌入到其他程式當配置和擴展語言&lt;/strong>，不是當獨立的通用語言。&lt;/p>
&lt;p>Hyprland（v0.55+ 的配置格式）、Neovim（整個 plugin 和配置生態）、WezTerm（terminal emulator 配置）都用 Lua 作為配置語言。在 dotfile 管理的脈絡裡，Lua 是讀懂和寫好這些工具配置的前提知識。&lt;/p>
&lt;h2 id="配置檔用到的核心語法">配置檔用到的核心語法&lt;/h2>
&lt;h3 id="變數和型別">變數和型別&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="n">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;hello&amp;#34;&lt;/span> &lt;span class="c1">-- 字串&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="n">count&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">42&lt;/span> &lt;span class="c1">-- 數字&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="n">enabled&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="c1">-- 布林&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="n">nothing&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="c1">-- 空值（類似其他語言的 null）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>local&lt;/code> 宣告區域變數。沒有 &lt;code>local&lt;/code> 的變數是全域的，配置檔裡幾乎都該用 &lt;code>local&lt;/code>。&lt;/p>
&lt;h3 id="table唯一的複合資料結構">Table：唯一的複合資料結構&lt;/h3>
&lt;p>Lua 只有一種複合型別——table，同時當 array 和 dictionary 用：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 當 array（index 從 1 開始，不是 0）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="n">fruits&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;apple&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;banana&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;cherry&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fruits&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="c1">-- &amp;#34;apple&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 當 dictionary&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="n">config&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="n">gaps_in&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="n">border_size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">layout&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;dwindle&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="n">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">config.gaps_in&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- 5&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 巢狀 table（配置檔最常見的形式）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="n">decoration&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="n">rounding&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="n">blur&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="n">enabled&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="n">size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="n">passes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Hyprland 的 &lt;code>hl.config()&lt;/code> 接收的就是一個巢狀 table：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">hl.config&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="n">general&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="n">gaps_in&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="n">gaps_out&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="n">decoration&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="n">rounding&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">})&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="function">Function&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="kr">function&lt;/span> &lt;span class="nf">greet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">who&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kr">return&lt;/span> &lt;span class="s2">&amp;#34;hello &amp;#34;&lt;/span> &lt;span class="o">..&lt;/span> &lt;span class="n">who&lt;/span> &lt;span class="c1">-- .. 是字串串接&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kr">end&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 匿名 function（Neovim 配置常見）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="n">vim.keymap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;leader&amp;gt;f&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">function&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="n">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;telescope.builtin&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">find_files&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="kr">end&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="條件判斷">條件判斷&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">if&lt;/span> &lt;span class="n">hostname&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;work-laptop&amp;#34;&lt;/span> &lt;span class="kr">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- 工作機設定&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kr">elseif&lt;/span> &lt;span class="n">hostname&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;home-desktop&amp;#34;&lt;/span> &lt;span class="kr">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- 家裡桌機設定&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kr">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- 預設&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="kr">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>只有 &lt;code>nil&lt;/code> 和 &lt;code>false&lt;/code> 是 falsy。&lt;code>0&lt;/code> 和 &lt;code>&amp;quot;&amp;quot;&lt;/code> 是 truthy（跟 Python 不同）。&lt;/p>
&lt;h3 id="迴圈">迴圈&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 數字 for（Hyprland 批次產生 workspace keybind）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="kr">for&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">9&lt;/span> &lt;span class="kr">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="n">hl.bind&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;SUPER&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tostring&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="s2">&amp;#34;workspace&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tostring&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="kr">end&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 遍歷 table&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="n">tools&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;zsh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;git&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;nvim&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tmux&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="kr">for&lt;/span> &lt;span class="n">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tool&lt;/span> &lt;span class="kr">in&lt;/span> &lt;span class="n">ipairs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tools&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kr">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tool&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="kr">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="模組化require">模組化（require）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- hyprland.lua 裡載入同目錄的其他 .lua 檔&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;keybinds&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- 載入 keybinds.lua&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;rules&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- 載入 rules.lua&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;appearance&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- 載入 appearance.lua&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>require()&lt;/code> 是 Lua 原生的模組載入，取代了舊 Hyprland &lt;code>.conf&lt;/code> 格式的 &lt;code>source = ...&lt;/code> 指令。&lt;/p>
&lt;h2 id="為什麼配置工具選-lua">為什麼配置工具選 Lua&lt;/h2>
&lt;p>Lua 被嵌入到配置層的原因是一組特定的 trade-off：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>比 JSON/TOML/YAML 強&lt;/strong>：有變數、迴圈、條件判斷。配置檔可以用 &lt;code>for&lt;/code> 產生重複項目、用 &lt;code>if&lt;/code> 處理機器差異，不需要外部 template engine&lt;/li>
&lt;li>&lt;strong>比 Python/JavaScript 輕&lt;/strong>：300KB 的直譯器可以嵌入 C/C++ 程式，不需要拖一個完整的 runtime&lt;/li>
&lt;li>&lt;strong>沙盒化容易&lt;/strong>：宿主程式可以控制 Lua 能存取哪些 API，限制配置檔的能力範圍&lt;/li>
&lt;/ul>
&lt;p>這也是 Neovim 從 VimScript 遷移到 Lua 的理由——plugin 生態需要一個真正的程式語言（有資料結構、有錯誤處理），但又不能讓配置檔變成一個安全隱患。&lt;/p></description><content:encoded><![CDATA[<p>Lua 是一個輕量級腳本語言，1993 年在巴西開發，名字是葡萄牙語的「月亮」。整個直譯器約 300KB，設計目標是<strong>嵌入到其他程式當配置和擴展語言</strong>，不是當獨立的通用語言。</p>
<p>Hyprland（v0.55+ 的配置格式）、Neovim（整個 plugin 和配置生態）、WezTerm（terminal emulator 配置）都用 Lua 作為配置語言。在 dotfile 管理的脈絡裡，Lua 是讀懂和寫好這些工具配置的前提知識。</p>
<h2 id="配置檔用到的核心語法">配置檔用到的核心語法</h2>
<h3 id="變數和型別">變數和型別</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">local</span> <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;hello&#34;</span>       <span class="c1">-- 字串</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">local</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">42</span>            <span class="c1">-- 數字</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">local</span> <span class="n">enabled</span> <span class="o">=</span> <span class="kc">true</span>        <span class="c1">-- 布林</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="kd">local</span> <span class="n">nothing</span> <span class="o">=</span> <span class="kc">nil</span>         <span class="c1">-- 空值（類似其他語言的 null）</span></span></span></code></pre></div><p><code>local</code> 宣告區域變數。沒有 <code>local</code> 的變數是全域的，配置檔裡幾乎都該用 <code>local</code>。</p>
<h3 id="table唯一的複合資料結構">Table：唯一的複合資料結構</h3>
<p>Lua 只有一種複合型別——table，同時當 array 和 dictionary 用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 當 array（index 從 1 開始，不是 0）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">local</span> <span class="n">fruits</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&#34;apple&#34;</span><span class="p">,</span> <span class="s2">&#34;banana&#34;</span><span class="p">,</span> <span class="s2">&#34;cherry&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">print</span><span class="p">(</span><span class="n">fruits</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>  <span class="c1">-- &#34;apple&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">-- 當 dictionary</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kd">local</span> <span class="n">config</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">gaps_in</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">border_size</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">layout</span> <span class="o">=</span> <span class="s2">&#34;dwindle&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">print</span><span class="p">(</span><span class="n">config.gaps_in</span><span class="p">)</span>  <span class="c1">-- 5</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">-- 巢狀 table（配置檔最常見的形式）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kd">local</span> <span class="n">decoration</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">rounding</span> <span class="o">=</span> <span class="mi">8</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">blur</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="n">enabled</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">size</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="n">passes</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Hyprland 的 <code>hl.config()</code> 接收的就是一個巢狀 table：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">general</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">gaps_in</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">gaps_out</span> <span class="o">=</span> <span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="n">decoration</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="n">rounding</span> <span class="o">=</span> <span class="mi">8</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><h3 id="function">Function</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">local</span> <span class="kr">function</span> <span class="nf">greet</span><span class="p">(</span><span class="n">who</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kr">return</span> <span class="s2">&#34;hello &#34;</span> <span class="o">..</span> <span class="n">who</span>   <span class="c1">-- .. 是字串串接</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kr">end</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">-- 匿名 function（Neovim 配置常見）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">vim.keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="s2">&#34;n&#34;</span><span class="p">,</span> <span class="s2">&#34;&lt;leader&gt;f&#34;</span><span class="p">,</span> <span class="kr">function</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">require</span><span class="p">(</span><span class="s2">&#34;telescope.builtin&#34;</span><span class="p">).</span><span class="n">find_files</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="kr">end</span><span class="p">)</span></span></span></code></pre></div><h3 id="條件判斷">條件判斷</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">if</span> <span class="n">hostname</span> <span class="o">==</span> <span class="s2">&#34;work-laptop&#34;</span> <span class="kr">then</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="c1">-- 工作機設定</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kr">elseif</span> <span class="n">hostname</span> <span class="o">==</span> <span class="s2">&#34;home-desktop&#34;</span> <span class="kr">then</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1">-- 家裡桌機設定</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kr">else</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="c1">-- 預設</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="kr">end</span></span></span></code></pre></div><p>只有 <code>nil</code> 和 <code>false</code> 是 falsy。<code>0</code> 和 <code>&quot;&quot;</code> 是 truthy（跟 Python 不同）。</p>
<h3 id="迴圈">迴圈</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 數字 for（Hyprland 批次產生 workspace keybind）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kr">for</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">9</span> <span class="kr">do</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="n">tostring</span><span class="p">(</span><span class="n">i</span><span class="p">),</span> <span class="s2">&#34;workspace&#34;</span><span class="p">,</span> <span class="n">tostring</span><span class="p">(</span><span class="n">i</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kr">end</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">-- 遍歷 table</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">local</span> <span class="n">tools</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&#34;zsh&#34;</span><span class="p">,</span> <span class="s2">&#34;git&#34;</span><span class="p">,</span> <span class="s2">&#34;nvim&#34;</span><span class="p">,</span> <span class="s2">&#34;tmux&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="kr">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">tool</span> <span class="kr">in</span> <span class="n">ipairs</span><span class="p">(</span><span class="n">tools</span><span class="p">)</span> <span class="kr">do</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">print</span><span class="p">(</span><span class="n">tool</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kr">end</span></span></span></code></pre></div><h3 id="模組化require">模組化（require）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- hyprland.lua 裡載入同目錄的其他 .lua 檔</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;keybinds&#34;</span><span class="p">)</span>     <span class="c1">-- 載入 keybinds.lua</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;rules&#34;</span><span class="p">)</span>        <span class="c1">-- 載入 rules.lua</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;appearance&#34;</span><span class="p">)</span>   <span class="c1">-- 載入 appearance.lua</span></span></span></code></pre></div><p><code>require()</code> 是 Lua 原生的模組載入，取代了舊 Hyprland <code>.conf</code> 格式的 <code>source = ...</code> 指令。</p>
<h2 id="為什麼配置工具選-lua">為什麼配置工具選 Lua</h2>
<p>Lua 被嵌入到配置層的原因是一組特定的 trade-off：</p>
<ul>
<li><strong>比 JSON/TOML/YAML 強</strong>：有變數、迴圈、條件判斷。配置檔可以用 <code>for</code> 產生重複項目、用 <code>if</code> 處理機器差異，不需要外部 template engine</li>
<li><strong>比 Python/JavaScript 輕</strong>：300KB 的直譯器可以嵌入 C/C++ 程式，不需要拖一個完整的 runtime</li>
<li><strong>沙盒化容易</strong>：宿主程式可以控制 Lua 能存取哪些 API，限制配置檔的能力範圍</li>
</ul>
<p>這也是 Neovim 從 VimScript 遷移到 Lua 的理由——plugin 生態需要一個真正的程式語言（有資料結構、有錯誤處理），但又不能讓配置檔變成一個安全隱患。</p>
<h2 id="其他使用-lua-的場景">其他使用 Lua 的場景</h2>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>用法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Neovim</td>
          <td>整個配置和 plugin 生態基於 Lua</td>
      </tr>
      <tr>
          <td>WezTerm</td>
          <td>terminal emulator 配置（<code>wezterm.lua</code>）</td>
      </tr>
      <tr>
          <td>Awesome WM</td>
          <td>X11 tiling WM 的配置和擴展</td>
      </tr>
      <tr>
          <td>Redis</td>
          <td><code>EVAL</code> 指令在 server 端執行 Lua script</td>
      </tr>
      <tr>
          <td>Nginx/OpenResty</td>
          <td>用 Lua 寫高效能的 request 處理邏輯</td>
      </tr>
      <tr>
          <td>遊戲</td>
          <td>World of Warcraft UI mod、Roblox、很多遊戲引擎的腳本層</td>
      </tr>
  </tbody>
</table>
<p>共同模式：一個用 C/C++ 寫的高效能核心，把 Lua 嵌入進去當配置和擴展語言。</p>
<h2 id="跟-pythonjavascript-的差異速查">跟 Python/JavaScript 的差異速查</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>Lua</th>
          <th>Python</th>
          <th>JavaScript</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Array index 起始</td>
          <td><strong>1</strong></td>
          <td>0</td>
          <td>0</td>
      </tr>
      <tr>
          <td>字串串接</td>
          <td><code>..</code></td>
          <td><code>+</code></td>
          <td><code>+</code></td>
      </tr>
      <tr>
          <td>不等於</td>
          <td><code>~=</code></td>
          <td><code>!=</code></td>
          <td><code>!==</code></td>
      </tr>
      <tr>
          <td>邏輯運算</td>
          <td><code>and</code> <code>or</code> <code>not</code></td>
          <td><code>and</code> <code>or</code> <code>not</code></td>
          <td><code>&amp;&amp;</code> <code>||</code> <code>!</code></td>
      </tr>
      <tr>
          <td>空值</td>
          <td><code>nil</code></td>
          <td><code>None</code></td>
          <td><code>null</code>/<code>undefined</code></td>
      </tr>
      <tr>
          <td>Falsy 值</td>
          <td><code>nil</code>, <code>false</code></td>
          <td><code>None</code>, <code>False</code>, <code>0</code>, <code>&quot;&quot;</code>, <code>[]</code></td>
          <td><code>null</code>, <code>undefined</code>, <code>false</code>, <code>0</code>, <code>&quot;&quot;</code></td>
      </tr>
      <tr>
          <td>沒有 <code>+=</code></td>
          <td><code>x = x + 1</code></td>
          <td><code>x += 1</code></td>
          <td><code>x += 1</code></td>
      </tr>
      <tr>
          <td>註解</td>
          <td><code>--</code></td>
          <td><code>#</code></td>
          <td><code>//</code></td>
      </tr>
      <tr>
          <td>多行註解</td>
          <td><code>--[[ ... ]]</code></td>
          <td><code>&quot;&quot;&quot; ... &quot;&quot;&quot;</code></td>
          <td><code>/* ... */</code></td>
      </tr>
  </tbody>
</table>
<p>寫 Hyprland 或 Neovim 配置用到的 Lua 知識量很小——主要是 table（配置結構）、for loop（批次 keybind）、if-else（機器差異）、require（模組拆分）。不需要學 metatable、coroutine、metatmethod 這些進階功能。</p>
]]></content:encoded></item><item><title>Terminal Emulator 配置</title><link>https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/terminal-emulator-config/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/terminal-emulator-config/</guid><description>&lt;p>Terminal emulator 是你看到的那個「視窗」本身——字型渲染、配色、透明度、快捷鍵、分頁行為。常見的選擇和它們的配置檔位置：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Terminal&lt;/th>
 &lt;th>OS&lt;/th>
 &lt;th>配置格式&lt;/th>
 &lt;th>配置路徑&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Alacritty&lt;/td>
 &lt;td>跨平台&lt;/td>
 &lt;td>TOML&lt;/td>
 &lt;td>&lt;code>~/.config/alacritty/alacritty.toml&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Kitty&lt;/td>
 &lt;td>跨平台&lt;/td>
 &lt;td>自定義 key=value&lt;/td>
 &lt;td>&lt;code>~/.config/kitty/kitty.conf&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>WezTerm&lt;/td>
 &lt;td>跨平台&lt;/td>
 &lt;td>Lua&lt;/td>
 &lt;td>&lt;code>~/.config/wezterm/wezterm.lua&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>iTerm2&lt;/td>
 &lt;td>macOS&lt;/td>
 &lt;td>plist（GUI 設定）&lt;/td>
 &lt;td>可匯出 JSON profile&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Foot&lt;/td>
 &lt;td>Linux/Wayland&lt;/td>
 &lt;td>INI&lt;/td>
 &lt;td>&lt;code>~/.config/foot/foot.ini&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Windows Terminal&lt;/td>
 &lt;td>Windows&lt;/td>
 &lt;td>JSON&lt;/td>
 &lt;td>特定路徑下的 settings.json&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Dotfile 管理的判讀：配置格式是純文字（TOML/Lua/INI/JSON）的 terminal emulator，配置檔可以直接進 dotfile repo。iTerm2 這種以 GUI 面板為主的，要用它的匯出功能另外處理。&lt;/p>
&lt;p>選型建議：如果跨 macOS + Linux 雙平台，Alacritty 或 WezTerm 的「一份配置兩邊通用」是明確優勢。如果只在 Linux 上用 Wayland，Foot 是輕量首選。&lt;/p>
&lt;h2 id="該放進配置的核心項目">該放進配置的核心項目&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>字型&lt;/strong>：字型家族、大小、行高。建議使用 Nerd Font（含 icon glyph 的程式字型），很多 TUI 工具和 prompt 依賴這些 glyph&lt;/li>
&lt;li>&lt;strong>配色&lt;/strong>：前景/背景色、ANSI 16 色的定義。配色方案（Catppuccin、Tokyo Night、Gruvbox 等）通常有各 terminal 的預設配置檔可直接套用&lt;/li>
&lt;li>&lt;strong>快捷鍵&lt;/strong>：分頁/分割畫面的快捷鍵。注意跟 tmux/zellij 的快捷鍵衝突問題&lt;/li>
&lt;li>&lt;strong>渲染&lt;/strong>：GPU 加速、字型 hinting、抗鋸齒設定&lt;/li>
&lt;/ul>
&lt;h2 id="配色系統的跨工具一致性">配色系統的跨工具一致性&lt;/h2>
&lt;p>配色方案（color scheme）會同時影響 terminal emulator、editor、tmux status bar、shell prompt。用同一套配色方案（例如 Catppuccin Mocha）跨工具統一視覺是 rice 的基礎。&lt;/p>
&lt;p>管理方式：&lt;/p>
&lt;ul>
&lt;li>每個工具各自的配色設定檔都放進 dotfile repo&lt;/li>
&lt;li>主題選擇集中記錄（例如 dotfile repo 的 README 寫「全域使用 Catppuccin Mocha」），換主題時有對照清單知道要改哪些檔案&lt;/li>
&lt;li>部分配色方案提供「一鍵安裝腳本」涵蓋多個工具，也可以放在 bootstrap script 裡&lt;/li>
&lt;/ul>
&lt;h2 id="字型管理">字型管理&lt;/h2>
&lt;p>Nerd Font 是需要安裝在系統上的，不是單純的配置檔。處理方式：&lt;/p>
&lt;ul>
&lt;li>macOS：Brewfile 裡加 &lt;code>cask &amp;quot;font-hack-nerd-font&amp;quot;&lt;/code>（透過 homebrew-cask-fonts tap）&lt;/li>
&lt;li>Linux：套件管理器安裝或手動下載到 &lt;code>~/.local/share/fonts/&lt;/code>&lt;/li>
&lt;li>字型檔案本身不進 dotfile repo（太大、有版權），只記錄「安裝哪個字型」在套件清單或 bootstrap script 裡&lt;/li>
&lt;/ul>
&lt;p>安裝字型後如果畫面仍然顯示豆腐方塊，原因通常不是字型沒裝好，而是顯示它的程式在安裝之前就已啟動。每個 process 的可用字型集合在啟動時決定，之後新裝的字型對它不可見——需要重啟該程式才生效。詳見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">字型的可用集合在 process 啟動時決定&lt;/a>。fontconfig 的工具分工與 fallback 機制見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/fontconfig/" data-link-title="fontconfig — 字型搜尋、匹配與 fallback 服務" data-link-desc="不確定 fc-list / fc-match / fc-cache 各做什麼、或 fontconfig fallback 機制怎麼運作時回來讀">fontconfig&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Terminal emulator 是你看到的那個「視窗」本身——字型渲染、配色、透明度、快捷鍵、分頁行為。常見的選擇和它們的配置檔位置：</p>
<table>
  <thead>
      <tr>
          <th>Terminal</th>
          <th>OS</th>
          <th>配置格式</th>
          <th>配置路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Alacritty</td>
          <td>跨平台</td>
          <td>TOML</td>
          <td><code>~/.config/alacritty/alacritty.toml</code></td>
      </tr>
      <tr>
          <td>Kitty</td>
          <td>跨平台</td>
          <td>自定義 key=value</td>
          <td><code>~/.config/kitty/kitty.conf</code></td>
      </tr>
      <tr>
          <td>WezTerm</td>
          <td>跨平台</td>
          <td>Lua</td>
          <td><code>~/.config/wezterm/wezterm.lua</code></td>
      </tr>
      <tr>
          <td>iTerm2</td>
          <td>macOS</td>
          <td>plist（GUI 設定）</td>
          <td>可匯出 JSON profile</td>
      </tr>
      <tr>
          <td>Foot</td>
          <td>Linux/Wayland</td>
          <td>INI</td>
          <td><code>~/.config/foot/foot.ini</code></td>
      </tr>
      <tr>
          <td>Windows Terminal</td>
          <td>Windows</td>
          <td>JSON</td>
          <td>特定路徑下的 settings.json</td>
      </tr>
  </tbody>
</table>
<p>Dotfile 管理的判讀：配置格式是純文字（TOML/Lua/INI/JSON）的 terminal emulator，配置檔可以直接進 dotfile repo。iTerm2 這種以 GUI 面板為主的，要用它的匯出功能另外處理。</p>
<p>選型建議：如果跨 macOS + Linux 雙平台，Alacritty 或 WezTerm 的「一份配置兩邊通用」是明確優勢。如果只在 Linux 上用 Wayland，Foot 是輕量首選。</p>
<h2 id="該放進配置的核心項目">該放進配置的核心項目</h2>
<ul>
<li><strong>字型</strong>：字型家族、大小、行高。建議使用 Nerd Font（含 icon glyph 的程式字型），很多 TUI 工具和 prompt 依賴這些 glyph</li>
<li><strong>配色</strong>：前景/背景色、ANSI 16 色的定義。配色方案（Catppuccin、Tokyo Night、Gruvbox 等）通常有各 terminal 的預設配置檔可直接套用</li>
<li><strong>快捷鍵</strong>：分頁/分割畫面的快捷鍵。注意跟 tmux/zellij 的快捷鍵衝突問題</li>
<li><strong>渲染</strong>：GPU 加速、字型 hinting、抗鋸齒設定</li>
</ul>
<h2 id="配色系統的跨工具一致性">配色系統的跨工具一致性</h2>
<p>配色方案（color scheme）會同時影響 terminal emulator、editor、tmux status bar、shell prompt。用同一套配色方案（例如 Catppuccin Mocha）跨工具統一視覺是 rice 的基礎。</p>
<p>管理方式：</p>
<ul>
<li>每個工具各自的配色設定檔都放進 dotfile repo</li>
<li>主題選擇集中記錄（例如 dotfile repo 的 README 寫「全域使用 Catppuccin Mocha」），換主題時有對照清單知道要改哪些檔案</li>
<li>部分配色方案提供「一鍵安裝腳本」涵蓋多個工具，也可以放在 bootstrap script 裡</li>
</ul>
<h2 id="字型管理">字型管理</h2>
<p>Nerd Font 是需要安裝在系統上的，不是單純的配置檔。處理方式：</p>
<ul>
<li>macOS：Brewfile 裡加 <code>cask &quot;font-hack-nerd-font&quot;</code>（透過 homebrew-cask-fonts tap）</li>
<li>Linux：套件管理器安裝或手動下載到 <code>~/.local/share/fonts/</code></li>
<li>字型檔案本身不進 dotfile repo（太大、有版權），只記錄「安裝哪個字型」在套件清單或 bootstrap script 裡</li>
</ul>
<p>安裝字型後如果畫面仍然顯示豆腐方塊，原因通常不是字型沒裝好，而是顯示它的程式在安裝之前就已啟動。每個 process 的可用字型集合在啟動時決定，之後新裝的字型對它不可見——需要重啟該程式才生效。詳見 <a href="/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">字型的可用集合在 process 啟動時決定</a>。fontconfig 的工具分工與 fallback 機制見 <a href="/blog/linux/dotfile/knowledge-cards/fontconfig/" data-link-title="fontconfig — 字型搜尋、匹配與 fallback 服務" data-link-desc="不確定 fc-list / fc-match / fc-cache 各做什麼、或 fontconfig fallback 機制怎麼運作時回來讀">fontconfig</a>。</p>
]]></content:encoded></item><item><title>Zsh 模組化配置</title><link>https://tarrragon.github.io/blog/linux/dotfile/02-shell-config/zsh-modular-config/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/02-shell-config/zsh-modular-config/</guid><description>&lt;p>Shell 配置是 dotfile 管理裡最基礎也最常失控的一層。&lt;code>.zshrc&lt;/code> 或 &lt;code>.bashrc&lt;/code> 通常是開發者第一個開始客製的檔案，也是最容易長成數百行無結構巨檔的對象。&lt;/p>
&lt;h2 id="zsh-vs-bash-的配置檔載入順序">Zsh vs Bash 的配置檔載入順序&lt;/h2>
&lt;p>理解配置檔的載入順序是結構化拆分的前提。不知道哪個檔案在什麼時機被讀取，就無法判斷設定該放在哪。&lt;/p>
&lt;h3 id="bash-的載入順序">Bash 的載入順序&lt;/h3>
&lt;p>Bash 區分 login shell 和 non-login shell，兩者讀取的檔案不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Login shell&lt;/strong>（SSH 進來、&lt;code>bash --login&lt;/code>）：讀 &lt;code>~/.bash_profile&lt;/code>（如果不存在，依序嘗試 &lt;code>~/.bash_login&lt;/code> → &lt;code>~/.profile&lt;/code>）&lt;/li>
&lt;li>&lt;strong>Non-login interactive shell&lt;/strong>（開一個新終端機視窗）：讀 &lt;code>~/.bashrc&lt;/code>&lt;/li>
&lt;li>常見做法：在 &lt;code>~/.bash_profile&lt;/code> 裡 source &lt;code>~/.bashrc&lt;/code>，確保設定不管怎麼進來都一致&lt;/li>
&lt;/ul>
&lt;h3 id="zsh-的載入順序">Zsh 的載入順序&lt;/h3>
&lt;p>Zsh 的載入鏈比 Bash 更細緻：&lt;/p>
&lt;ol>
&lt;li>&lt;code>~/.zshenv&lt;/code> — 每次都讀（login、non-login、script 都會），放環境變數&lt;/li>
&lt;li>&lt;code>~/.zprofile&lt;/code> — 只有 login shell 讀，對應 Bash 的 &lt;code>~/.bash_profile&lt;/code>&lt;/li>
&lt;li>&lt;code>~/.zshrc&lt;/code> — interactive shell 讀，放 alias、function、prompt、plugin&lt;/li>
&lt;li>&lt;code>~/.zlogin&lt;/code> — login shell 在 &lt;code>.zshrc&lt;/code> 之後讀（少用）&lt;/li>
&lt;li>&lt;code>~/.zlogout&lt;/code> — logout 時讀（少用）&lt;/li>
&lt;/ol>
&lt;p>實務上 90% 的設定都進 &lt;code>.zshrc&lt;/code>，環境變數（&lt;code>PATH&lt;/code>、&lt;code>EDITOR&lt;/code>）放 &lt;code>.zshenv&lt;/code>。&lt;/p>
&lt;h2 id="結構化拆分從單一巨檔到模組化">結構化拆分：從單一巨檔到模組化&lt;/h2>
&lt;p>一個典型的失控 &lt;code>.zshrc&lt;/code> 長這樣：PATH 設定、alias、function、plugin 載入、prompt 配置、各種工具的 eval/source 全混在一起，改一個東西要在五百行裡找位置。&lt;/p>
&lt;p>模組化的目標是依職責拆檔，&lt;code>.zshrc&lt;/code> 本身只負責 source 這些模組：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.zshrc — 只做 source，不放具體設定&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 環境變數（PATH 在 .zshenv，這裡放其他 interactive 專用的）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="nb">source&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.config/zsh/env.zsh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># Alias&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="nb">source&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.config/zsh/aliases.zsh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># Function&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="nb">source&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.config/zsh/functions.zsh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># Plugin manager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="nb">source&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.config/zsh/plugins.zsh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="c1"># Prompt / theme&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="nb">source&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.config/zsh/prompt.zsh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="c1"># 工具整合（fzf, nvm, pyenv, etc.）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="nb">source&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.config/zsh/tools.zsh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="c1"># 機器專屬設定（不進 Git）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="o">[[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.config/zsh/local.zsh&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">source&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.config/zsh/local.zsh&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="各模組的職責">各模組的職責&lt;/h2>
&lt;h3 id="aliaseszsh--短指令對映">aliases.zsh — 短指令對映&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 檔案操作&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> &lt;span class="nv">ll&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;ls -alF&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> &lt;span class="nv">la&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;ls -A&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># Git 常用&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> &lt;span class="nv">gs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;git status&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> &lt;span class="nv">gd&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;git diff&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> &lt;span class="nv">gco&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;git checkout&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> &lt;span class="nv">gp&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;git push&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># 導航&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> ..&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;cd ..&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> ...&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;cd ../..&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>判讀準則：alias 適合「不帶參數的簡單替換」。如果需要參數處理或條件判斷，改用 function。&lt;/p>
&lt;h3 id="functionszsh--帶邏輯的常用操作">functions.zsh — 帶邏輯的常用操作&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 建目錄並進入&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">mkcd&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> mkdir -p &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 在 Git repo 根目錄搜尋&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">ggrep&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> git grep &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$@&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>git rev-parse --show-toplevel&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="o">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="toolszsh--第三方工具的初始化">tools.zsh — 第三方工具的初始化&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># fzf&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="o">[[&lt;/span> -f ~/.fzf.zsh &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">source&lt;/span> ~/.fzf.zsh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># nvm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">NVM_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.nvm&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="o">[[&lt;/span> -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$NVM_DIR&lt;/span>&lt;span class="s2">/nvm.sh&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">source&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$NVM_DIR&lt;/span>&lt;span class="s2">/nvm.sh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># pyenv&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="nb">command&lt;/span> -v pyenv &amp;gt;/dev/null &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">eval&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>pyenv init -&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個工具的 init 前面加存在性檢查（&lt;code>command -v&lt;/code> 或 &lt;code>[[ -f ]]&lt;/code>），避免在沒裝該工具的機器上報錯。&lt;/p></description><content:encoded><![CDATA[<p>Shell 配置是 dotfile 管理裡最基礎也最常失控的一層。<code>.zshrc</code> 或 <code>.bashrc</code> 通常是開發者第一個開始客製的檔案，也是最容易長成數百行無結構巨檔的對象。</p>
<h2 id="zsh-vs-bash-的配置檔載入順序">Zsh vs Bash 的配置檔載入順序</h2>
<p>理解配置檔的載入順序是結構化拆分的前提。不知道哪個檔案在什麼時機被讀取，就無法判斷設定該放在哪。</p>
<h3 id="bash-的載入順序">Bash 的載入順序</h3>
<p>Bash 區分 login shell 和 non-login shell，兩者讀取的檔案不同：</p>
<ul>
<li><strong>Login shell</strong>（SSH 進來、<code>bash --login</code>）：讀 <code>~/.bash_profile</code>（如果不存在，依序嘗試 <code>~/.bash_login</code> → <code>~/.profile</code>）</li>
<li><strong>Non-login interactive shell</strong>（開一個新終端機視窗）：讀 <code>~/.bashrc</code></li>
<li>常見做法：在 <code>~/.bash_profile</code> 裡 source <code>~/.bashrc</code>，確保設定不管怎麼進來都一致</li>
</ul>
<h3 id="zsh-的載入順序">Zsh 的載入順序</h3>
<p>Zsh 的載入鏈比 Bash 更細緻：</p>
<ol>
<li><code>~/.zshenv</code> — 每次都讀（login、non-login、script 都會），放環境變數</li>
<li><code>~/.zprofile</code> — 只有 login shell 讀，對應 Bash 的 <code>~/.bash_profile</code></li>
<li><code>~/.zshrc</code> — interactive shell 讀，放 alias、function、prompt、plugin</li>
<li><code>~/.zlogin</code> — login shell 在 <code>.zshrc</code> 之後讀（少用）</li>
<li><code>~/.zlogout</code> — logout 時讀（少用）</li>
</ol>
<p>實務上 90% 的設定都進 <code>.zshrc</code>，環境變數（<code>PATH</code>、<code>EDITOR</code>）放 <code>.zshenv</code>。</p>
<h2 id="結構化拆分從單一巨檔到模組化">結構化拆分：從單一巨檔到模組化</h2>
<p>一個典型的失控 <code>.zshrc</code> 長這樣：PATH 設定、alias、function、plugin 載入、prompt 配置、各種工具的 eval/source 全混在一起，改一個東西要在五百行裡找位置。</p>
<p>模組化的目標是依職責拆檔，<code>.zshrc</code> 本身只負責 source 這些模組：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># ~/.zshrc — 只做 source，不放具體設定</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 環境變數（PATH 在 .zshenv，這裡放其他 interactive 專用的）</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/zsh/env.zsh&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># Alias</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/zsh/aliases.zsh&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># Function</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/zsh/functions.zsh&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># Plugin manager</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/zsh/plugins.zsh&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># Prompt / theme</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/zsh/prompt.zsh&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># 工具整合（fzf, nvm, pyenv, etc.）</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/zsh/tools.zsh&#34;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># 機器專屬設定（不進 Git）</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="o">[[</span> -f <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/zsh/local.zsh&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/zsh/local.zsh&#34;</span></span></span></code></pre></div><h2 id="各模組的職責">各模組的職責</h2>
<h3 id="aliaseszsh--短指令對映">aliases.zsh — 短指令對映</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 檔案操作</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nb">alias</span> <span class="nv">ll</span><span class="o">=</span><span class="s1">&#39;ls -alF&#39;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nb">alias</span> <span class="nv">la</span><span class="o">=</span><span class="s1">&#39;ls -A&#39;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># Git 常用</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nb">alias</span> <span class="nv">gs</span><span class="o">=</span><span class="s1">&#39;git status&#39;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nb">alias</span> <span class="nv">gd</span><span class="o">=</span><span class="s1">&#39;git diff&#39;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nb">alias</span> <span class="nv">gco</span><span class="o">=</span><span class="s1">&#39;git checkout&#39;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nb">alias</span> <span class="nv">gp</span><span class="o">=</span><span class="s1">&#39;git push&#39;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 導航</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nb">alias</span> ..<span class="o">=</span><span class="s1">&#39;cd ..&#39;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nb">alias</span> ...<span class="o">=</span><span class="s1">&#39;cd ../..&#39;</span></span></span></code></pre></div><p>判讀準則：alias 適合「不帶參數的簡單替換」。如果需要參數處理或條件判斷，改用 function。</p>
<h3 id="functionszsh--帶邏輯的常用操作">functions.zsh — 帶邏輯的常用操作</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 建目錄並進入</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mkcd<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    mkdir -p <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">cd</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 在 Git repo 根目錄搜尋</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">ggrep<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    git grep <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="k">$(</span>git rev-parse --show-toplevel<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="o">}</span></span></span></code></pre></div><h3 id="toolszsh--第三方工具的初始化">tools.zsh — 第三方工具的初始化</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># fzf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">[[</span> -f ~/.fzf.zsh <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">source</span> ~/.fzf.zsh
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># nvm</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">export</span> <span class="nv">NVM_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.nvm&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="o">[[</span> -s <span class="s2">&#34;</span><span class="nv">$NVM_DIR</span><span class="s2">/nvm.sh&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$NVM_DIR</span><span class="s2">/nvm.sh&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># pyenv</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="nb">command</span> -v pyenv &gt;/dev/null <span class="o">&amp;&amp;</span> <span class="nb">eval</span> <span class="s2">&#34;</span><span class="k">$(</span>pyenv init -<span class="k">)</span><span class="s2">&#34;</span></span></span></code></pre></div><p>每個工具的 init 前面加存在性檢查（<code>command -v</code> 或 <code>[[ -f ]]</code>），避免在沒裝該工具的機器上報錯。</p>
<h3 id="localzsh--機器專屬不進-git">local.zsh — 機器專屬、不進 Git</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 公司 VPN 設定</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">export</span> <span class="nv">CORP_PROXY</span><span class="o">=</span><span class="s2">&#34;http://proxy.corp:8080&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 只有這台機器需要的 PATH</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/corp-tools/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span></span></span></code></pre></div><p>在 dotfile repo 的 <code>.gitignore</code> 裡排除這個檔案。<code>.zshrc</code> 裡用 <code>[[ -f ... ]] &amp;&amp; source</code> 確保不存在也不報錯。</p>
]]></content:encoded></item><item><title>拍照 vs 重建指令：環境重建的兩種思路</title><link>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/snapshot-vs-rebuild/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/snapshot-vs-rebuild/</guid><description>&lt;p>環境重建是 dotfile 管理的最終目的：拿到一台空白機器，能在可預期的時間內還原成你熟悉的工作環境。這件事有兩條根本不同的路線，選哪條決定了你之後所有的管理策略。&lt;/p>
&lt;h2 id="拍照vm-快照與磁碟映像">拍照：VM 快照與磁碟映像&lt;/h2>
&lt;p>第一種是&lt;strong>拍照&lt;/strong>。VM 快照和磁碟映像（Clonezilla、&lt;code>dd&lt;/code>）做的事是把整台機器某一刻的完整狀態凍結存檔——整個虛擬硬碟的 block-level 複製，有時連記憶體狀態都包含。還原就是把映像寫回去，系統回到那一刻，像時光倒流。Docker 的 &lt;code>docker commit&lt;/code> 也屬於這個方向：把正在跑的 container 的檔案系統快照成一個 image。&lt;/p>
&lt;p>拍照產出的是&lt;strong>黑盒子&lt;/strong>。一個磁碟映像是二進制檔案，沒人能看出裡面到底做了什麼設定、裝了什麼、改過什麼。它大（動輒 GB 級）、跟硬體耦合（換不同架構或不同顯卡可能開不起來）、無法做 diff 或 code review。&lt;/p>
&lt;h2 id="重建指令dotfile-repo--install-script">重建指令：Dotfile repo + install script&lt;/h2>
&lt;p>第二種是&lt;strong>重建指令&lt;/strong>。Dotfile repo + install script 描述的是「怎麼從一台空白機器組出這個環境」，每次都從零開始執行。Dockerfile 也是重建指令——一份「照著做就能重現」的食譜，描述每一步該安裝什麼、複製什麼、怎麼啟動。&lt;/p>
&lt;p>重建指令產出的是&lt;strong>白盒子&lt;/strong>。每一步都是可讀的文字——這行裝 zsh、那行設定 Hyprland 的 keybind——可以被 review、被 diff、被另一個人讀懂。它小（通常幾十 KB）、跨硬體（同一份 script 加 OS 判斷就能跑在不同機器）、可以進版控走 PR 流程。&lt;/p>
&lt;p>dotfile 管理選的是重建指令這條路。代價是你必須把環境建構的過程記錄清楚——每裝一個新工具、每改一個配置都要同步更新 repo。回報是任何一台機器、任何時間點，都能用一份 Git repo 重現你的工作環境。&lt;/p>
&lt;h2 id="場景判讀">場景判讀&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>需求&lt;/th>
 &lt;th>VM 快照&lt;/th>
 &lt;th>Dotfile 重建&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>保留某一刻的完整系統狀態&lt;/td>
 &lt;td>適合（block-level 完整備份）&lt;/td>
 &lt;td>不適合（只管配置層）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在新機器還原工作環境&lt;/td>
 &lt;td>不適合（硬體耦合、映像大）&lt;/td>
 &lt;td>適合（跨硬體、輕量）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讓環境可被他人重現（onboarding）&lt;/td>
 &lt;td>勉強（黑盒子、難維護）&lt;/td>
 &lt;td>適合（白盒子、可 review）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在多台機器維持一致&lt;/td>
 &lt;td>不適合（每台都要拍照）&lt;/td>
 &lt;td>適合（一份 repo、多台 apply）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>做實驗後回滾（改壞了想恢復）&lt;/td>
 &lt;td>適合（秒級回滾）&lt;/td>
 &lt;td>要靠 git revert + 重新 apply&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讓桌面配置進 review 流程&lt;/td>
 &lt;td>不適合（二進制映像無法 diff）&lt;/td>
 &lt;td>適合（純文字、可 diff、可 PR）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩者不互斥——常見的組合是：用 dotfile 管理配置（長期可維護的基線），VM 快照用於短期實驗保護（改爛了可以秒回）。&lt;/p></description><content:encoded><![CDATA[<p>環境重建是 dotfile 管理的最終目的：拿到一台空白機器，能在可預期的時間內還原成你熟悉的工作環境。這件事有兩條根本不同的路線，選哪條決定了你之後所有的管理策略。</p>
<h2 id="拍照vm-快照與磁碟映像">拍照：VM 快照與磁碟映像</h2>
<p>第一種是<strong>拍照</strong>。VM 快照和磁碟映像（Clonezilla、<code>dd</code>）做的事是把整台機器某一刻的完整狀態凍結存檔——整個虛擬硬碟的 block-level 複製，有時連記憶體狀態都包含。還原就是把映像寫回去，系統回到那一刻，像時光倒流。Docker 的 <code>docker commit</code> 也屬於這個方向：把正在跑的 container 的檔案系統快照成一個 image。</p>
<p>拍照產出的是<strong>黑盒子</strong>。一個磁碟映像是二進制檔案，沒人能看出裡面到底做了什麼設定、裝了什麼、改過什麼。它大（動輒 GB 級）、跟硬體耦合（換不同架構或不同顯卡可能開不起來）、無法做 diff 或 code review。</p>
<h2 id="重建指令dotfile-repo--install-script">重建指令：Dotfile repo + install script</h2>
<p>第二種是<strong>重建指令</strong>。Dotfile repo + install script 描述的是「怎麼從一台空白機器組出這個環境」，每次都從零開始執行。Dockerfile 也是重建指令——一份「照著做就能重現」的食譜，描述每一步該安裝什麼、複製什麼、怎麼啟動。</p>
<p>重建指令產出的是<strong>白盒子</strong>。每一步都是可讀的文字——這行裝 zsh、那行設定 Hyprland 的 keybind——可以被 review、被 diff、被另一個人讀懂。它小（通常幾十 KB）、跨硬體（同一份 script 加 OS 判斷就能跑在不同機器）、可以進版控走 PR 流程。</p>
<p>dotfile 管理選的是重建指令這條路。代價是你必須把環境建構的過程記錄清楚——每裝一個新工具、每改一個配置都要同步更新 repo。回報是任何一台機器、任何時間點，都能用一份 Git repo 重現你的工作環境。</p>
<h2 id="場景判讀">場景判讀</h2>
<table>
  <thead>
      <tr>
          <th>需求</th>
          <th>VM 快照</th>
          <th>Dotfile 重建</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>保留某一刻的完整系統狀態</td>
          <td>適合（block-level 完整備份）</td>
          <td>不適合（只管配置層）</td>
      </tr>
      <tr>
          <td>在新機器還原工作環境</td>
          <td>不適合（硬體耦合、映像大）</td>
          <td>適合（跨硬體、輕量）</td>
      </tr>
      <tr>
          <td>讓環境可被他人重現（onboarding）</td>
          <td>勉強（黑盒子、難維護）</td>
          <td>適合（白盒子、可 review）</td>
      </tr>
      <tr>
          <td>在多台機器維持一致</td>
          <td>不適合（每台都要拍照）</td>
          <td>適合（一份 repo、多台 apply）</td>
      </tr>
      <tr>
          <td>做實驗後回滾（改壞了想恢復）</td>
          <td>適合（秒級回滾）</td>
          <td>要靠 git revert + 重新 apply</td>
      </tr>
      <tr>
          <td>讓桌面配置進 review 流程</td>
          <td>不適合（二進制映像無法 diff）</td>
          <td>適合（純文字、可 diff、可 PR）</td>
      </tr>
  </tbody>
</table>
<p>兩者不互斥——常見的組合是：用 dotfile 管理配置（長期可維護的基線），VM 快照用於短期實驗保護（改爛了可以秒回）。</p>
]]></content:encoded></item><item><title>桌面 Shell 元件：狀態列、啟動器與通知</title><link>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/desktop-shell-components/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/desktop-shell-components/</guid><description>&lt;p>完整桌面環境（GNOME、KDE）把這些元件整合在一起出貨。平鋪式 WM 的桌面是拼裝的——每個位置自己選工具：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>功能&lt;/th>
 &lt;th>常見工具&lt;/th>
 &lt;th>配置格式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>狀態列&lt;/td>
 &lt;td>Waybar, Eww, AGS&lt;/td>
 &lt;td>JSON/JSONC, Yuck, JS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>啟動器&lt;/td>
 &lt;td>Wofi, Rofi (wayland), Fuzzel&lt;/td>
 &lt;td>CSS + 設定檔&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>通知&lt;/td>
 &lt;td>Mako, Dunst, SwayNC&lt;/td>
 &lt;td>INI/TOML&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>鎖屏&lt;/td>
 &lt;td>Hyprlock, Swaylock&lt;/td>
 &lt;td>自定義格式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>桌布&lt;/td>
 &lt;td>Hyprpaper, Swww, Mpvpaper&lt;/td>
 &lt;td>自定義格式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>剪貼簿&lt;/td>
 &lt;td>wl-clipboard + Cliphist&lt;/td>
 &lt;td>CLI&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>螢幕截圖&lt;/td>
 &lt;td>Grimblast, Grim + Slurp&lt;/td>
 &lt;td>CLI&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Caelestia 這類「desktop shell 專案」做的就是把上述元件統一設計、統一配色、統一出貨，省去自己一個個挑的功夫。它用的是 Quickshell（QML 框架）把所有元件包成一套風格一致的桌面。本模組教的是自己組裝的方式——理解各元件的配置，之後要用 Caelestia 或自己拼都能做。&lt;/p>
&lt;h2 id="waybar狀態列">Waybar：狀態列&lt;/h2>
&lt;p>Waybar 是 Hyprland 桌面最常用的狀態列。配置在 &lt;code>~/.config/waybar/&lt;/code>，分兩個檔案：&lt;code>config.jsonc&lt;/code>（結構和模組）和 &lt;code>style.css&lt;/code>（外觀）。&lt;/p>
&lt;h3 id="結構配置">結構配置&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-jsonc" data-lang="jsonc">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// ~/.config/waybar/config.jsonc
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;layer&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;top&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;position&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;top&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;height&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">36&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;spacing&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 左中右三區塊各放哪些模組
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nt">&amp;#34;modules-left&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;hyprland/workspaces&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;hyprland/window&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;modules-center&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;clock&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;modules-right&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;pulseaudio&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;network&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;cpu&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;memory&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;battery&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tray&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 各模組設定
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nt">&amp;#34;hyprland/workspaces&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;format&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{id}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;on-click&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;activate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;clock&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;format&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{:%H:%M}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;format-alt&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{:%Y-%m-%d %H:%M}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;tooltip-format&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;tt&amp;gt;{calendar}&amp;lt;/tt&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;battery&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;format&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{capacity}% {icon}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;format-icons&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;states&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;warning&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;critical&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">15&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;network&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;format-wifi&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{essid} ({signalStrength}%)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;format-ethernet&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{ifname}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;format-disconnected&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Disconnected&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">35&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;pulseaudio&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">37&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;format&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{volume}%&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">38&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;on-click&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;pavucontrol&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">39&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">40&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>format&lt;/code> 欄位裡的 icon 字元來自 Nerd Font——&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">終端機與編輯器&lt;/a>提到的字型安裝是這裡正常顯示的前提。&lt;/p>
&lt;h3 id="外觀-css">外觀 CSS&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c">/* ~/.config/waybar/style.css */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="o">*&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">font-family&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;MesloLGS Nerd Font&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">monospace&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">font-size&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">13&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="nt">window&lt;/span>&lt;span class="p">#&lt;/span>&lt;span class="nn">waybar&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">background-color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">rgba&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">46&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">0.85&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#cdd6f4&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">#&lt;/span>&lt;span class="nn">workspaces&lt;/span> &lt;span class="nt">button&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="k">padding&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#6c7086&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="k">border-radius&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="p">#&lt;/span>&lt;span class="nn">workspaces&lt;/span> &lt;span class="nt">button&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">active&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#cdd6f4&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="k">background&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">rgba&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">137&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">180&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">250&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">0.2&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="p">#&lt;/span>&lt;span class="nn">clock&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="p">#&lt;/span>&lt;span class="nn">battery&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="p">#&lt;/span>&lt;span class="nn">network&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="p">#&lt;/span>&lt;span class="nn">pulseaudio&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl"> &lt;span class="k">padding&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="p">#&lt;/span>&lt;span class="nn">battery&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">warning&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl"> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#f9e2af&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl">&lt;span class="p">#&lt;/span>&lt;span class="nn">battery&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">critical&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl"> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#f38ba8&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>CSS 裡的色碼（&lt;code>#cdd6f4&lt;/code>、&lt;code>#89b4fa&lt;/code>、&lt;code>#f38ba8&lt;/code>）來自配色方案（這個範例用的是 Catppuccin Mocha）。統一使用同一套色碼是 rice 視覺協調的基礎。&lt;/p>
&lt;p>Config 裡的字族名必須跟系統實際安裝的字族逐字相符。Nerd Font 的圖示落在專屬碼位範圍，只有對應的那支字帶這些 glyph——指定一個沒裝的字族名，文字排版引擎找不到 glyph 就會顯示豆腐方塊。確認實裝字族名的方式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">fc-list &lt;span class="p">|&lt;/span> grep -i meslo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># MesloLGSNerdFont-Regular.ttf: &amp;#34;MesloLGS Nerd Font&amp;#34;:style=Regular&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>引號內的字串是 config 該填的字族名。&lt;/p>
&lt;p>同一份 Waybar config 能同時服務筆電、桌機與 VM，靠的是模組對缺少硬體的自動退化：&lt;code>battery&lt;/code> 在沒有電池的機器直接隱藏該模組、不報錯也不留空位；&lt;code>pulseaudio&lt;/code> 在沒有音訊服務時顯示為空；&lt;code>network&lt;/code> 顯示當下實際在用的介面。不必為不同機器維護多份 config——把可能用到的模組都列上，用不到的那台自己消失。&lt;/p></description><content:encoded><![CDATA[<p>完整桌面環境（GNOME、KDE）把這些元件整合在一起出貨。平鋪式 WM 的桌面是拼裝的——每個位置自己選工具：</p>
<table>
  <thead>
      <tr>
          <th>功能</th>
          <th>常見工具</th>
          <th>配置格式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>狀態列</td>
          <td>Waybar, Eww, AGS</td>
          <td>JSON/JSONC, Yuck, JS</td>
      </tr>
      <tr>
          <td>啟動器</td>
          <td>Wofi, Rofi (wayland), Fuzzel</td>
          <td>CSS + 設定檔</td>
      </tr>
      <tr>
          <td>通知</td>
          <td>Mako, Dunst, SwayNC</td>
          <td>INI/TOML</td>
      </tr>
      <tr>
          <td>鎖屏</td>
          <td>Hyprlock, Swaylock</td>
          <td>自定義格式</td>
      </tr>
      <tr>
          <td>桌布</td>
          <td>Hyprpaper, Swww, Mpvpaper</td>
          <td>自定義格式</td>
      </tr>
      <tr>
          <td>剪貼簿</td>
          <td>wl-clipboard + Cliphist</td>
          <td>CLI</td>
      </tr>
      <tr>
          <td>螢幕截圖</td>
          <td>Grimblast, Grim + Slurp</td>
          <td>CLI</td>
      </tr>
  </tbody>
</table>
<p>Caelestia 這類「desktop shell 專案」做的就是把上述元件統一設計、統一配色、統一出貨，省去自己一個個挑的功夫。它用的是 Quickshell（QML 框架）把所有元件包成一套風格一致的桌面。本模組教的是自己組裝的方式——理解各元件的配置，之後要用 Caelestia 或自己拼都能做。</p>
<h2 id="waybar狀態列">Waybar：狀態列</h2>
<p>Waybar 是 Hyprland 桌面最常用的狀態列。配置在 <code>~/.config/waybar/</code>，分兩個檔案：<code>config.jsonc</code>（結構和模組）和 <code>style.css</code>（外觀）。</p>
<h3 id="結構配置">結構配置</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// ~/.config/waybar/config.jsonc
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nt">&#34;layer&#34;</span><span class="p">:</span> <span class="s2">&#34;top&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nt">&#34;position&#34;</span><span class="p">:</span> <span class="s2">&#34;top&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nt">&#34;height&#34;</span><span class="p">:</span> <span class="mi">36</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nt">&#34;spacing&#34;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="c1">// 左中右三區塊各放哪些模組
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>    <span class="nt">&#34;modules-left&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;hyprland/workspaces&#34;</span><span class="p">,</span> <span class="s2">&#34;hyprland/window&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nt">&#34;modules-center&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;clock&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nt">&#34;modules-right&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;pulseaudio&#34;</span><span class="p">,</span> <span class="s2">&#34;network&#34;</span><span class="p">,</span> <span class="s2">&#34;cpu&#34;</span><span class="p">,</span> <span class="s2">&#34;memory&#34;</span><span class="p">,</span> <span class="s2">&#34;battery&#34;</span><span class="p">,</span> <span class="s2">&#34;tray&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="c1">// 各模組設定
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>    <span class="nt">&#34;hyprland/workspaces&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{id}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="nt">&#34;on-click&#34;</span><span class="p">:</span> <span class="s2">&#34;activate&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nt">&#34;clock&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{:%H:%M}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="nt">&#34;format-alt&#34;</span><span class="p">:</span> <span class="s2">&#34;{:%Y-%m-%d %H:%M}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="nt">&#34;tooltip-format&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;tt&gt;{calendar}&lt;/tt&gt;&#34;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="nt">&#34;battery&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{capacity}% {icon}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="nt">&#34;format-icons&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="nt">&#34;states&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">            <span class="nt">&#34;warning&#34;</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">            <span class="nt">&#34;critical&#34;</span><span class="p">:</span> <span class="mi">15</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="nt">&#34;network&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="nt">&#34;format-wifi&#34;</span><span class="p">:</span> <span class="s2">&#34;{essid} ({signalStrength}%)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="nt">&#34;format-ethernet&#34;</span><span class="p">:</span> <span class="s2">&#34;{ifname}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="nt">&#34;format-disconnected&#34;</span><span class="p">:</span> <span class="s2">&#34;Disconnected&#34;</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="nt">&#34;pulseaudio&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{volume}%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="nt">&#34;on-click&#34;</span><span class="p">:</span> <span class="s2">&#34;pavucontrol&#34;</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>format</code> 欄位裡的 icon 字元來自 Nerd Font——<a href="/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">終端機與編輯器</a>提到的字型安裝是這裡正常顯示的前提。</p>
<h3 id="外觀-css">外觀 CSS</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c">/* ~/.config/waybar/style.css */</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="o">*</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">font-family</span><span class="p">:</span> <span class="s2">&#34;MesloLGS Nerd Font&#34;</span><span class="p">,</span> <span class="kc">monospace</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">font-size</span><span class="p">:</span> <span class="mi">13</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nt">window</span><span class="p">#</span><span class="nn">waybar</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">background-color</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">30</span><span class="p">,</span> <span class="mi">30</span><span class="p">,</span> <span class="mi">46</span><span class="p">,</span> <span class="mf">0.85</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#cdd6f4</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">#</span><span class="nn">workspaces</span> <span class="nt">button</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">padding</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">8</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#6c7086</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">border-radius</span><span class="p">:</span> <span class="mi">6</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">#</span><span class="nn">workspaces</span> <span class="nt">button</span><span class="p">.</span><span class="nc">active</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#cdd6f4</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">background</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">137</span><span class="p">,</span> <span class="mi">180</span><span class="p">,</span> <span class="mi">250</span><span class="p">,</span> <span class="mf">0.2</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="p">#</span><span class="nn">clock</span><span class="o">,</span> <span class="p">#</span><span class="nn">battery</span><span class="o">,</span> <span class="p">#</span><span class="nn">network</span><span class="o">,</span> <span class="p">#</span><span class="nn">pulseaudio</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="k">padding</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="p">#</span><span class="nn">battery</span><span class="p">.</span><span class="nc">warning</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#f9e2af</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="p">#</span><span class="nn">battery</span><span class="p">.</span><span class="nc">critical</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#f38ba8</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>CSS 裡的色碼（<code>#cdd6f4</code>、<code>#89b4fa</code>、<code>#f38ba8</code>）來自配色方案（這個範例用的是 Catppuccin Mocha）。統一使用同一套色碼是 rice 視覺協調的基礎。</p>
<p>Config 裡的字族名必須跟系統實際安裝的字族逐字相符。Nerd Font 的圖示落在專屬碼位範圍，只有對應的那支字帶這些 glyph——指定一個沒裝的字族名，文字排版引擎找不到 glyph 就會顯示豆腐方塊。確認實裝字族名的方式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">fc-list <span class="p">|</span> grep -i meslo
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># MesloLGSNerdFont-Regular.ttf: &#34;MesloLGS Nerd Font&#34;:style=Regular</span></span></span></code></pre></div><p>引號內的字串是 config 該填的字族名。</p>
<p>同一份 Waybar config 能同時服務筆電、桌機與 VM，靠的是模組對缺少硬體的自動退化：<code>battery</code> 在沒有電池的機器直接隱藏該模組、不報錯也不留空位；<code>pulseaudio</code> 在沒有音訊服務時顯示為空；<code>network</code> 顯示當下實際在用的介面。不必為不同機器維護多份 config——把可能用到的模組都列上，用不到的那台自己消失。</p>
<h2 id="wofi--rofi啟動器">Wofi / Rofi：啟動器</h2>
<p>啟動器是按快捷鍵彈出的搜尋框，用來啟動應用程式、執行指令、搜尋檔案。</p>
<p>Wofi（Wayland 原生）配置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># ~/.config/wofi/config</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">show</span><span class="o">=</span><span class="s">drun</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">width</span><span class="o">=</span><span class="s">600</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">height</span><span class="o">=</span><span class="s">400</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">prompt</span><span class="o">=</span><span class="s">Search...</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">insensitive</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="na">allow_markup</span><span class="o">=</span><span class="s">true</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c">/* ~/.config/wofi/style.css */</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">window</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">background-color</span><span class="p">:</span> <span class="mh">#1e1e2e</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">border</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#89b4fa</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">border-radius</span><span class="p">:</span> <span class="mi">12</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">#</span><span class="nn">input</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">background-color</span><span class="p">:</span> <span class="mh">#313244</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#cdd6f4</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">border-radius</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">padding</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span> <span class="mi">12</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">#</span><span class="nn">entry</span><span class="p">:</span><span class="nd">selected</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">background-color</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">137</span><span class="p">,</span> <span class="mi">180</span><span class="p">,</span> <span class="mi">250</span><span class="p">,</span> <span class="mf">0.2</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Rofi（需要 wayland fork rofi-lbonn-wayland）功能更豐富——支援多種 mode（drun、window、ssh、自定義 script）、主題系統更完整。如果需要進階功能（例如 emoji picker、密碼管理器整合），Rofi 是更好的選擇。</p>
<h2 id="mako--dunst通知">Mako / Dunst：通知</h2>
<p>Mako 是 Wayland 原生的通知 daemon，負責<strong>顯示</strong>通知——它監聽 D-Bus 的 <code>org.freedesktop.Notifications</code> 介面、把收到的通知畫出來。產生通知的是應用程式，透過 <code>libnotify</code> 送上 D-Bus。所以一套能用的通知鏈需要兩半：daemon（顯示）和 <code>libnotify</code>（產生與遞送）。缺了 <code>libnotify</code>，連命令列自測用的 <code>notify-send</code> 都沒有。套件清單要同時列 <code>mako</code> 和 <code>libnotify</code>。</p>
<p>配置簡潔：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># ~/.config/mako/config</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">font</span><span class="o">=</span><span class="s">MesloLGS Nerd Font 11</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">background-color</span><span class="o">=</span><span class="s">#1e1e2e</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">text-color</span><span class="o">=</span><span class="s">#cdd6f4</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="na">border-color</span><span class="o">=</span><span class="s">#89b4fa</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">border-radius</span><span class="o">=</span><span class="s">8</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">border-size</span><span class="o">=</span><span class="s">2</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">padding</span><span class="o">=</span><span class="s">12</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="na">default-timeout</span><span class="o">=</span><span class="s">5000</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="na">max-visible</span><span class="o">=</span><span class="s">3</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">[urgency=critical]</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="na">border-color</span><span class="o">=</span><span class="s">#f38ba8</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="na">default-timeout</span><span class="o">=</span><span class="s">0</span></span></span></code></pre></div><p>通知的視覺風格（圓角、配色、字型）要跟 waybar 和啟動器一致，這是整體 rice 不散的關鍵。</p>
<p>Nerd Font 的字符集只含 Latin、圖示與 Powerline 符號，不含中日韓。任何 CJK 文字（通知內文、視窗標題）若系統沒有 CJK 字型可 fallback 會變豆腐方塊。修法是安裝 CJK fallback（如 <code>noto-fonts-cjk</code>），fontconfig 會自動補字、不需改各工具的 config。另外，中途補裝字型後已在跑的 daemon 需重啟才看得到——<code>reload</code> 類指令只重讀設定檔、不重建記憶體中的字型快照（原理見 <a href="/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">字型的可用集合在 process 啟動時決定</a>）。</p>
<h2 id="grim--slurp截圖">Grim + Slurp：截圖</h2>
<p>Grim 負責截圖、Slurp 負責框選區域，兩者搭配使用。截圖結果透過 <code>wl-copy</code> 送進剪貼簿時，需要明確指定 MIME 型別：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 全螢幕截圖到剪貼簿</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grim - <span class="p">|</span> wl-copy --type image/png
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 框選區域截圖到剪貼簿</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">grim -g <span class="s2">&#34;</span><span class="k">$(</span>slurp<span class="k">)</span><span class="s2">&#34;</span> - <span class="p">|</span> wl-copy --type image/png
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 全螢幕截圖存檔</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">grim ~/screenshot.png</span></span></code></pre></div><p><code>wl-copy</code> 不帶 <code>--type</code> 時會嘗試透過 <code>xdg-utils</code>（<code>xdg-mime</code>）推斷 stdin 的型別。最小安裝環境沒有 <code>xdg-utils</code> 的情況下，PNG bytes 會被誤標成 <code>text/plain</code>，貼進影像應用程式就拿不到圖。明確帶 <code>--type image/png</code> 讓行為不依賴環境是否安裝了 <code>xdg-utils</code>。確認剪貼簿內容型別：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">wl-paste --list-types
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 應顯示 image/png</span></span></span></code></pre></div><p>Hyprland keybind 範例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">bind</span> <span class="o">=</span> , Print, exec, grim - <span class="p">|</span> wl-copy --type image/png
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">bind</span> <span class="o">=</span> SHIFT, Print, exec, grim -g <span class="s2">&#34;</span><span class="k">$(</span>slurp<span class="k">)</span><span class="s2">&#34;</span> - <span class="p">|</span> wl-copy --type image/png</span></span></code></pre></div>]]></content:encoded></item><item><title>浮動式 vs 平鋪式視窗管理</title><link>https://tarrragon.github.io/blog/linux/dotfile/04-window-management/floating-vs-tiling/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/04-window-management/floating-vs-tiling/</guid><description>&lt;p>桌面視窗管理分成兩種基本模式。&lt;/p>
&lt;p>&lt;strong>浮動式（floating）&lt;/strong> 是多數人熟悉的模式。視窗可以重疊、可以任意拖拉調整大小、可以最小化藏起來。macOS、Windows、GNOME、KDE 預設都是浮動式。操作直覺是「每個視窗是一張紙，自己決定放哪裡」。&lt;/p>
&lt;p>&lt;strong>平鋪式（tiling）&lt;/strong> 的規則不同：視窗自動排列填滿螢幕、不重疊，由 WM 的規則決定版面怎麼切割。開一個新視窗時，WM 自動把現有空間分一半給它；關掉一個視窗時，相鄰視窗自動擴展填補。操作直覺是「螢幕是一塊蛋糕，WM 負責切」。&lt;/p>
&lt;p>多數平鋪式 WM 支援&lt;strong>混合模式&lt;/strong>：特定視窗可以設為浮動，脫離平鋪規則。設定面板、密碼輸入框、小工具這類不適合塞進格子的視窗，通常會設成浮動例外。平鋪是預設，浮動是按需啟用的例外。&lt;/p>
&lt;h2 id="手動貼齊-vs-自動平鋪">手動貼齊 vs 自動平鋪&lt;/h2>
&lt;p>在進入平鋪式 WM 之前，macOS 和 Windows 都提供了「手動貼齊」功能——用快捷鍵或拖拉把視窗貼到螢幕的半邊、角落、三分之一。macOS 原生的 window snapping、Windows 的 Snap Layout、以及 Rectangle 和 Magnet 這類第三方工具都屬於這個範疇。&lt;/p>
&lt;p>手動貼齊跟自動平鋪的差距，在視窗數量少的時候幾乎感覺不到。開兩個視窗、左右各半，手動按一下快捷鍵就到位，完全夠用。&lt;/p>
&lt;p>差距在視窗數量多的時候才出現。每開一個新視窗都要決定它放哪、按對應的快捷鍵；關掉一個視窗就留下空洞，要手動拖拉其他視窗去填——當這種版面管理的決策負擔開始分散你對工作本身的注意力時，就是自動平鋪開始有價值的時機。這個斷點因螢幕大小和工作類型而異：23 吋單螢幕上同時開終端機、編輯器、瀏覽器、文件，版面很快就不夠分；雙螢幕寬螢幕上同樣的視窗數量可能還很從容。&lt;/p>
&lt;p>自動平鋪在這個情境下的優勢有三層。&lt;/p>
&lt;p>第一層是&lt;strong>自動回填&lt;/strong>。開視窗、關視窗，WM 自動重新分配空間，版面永遠是滿的、整齊的。你不用做任何版面決策。&lt;/p>
&lt;p>第二層是&lt;strong>操作對象的轉換&lt;/strong>。手動貼齊的操作對象是「某個視窗」——把 A 視窗貼到左邊、把 B 視窗貼到右上。平鋪式 WM 的操作對象是「版面結構」——把焦點往右移、把當前視窗跟隔壁交換、把這一格再水平切一半。你操作的是位置關係，不是絕對座標。&lt;/p>
&lt;p>第三層是&lt;strong>工作區整合&lt;/strong>。平鋪式工作流通常搭配多個工作區（workspace），每個工作區是一套獨立的平鋪佈局。「編輯器和終端機在工作區 1、瀏覽器在工作區 2、通訊軟體在工作區 4」——用快捷鍵瞬間切換整套上下文，而不是在一堆重疊視窗裡找。手動貼齊工具通常不帶工作區管理。&lt;/p>
&lt;h2 id="適用判讀">適用判讀&lt;/h2>
&lt;p>平鋪式視窗管理的投資報酬率取決於你的工作型態。&lt;/p>
&lt;p>&lt;strong>高回報情境&lt;/strong>：經常同時操作多個視窗且版面管理開始分散注意力、多數是「方方正正、可平鋪」的 app（終端機、編輯器、瀏覽器、文件閱讀器）、鍵盤操作為主、多螢幕、工作需要頻繁切換上下文（多個專案、不同任務區）。&lt;/p>
&lt;p>&lt;strong>低回報情境&lt;/strong>：大量使用需要特定比例或自由拖拉的 app（設計工具、影片剪輯、簡報製作）、很少同時開多個視窗、已經習慣且滿意目前的工作流、不想花時間學新鍵位。&lt;/p>
&lt;p>&lt;strong>折衷方案&lt;/strong>：所有平鋪式工具都支援 per-app 的浮動例外。不適合平鋪的 app（設定面板、計算機、某些對話框）設成浮動，其餘維持平鋪。這不是全有全無的選擇。&lt;/p>
&lt;p>一個常見的踩坑模式是：看到 Hyprland 的截圖很漂亮，衝動裝了，發現日常有一半 app 不適合平鋪、鍵位記不住、每次更新都要修配置，兩週後放棄。務實的進入路徑是先在目前的系統上試手動貼齊工具（macOS 的 Rectangle 或 AeroSpace），確認自己真的享受鍵盤操作視窗的節奏，再往 Linux tiling WM 推進。或者用 VM 跑 Hyprland 體驗看看——體驗打折（VM 沒有 GPU 加速，動畫會卡），但能確認自己是否喜歡這種操作邏輯，再決定要不要花時間在實體機上搭建。&lt;/p></description><content:encoded><![CDATA[<p>桌面視窗管理分成兩種基本模式。</p>
<p><strong>浮動式（floating）</strong> 是多數人熟悉的模式。視窗可以重疊、可以任意拖拉調整大小、可以最小化藏起來。macOS、Windows、GNOME、KDE 預設都是浮動式。操作直覺是「每個視窗是一張紙，自己決定放哪裡」。</p>
<p><strong>平鋪式（tiling）</strong> 的規則不同：視窗自動排列填滿螢幕、不重疊，由 WM 的規則決定版面怎麼切割。開一個新視窗時，WM 自動把現有空間分一半給它；關掉一個視窗時，相鄰視窗自動擴展填補。操作直覺是「螢幕是一塊蛋糕，WM 負責切」。</p>
<p>多數平鋪式 WM 支援<strong>混合模式</strong>：特定視窗可以設為浮動，脫離平鋪規則。設定面板、密碼輸入框、小工具這類不適合塞進格子的視窗，通常會設成浮動例外。平鋪是預設，浮動是按需啟用的例外。</p>
<h2 id="手動貼齊-vs-自動平鋪">手動貼齊 vs 自動平鋪</h2>
<p>在進入平鋪式 WM 之前，macOS 和 Windows 都提供了「手動貼齊」功能——用快捷鍵或拖拉把視窗貼到螢幕的半邊、角落、三分之一。macOS 原生的 window snapping、Windows 的 Snap Layout、以及 Rectangle 和 Magnet 這類第三方工具都屬於這個範疇。</p>
<p>手動貼齊跟自動平鋪的差距，在視窗數量少的時候幾乎感覺不到。開兩個視窗、左右各半，手動按一下快捷鍵就到位，完全夠用。</p>
<p>差距在視窗數量多的時候才出現。每開一個新視窗都要決定它放哪、按對應的快捷鍵；關掉一個視窗就留下空洞，要手動拖拉其他視窗去填——當這種版面管理的決策負擔開始分散你對工作本身的注意力時，就是自動平鋪開始有價值的時機。這個斷點因螢幕大小和工作類型而異：23 吋單螢幕上同時開終端機、編輯器、瀏覽器、文件，版面很快就不夠分；雙螢幕寬螢幕上同樣的視窗數量可能還很從容。</p>
<p>自動平鋪在這個情境下的優勢有三層。</p>
<p>第一層是<strong>自動回填</strong>。開視窗、關視窗，WM 自動重新分配空間，版面永遠是滿的、整齊的。你不用做任何版面決策。</p>
<p>第二層是<strong>操作對象的轉換</strong>。手動貼齊的操作對象是「某個視窗」——把 A 視窗貼到左邊、把 B 視窗貼到右上。平鋪式 WM 的操作對象是「版面結構」——把焦點往右移、把當前視窗跟隔壁交換、把這一格再水平切一半。你操作的是位置關係，不是絕對座標。</p>
<p>第三層是<strong>工作區整合</strong>。平鋪式工作流通常搭配多個工作區（workspace），每個工作區是一套獨立的平鋪佈局。「編輯器和終端機在工作區 1、瀏覽器在工作區 2、通訊軟體在工作區 4」——用快捷鍵瞬間切換整套上下文，而不是在一堆重疊視窗裡找。手動貼齊工具通常不帶工作區管理。</p>
<h2 id="適用判讀">適用判讀</h2>
<p>平鋪式視窗管理的投資報酬率取決於你的工作型態。</p>
<p><strong>高回報情境</strong>：經常同時操作多個視窗且版面管理開始分散注意力、多數是「方方正正、可平鋪」的 app（終端機、編輯器、瀏覽器、文件閱讀器）、鍵盤操作為主、多螢幕、工作需要頻繁切換上下文（多個專案、不同任務區）。</p>
<p><strong>低回報情境</strong>：大量使用需要特定比例或自由拖拉的 app（設計工具、影片剪輯、簡報製作）、很少同時開多個視窗、已經習慣且滿意目前的工作流、不想花時間學新鍵位。</p>
<p><strong>折衷方案</strong>：所有平鋪式工具都支援 per-app 的浮動例外。不適合平鋪的 app（設定面板、計算機、某些對話框）設成浮動，其餘維持平鋪。這不是全有全無的選擇。</p>
<p>一個常見的踩坑模式是：看到 Hyprland 的截圖很漂亮，衝動裝了，發現日常有一半 app 不適合平鋪、鍵位記不住、每次更新都要修配置，兩週後放棄。務實的進入路徑是先在目前的系統上試手動貼齊工具（macOS 的 Rectangle 或 AeroSpace），確認自己真的享受鍵盤操作視窗的節奏，再往 Linux tiling WM 推進。或者用 VM 跑 Hyprland 體驗看看——體驗打折（VM 沒有 GPU 加速，動畫會卡），但能確認自己是否喜歡這種操作邏輯，再決定要不要花時間在實體機上搭建。</p>
]]></content:encoded></item><item><title>管理策略與選型</title><link>https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/management-strategies/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/management-strategies/</guid><description>&lt;p>Dotfile 管理的核心動作是把散落在家目錄各處的配置檔集中到一個 Git repo 裡版控。工具只是幫你處理「repo 裡的檔案怎麼對應到家目錄正確位置」這一層映射，選型看的是你的機器數量、OS 組合和 secret 需求。&lt;/p>
&lt;h2 id="git-bare-repo直接把家目錄當-work-tree">Git bare repo：直接把家目錄當 work tree&lt;/h2>
&lt;p>bare repo 的概念是在家目錄建一個沒有工作目錄的 Git 倉庫，然後用 alias 指定 &lt;code>--work-tree=$HOME&lt;/code>，讓 Git 直接追蹤家目錄下的檔案，不需要 symlink、不需要額外工具。&lt;/p>
&lt;p>初始化：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">git init --bare &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.dotfiles&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 shell 配置裡加一行 alias：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> &lt;span class="nv">dotfiles&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;git --git-dir=&amp;#34;$HOME/.dotfiles&amp;#34; --work-tree=&amp;#34;$HOME&amp;#34;&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>之後所有 dotfile 操作都透過這個 alias：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">dotfiles add ~/.zshrc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">dotfiles commit -m &lt;span class="s2">&amp;#34;add zshrc&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">dotfiles remote add origin git@github.com:you/dotfiles.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">dotfiles push -u origin main&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第一件要做的事是隱藏未追蹤檔案。家目錄底下有成千上萬個檔案，如果不設定這一行，&lt;code>dotfiles status&lt;/code> 會列出所有未追蹤的東西：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">dotfiles config --local status.showUntrackedFiles no&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>新機器還原的流程：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">git clone --bare git@github.com:you/dotfiles.git &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.dotfiles&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> &lt;span class="nv">dotfiles&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;git --git-dir=&amp;#34;$HOME/.dotfiles&amp;#34; --work-tree=&amp;#34;$HOME&amp;#34;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">dotfiles config --local status.showUntrackedFiles no
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">dotfiles checkout&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>checkout&lt;/code> 這步會把 repo 裡的檔案寫到家目錄。如果家目錄已經有同名檔案（例如系統預設的 &lt;code>.bashrc&lt;/code>），checkout 會失敗並列出衝突檔案，需要先手動備份或刪除。&lt;/p>
&lt;p>bare repo 適合配置量少、只管一台機器、不想安裝任何額外工具的人。它的限制是：概念對 Git 初學者不直覺（bare repo + work-tree 的組合不常見）、沒有模組化的概念（無法選擇性安裝某些配置）、多 profile 支援弱（不同機器要不同配置時只能靠 branch，長期維護困難）。&lt;/p>
&lt;h2 id="gnu-stowsymlink-農場管理器">GNU Stow：symlink 農場管理器&lt;/h2>
&lt;p>Stow 的概念是把 dotfile 集中放在一個普通目錄（如 &lt;code>~/dotfiles&lt;/code>），然後用 stow 指令在家目錄建立 symlink。stow 的核心規則是：package 目錄內的路徑結構，就是安裝後相對於目標目錄的路徑結構。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 安裝 stow&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># macOS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">brew install stow
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># Arch Linux&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">sudo pacman -S stow
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># Ubuntu/Debian&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">sudo apt install stow&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>以 zsh 配置為例，目錄結構長這樣：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">~/dotfiles/zsh/.zshrc&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>執行 &lt;code>stow zsh&lt;/code> 後，stow 會在 &lt;code>$HOME&lt;/code> 建一個 symlink：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">~/.zshrc -&amp;gt; ~/dotfiles/zsh/.zshrc&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>對於放在 &lt;code>~/.config/&lt;/code> 底下的工具（XDG 規範），目錄結構映射同樣的邏輯：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">~/dotfiles/nvim/.config/nvim/init.lua
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">~/dotfiles/nvim/.config/nvim/lua/plugins.lua&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>執行 &lt;code>stow nvim&lt;/code> 後：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">~/.config/nvim -&amp;gt; ~/dotfiles/nvim/.config/nvim&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>stow 會自動判斷該 symlink 整個目錄還是個別檔案——如果目標目錄不存在或目錄內所有檔案都由同一個 package 管理，stow 會 symlink 整個目錄（folding）；如果目標目錄已有其他檔案，stow 會展開（unfolding）成逐檔 symlink。&lt;/p>
&lt;p>初始化和日常操作：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 初始化&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">mkdir ~/dotfiles &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">cd&lt;/span> ~/dotfiles
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">git init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">git remote add origin git@github.com:you/dotfiles.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 把現有 .zshrc 搬進 dotfiles&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">mkdir -p ~/dotfiles/zsh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">mv ~/.zshrc ~/dotfiles/zsh/.zshrc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/dotfiles &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> stow zsh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># 現在 ~/.zshrc 是一個 symlink，指向 ~/dotfiles/zsh/.zshrc&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># 日常修改：直接編輯，symlink 透通&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">vim ~/.zshrc &lt;span class="c1"># 實際編輯的是 ~/dotfiles/zsh/.zshrc&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/dotfiles &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> git add -A &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> git commit -m &lt;span class="s2">&amp;#34;update zshrc&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1"># 新機器還原&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">git clone git@github.com:you/dotfiles.git ~/dotfiles
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/dotfiles &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> stow zsh git nvim tmux&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>批次安裝所有 package：&lt;/p></description><content:encoded><![CDATA[<p>Dotfile 管理的核心動作是把散落在家目錄各處的配置檔集中到一個 Git repo 裡版控。工具只是幫你處理「repo 裡的檔案怎麼對應到家目錄正確位置」這一層映射，選型看的是你的機器數量、OS 組合和 secret 需求。</p>
<h2 id="git-bare-repo直接把家目錄當-work-tree">Git bare repo：直接把家目錄當 work tree</h2>
<p>bare repo 的概念是在家目錄建一個沒有工作目錄的 Git 倉庫，然後用 alias 指定 <code>--work-tree=$HOME</code>，讓 Git 直接追蹤家目錄下的檔案，不需要 symlink、不需要額外工具。</p>
<p>初始化：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git init --bare <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.dotfiles&#34;</span></span></span></code></pre></div><p>在 shell 配置裡加一行 alias：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">alias</span> <span class="nv">dotfiles</span><span class="o">=</span><span class="s1">&#39;git --git-dir=&#34;$HOME/.dotfiles&#34; --work-tree=&#34;$HOME&#34;&#39;</span></span></span></code></pre></div><p>之後所有 dotfile 操作都透過這個 alias：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">dotfiles add ~/.zshrc
</span></span><span class="line"><span class="ln">2</span><span class="cl">dotfiles commit -m <span class="s2">&#34;add zshrc&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">dotfiles remote add origin git@github.com:you/dotfiles.git
</span></span><span class="line"><span class="ln">4</span><span class="cl">dotfiles push -u origin main</span></span></code></pre></div><p>第一件要做的事是隱藏未追蹤檔案。家目錄底下有成千上萬個檔案，如果不設定這一行，<code>dotfiles status</code> 會列出所有未追蹤的東西：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">dotfiles config --local status.showUntrackedFiles no</span></span></code></pre></div><p>新機器還原的流程：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git clone --bare git@github.com:you/dotfiles.git <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.dotfiles&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">alias</span> <span class="nv">dotfiles</span><span class="o">=</span><span class="s1">&#39;git --git-dir=&#34;$HOME/.dotfiles&#34; --work-tree=&#34;$HOME&#34;&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">dotfiles config --local status.showUntrackedFiles no
</span></span><span class="line"><span class="ln">4</span><span class="cl">dotfiles checkout</span></span></code></pre></div><p><code>checkout</code> 這步會把 repo 裡的檔案寫到家目錄。如果家目錄已經有同名檔案（例如系統預設的 <code>.bashrc</code>），checkout 會失敗並列出衝突檔案，需要先手動備份或刪除。</p>
<p>bare repo 適合配置量少、只管一台機器、不想安裝任何額外工具的人。它的限制是：概念對 Git 初學者不直覺（bare repo + work-tree 的組合不常見）、沒有模組化的概念（無法選擇性安裝某些配置）、多 profile 支援弱（不同機器要不同配置時只能靠 branch，長期維護困難）。</p>
<h2 id="gnu-stowsymlink-農場管理器">GNU Stow：symlink 農場管理器</h2>
<p>Stow 的概念是把 dotfile 集中放在一個普通目錄（如 <code>~/dotfiles</code>），然後用 stow 指令在家目錄建立 symlink。stow 的核心規則是：package 目錄內的路徑結構，就是安裝後相對於目標目錄的路徑結構。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 安裝 stow</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># macOS</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">brew install stow
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Arch Linux</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">sudo pacman -S stow
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># Ubuntu/Debian</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">sudo apt install stow</span></span></code></pre></div><p>以 zsh 配置為例，目錄結構長這樣：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">~/dotfiles/zsh/.zshrc</span></span></code></pre></div><p>執行 <code>stow zsh</code> 後，stow 會在 <code>$HOME</code> 建一個 symlink：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">~/.zshrc -&gt; ~/dotfiles/zsh/.zshrc</span></span></code></pre></div><p>對於放在 <code>~/.config/</code> 底下的工具（XDG 規範），目錄結構映射同樣的邏輯：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">~/dotfiles/nvim/.config/nvim/init.lua
</span></span><span class="line"><span class="ln">2</span><span class="cl">~/dotfiles/nvim/.config/nvim/lua/plugins.lua</span></span></code></pre></div><p>執行 <code>stow nvim</code> 後：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">~/.config/nvim -&gt; ~/dotfiles/nvim/.config/nvim</span></span></code></pre></div><p>stow 會自動判斷該 symlink 整個目錄還是個別檔案——如果目標目錄不存在或目錄內所有檔案都由同一個 package 管理，stow 會 symlink 整個目錄（folding）；如果目標目錄已有其他檔案，stow 會展開（unfolding）成逐檔 symlink。</p>
<p>初始化和日常操作：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 初始化</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">mkdir ~/dotfiles <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/dotfiles
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">git init
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">git remote add origin git@github.com:you/dotfiles.git
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 把現有 .zshrc 搬進 dotfiles</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">mkdir -p ~/dotfiles/zsh
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">mv ~/.zshrc ~/dotfiles/zsh/.zshrc
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nb">cd</span> ~/dotfiles <span class="o">&amp;&amp;</span> stow zsh
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 現在 ~/.zshrc 是一個 symlink，指向 ~/dotfiles/zsh/.zshrc</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 日常修改：直接編輯，symlink 透通</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">vim ~/.zshrc  <span class="c1"># 實際編輯的是 ~/dotfiles/zsh/.zshrc</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nb">cd</span> ~/dotfiles <span class="o">&amp;&amp;</span> git add -A <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;update zshrc&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 新機器還原</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">git clone git@github.com:you/dotfiles.git ~/dotfiles
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="nb">cd</span> ~/dotfiles <span class="o">&amp;&amp;</span> stow zsh git nvim tmux</span></span></code></pre></div><p>批次安裝所有 package：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">cd</span> ~/dotfiles
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># stow 會把每個頂層目錄當成一個 package</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">stow */</span></span></code></pre></div><p>移除某個 package 的 symlink：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">cd</span> ~/dotfiles <span class="o">&amp;&amp;</span> stow -D nvim</span></span></code></pre></div><p>stow 適合中等複雜度的配置管理。它的優勢是模組化（每個工具獨立一個 package、可選擇性安裝）和概念直覺（目錄結構就是安裝後的樣子）。它的限制是只管 symlink 映射，不管套件安裝；跨 OS 的路徑差異（macOS 和 Linux 某些工具的配置路徑不同）需要自己處理；stow 也不管 file permission——需要 0600 權限的 secret 檔（SSH private key、API token config）靠 symlink 繼承來源檔案權限，不能在部署過程中自動設定。</p>
<h2 id="yadmbare-repo-的升級版">yadm：bare repo 的升級版</h2>
<p>yadm 包裝了 Git bare repo 的操作，加上三個 bare repo 缺少的能力：alternate files（依 OS、hostname、甚至 user 條件選擇安裝不同版本的配置檔）、encrypt（用 GPG 或 OpenSSL 加密敏感檔案、不依賴外部密碼管理器）、bootstrap script（clone 後自動跑初始化）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 安裝</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">brew install yadm          <span class="c1"># macOS</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">sudo pacman -S yadm        <span class="c1"># Arch</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 初始化（等同 git init --bare + 自動設定 alias）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">yadm init
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">yadm remote add origin git@github.com:you/dotfiles.git
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 操作方式跟 Git 完全一樣</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">yadm add ~/.zshrc
</span></span><span class="line"><span class="ln">11</span><span class="cl">yadm commit -m <span class="s2">&#34;add zshrc&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">yadm push</span></span></code></pre></div><p>Alternate files 的概念是在同一個 repo 裡放多個版本的同一個檔案，yadm 依條件決定用哪一個：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">~/.config/alacritty/alacritty.toml##os.Darwin
</span></span><span class="line"><span class="ln">2</span><span class="cl">~/.config/alacritty/alacritty.toml##os.Linux</span></span></code></pre></div><p>macOS 上 yadm 自動 checkout Darwin 版本、Linux 上 checkout Linux 版本。比 stow 的 shell if-else 判斷更乾淨，比 chezmoi 的 Go template 學習曲線低。</p>
<p>yadm 適合想要 bare repo 的簡單性、但需要條件安裝或 secret 加密的人。它的限制是沒有 stow 的模組化概念（無法選擇性只安裝某些工具的配置）、沒有 chezmoi 的 template 細粒度（alternate files 是整個檔案切換，不是檔案內的段落條件）。</p>
<h2 id="chezmoi多機器-dotfile-管理工具">Chezmoi：多機器 dotfile 管理工具</h2>
<p>Chezmoi 是專為 dotfile 管理設計的工具，原生處理 template、secret 管理和多機器差異。它把 dotfile 存在自己的 source directory（<code>~/.local/share/chezmoi</code>），用 <code>chezmoi apply</code> 把檔案實際寫入目標位置（不是 symlink，是複製）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew install chezmoi        <span class="c1"># macOS</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">pacman -S chezmoi           <span class="c1"># Arch</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">sh -c <span class="s2">&#34;</span><span class="k">$(</span>curl -fsLS get.chezmoi.io<span class="k">)</span><span class="s2">&#34;</span>  <span class="c1"># 通用</span></span></span></code></pre></div><p>基本操作：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 初始化（會建立 source directory 和 Git repo）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">chezmoi init
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 加入現有配置</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">chezmoi add ~/.zshrc
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">chezmoi add ~/.config/nvim
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 編輯（在 source directory 裡編輯，不是直接改家目錄的檔案）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">chezmoi edit ~/.zshrc
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 預覽差異</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">chezmoi diff
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 套用到家目錄</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">chezmoi apply
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># 推上遠端</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">chezmoi <span class="nb">cd</span>  <span class="c1"># 進入 source directory</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">git add -A <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;update&#34;</span> <span class="o">&amp;&amp;</span> git push</span></span></code></pre></div><p>chezmoi 的核心優勢是 template。同一份配置檔在不同機器可以產生不同內容：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># chezmoi 的 source directory 裡，檔案名稱加 .tmpl 後綴</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># dot_zshrc.tmpl</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nb">export</span> <span class="nv">EDITOR</span><span class="o">=</span><span class="s2">&#34;nvim&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="o">{{</span>- <span class="k">if</span> eq .chezmoi.os <span class="s2">&#34;darwin&#34;</span> <span class="o">}}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nb">export</span> <span class="nv">HOMEBREW_PREFIX</span><span class="o">=</span><span class="s2">&#34;/opt/homebrew&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nb">eval</span> <span class="s2">&#34;</span><span class="k">$(</span><span class="nv">$HOMEBREW_PREFIX</span>/bin/brew shellenv<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="o">{{</span>- end <span class="o">}}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="o">{{</span>- <span class="k">if</span> eq .chezmoi.os <span class="s2">&#34;linux&#34;</span> <span class="o">}}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="o">{{</span>- end <span class="o">}}</span></span></span></code></pre></div><p><code>chezmoi apply</code> 會根據當前機器的 OS 展開 template，macOS 上產生的 <code>.zshrc</code> 會包含 Homebrew 設定，Linux 上不會。</p>
<p>Secret 管理是另一個殺手功能。chezmoi 整合了 1Password、Bitwarden、pass、gopass、LastPass 等密碼管理器：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl"># dot_gitconfig.tmpl
</span></span><span class="line"><span class="ln">2</span><span class="cl">[user]
</span></span><span class="line"><span class="ln">3</span><span class="cl">    name = Your Name
</span></span><span class="line"><span class="ln">4</span><span class="cl">    email = {{ (onepasswordRead &#34;op://Personal/Git Config/email&#34;).value }}</span></span></code></pre></div><p><code>chezmoi apply</code> 時會即時從 1Password 拉值填入，secret 不會存在 Git repo 裡。</p>
<p>chezmoi 適合管理多台異質機器（macOS 工作機 + Linux 伺服器 + Linux 桌面 VM）且有 secret 需求的人。它的代價是學習曲線最陡——要理解 chezmoi 自己的目錄命名慣例（<code>dot_</code> 前綴代表 <code>.</code> 開頭、<code>private_</code> 前綴代表權限 0600）、template 語法（Go template）、以及「source directory 和目標位置是兩份獨立的檔案」這個心智模型。</p>
<h2 id="選型判讀">選型判讀</h2>
<p>選工具看三個維度：機器數量、OS 組合、secret 需求。</p>
<p>只有一台機器、配置簡單 — bare repo 或 stow 都夠用，差別在於你喜不喜歡 symlink 的管理方式。bare repo 最輕量，stow 多一層模組化。</p>
<p>多台同質機器（都是 macOS 或都是 Linux）— stow。配置檔在同 OS 間差異小，不需要 template，stow 的模組化讓你可以只在桌面機安裝 hyprland package、伺服器只裝 zsh + git + tmux。</p>
<p>多台異質機器（macOS + Linux）但 secret 需求不高 — stow 加上 OS 分流仍然可行，<a href="/blog/linux/dotfile/01-dotfile-management/cross-platform-one-repo/" data-link-title="跨平台共用一個 Repo" data-link-desc="macOS 跟 Linux 要共用同一個 dotfile repo、不想維護兩份時回來讀">跨平台共用一個 Repo</a> 會完整說明做法。</p>
<p>多台異質機器、需要條件安裝但不想學 template 語法 — yadm。alternate files 讓你依 OS/hostname 切換整個配置檔，內建 encrypt 處理 secret，Git 操作方式跟 bare repo 相同。</p>
<p>多台異質機器（macOS + Linux）、有細粒度 template 或密碼管理器整合需求 — chezmoi。檔案內的段落條件、跟 1Password/Bitwarden 的整合、<code>private_</code> 前綴的 permission 管理是它存在的理由。</p>
<p>不確定 — 從 stow 開始。它的概念最直覺（目錄結構 = 安裝後位置）、遷移成本最低（要換到 yadm 是加一層 wrapper、要換到 chezmoi 時目錄結構的概念是相通的）。</p>
]]></content:encoded></item><item><title>模組一：管理工具與目錄結構</title><link>https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/</guid><description>&lt;p>Dotfile 管理的核心動作是把散落在家目錄各處的配置檔集中到一個 Git repo 裡版控。工具只是幫你處理「repo 裡的檔案怎麼對應到家目錄正確位置」這一層映射，選型看的是你的機器數量、OS 組合和 secret 需求。&lt;/p>
&lt;p>開始之前，確認 SSH key 和 Git 已經設好、dotfile repo 已經 clone 到本機——這些前置步驟見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">環境建置的操作順序&lt;/a>的階段一。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/management-strategies/" data-link-title="管理策略與選型" data-link-desc="要選 dotfile 管理工具時回來讀 — bare repo、stow、chezmoi 的適用場景與選型判讀">管理策略與選型&lt;/a>&lt;/td>
 &lt;td>bare repo / stow / chezmoi 三種策略的操作方式、優劣與選型判讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/cross-platform-one-repo/" data-link-title="跨平台共用一個 Repo" data-link-desc="macOS 跟 Linux 要共用同一個 dotfile repo、不想維護兩份時回來讀">跨平台共用一個 Repo&lt;/a>&lt;/td>
 &lt;td>macOS + Linux 用同一個 repo 的三層模型：stow 選擇性安裝、OS 分流、local.zsh 機器專屬&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/directory-structure-workflow/" data-link-title="目錄結構、Git 工作流與常見陷阱" data-link-desc="設計 dotfile repo 的目錄結構、或遇到 symlink 衝突和私鑰外洩等問題時回來讀">目錄結構、Git 工作流與常見陷阱&lt;/a>&lt;/td>
 &lt;td>stow 的目錄結構設計原則、日常 Git 操作流程、私鑰外洩等常見陷阱&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">模組零：Dotfile 心智模型&lt;/a>：為什麼要管理、哪些東西該進 repo&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/02-shell-config/" data-link-title="模組二：Shell 配置" data-link-desc="shell 配置檔長成一坨不敢動時回來讀 — .zshrc/.bashrc 的結構化拆分、alias/function/PATH 的模組化設計">模組二：Shell 配置&lt;/a>：目錄結構裡 zsh package 的具體拆法&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建&lt;/a>：跨機器同步策略與 secret 管理&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Dotfile 管理的核心動作是把散落在家目錄各處的配置檔集中到一個 Git repo 裡版控。工具只是幫你處理「repo 裡的檔案怎麼對應到家目錄正確位置」這一層映射，選型看的是你的機器數量、OS 組合和 secret 需求。</p>
<p>開始之前，確認 SSH key 和 Git 已經設好、dotfile repo 已經 clone 到本機——這些前置步驟見<a href="/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">環境建置的操作順序</a>的階段一。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/01-dotfile-management/management-strategies/" data-link-title="管理策略與選型" data-link-desc="要選 dotfile 管理工具時回來讀 — bare repo、stow、chezmoi 的適用場景與選型判讀">管理策略與選型</a></td>
          <td>bare repo / stow / chezmoi 三種策略的操作方式、優劣與選型判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/01-dotfile-management/cross-platform-one-repo/" data-link-title="跨平台共用一個 Repo" data-link-desc="macOS 跟 Linux 要共用同一個 dotfile repo、不想維護兩份時回來讀">跨平台共用一個 Repo</a></td>
          <td>macOS + Linux 用同一個 repo 的三層模型：stow 選擇性安裝、OS 分流、local.zsh 機器專屬</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/01-dotfile-management/directory-structure-workflow/" data-link-title="目錄結構、Git 工作流與常見陷阱" data-link-desc="設計 dotfile repo 的目錄結構、或遇到 symlink 衝突和私鑰外洩等問題時回來讀">目錄結構、Git 工作流與常見陷阱</a></td>
          <td>stow 的目錄結構設計原則、日常 Git 操作流程、私鑰外洩等常見陷阱</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">模組零：Dotfile 心智模型</a>：為什麼要管理、哪些東西該進 repo</li>
<li>→ <a href="/blog/linux/dotfile/02-shell-config/" data-link-title="模組二：Shell 配置" data-link-desc="shell 配置檔長成一坨不敢動時回來讀 — .zshrc/.bashrc 的結構化拆分、alias/function/PATH 的模組化設計">模組二：Shell 配置</a>：目錄結構裡 zsh package 的具體拆法</li>
<li>→ <a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建</a>：跨機器同步策略與 secret 管理</li>
</ul>
]]></content:encoded></item><item><title>環境可重現性與配置分類</title><link>https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/environment-reproducibility/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/environment-reproducibility/</guid><description>&lt;p>Dotfile 管理的核心能力是&lt;strong>環境可重現性&lt;/strong>：把個人開發環境的配置狀態變成版控下的代碼，讓任何一台空白機器都能用一份 Git repo 還原成你熟悉的工作桌面。&lt;/p>
&lt;h2 id="什麼是-dotfile">什麼是 Dotfile&lt;/h2>
&lt;p>Unix 系統用檔名開頭的 &lt;code>.&lt;/code> 標記隱藏檔。shell 配置（&lt;code>.bashrc&lt;/code>、&lt;code>.zshrc&lt;/code>）、Git 設定（&lt;code>.gitconfig&lt;/code>）、SSH 設定（&lt;code>.ssh/config&lt;/code>）、以及 &lt;code>~/.config/&lt;/code> 底下各種工具的配置目錄，都屬於這個範疇。這些檔案決定了你的工作環境怎麼運作：shell 的 prompt 長什麼樣、alias 有哪些、editor 用什麼 keymap、terminal 的配色方案、視窗管理器怎麼排列畫面。&lt;/p>
&lt;p>「dotfile 管理」指的是把這些散落在家目錄各處的配置檔&lt;strong>集中到一個 Git repo&lt;/strong>，建立版本歷史、可以跨機器同步、可以在新環境一鍵部署。跟手動備份的差異在於：備份是搬檔案，dotfile 管理是建立一套可重複執行的環境建構流程。&lt;/p>
&lt;h2 id="為什麼要管理-dotfile">為什麼要管理 Dotfile&lt;/h2>
&lt;p>開發環境是累積出來的。今天加一個 alias、明天改一個 Git 設定、下週裝了一個 terminal 外掛調了字型。這些微調加起來就是「你順手的工作環境」，但因為都是零碎的小改動，很少有人會主動記錄。&lt;/p>
&lt;p>問題在累積到一定程度後、環境需要重建的那一刻才會浮出來：&lt;/p>
&lt;p>&lt;strong>換機器&lt;/strong>。拿到新筆電，開始從零設定。裝完 shell、editor、terminal，發現少了一堆 alias 和 function，但想不起來之前到底加了哪些。花兩天勉強恢復到七八成，剩下的在未來幾週慢慢「撞到才發現少了」。&lt;/p>
&lt;p>&lt;strong>設備故障或遺失&lt;/strong>。公司配的筆電硬碟壞了。如果配置沒有外部副本，那台機器上所有的自訂設定（有些可能花了半年慢慢調出來的）全部歸零。復原速度直接取決於「你有沒有把配置存在機器以外的地方」。&lt;/p>
&lt;p>&lt;strong>在 VM 或容器裡重現環境&lt;/strong>。想在虛擬機裡測試一套 Linux 桌面（例如 Hyprland），或在 Docker 容器裡重現自己的 shell 環境做 CI 除錯。沒有版控的配置，就得手動複製貼上，還要記住哪些檔案在哪個路徑。&lt;/p>
&lt;p>&lt;strong>跨機器一致性&lt;/strong>。同時用筆電、桌機、遠端伺服器，希望每台機器的 shell 行為、Git 設定、editor 快捷鍵都一致。手動同步的成本隨機器數量線性增長，而且很容易漏改某一台，導致操作習慣在不同機器間不一致。&lt;/p>
&lt;p>這些場景的共通點是：&lt;strong>配置的價值在累積，但累積的前提是有記錄&lt;/strong>。沒有記錄的累積，只是暫存在某一台機器上的隱性知識，機器一換就歸零。&lt;/p>
&lt;h2 id="哪些東西應該建立-dotfile">哪些東西應該建立 Dotfile&lt;/h2>
&lt;p>依配置的普遍性和適用場景，分成三層來判斷：&lt;/p>
&lt;h3 id="核心層幾乎所有開發者都該管的">核心層：幾乎所有開發者都該管的&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Shell 配置&lt;/strong>（&lt;code>.zshrc&lt;/code> / &lt;code>.bashrc&lt;/code>）：alias、function、PATH、prompt、completion 設定。這是最高頻修改、也最容易累積隱性知識的地方。&lt;/li>
&lt;li>&lt;strong>Git 配置&lt;/strong>（&lt;code>.gitconfig&lt;/code>）：使用者名稱、email、預設 editor、alias（&lt;code>git lg&lt;/code> = &lt;code>git log --oneline --graph&lt;/code>）、diff/merge tool。&lt;/li>
&lt;li>&lt;strong>SSH 配置&lt;/strong>（&lt;code>.ssh/config&lt;/code>）：Host 別名、ProxyJump 跳板設定、每台主機的 IdentityFile 指定。注意：&lt;strong>config 檔進 repo，私鑰不進 repo&lt;/strong>。&lt;/li>
&lt;li>&lt;strong>Editor 配置&lt;/strong>：&lt;code>.vimrc&lt;/code>（Vim）、&lt;code>~/.config/nvim/&lt;/code>（Neovim）、VS Code 的 &lt;code>settings.json&lt;/code> / &lt;code>keybindings.json&lt;/code>。&lt;/li>
&lt;/ul>
&lt;h3 id="工具層依個人工具鏈而定">工具層：依個人工具鏈而定&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Terminal multiplexer&lt;/strong>：tmux（&lt;code>.tmux.conf&lt;/code>）、zellij（&lt;code>~/.config/zellij/&lt;/code>）的面板配置、快捷鍵、狀態列。&lt;/li>
&lt;li>&lt;strong>Terminal emulator&lt;/strong>：Alacritty、WezTerm、Kitty 的字型、配色、快捷鍵設定。&lt;/li>
&lt;li>&lt;strong>套件清單&lt;/strong>：macOS 的 &lt;code>Brewfile&lt;/code>（&lt;code>brew bundle dump&lt;/code>）、Arch 的 &lt;code>pacman -Qqe &amp;gt; pkglist.txt&lt;/code>。這份清單讓新機器知道該裝哪些軟體。&lt;/li>
&lt;li>&lt;strong>開發工具的全域配置&lt;/strong>：&lt;code>.npmrc&lt;/code>、&lt;code>.cargo/config.toml&lt;/code>、&lt;code>.pypirc&lt;/code> 等。但要注意區分：&lt;code>.eslintrc&lt;/code> / &lt;code>.prettierrc&lt;/code> 這類通常跟著&lt;strong>專案&lt;/strong> repo 走（每個專案可能規則不同），不跟著人走。&lt;/li>
&lt;/ul>
&lt;h3 id="桌面層linux-桌面環境才需要">桌面層：Linux 桌面環境才需要&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Window manager&lt;/strong>：Hyprland（&lt;code>~/.config/hypr/&lt;/code>）、i3（&lt;code>~/.config/i3/&lt;/code>）、sway 的配置。&lt;/li>
&lt;li>&lt;strong>桌面元件&lt;/strong>：狀態列（waybar）、應用程式啟動器（rofi / wofi）、通知服務（mako / dunst）、鎖屏（swaylock / hyprlock）。&lt;/li>
&lt;li>&lt;strong>主題與配色&lt;/strong>：GTK / Qt 主題設定、游標主題、字型配置。&lt;/li>
&lt;/ul>
&lt;h3 id="判讀準則">判讀準則&lt;/h3>
&lt;p>區分一個配置該不該進 dotfile repo，核心問題是：&lt;strong>這個設定是跟著人走，還是跟著專案走？&lt;/strong>&lt;/p>
&lt;p>跟著人走的（不管開哪個專案都要用的）→ 進 dotfile repo。跟著專案走的（專案 A 用 ESLint、專案 B 用 Biome）→ 留在專案 repo。&lt;/p>
&lt;h3 id="不該進-dotfile-repo-的">不該進 Dotfile Repo 的&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>私鑰、API key、token、密碼&lt;/strong>。用 &lt;code>.gitignore&lt;/code> 排除，敏感資訊放 secret manager 或加密管理（具體做法見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/sync-strategy-secret/" data-link-title="跨機器同步、Secret 管理與環境重建流程" data-link-desc="多台機器的 dotfile 怎麼同步、哪些東西不該進 repo 時回來讀">同步與 Secret 管理&lt;/a>）。&lt;/li>
&lt;li>&lt;strong>暫存檔、cache、log&lt;/strong>。&lt;code>.zsh_history&lt;/code> 很大且含敏感指令；各種工具的 cache 目錄是 generated 檔案，重建時自動產生。&lt;/li>
&lt;li>&lt;strong>OS 層級的二進位設定&lt;/strong>。macOS 的 plist 可以選擇性管理（&lt;code>defaults write&lt;/code> 指令可以版控），但整個 &lt;code>~/Library/Preferences/&lt;/code> 不適合直接丟進 Git——檔案格式不穩定、diff 不可讀、很多是應用程式自動產生的。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Dotfile 管理的核心能力是<strong>環境可重現性</strong>：把個人開發環境的配置狀態變成版控下的代碼，讓任何一台空白機器都能用一份 Git repo 還原成你熟悉的工作桌面。</p>
<h2 id="什麼是-dotfile">什麼是 Dotfile</h2>
<p>Unix 系統用檔名開頭的 <code>.</code> 標記隱藏檔。shell 配置（<code>.bashrc</code>、<code>.zshrc</code>）、Git 設定（<code>.gitconfig</code>）、SSH 設定（<code>.ssh/config</code>）、以及 <code>~/.config/</code> 底下各種工具的配置目錄，都屬於這個範疇。這些檔案決定了你的工作環境怎麼運作：shell 的 prompt 長什麼樣、alias 有哪些、editor 用什麼 keymap、terminal 的配色方案、視窗管理器怎麼排列畫面。</p>
<p>「dotfile 管理」指的是把這些散落在家目錄各處的配置檔<strong>集中到一個 Git repo</strong>，建立版本歷史、可以跨機器同步、可以在新環境一鍵部署。跟手動備份的差異在於：備份是搬檔案，dotfile 管理是建立一套可重複執行的環境建構流程。</p>
<h2 id="為什麼要管理-dotfile">為什麼要管理 Dotfile</h2>
<p>開發環境是累積出來的。今天加一個 alias、明天改一個 Git 設定、下週裝了一個 terminal 外掛調了字型。這些微調加起來就是「你順手的工作環境」，但因為都是零碎的小改動，很少有人會主動記錄。</p>
<p>問題在累積到一定程度後、環境需要重建的那一刻才會浮出來：</p>
<p><strong>換機器</strong>。拿到新筆電，開始從零設定。裝完 shell、editor、terminal，發現少了一堆 alias 和 function，但想不起來之前到底加了哪些。花兩天勉強恢復到七八成，剩下的在未來幾週慢慢「撞到才發現少了」。</p>
<p><strong>設備故障或遺失</strong>。公司配的筆電硬碟壞了。如果配置沒有外部副本，那台機器上所有的自訂設定（有些可能花了半年慢慢調出來的）全部歸零。復原速度直接取決於「你有沒有把配置存在機器以外的地方」。</p>
<p><strong>在 VM 或容器裡重現環境</strong>。想在虛擬機裡測試一套 Linux 桌面（例如 Hyprland），或在 Docker 容器裡重現自己的 shell 環境做 CI 除錯。沒有版控的配置，就得手動複製貼上，還要記住哪些檔案在哪個路徑。</p>
<p><strong>跨機器一致性</strong>。同時用筆電、桌機、遠端伺服器，希望每台機器的 shell 行為、Git 設定、editor 快捷鍵都一致。手動同步的成本隨機器數量線性增長，而且很容易漏改某一台，導致操作習慣在不同機器間不一致。</p>
<p>這些場景的共通點是：<strong>配置的價值在累積，但累積的前提是有記錄</strong>。沒有記錄的累積，只是暫存在某一台機器上的隱性知識，機器一換就歸零。</p>
<h2 id="哪些東西應該建立-dotfile">哪些東西應該建立 Dotfile</h2>
<p>依配置的普遍性和適用場景，分成三層來判斷：</p>
<h3 id="核心層幾乎所有開發者都該管的">核心層：幾乎所有開發者都該管的</h3>
<ul>
<li><strong>Shell 配置</strong>（<code>.zshrc</code> / <code>.bashrc</code>）：alias、function、PATH、prompt、completion 設定。這是最高頻修改、也最容易累積隱性知識的地方。</li>
<li><strong>Git 配置</strong>（<code>.gitconfig</code>）：使用者名稱、email、預設 editor、alias（<code>git lg</code> = <code>git log --oneline --graph</code>）、diff/merge tool。</li>
<li><strong>SSH 配置</strong>（<code>.ssh/config</code>）：Host 別名、ProxyJump 跳板設定、每台主機的 IdentityFile 指定。注意：<strong>config 檔進 repo，私鑰不進 repo</strong>。</li>
<li><strong>Editor 配置</strong>：<code>.vimrc</code>（Vim）、<code>~/.config/nvim/</code>（Neovim）、VS Code 的 <code>settings.json</code> / <code>keybindings.json</code>。</li>
</ul>
<h3 id="工具層依個人工具鏈而定">工具層：依個人工具鏈而定</h3>
<ul>
<li><strong>Terminal multiplexer</strong>：tmux（<code>.tmux.conf</code>）、zellij（<code>~/.config/zellij/</code>）的面板配置、快捷鍵、狀態列。</li>
<li><strong>Terminal emulator</strong>：Alacritty、WezTerm、Kitty 的字型、配色、快捷鍵設定。</li>
<li><strong>套件清單</strong>：macOS 的 <code>Brewfile</code>（<code>brew bundle dump</code>）、Arch 的 <code>pacman -Qqe &gt; pkglist.txt</code>。這份清單讓新機器知道該裝哪些軟體。</li>
<li><strong>開發工具的全域配置</strong>：<code>.npmrc</code>、<code>.cargo/config.toml</code>、<code>.pypirc</code> 等。但要注意區分：<code>.eslintrc</code> / <code>.prettierrc</code> 這類通常跟著<strong>專案</strong> repo 走（每個專案可能規則不同），不跟著人走。</li>
</ul>
<h3 id="桌面層linux-桌面環境才需要">桌面層：Linux 桌面環境才需要</h3>
<ul>
<li><strong>Window manager</strong>：Hyprland（<code>~/.config/hypr/</code>）、i3（<code>~/.config/i3/</code>）、sway 的配置。</li>
<li><strong>桌面元件</strong>：狀態列（waybar）、應用程式啟動器（rofi / wofi）、通知服務（mako / dunst）、鎖屏（swaylock / hyprlock）。</li>
<li><strong>主題與配色</strong>：GTK / Qt 主題設定、游標主題、字型配置。</li>
</ul>
<h3 id="判讀準則">判讀準則</h3>
<p>區分一個配置該不該進 dotfile repo，核心問題是：<strong>這個設定是跟著人走，還是跟著專案走？</strong></p>
<p>跟著人走的（不管開哪個專案都要用的）→ 進 dotfile repo。跟著專案走的（專案 A 用 ESLint、專案 B 用 Biome）→ 留在專案 repo。</p>
<h3 id="不該進-dotfile-repo-的">不該進 Dotfile Repo 的</h3>
<ul>
<li><strong>私鑰、API key、token、密碼</strong>。用 <code>.gitignore</code> 排除，敏感資訊放 secret manager 或加密管理（具體做法見<a href="/blog/linux/dotfile/08-sync-bootstrap/sync-strategy-secret/" data-link-title="跨機器同步、Secret 管理與環境重建流程" data-link-desc="多台機器的 dotfile 怎麼同步、哪些東西不該進 repo 時回來讀">同步與 Secret 管理</a>）。</li>
<li><strong>暫存檔、cache、log</strong>。<code>.zsh_history</code> 很大且含敏感指令；各種工具的 cache 目錄是 generated 檔案，重建時自動產生。</li>
<li><strong>OS 層級的二進位設定</strong>。macOS 的 plist 可以選擇性管理（<code>defaults write</code> 指令可以版控），但整個 <code>~/Library/Preferences/</code> 不適合直接丟進 Git——檔案格式不穩定、diff 不可讀、很多是應用程式自動產生的。</li>
</ul>
]]></content:encoded></item><item><title>Linux 安裝選項判讀</title><link>https://tarrragon.github.io/blog/linux/install/install-option-decisions/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/install-option-decisions/</guid><description>&lt;p>Linux 安裝程式的每個選項都是一個會往後傳遞代價的決策。選錯的後果不會在當下浮現，而是在重開機進不了系統、磁碟某個分區先爆掉、或裝好的機器一重開就斷網時才出現。判斷一個選項該怎麼選，靠的是同一個問題：這台機器是用來幹嘛的。一台用完即丟的測試 VM、一台跑三年的主力機、一台對外服務的伺服器，同一個選項的正確答案可能完全不同。&lt;/p>
&lt;p>這篇給每個關鍵選項一條判斷軸，而不是一份「照著點」的步驟。底下用一次具體的安裝當作貫穿例子：在 Apple Silicon 的 UTM 上用 archboot ISO 裝 Arch Linux ARM，目標是一台演練 dotfile 部署的測試 VM。例子是具體的，但判斷軸跨發行版通用——locale、分割、bootloader 這些抉擇在多數 Linux 安裝程式裡都會以類似形式出現。&lt;/p>
&lt;p>怎麼建立 VM、燒錄並開機到安裝程式，是跟環境綁定的前置——UTM、VirtualBox、實體機各不相同——不在這條判讀軸的範圍；這篇從「安裝程式已經跑起來、開始問你選項」接手，給的是每個選項的判斷軸，不逐頁帶某個安裝程式的選單怎麼點。底下的指令與選單名稱以 archboot / Arch 呈現，換 Ubuntu（Subiquity 安裝程式）或 Fedora 時，選項的判斷軸一樣成立，但選單長相、套件組名稱、指令會不同。&lt;/p>
&lt;h2 id="系統語系與時間">系統語系與時間&lt;/h2>
&lt;p>系統語系決定的是錯誤訊息、log、系統工具輸出用哪種語言，不是你日常打字的語言。這兩件事容易混為一談。日常輸入中文是桌面層的字型與輸入法問題，跟系統 locale 無關；系統 locale 影響的是當某個服務崩潰、你在 &lt;code>journalctl&lt;/code> 裡讀它吐出來的那行訊息時，那行字是英文還是被翻譯過。&lt;/p>
&lt;p>把系統 locale 留在 &lt;code>en_US.UTF-8&lt;/code> 的理由是可搜尋性。當你把一段錯誤訊息貼到搜尋引擎或問別人，英文訊息能對上絕大多數的文件、issue、Stack Overflow 答案；翻譯過的訊息往往一個結果都搜不到。這條判斷軸對伺服器、開發機、任何你預期會除錯的機器都成立。會選非英文 locale 的情境通常是給終端使用者的桌面，且該使用者不除錯——那是另一種機器。&lt;/p>
&lt;p>時區的選擇影響 log 的時間戳跟排程任務的觸發時刻，挑你所在地即可。另一個相關的決策是「硬體時鐘存 UTC 還是本地時間」：選 UTC。Linux 慣例是硬體時鐘存 UTC、顯示時再換算成時區，這樣跨時區搬機器、或 NTP 校時都不會錯亂。會需要存本地時間的唯一常見情境是跟 Windows 雙開——Windows 預設把硬體時鐘當本地時間——而 VM 或純 Linux 機器沒有這個包袱。&lt;/p>
&lt;h2 id="網路">網路&lt;/h2>
&lt;p>安裝階段的網路設定要回答兩個層次：當下能不能連、以及這份設定會不會帶進裝好的系統。第一層通常很直覺——選到對的網卡、用 DHCP 讓它自動拿 IP。虛擬機的 NAT 網路會自動發 IP，所以選 DHCP、不要手動設 static，省去算網段的麻煩。&lt;/p>
&lt;p>第二層是真正會咬人的地方：安裝程式裡設好的網路，不保證會出現在重開機後的系統裡。這是 VM 裝 Linux 常見的斷網點——安裝時明明能上網裝套件，重開機後卻連不出去，因為安裝環境的網路設定沒被複製到目標系統。判讀方式是裝好首次開機後立刻驗證：看網卡有沒有拿到 IP、能不能解析一個域名。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ip -brief a &lt;span class="c1"># 網卡有沒有 IP、狀態是不是 UP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">ping -c &lt;span class="m">3&lt;/span> archlinux.org &lt;span class="c1"># 解析成功就證明對外連線 + DNS 都通&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DNS 能把域名解析成 IP，本身就證明對外連線是通的（DNS 查詢就是一次網路往返），所以這條 &lt;code>ping&lt;/code> 即使對方不回 ICMP 也已經給了答案。在前述的 archboot 例子裡，網路設定確實有被複製進目標系統並由 systemd-networkd 接手，重開機後免再手動設——但這是該安裝程式的行為，不能假設每個安裝程式都這樣。把「首次開機驗網路」當成固定動作，比預設它一定會通安全。&lt;/p>
&lt;h2 id="套件鏡像">套件鏡像&lt;/h2>
&lt;p>鏡像源決定你從哪裡下載套件，挑地理上接近的那個。基礎系統加上一套桌面動輒上 GB，選對岸的鏡像跟選同城的鏡像，下載時間差好幾倍。安裝程式給的鏡像清單通常按國家排，往下捲找你所在地區的；找不到完全同國的，就退而求其次選同區域、且穩定的大型鏡像。&lt;/p>
&lt;p>這個選擇的另一個作用是順帶確認你裝的是哪個發行版分支。前述例子的鏡像清單全是 &lt;code>archlinuxarm.org&lt;/code>，這證實了 archboot 的 aarch64 ISO 裝出來的是 Arch Linux ARM（ARM 移植版），而不是 x86 的 Arch——同一條安裝路徑產出的是哪個分支，鏡像來源會洩漏給你看。&lt;/p>
&lt;h2 id="磁碟分割">磁碟分割&lt;/h2>
&lt;p>磁碟分割是整個安裝裡選項最多、也最不可逆的一段，但判斷軸只有一條：這台機器需不需要在分區層面做隔離。需要隔離的情境——多系統共存、加密、資料與系統分離以便重灌不丟資料——每多一個，分割就多一層結構。不需要的情境，多切一刀都只是增加「一邊爆一邊空」的風險。下面逐項拆解，但它們服務的是同一個判斷。&lt;/p>
&lt;h3 id="自動分割-vs-手動分割">自動分割 vs 手動分割&lt;/h3>
&lt;p>自動分割（清空整碟、安裝程式幫你建標準佈局）適用於整碟專屬、沒有要保留任何既有資料的機器。測試 VM 的磁碟是全新的、整碟給這個系統用，自動分割沒有任何代價，還省去手動算 EFI 大小、root 大小、對齊、格式化、掛載這一連串容易錯的步驟。&lt;/p>
&lt;p>手動分割的價值在你需要非標準佈局時才浮現：多系統共用某個分區、要留一塊不格式化的資料區、要套 LVM 或 LUKS。這些是真實主力機與伺服器會遇到的需求，但對一台「目標是驗證 dotfile 部署」的 VM 是純雜訊——它們屬於另一個主題，不該混進這次的安裝。判讀訊號很簡單：你說得出一個具體的隔離需求，才手動分割；說不出來，自動分割就是對的。&lt;/p>
&lt;h3 id="分區識別方式partuuid">分區識別方式（PARTUUID）&lt;/h3>
&lt;p>分區識別方式決定 &lt;code>fstab&lt;/code>（開機時決定哪個分區掛到哪的設定檔）跟 bootloader 怎麼指涉每個分區，在 GPT（現代 UEFI 機器的分區表格式）磁碟上選 PARTUUID。這個選擇的後果是「重開機後系統找不找得到自己的分區」。PARTUUID 綁在分區本身、跨重開機穩定，而且重新格式化檔案系統也不會變；相對地，檔案系統層級的 UUID 一重格就變，會讓 &lt;code>fstab&lt;/code> 失效，而 &lt;code>/dev/vda1&lt;/code> 那種 kernel 名稱會隨偵測順序浮動，最不穩。穩定性的排序是 PARTUUID 優於 FSUUID 優於 kernel 名稱，GPT 磁碟用最穩的那個（這三種識別方式的細節見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/partition-identification/" data-link-title="分區識別（PARTUUID / FSUUID）" data-link-desc="在 fstab 或 bootloader 設定要指定一個分區、不確定該用 PARTUUID、UUID 還是 /dev/sda1、或重格式化後系統開不了機時讀 — 分區的穩定識別方式">分區識別卡&lt;/a>）。&lt;/p>
&lt;h3 id="efi-分區的掛載點與大小">EFI 分區的掛載點與大小&lt;/h3>
&lt;p>EFI 系統分區（ESP）放開機載入器與 kernel，掛載點的選擇取決於這台機器是不是單一作業系統。把 ESP 掛在 &lt;code>/boot&lt;/code>（單系統佈局）讓 kernel 跟開機檔住在同一個分區、維護最單純；把 ESP 掛在 &lt;code>/efi&lt;/code>、kernel 另放（多系統佈局）是為了多個 OS 共用同一個 ESP 才需要的結構。單系統的機器選多系統佈局，只是憑空多一層目錄。&lt;/p>
&lt;p>ESP 大小在單系統佈局下要算進 kernel 與 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/initramfs/" data-link-title="initramfs" data-link-desc="看到 ESP 大小要算進 initramfs、或開機卡在掛載 root 之前、不知道 initramfs 是什麼時讀 — 開機初期掛真 root 之前的臨時根檔系統">initramfs&lt;/a>（開機初期把真正的 root 掛起來之前、用來載入驅動的小型臨時根檔系統）。一個 kernel 加上它的 initramfs（含 fallback）大約一兩百 MB，再加上 FAT32 ESP 約 260 MiB 的實務下限，512 MiB 是在下限之上留餘裕。會需要更大的情境是你要同時保留多個 kernel 版本——但單 kernel 的 VM 用不到，給太大只是浪費。&lt;/p></description><content:encoded><![CDATA[<p>Linux 安裝程式的每個選項都是一個會往後傳遞代價的決策。選錯的後果不會在當下浮現，而是在重開機進不了系統、磁碟某個分區先爆掉、或裝好的機器一重開就斷網時才出現。判斷一個選項該怎麼選，靠的是同一個問題：這台機器是用來幹嘛的。一台用完即丟的測試 VM、一台跑三年的主力機、一台對外服務的伺服器，同一個選項的正確答案可能完全不同。</p>
<p>這篇給每個關鍵選項一條判斷軸，而不是一份「照著點」的步驟。底下用一次具體的安裝當作貫穿例子：在 Apple Silicon 的 UTM 上用 archboot ISO 裝 Arch Linux ARM，目標是一台演練 dotfile 部署的測試 VM。例子是具體的，但判斷軸跨發行版通用——locale、分割、bootloader 這些抉擇在多數 Linux 安裝程式裡都會以類似形式出現。</p>
<p>怎麼建立 VM、燒錄並開機到安裝程式，是跟環境綁定的前置——UTM、VirtualBox、實體機各不相同——不在這條判讀軸的範圍；這篇從「安裝程式已經跑起來、開始問你選項」接手，給的是每個選項的判斷軸，不逐頁帶某個安裝程式的選單怎麼點。底下的指令與選單名稱以 archboot / Arch 呈現，換 Ubuntu（Subiquity 安裝程式）或 Fedora 時，選項的判斷軸一樣成立，但選單長相、套件組名稱、指令會不同。</p>
<h2 id="系統語系與時間">系統語系與時間</h2>
<p>系統語系決定的是錯誤訊息、log、系統工具輸出用哪種語言，不是你日常打字的語言。這兩件事容易混為一談。日常輸入中文是桌面層的字型與輸入法問題，跟系統 locale 無關；系統 locale 影響的是當某個服務崩潰、你在 <code>journalctl</code> 裡讀它吐出來的那行訊息時，那行字是英文還是被翻譯過。</p>
<p>把系統 locale 留在 <code>en_US.UTF-8</code> 的理由是可搜尋性。當你把一段錯誤訊息貼到搜尋引擎或問別人，英文訊息能對上絕大多數的文件、issue、Stack Overflow 答案；翻譯過的訊息往往一個結果都搜不到。這條判斷軸對伺服器、開發機、任何你預期會除錯的機器都成立。會選非英文 locale 的情境通常是給終端使用者的桌面，且該使用者不除錯——那是另一種機器。</p>
<p>時區的選擇影響 log 的時間戳跟排程任務的觸發時刻，挑你所在地即可。另一個相關的決策是「硬體時鐘存 UTC 還是本地時間」：選 UTC。Linux 慣例是硬體時鐘存 UTC、顯示時再換算成時區，這樣跨時區搬機器、或 NTP 校時都不會錯亂。會需要存本地時間的唯一常見情境是跟 Windows 雙開——Windows 預設把硬體時鐘當本地時間——而 VM 或純 Linux 機器沒有這個包袱。</p>
<h2 id="網路">網路</h2>
<p>安裝階段的網路設定要回答兩個層次：當下能不能連、以及這份設定會不會帶進裝好的系統。第一層通常很直覺——選到對的網卡、用 DHCP 讓它自動拿 IP。虛擬機的 NAT 網路會自動發 IP，所以選 DHCP、不要手動設 static，省去算網段的麻煩。</p>
<p>第二層是真正會咬人的地方：安裝程式裡設好的網路，不保證會出現在重開機後的系統裡。這是 VM 裝 Linux 常見的斷網點——安裝時明明能上網裝套件，重開機後卻連不出去，因為安裝環境的網路設定沒被複製到目標系統。判讀方式是裝好首次開機後立刻驗證：看網卡有沒有拿到 IP、能不能解析一個域名。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ip -brief a          <span class="c1"># 網卡有沒有 IP、狀態是不是 UP</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ping -c <span class="m">3</span> archlinux.org   <span class="c1"># 解析成功就證明對外連線 + DNS 都通</span></span></span></code></pre></div><p>DNS 能把域名解析成 IP，本身就證明對外連線是通的（DNS 查詢就是一次網路往返），所以這條 <code>ping</code> 即使對方不回 ICMP 也已經給了答案。在前述的 archboot 例子裡，網路設定確實有被複製進目標系統並由 systemd-networkd 接手，重開機後免再手動設——但這是該安裝程式的行為，不能假設每個安裝程式都這樣。把「首次開機驗網路」當成固定動作，比預設它一定會通安全。</p>
<h2 id="套件鏡像">套件鏡像</h2>
<p>鏡像源決定你從哪裡下載套件，挑地理上接近的那個。基礎系統加上一套桌面動輒上 GB，選對岸的鏡像跟選同城的鏡像，下載時間差好幾倍。安裝程式給的鏡像清單通常按國家排，往下捲找你所在地區的；找不到完全同國的，就退而求其次選同區域、且穩定的大型鏡像。</p>
<p>這個選擇的另一個作用是順帶確認你裝的是哪個發行版分支。前述例子的鏡像清單全是 <code>archlinuxarm.org</code>，這證實了 archboot 的 aarch64 ISO 裝出來的是 Arch Linux ARM（ARM 移植版），而不是 x86 的 Arch——同一條安裝路徑產出的是哪個分支，鏡像來源會洩漏給你看。</p>
<h2 id="磁碟分割">磁碟分割</h2>
<p>磁碟分割是整個安裝裡選項最多、也最不可逆的一段，但判斷軸只有一條：這台機器需不需要在分區層面做隔離。需要隔離的情境——多系統共存、加密、資料與系統分離以便重灌不丟資料——每多一個，分割就多一層結構。不需要的情境，多切一刀都只是增加「一邊爆一邊空」的風險。下面逐項拆解，但它們服務的是同一個判斷。</p>
<h3 id="自動分割-vs-手動分割">自動分割 vs 手動分割</h3>
<p>自動分割（清空整碟、安裝程式幫你建標準佈局）適用於整碟專屬、沒有要保留任何既有資料的機器。測試 VM 的磁碟是全新的、整碟給這個系統用，自動分割沒有任何代價，還省去手動算 EFI 大小、root 大小、對齊、格式化、掛載這一連串容易錯的步驟。</p>
<p>手動分割的價值在你需要非標準佈局時才浮現：多系統共用某個分區、要留一塊不格式化的資料區、要套 LVM 或 LUKS。這些是真實主力機與伺服器會遇到的需求，但對一台「目標是驗證 dotfile 部署」的 VM 是純雜訊——它們屬於另一個主題，不該混進這次的安裝。判讀訊號很簡單：你說得出一個具體的隔離需求，才手動分割；說不出來，自動分割就是對的。</p>
<h3 id="分區識別方式partuuid">分區識別方式（PARTUUID）</h3>
<p>分區識別方式決定 <code>fstab</code>（開機時決定哪個分區掛到哪的設定檔）跟 bootloader 怎麼指涉每個分區，在 GPT（現代 UEFI 機器的分區表格式）磁碟上選 PARTUUID。這個選擇的後果是「重開機後系統找不找得到自己的分區」。PARTUUID 綁在分區本身、跨重開機穩定，而且重新格式化檔案系統也不會變；相對地，檔案系統層級的 UUID 一重格就變，會讓 <code>fstab</code> 失效，而 <code>/dev/vda1</code> 那種 kernel 名稱會隨偵測順序浮動，最不穩。穩定性的排序是 PARTUUID 優於 FSUUID 優於 kernel 名稱，GPT 磁碟用最穩的那個（這三種識別方式的細節見 <a href="/blog/linux/dotfile/knowledge-cards/partition-identification/" data-link-title="分區識別（PARTUUID / FSUUID）" data-link-desc="在 fstab 或 bootloader 設定要指定一個分區、不確定該用 PARTUUID、UUID 還是 /dev/sda1、或重格式化後系統開不了機時讀 — 分區的穩定識別方式">分區識別卡</a>）。</p>
<h3 id="efi-分區的掛載點與大小">EFI 分區的掛載點與大小</h3>
<p>EFI 系統分區（ESP）放開機載入器與 kernel，掛載點的選擇取決於這台機器是不是單一作業系統。把 ESP 掛在 <code>/boot</code>（單系統佈局）讓 kernel 跟開機檔住在同一個分區、維護最單純；把 ESP 掛在 <code>/efi</code>、kernel 另放（多系統佈局）是為了多個 OS 共用同一個 ESP 才需要的結構。單系統的機器選多系統佈局，只是憑空多一層目錄。</p>
<p>ESP 大小在單系統佈局下要算進 kernel 與 <a href="/blog/linux/dotfile/knowledge-cards/initramfs/" data-link-title="initramfs" data-link-desc="看到 ESP 大小要算進 initramfs、或開機卡在掛載 root 之前、不知道 initramfs 是什麼時讀 — 開機初期掛真 root 之前的臨時根檔系統">initramfs</a>（開機初期把真正的 root 掛起來之前、用來載入驅動的小型臨時根檔系統）。一個 kernel 加上它的 initramfs（含 fallback）大約一兩百 MB，再加上 FAT32 ESP 約 260 MiB 的實務下限，512 MiB 是在下限之上留餘裕。會需要更大的情境是你要同時保留多個 kernel 版本——但單 kernel 的 VM 用不到，給太大只是浪費。</p>
<h3 id="swap">Swap</h3>
<p>Swap 是記憶體不足時的安全墊，大小取決於這台機器的記憶體壓力型態，不是一個固定公式。對一台只有 4 GB RAM、且要在上面從原始碼編譯套件的 VM，編譯瞬間的記憶體尖峰很容易把實體記憶體吃爆、觸發 OOM 把進程殺掉。給 2 GB swap 當緩衝，擋住這種尖峰、避免安裝跑到一半被中斷。</p>
<p>swap 分區的磁碟成本不是一次付清的。<code>mkswap</code> 只寫一個 header，實際沒被換出的頁不會佔用宿主磁碟空間（在稀疏配置的虛擬磁碟上尤其明顯），用到才寫。所以「為了保險多給一點 swap」的代價，比直覺以為的小。判讀軸是看工作負載：會編譯、會跑吃記憶體的服務、RAM 又緊，就給足 swap；純文書、RAM 寬裕，小一點或不給都行。</p>
<p>swap 還有形態的選擇，這裡用分區 swap 是因為在安裝程式階段一併切好最省事，不代表它比另兩種好。swapfile（一個檔案，事後可隨時調大小或移除）避開了「分割最不可逆」的痛點；zram（壓縮記憶體 swap，不碰磁碟）對低 RAM 加編譯尖峰正是設計情境，現代發行版很多預設用它。換句話說，2 GB 這個量是看編譯尖峰定的，而「切成分區」只是配合安裝當下一次到位——若你跳過安裝期的 swap、事後用 swapfile 或 zram 補，是等價的可逆路徑。</p>
<h3 id="檔案系統">檔案系統</h3>
<p>檔案系統的選擇是在「簡單可靠」與「進階功能」之間取捨，預設往簡單那邊靠。<code>ext4</code> 簡單、穩、在各平台行為一致、修復工具成熟，對一台只要求「可靠地存取檔案」的機器是零驚喜的選擇。<code>btrfs</code> 提供快照、subvolume、透明壓縮，但代價是要規劃 subvolume 佈局、還要理解它寫時複製（CoW）的一些行為差異；這些功能在你會用快照回滾的主力機上很有價值，在演練 VM 上是雜訊。<code>xfs</code> 同樣穩定，但對這類用途相對 ext4 沒有決定性優勢。更特定的 <code>zfs</code>、<code>f2fs</code> 不在一般 VM 的考慮範圍——zfs 在 Arch / ARM 上是非主線 kernel 的 out-of-tree 模組、授權與維護成本高，f2fs 是為快閃裝置設計、VM 用不到。</p>
<p>判讀軸是你會不會用到進階功能。會固定用快照回滾系統狀態——選 btrfs 並接受它的佈局複雜度；只是要一個可靠的檔案系統——ext4。快照這類能力跟下面的獨立 <code>/home</code> 一樣，是真實機器的儲存規劃主題，值得另外深入，但別為了「聽起來比較強」就把它的複雜度帶進一台用完即丟的機器。</p>
<h3 id="獨立-home-vs-單一-root">獨立 /home vs 單一 root</h3>
<p>獨立 <code>/home</code> 分區的價值是「重灌系統不丟個人資料」，這是主力機的需求，不是每台機器都需要。把 <code>/home</code> 切成獨立分區，重裝系統時可以只格式化 root、保留 <code>/home</code> 裡的設定與檔案。一台用完即丟的演練 VM 沒有這個需求——它的整個生命週期就是裝起來、驗證、丟掉。如果你想跨多次實驗保留狀態，VM 情境更貼切的手段是宿主層的快照或共享資料夾，而不是在 guest 裡切獨立 <code>/home</code>——後者解決的是「重灌 OS 保資料」，不是「保留實驗狀態」。</p>
<p>把空間切兩半的隱性代價是失衡風險。假設總共十幾 GB，照預設切一塊給 <code>/</code>、剩下給 <code>/home</code>，所有系統套件（桌面全套依賴都裝在 <code>/usr</code>、算 <code>/</code>）擠在前者、容易先滿，而 <code>/home</code> 那邊空著——典型的一邊爆一邊空。把全部空間當一個池子用的單一 root 佈局，不會人為卡死任一邊，對不需要「重灌保資料」的機器最不容易出事。</p>
<h2 id="bootloader">Bootloader</h2>
<p>開機載入器決定韌體怎麼找到並載入 kernel，在虛擬機上選 GRUB 而非直接用 EFISTUB，理由是可靠性（韌體到 kernel 的整條交棒過程見 <a href="/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/" data-link-title="UEFI 開機鏈" data-link-desc="在 bootloader 選型（GRUB / EFISTUB / systemd-boot）卡住、或機器重開後找不到 kernel、需要理解韌體怎麼找到並載入系統時讀 — 韌體到 kernel 的交棒過程">UEFI 開機鏈卡</a>）。EFISTUB 讓 UEFI 韌體直接載入 kernel、不經過獨立的 bootloader，最精簡，但它完全依賴寫進 UEFI NVRAM（韌體用來存開機項的非揮發記憶體）的開機項。問題在於 QEMU 系的虛擬機（UTM 底層即是）對 EFI 變數的儲存有時不穩，一旦 NVRAM 裡的開機項掉了，韌體就找不到 kernel、機器開不了——這在 VM 環境是會實際踩到的坑。</p>
<p>GRUB 的容錯來自它（以 removable 模式安裝時）多寫了一份。除了 NVRAM 開機項，<code>grub-install --removable</code> 會在 ESP 的標準 fallback 路徑（aarch64 是 <code>\EFI\BOOT\BOOTAA64.EFI</code>）也放一份，就算 NVRAM 開機項丟了，韌體仍會從 fallback 路徑找到 GRUB；VM 環境的安裝程式通常以這個模式裝 GRUB，正是看上這層保險。它還附帶一個開機選單，當 kernel 或 initramfs 出問題時，可以進選單救援、加開機參數除錯——演練時的容錯空間大很多。</p>
<p>判讀軸是環境的 NVRAM 可靠度。在 NVRAM 穩定的實體機上，EFISTUB 的極簡是漂亮的選擇；在 NVRAM 可能不穩的 VM 上，可靠性優先，GRUB 的「多寫一份 fallback + 救援選單」更穩妥。GRUB 不是唯一的可靠選擇——systemd-boot 同樣是有開機選單、能裝到 fallback 路徑的獨立 bootloader，又比 GRUB 輕，在 VM / 單系統同樣站得住；這裡落在 GRUB 是因為 archboot 安裝程式預設以 removable 模式裝它，不是 GRUB 獨佔可靠性。GRUB 自己的設定檔在 VM 上用預設值即可，不需要額外的 kernel 參數。</p>
<h2 id="下一步">下一步</h2>
<p>選項選完、系統裝好、重開機進得去之後，先別急著開始用——「裝好了」跟「能用了」之間往往還缺一截：這台最小系統不一定有你需要的基本工具。<a href="../minimal-install-verify/">最小安裝後的工具驗證與補足</a> 就是在補那一截。</p>
<p>從安裝到桌面就緒的完整依賴順序，見 <a href="/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">模組零的操作順序指引</a>；本篇是它「安裝作業系統」那一步的展開。</p>
]]></content:encoded></item><item><title>常見故障場景與恢復操作</title><link>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/</guid><description>&lt;p>這篇按故障場景組織，每個場景列出症狀、原因、恢復步驟和預防措施。出問題時按症狀找到對應場景，照步驟操作。&lt;/p>
&lt;h2 id="場景一hyprland-compositor-crash">場景一：Hyprland compositor crash&lt;/h2>
&lt;p>&lt;strong>症狀&lt;/strong>：所有視窗同時消失，螢幕變黑或回到 TTY 登入畫面。滑鼠鍵盤有反應（可以切 TTY），但沒有桌面。&lt;/p>
&lt;p>&lt;strong>原因&lt;/strong>：Compositor process 遇到 fatal error 被 kernel 終止。常見觸發條件包括 plugin 相容性問題、特定 Wayland 協議操作觸發的 bug、GPU driver 回傳異常狀態。&lt;/p>
&lt;p>&lt;strong>恢復步驟&lt;/strong>：&lt;/p>
&lt;p>注意：以下步驟中 &lt;code>killall Hyprland&lt;/code> 或重啟 Hyprland 會終止所有由 compositor 管理的視窗，未存檔的工作會遺失。如果可能，先透過 TTY 或 SSH 嘗試存檔（如 &lt;code>kill -USR1 &amp;lt;pid&amp;gt;&lt;/code> 對支援的應用程式觸發存檔）。&lt;/p>
&lt;ol>
&lt;li>&lt;code>Ctrl+Alt+F2&lt;/code> 切到 TTY2&lt;/li>
&lt;li>用你的帳號登入&lt;/li>
&lt;li>檢查 Hyprland 的最後錯誤訊息：&lt;/li>
&lt;/ol>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 通用方式（不管 Hyprland 怎麼啟動都有效）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">journalctl -b &lt;span class="p">|&lt;/span> grep -i hypr &lt;span class="p">|&lt;/span> tail -30
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 如果 Hyprland 是 systemd user unit，可以更精準地查：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">journalctl --user -u hyprland -n &lt;span class="m">50&lt;/span> --no-pager&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="4">
&lt;li>重新啟動 Hyprland：&lt;/li>
&lt;/ol>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Hyprland&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="5">
&lt;li>如果反覆 crash，檢查最近改過的 config：&lt;/li>
&lt;/ol>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/.config/hypr
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">git diff &lt;span class="c1"># 如果 dotfile 有版控&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>預防&lt;/strong>：config 改動後用 &lt;code>hyprctl reload&lt;/code> 測試，不要直接重啟。啟用 plugin 前確認版本跟 Hyprland 版本相容。&lt;/p>
&lt;h2 id="場景二單一桌面工具掛了">場景二：單一桌面工具掛了&lt;/h2>
&lt;p>&lt;strong>症狀&lt;/strong>：狀態列（waybar）消失、啟動器（wofi/rofi）叫不出來、通知（mako/dunst）不跳了。桌面其他功能正常，視窗可以操作。&lt;/p>
&lt;p>&lt;strong>原因&lt;/strong>：這些工具各自是獨立的 process。掛了只影響自己的功能，不影響 compositor 或其他工具。常見原因是 config 語法錯誤（改完 config 後觸發）、記憶體洩漏（長時間運作後）、或外部服務連線異常（如 waybar 的某個 module 連不到系統匯流排）。&lt;/p>
&lt;p>&lt;strong>恢復步驟&lt;/strong>：&lt;/p>
&lt;p>判斷啟動方式：如果工具是在 Hyprland config 裡用 &lt;code>exec-once&lt;/code>（Hyprland 的自動啟動指令，compositor 啟動時執行一次）啟動的，用 &lt;code>killall&lt;/code> + 手動重啟；如果是 systemd user unit，用 &lt;code>systemctl --user&lt;/code>。&lt;/p>
&lt;p>&lt;code>exec-once&lt;/code> 啟動方式（多數 Hyprland 安裝的預設做法）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># waybar 掛了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">killall waybar&lt;span class="p">;&lt;/span> waybar &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># wofi 掛了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">killall wofi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># wofi 只在需要時啟動，不用常駐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># mako 掛了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">killall mako&lt;span class="p">;&lt;/span> mako &lt;span class="p">&amp;amp;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>systemd user unit 啟動方式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">systemctl --user restart waybar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">systemctl --user restart mako&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>確認工具是否在跑&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">pgrep waybar &lt;span class="c1"># 有輸出 = 在跑&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">pgrep mako &lt;span class="c1"># 沒輸出 = 沒在跑&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>預防&lt;/strong>：改 config 後重啟對應的工具確認語法正確。Waybar 的 config 是 JSON 格式，語法錯誤會導致它無法啟動——改完後先用 &lt;code>waybar&lt;/code> 前台跑一次看有沒有錯誤訊息。&lt;/p>
&lt;h2 id="場景二點五鎖屏卡死hyprlock-異常結束">場景二點五：鎖屏卡死（hyprlock 異常結束）&lt;/h2>
&lt;p>&lt;strong>症狀&lt;/strong>：鎖屏畫面消失但桌面沒回來，螢幕顯示 Hyprland 的失效保護訊息（「it looks like you locked your screen but the lockscreen app died」），或畫面全黑但系統有回應（SSH 能連、TTY 可能切得到也可能切不到）。&lt;/p>
&lt;p>&lt;strong>原因&lt;/strong>：鎖屏工具（Hyprlock、Swaylock）透過 Wayland 的 ext-session-lock 協議向 compositor 請求鎖定。鎖定狀態由 compositor 持有，唯一正常解鎖動作是鎖屏 client 通過認證後呼叫 unlock_and_destroy。如果鎖屏 client 在持鎖狀態下被殺（&lt;code>pkill&lt;/code>、crash），compositor 沒收到認證信號，會維持鎖定並顯示失效保護畫面。這跟殺 waybar/mako 不同——那些是普通 process，殺了重啟就好；鎖屏 client 持有安全狀態，殺了反而卡住。&lt;/p></description><content:encoded><![CDATA[<p>這篇按故障場景組織，每個場景列出症狀、原因、恢復步驟和預防措施。出問題時按症狀找到對應場景，照步驟操作。</p>
<h2 id="場景一hyprland-compositor-crash">場景一：Hyprland compositor crash</h2>
<p><strong>症狀</strong>：所有視窗同時消失，螢幕變黑或回到 TTY 登入畫面。滑鼠鍵盤有反應（可以切 TTY），但沒有桌面。</p>
<p><strong>原因</strong>：Compositor process 遇到 fatal error 被 kernel 終止。常見觸發條件包括 plugin 相容性問題、特定 Wayland 協議操作觸發的 bug、GPU driver 回傳異常狀態。</p>
<p><strong>恢復步驟</strong>：</p>
<p>注意：以下步驟中 <code>killall Hyprland</code> 或重啟 Hyprland 會終止所有由 compositor 管理的視窗，未存檔的工作會遺失。如果可能，先透過 TTY 或 SSH 嘗試存檔（如 <code>kill -USR1 &lt;pid&gt;</code> 對支援的應用程式觸發存檔）。</p>
<ol>
<li><code>Ctrl+Alt+F2</code> 切到 TTY2</li>
<li>用你的帳號登入</li>
<li>檢查 Hyprland 的最後錯誤訊息：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 通用方式（不管 Hyprland 怎麼啟動都有效）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">journalctl -b <span class="p">|</span> grep -i hypr <span class="p">|</span> tail -30
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 如果 Hyprland 是 systemd user unit，可以更精準地查：</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">journalctl --user -u hyprland -n <span class="m">50</span> --no-pager</span></span></code></pre></div><ol start="4">
<li>重新啟動 Hyprland：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">Hyprland</span></span></code></pre></div><ol start="5">
<li>如果反覆 crash，檢查最近改過的 config：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">cd</span> ~/.config/hypr
</span></span><span class="line"><span class="ln">2</span><span class="cl">git diff  <span class="c1"># 如果 dotfile 有版控</span></span></span></code></pre></div><p><strong>預防</strong>：config 改動後用 <code>hyprctl reload</code> 測試，不要直接重啟。啟用 plugin 前確認版本跟 Hyprland 版本相容。</p>
<h2 id="場景二單一桌面工具掛了">場景二：單一桌面工具掛了</h2>
<p><strong>症狀</strong>：狀態列（waybar）消失、啟動器（wofi/rofi）叫不出來、通知（mako/dunst）不跳了。桌面其他功能正常，視窗可以操作。</p>
<p><strong>原因</strong>：這些工具各自是獨立的 process。掛了只影響自己的功能，不影響 compositor 或其他工具。常見原因是 config 語法錯誤（改完 config 後觸發）、記憶體洩漏（長時間運作後）、或外部服務連線異常（如 waybar 的某個 module 連不到系統匯流排）。</p>
<p><strong>恢復步驟</strong>：</p>
<p>判斷啟動方式：如果工具是在 Hyprland config 裡用 <code>exec-once</code>（Hyprland 的自動啟動指令，compositor 啟動時執行一次）啟動的，用 <code>killall</code> + 手動重啟；如果是 systemd user unit，用 <code>systemctl --user</code>。</p>
<p><code>exec-once</code> 啟動方式（多數 Hyprland 安裝的預設做法）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># waybar 掛了</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">killall waybar<span class="p">;</span> waybar <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># wofi 掛了</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">killall wofi
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># wofi 只在需要時啟動，不用常駐</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># mako 掛了</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">killall mako<span class="p">;</span> mako <span class="p">&amp;</span></span></span></code></pre></div><p>systemd user unit 啟動方式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">systemctl --user restart waybar
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl --user restart mako</span></span></code></pre></div><p><strong>確認工具是否在跑</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pgrep waybar  <span class="c1"># 有輸出 = 在跑</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pgrep mako    <span class="c1"># 沒輸出 = 沒在跑</span></span></span></code></pre></div><p><strong>預防</strong>：改 config 後重啟對應的工具確認語法正確。Waybar 的 config 是 JSON 格式，語法錯誤會導致它無法啟動——改完後先用 <code>waybar</code> 前台跑一次看有沒有錯誤訊息。</p>
<h2 id="場景二點五鎖屏卡死hyprlock-異常結束">場景二點五：鎖屏卡死（hyprlock 異常結束）</h2>
<p><strong>症狀</strong>：鎖屏畫面消失但桌面沒回來，螢幕顯示 Hyprland 的失效保護訊息（「it looks like you locked your screen but the lockscreen app died」），或畫面全黑但系統有回應（SSH 能連、TTY 可能切得到也可能切不到）。</p>
<p><strong>原因</strong>：鎖屏工具（Hyprlock、Swaylock）透過 Wayland 的 ext-session-lock 協議向 compositor 請求鎖定。鎖定狀態由 compositor 持有，唯一正常解鎖動作是鎖屏 client 通過認證後呼叫 unlock_and_destroy。如果鎖屏 client 在持鎖狀態下被殺（<code>pkill</code>、crash），compositor 沒收到認證信號，會維持鎖定並顯示失效保護畫面。這跟殺 waybar/mako 不同——那些是普通 process，殺了重啟就好；鎖屏 client 持有安全狀態，殺了反而卡住。</p>
<p><strong>恢復步驟</strong>：</p>
<ol>
<li>嘗試切到另一個 TTY（<code>Ctrl+Alt+F2</code>）。注意：ext-session-lock 的安全語意允許 compositor 攔截 VT 切換快捷鍵，此時 TTY 切不過去，改用 SSH 從另一台機器連入</li>
<li>允許新的鎖屏 client 接管既有的鎖：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hyprctl --instance <span class="m">0</span> <span class="s1">&#39;keyword misc:allow_session_lock_restore 1&#39;</span></span></span></code></pre></div><ol start="3">
<li>重新拉一個鎖屏 client：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hyprctl --instance <span class="m">0</span> <span class="s1">&#39;dispatch exec hyprlock&#39;</span></span></span></code></pre></div><ol start="4">
<li>回到鎖屏畫面，用密碼正常解鎖</li>
</ol>
<p><strong>判讀</strong>：<code>loginctl show-session &lt;id&gt; -p LockedHint</code> 可能顯示 <code>LockedHint=no</code>（logind 層認為沒鎖），但畫面仍進不去——因為擋住畫面的是 compositor 的 ext-session-lock，跟 logind 的提示是獨立的兩層。判斷畫面鎖定狀態看 compositor 層，不看 logind。</p>
<p><strong>預防</strong>：測試鎖屏時備好恢復路徑（知道密碼、或預先開 SSH）。不要用殺 process 的方式結束鎖屏——要結束就走認證解鎖。自動化流程若會啟動鎖屏，把「需要人工解鎖」算進代價。鎖屏安全模型的完整說明見 <a href="/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock</a>。</p>
<h2 id="場景二點六桌面-shell-畫得出來但互動死掉進程活著卻-wedged">場景二點六：桌面 shell 畫得出來但互動死掉（進程活著卻 wedged）</h2>
<p><strong>症狀</strong>：bar / 狀態列還在螢幕上、看起來一切正常，但點它的按鈕（工作區切換、系統匣圖示）沒反應，keybind 叫不出啟動器（wofi / 內建 launcher）。同時焦點視窗（例如終端機）打字完全正常——鍵盤到得了應用程式，只是桌面 shell 的互動死了。</p>
<p><strong>原因</strong>：這是跟場景二（工具掛了）不同的一類故障，關鍵差別在<strong>進程還活著</strong>。場景二是 process 崩潰退出（<code>pgrep</code> 沒輸出），殺了重啟就好；這裡的桌面 shell（如 caelestia / Quickshell）進程還在跑（<code>pgrep</code> 找得到、STAT 是正常的 <code>S</code>、在 <code>poll</code> 等事件、CPU 不高），但它內部的某個子系統初始化失敗了——常見是 QML scene 的某個物件因為上游錯誤沒建起來、變成 null，於是負責「keybind → 開抽屜」「bar 按鈕互動」的模組對 null 讀屬性、整條互動接線死掉。bar 之所以還畫得出來，是它還停在初始化失敗前那一幀的畫面：<strong>畫得出來不等於還活著</strong>，跟鎖屏那課（畫面有密碼框不等於真的在鎖）是同一個陷阱。</p>
<p>上游觸發常是渲染層。實測案例：VM 的 GPU 只提供到 GLSL 1.20，而 shell 的 shader 需要 GLES 100/300/330，pipeline 建不起來（log 狂噴 <code>Failed to build graphics pipeline state</code>），這次渲染失敗把 scene 初始化打斷，drawers 狀態物件變 null。</p>
<p><strong>診斷（別看 pgrep，讀 shell 自己的 log）</strong>：</p>
<p><code>pgrep</code> 在這裡會騙你——它回報「在跑」，但那不等於「在運作」。權威來源是 shell 自己的 log，而且這種 log 常常<strong>不在 journalctl、也不在你猜的路徑</strong>，要用該 shell 專屬的 log 指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># caelestia 的例子：用它自己的 CLI 印 shell log</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">caelestia shell -l 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">|</span> tail -40
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 看的是 QML 的 TypeError：對 null 讀屬性 = 那個子系統死了</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">#   scene: @modules/Shortcuts.qml: TypeError: Cannot read property &#39;launcher&#39; of null</span></span></span></code></pre></div><p>另一個活性探針是 shell 的 <strong>IPC 回不回真實狀態</strong>：正常時查抽屜列表會回傳名字，子系統死掉時回空——這比「進程在不在」精準得多：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 子系統活著 → 列出 bar/launcher/session…；死掉 → 回空</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">caelestia shell ipc call drawers list</span></span></code></pre></div><p><strong>恢復步驟</strong>：重啟 shell 讓 scene 重建。以 caelestia 為例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">caelestia shell -k     <span class="c1"># 殺掉卡住的 shell</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">caelestia shell -d     <span class="c1"># 重新啟動（detached）</span></span></span></code></pre></div><p><strong>驗證修好了，看子系統回來、不是看 pgrep</strong>：重啟後 process 一定在（<code>pgrep</code> 本來就會有），要確認的是接線恢復——<code>caelestia shell ipc call drawers list</code> 從「回空」變成列出真實抽屜名、log 不再噴 null 的 TypeError。這對應「重啟成功要驗子系統狀態、不是驗 process 存在」的通用紀律。</p>
<p><strong>判讀與其他場景的界線</strong>：<code>pgrep</code> 有輸出 + bar 畫得出來 → 別急著判「正常」；點不動 / keybind 死掉就是 wedged 的訊號，往 shell 自己的 log 查。這跟場景二（process 真的沒了、<code>pgrep</code> 空）、場景三（compositor 整個凍結、連終端機打字都不行）都不同——這裡 compositor 正常、焦點視窗鍵盤正常，只有 shell 的互動接線死。判「進程活著到底有沒有在運作」的通用招式，見 <a href="/blog/linux/debug/process-service-state-diagnosis/" data-link-title="程序、服務與狀態怎麼判" data-link-desc="要判斷一個程式活著沒、某個系統服務現在由誰提供、桌面 session 有沒有被鎖、或終端機多工器的 session 還在不在時，用對的權威來源而不是靠畫面或猜的名字">程序、服務與狀態怎麼判</a>。</p>
<p><strong>預防</strong>：留意 shell log 裡持續出現的 shader / 渲染 pipeline 錯誤——在 VM 或 GL 支援不足的環境，這類錯誤可能非致命地存在（shell 大致能用），但一次渲染失敗就可能打斷 scene 初始化、把互動接線弄死。VM 環境要確認 GPU 提供的 GL / GLSL 版本足夠（virtio-gpu 走 mesa/zink 提供 GL 3.3+），或在 shell 設定關掉需要高階 shader 的效果。</p>
<h2 id="場景三gpu-driver-hang畫面凍結">場景三：GPU driver hang（畫面凍結）</h2>
<p><strong>症狀</strong>：桌面畫面完全凍結——滑鼠不動、鍵盤不回應、<code>Ctrl+Alt+F2</code> 切 TTY 也沒反應或延遲很久才回應。但如果從另一台機器 SSH 進來，系統是活的，process 都在跑。</p>
<p><strong>原因</strong>：GPU driver 進入異常狀態。NVIDIA 閉源驅動在 Linux 上的穩定性不如 AMD 開源驅動（amdgpu），特別是在 Wayland 環境下。常見觸發條件包括 suspend/resume 之後 GPU 沒正確恢復、某些 OpenGL/Vulkan 操作觸發 driver bug、顯示輸出切換（接上或拔掉外接螢幕）。</p>
<p><strong>恢復步驟</strong>：</p>
<p>方法 A — 如果 TTY 能切過去：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 切到 TTY2</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">Ctrl+Alt+F2
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 殺掉 Hyprland（它會帶走所有視窗）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">killall Hyprland
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 重新啟動</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">Hyprland</span></span></code></pre></div><p>方法 B — 如果 TTY 也凍結、但 SSH 能連：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 從另一台機器 SSH 進來（需事先知道 IP，見下方預防段）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ssh user@machine-ip
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 殺掉 compositor</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">killall Hyprland
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 如果需要 reset GPU（NVIDIA，且 driver 仍回應）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 前提：所有使用 GPU 的 process 已停止（compositor 已 kill）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">sudo nvidia-smi --gpu-reset
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 切回機器前面重啟 Hyprland</span></span></span></code></pre></div><p>方法 C — 如果完全無回應，先嘗試 Magic SysRq：</p>
<p>Magic SysRq 是 kernel 層級的緊急操作介面，即使 userspace 完全卡死也能回應。按住 <code>Alt+SysRq</code>（筆電通常是 <code>Alt+Fn+SysRq</code>），然後依序按 <code>R E I S U B</code>（每個鍵間隔幾秒）：</p>
<ul>
<li><code>R</code> — 把鍵盤從 raw mode 搶回來</li>
<li><code>E</code> — 對所有 process 送 SIGTERM</li>
<li><code>I</code> — 對所有 process 送 SIGKILL</li>
<li><code>S</code> — sync 所有檔案系統</li>
<li><code>U</code> — remount 所有檔案系統為 read-only</li>
<li><code>B</code> — 立即 reboot</li>
</ul>
<p>這比直接斷電安全——sync + unmount 步驟會盡量保護磁碟上的資料。Arch Linux 預設可能停用 SysRq，需在 <code>/etc/sysctl.d/</code> 設定 <code>kernel.sysrq=1</code> 啟用。</p>
<p>方法 D — 如果 SysRq 也無效，按住電源鍵強制關機：</p>
<p>這是最後手段。Linux 的 ext4/btrfs 檔案系統有 journal 保護，強制關機通常不會損壞<strong>檔案系統結構</strong>。但 journal 保護的是 metadata 一致性，正在寫入的使用者資料（未存檔的文件、正在下載的檔案）仍然可能遺失或損壞。重開機後正常登入 TTY、啟動 Hyprland 即可。如果開機過程有異常，用 <code>journalctl -b -1 -p err</code> 查看上次開機的錯誤訊息，確認是否有檔案系統修復紀錄。</p>
<p><strong>預防</strong>：</p>
<ul>
<li>NVIDIA 用戶：關注 driver 版本的 release notes，已知有 Wayland 問題的版本避開</li>
<li>配置 suspend 後的 GPU 恢復：在 Hyprland config 或 systemd sleep hook 裡加入 GPU reset 操作</li>
<li>事先記錄機器的 IP 位址（<code>ip addr show</code>）或設定固定 hostname（如 mDNS 的 <code>machine.local</code>），桌面凍結時才有辦法從另一台機器 SSH 進來</li>
<li>考慮開啟 SSH server，出問題時可以遠端救援。開啟後應配置 key-based authentication 並停用密碼登入（<code>PasswordAuthentication no</code>），避免在網路上暴露密碼登入通道：</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl <span class="nb">enable</span> sshd
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo systemctl start sshd
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 安全配置：停用密碼登入（確保已設好 SSH key 再改）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 編輯 /etc/ssh/sshd_config，設定 PasswordAuthentication no</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 然後 sudo systemctl restart sshd</span></span></span></code></pre></div><h2 id="場景四記憶體耗盡oom">場景四：記憶體耗盡（OOM）</h2>
<p><strong>症狀</strong>：系統變得極慢，操作有明顯延遲（幾秒到幾十秒）。隨後可能某些 process 突然被殺掉——瀏覽器分頁消失、IDE 視窗關閉，嚴重時 Hyprland 本身被 OOM Killer 終止導致桌面消失。</p>
<p><strong>原因</strong>：實體記憶體和 swap 都用完了。常見觸發者是瀏覽器（Chrome/Firefox 的分頁越開越多）、IDE（大型專案的 language server）、Docker container、或應用程式的記憶體洩漏。</p>
<p><strong>恢復步驟</strong>：</p>
<p>如果還能操作：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 找出誰在吃記憶體</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">top -o %MEM
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 或用 htop/btop 的互動介面</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 殺掉佔最多記憶體的 process</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">kill</span> &lt;PID&gt;</span></span></code></pre></div><p>如果桌面已經被殺、在 TTY 裡：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 看 OOM Killer 殺了誰</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">journalctl -b <span class="p">|</span> grep -i <span class="s2">&#34;out of memory&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">journalctl -b <span class="p">|</span> grep -i <span class="s2">&#34;oom&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 清理完後重啟桌面</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">Hyprland</span></span></code></pre></div><p><strong>預防</strong>：</p>
<p>設定 swap（即使 RAM 夠大，swap 提供 OOM 前的緩衝時間讓你有機會手動清理 process）。RAM 16GB 以上的機器，2-4GB swap 作緩衝通常足夠：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 查看是否有 swap</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">swapon --show
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 如果沒有，建立一個 4GB 的 swap file（ext4 檔案系統）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">sudo fallocate -l 4G /swapfile
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">sudo chmod <span class="m">600</span> /swapfile
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">sudo mkswap /swapfile
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">sudo swapon /swapfile
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 永久生效：加入 /etc/fstab</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;/swapfile none swap defaults 0 0&#39;</span> <span class="p">|</span> sudo tee -a /etc/fstab</span></span></code></pre></div><p>Btrfs 檔案系統不支援 <code>fallocate</code> 建立 swap file。Btrfs 用戶需改用 <code>btrfs filesystem mkswapfile</code> 或建立專屬的 swap subvolume，具體做法參考 Arch Wiki 的 Btrfs swap 段落。</p>
<p>啟用 systemd-oomd（比 kernel OOM Killer 更早介入、更可控）。systemd-oomd 在 cgroup 的記憶體壓力達到閾值時就開始清理，預設配置對多數桌面場景足夠。進階調整可透過 <code>/etc/systemd/oomd.conf</code> 設定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl <span class="nb">enable</span> systemd-oomd
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo systemctl start systemd-oomd</span></span></code></pre></div><h2 id="場景五config-寫錯導致-hyprland-啟動失敗">場景五：Config 寫錯導致 Hyprland 啟動失敗</h2>
<p><strong>症狀</strong>：從 display manager（圖形登入畫面，如 SDDM、GDM）登入後立刻黑屏又回到登入畫面，或直接回到 TTY。如果從 TTY 手動執行 <code>Hyprland</code>，看到錯誤訊息後立即退出。</p>
<p><strong>原因</strong>：Hyprland config 有語法錯誤或引用了不存在的資源。常見錯誤包括 <code>source</code> 指定的檔案不存在、keybind 語法寫錯、monitor 設定格式錯誤。</p>
<p><strong>恢復步驟</strong>：</p>
<ol>
<li>切到 TTY（<code>Ctrl+Alt+F2</code>）</li>
<li>登入後直接跑 Hyprland 看錯誤訊息：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 看 Hyprland 的啟動錯誤（也可用 journalctl -b | grep -i hypr）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">Hyprland
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># Hyprland 如果因 config 錯誤無法啟動，會直接印出錯誤訊息後退出</span></span></span></code></pre></div><ol start="3">
<li>根據錯誤訊息修改 config：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Hyprland 的主 config</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">vim ~/.config/hypr/hyprland.conf
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 如果用了 source 拆分，錯誤訊息會指出是哪個檔案</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">vim ~/.config/hypr/keybinds.conf</span></span></code></pre></div><ol start="4">
<li>修完後重新啟動：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">Hyprland</span></span></code></pre></div><p><strong>常見 config 錯誤</strong>：</p>
<p><code>source</code> 路徑錯誤——檔案不存在或路徑拼錯：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 確認 source 指定的檔案都存在</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep <span class="s2">&#34;^source&#34;</span> ~/.config/hypr/hyprland.conf
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 逐一檢查每個路徑</span></span></span></code></pre></div><p>Monitor 設定錯誤——指定了不存在的螢幕名稱：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 查看系統實際的螢幕名稱</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 在能進桌面時記下來，或用 wlr-randr</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">wlr-randr</span></span></code></pre></div><p>Keybind 語法錯誤——dispatcher 名稱拼錯或參數格式不對。Hyprland 的 keybind 格式是 <code>bind = MOD, key, dispatcher, params</code>，少一個欄位或 dispatcher 拼錯就會報錯。</p>
<p><strong>預防</strong>：config 改動時用 <code>hyprctl reload</code> 即時測試，不要改完 config 就直接重啟 Hyprland。如果 dotfile 用 Git 管理，改壞了可以 <code>git checkout</code> 回退。</p>
<h2 id="場景六suspendresume-後桌面異常">場景六：Suspend/resume 後桌面異常</h2>
<p><strong>症狀</strong>：筆電蓋上或手動 suspend 後喚醒，出現以下任一情況——螢幕黑屏但系統有反應（鍵盤背光亮）、解析度跑掉、多螢幕配置丟失（所有視窗擠到一個螢幕）、compositor 直接 crash 回到 TTY。</p>
<p><strong>原因</strong>：GPU driver 在 suspend/resume 過程中需要保存和恢復 GPU 狀態。NVIDIA 閉源驅動在 Wayland 上的 suspend/resume 支援不如 AMD 開源驅動穩定，特別是多螢幕配置和高刷新率模式下容易出問題。</p>
<p><strong>恢復步驟</strong>：</p>
<p>如果螢幕黑屏但系統有反應：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 切到 TTY</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">Ctrl+Alt+F2
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 檢查 Hyprland 是否還在跑</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">pgrep Hyprland
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 如果在跑但沒畫面，kill 再重啟</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">killall Hyprland
</span></span><span class="line"><span class="ln">9</span><span class="cl">Hyprland</span></span></code></pre></div><p>如果解析度或螢幕配置跑掉：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 在 Hyprland 內重新套用 monitor 設定</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">hyprctl reload</span></span></code></pre></div><p>如果 compositor 已經 crash：按場景一的步驟從 TTY 重啟。</p>
<p><strong>預防</strong>：</p>
<ul>
<li>NVIDIA 用戶：在 <code>/etc/modprobe.d/nvidia.conf</code> 啟用 preserve video memory allocations：</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># /etc/modprobe.d/nvidia.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">options nvidia <span class="nv">NVreg_PreserveVideoMemoryAllocations</span><span class="o">=</span><span class="m">1</span></span></span></code></pre></div><p>同時啟用 NVIDIA 的 suspend/resume systemd service：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl <span class="nb">enable</span> nvidia-suspend
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo systemctl <span class="nb">enable</span> nvidia-resume
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo systemctl <span class="nb">enable</span> nvidia-hibernate</span></span></code></pre></div><ul>
<li>AMD 用戶：amdgpu driver 的 suspend/resume 通常開箱即用，遇到問題先更新 kernel（<code>pacman -Syu linux</code>）。</li>
</ul>
]]></content:encoded></item><item><title>Bootstrap Script 與套件清單管理</title><link>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/</guid><description>&lt;p>一份 bootstrap script 是重建指令的入口。它做三件事：安裝套件、部署配置檔、執行初始化設定。&lt;/p>
&lt;h2 id="範例結構">範例結構&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="cp">#!/usr/bin/env bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="nb">set&lt;/span> -euo pipefail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="nv">DOTFILES_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>dirname &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$0&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">/..&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># --- 偵測 OS ---&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="nv">OS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname -s&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">install_packages&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$OS&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;Darwin&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nb">command&lt;/span> -v brew &amp;gt;/dev/null &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Installing Homebrew...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> /bin/bash -c &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> brew bundle --file&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">/Brewfile&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="o">[[&lt;/span> -f /etc/arch-release &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> sudo pacman -Syu --noconfirm
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> sudo pacman -S --needed - &amp;lt; &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">/packages.txt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="c1"># AUR 套件需要 AUR helper（假設已安裝 yay）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">command&lt;/span> -v yay &amp;gt;/dev/null &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">[[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">/aur-packages.txt&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> yay -S --needed - &amp;lt; &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">/aur-packages.txt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="o">[[&lt;/span> -f /etc/debian_version &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> sudo apt update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> xargs -a &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">/apt-packages.txt&amp;#34;&lt;/span> sudo apt install -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">deploy_configs&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> ! &lt;span class="nb">command&lt;/span> -v stow &amp;gt;/dev/null&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;stow not found, skipping config deployment&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">35&lt;/span>&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl"> &lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOTFILES_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">37&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> dir in zsh git nvim tmux hypr waybar&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">38&lt;/span>&lt;span class="cl"> &lt;span class="o">[[&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> stow -v --target&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">39&lt;/span>&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">40&lt;/span>&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">41&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">42&lt;/span>&lt;span class="cl">post_setup&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">43&lt;/span>&lt;span class="cl"> &lt;span class="c1"># 切換預設 shell&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">44&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SHELL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> !&lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">command&lt;/span> -v zsh&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">command&lt;/span> -v zsh &amp;gt;/dev/null&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">45&lt;/span>&lt;span class="cl"> chsh -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">command&lt;/span> -v zsh&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">46&lt;/span>&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">47&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">48&lt;/span>&lt;span class="cl"> &lt;span class="c1"># neovim plugin 安裝（headless 模式）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">49&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">command&lt;/span> -v nvim &amp;gt;/dev/null&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">50&lt;/span>&lt;span class="cl"> nvim --headless &lt;span class="s2">&amp;#34;+Lazy! sync&amp;#34;&lt;/span> +qa 2&amp;gt;/dev/null &lt;span class="o">||&lt;/span> &lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">51&lt;/span>&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">52&lt;/span>&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">53&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">54&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Installing packages ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">55&lt;/span>&lt;span class="cl">install_packages
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">56&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">57&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Deploying configs ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">58&lt;/span>&lt;span class="cl">deploy_configs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">59&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">60&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Post-setup ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">61&lt;/span>&lt;span class="cl">post_setup
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">62&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">63&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Done. Log out and back in for shell changes to take effect.&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="設計原則">設計原則&lt;/h2>
&lt;p>&lt;strong>冪等性&lt;/strong>是最重要的性質。跑一次和跑十次結果相同。&lt;code>pacman -S --needed&lt;/code> 只安裝缺少的套件、&lt;code>stow&lt;/code> 只建立不存在的 symlink、&lt;code>command -v&lt;/code> 在工具已存在時跳過安裝（用 &lt;code>command -v&lt;/code> 不用 &lt;code>which&lt;/code>——後者在最小系統可能不存在）。冪等的 script 可以放心重跑——改了一個配置後重新 deploy，不會弄壞其他已經正確的部分。&lt;/p></description><content:encoded><![CDATA[<p>一份 bootstrap script 是重建指令的入口。它做三件事：安裝套件、部署配置檔、執行初始化設定。</p>
<h2 id="範例結構">範例結構</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cp"></span><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">DOTFILES_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span><span class="nb">cd</span> <span class="s2">&#34;</span><span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="nv">$0</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">/..&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">pwd</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># --- 偵測 OS ---</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nv">OS</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>uname -s<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">install_packages<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$OS</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;Darwin&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nb">command</span> -v brew &gt;/dev/null <span class="o">||</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="nb">echo</span> <span class="s2">&#34;Installing Homebrew...&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            /bin/bash -c <span class="s2">&#34;</span><span class="k">$(</span>curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="o">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        brew bundle --file<span class="o">=</span><span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">/Brewfile&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">elif</span> <span class="o">[[</span> -f /etc/arch-release <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        sudo pacman -Syu --noconfirm
</span></span><span class="line"><span class="ln">19</span><span class="cl">        sudo pacman -S --needed - &lt; <span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">/packages.txt&#34;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="c1"># AUR 套件需要 AUR helper（假設已安裝 yay）</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="k">if</span> <span class="nb">command</span> -v yay &gt;/dev/null <span class="o">&amp;&amp;</span> <span class="o">[[</span> -f <span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">/aur-packages.txt&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">            yay -S --needed - &lt; <span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">/aur-packages.txt&#34;</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="k">fi</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="k">elif</span> <span class="o">[[</span> -f /etc/debian_version <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        sudo apt update
</span></span><span class="line"><span class="ln">27</span><span class="cl">        xargs -a <span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">/apt-packages.txt&#34;</span> sudo apt install -y
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">
</span></span><span class="line"><span class="ln">31</span><span class="cl">deploy_configs<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">    <span class="k">if</span> ! <span class="nb">command</span> -v stow &gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="nb">echo</span> <span class="s2">&#34;stow not found, skipping config deployment&#34;</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="nb">cd</span> <span class="s2">&#34;</span><span class="nv">$DOTFILES_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="k">for</span> dir in zsh git nvim tmux hypr waybar<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="o">[[</span> -d <span class="s2">&#34;</span><span class="nv">$dir</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> stow -v --target<span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$dir</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">    <span class="k">done</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">
</span></span><span class="line"><span class="ln">42</span><span class="cl">post_setup<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">    <span class="c1"># 切換預設 shell</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">    <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$SHELL</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">command</span> -v zsh<span class="k">)</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">command</span> -v zsh &gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">        chsh -s <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">command</span> -v zsh<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">
</span></span><span class="line"><span class="ln">48</span><span class="cl">    <span class="c1"># neovim plugin 安裝（headless 模式）</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">    <span class="k">if</span> <span class="nb">command</span> -v nvim &gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">        nvim --headless <span class="s2">&#34;+Lazy! sync&#34;</span> +qa 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">
</span></span><span class="line"><span class="ln">54</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;=== Installing packages ===&#34;</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">install_packages
</span></span><span class="line"><span class="ln">56</span><span class="cl">
</span></span><span class="line"><span class="ln">57</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;=== Deploying configs ===&#34;</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">deploy_configs
</span></span><span class="line"><span class="ln">59</span><span class="cl">
</span></span><span class="line"><span class="ln">60</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;=== Post-setup ===&#34;</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl">post_setup
</span></span><span class="line"><span class="ln">62</span><span class="cl">
</span></span><span class="line"><span class="ln">63</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Done. Log out and back in for shell changes to take effect.&#34;</span></span></span></code></pre></div><h2 id="設計原則">設計原則</h2>
<p><strong>冪等性</strong>是最重要的性質。跑一次和跑十次結果相同。<code>pacman -S --needed</code> 只安裝缺少的套件、<code>stow</code> 只建立不存在的 symlink、<code>command -v</code> 在工具已存在時跳過安裝（用 <code>command -v</code> 不用 <code>which</code>——後者在最小系統可能不存在）。冪等的 script 可以放心重跑——改了一個配置後重新 deploy，不會弄壞其他已經正確的部分。</p>
<p><strong>失敗可診斷</strong>是這支範例為了聚焦結構而省略、但實際該有的性質。bootstrap 在陌生機器上失敗是常態，怎麼讓它在某一步掛掉時留下可定位的痕跡（全輸出 tee 落地 + ERR trap 點名出錯的行與指令），見 <a href="/blog/linux/install/observable-bootstrap/" data-link-title="可除錯的 bootstrap：把可觀測性內建進安裝腳本" data-link-desc="安裝腳本中途失敗卻只能對著終端機捲動瞎找原因、想在 bootstrap 設計階段就讓失敗可定位時回來讀">可除錯的 bootstrap</a>——那篇是這支腳本的「失敗時看得見」那一層。</p>
<p><strong>偵測 OS 分流</strong>處理的是跨平台差異。macOS 用 Homebrew、Arch 用 pacman、Debian 系用 apt——套件管理器不同、套件名稱有時也不同（macOS 的 <code>coreutils</code> 在 Linux 是預裝的）。分流邏輯集中在 bootstrap script 裡，配置檔本身盡量保持跨平台一致。</p>
<p><strong>最少依賴</strong>原則：script 本身只依賴 bash 和 curl（幾乎所有系統預裝），其他工具由 script 自己安裝。這確保你可以在一台只有 base system 的機器上直接跑 script。不過「base system 直接跑」有個前提——最小安裝可能連 <code>sudo</code> 都沒有，而 script 裝套件正要靠它。跑這支 script 之前該驗證並補齊的前置工具，見 <a href="/blog/linux/install/minimal-install-verify/" data-link-title="最小安裝後的工具驗證與補足" data-link-desc="最小化安裝的 Linux 裝完發現連 sudo 或 which 都沒有、bootstrap 腳本第一行就炸、需要先確認系統缺哪些必要工具再補時回來讀">最小安裝後的工具驗證與補足</a>。</p>
<p><strong>交付完整可用的環境</strong>：script 的職責是讓部署完的配置「能直接用」，所以它必須裝齊配置實際引用的每一樣東西，而不是假設它們已經在。一個常見的破綻是把依賴寫進 README 的「dependencies」清單、卻沒在 script 裡實作安裝——例如 <code>.zshrc</code> 引用了 oh-my-zsh、主題、外掛，但 install script 只裝了 zsh 本身，結果 stow 部署完、第一次開 shell 就因為找不到那些東西而報錯。README 列依賴是給人看的、不會被執行；要讓配置真的能用，那些依賴得由 script 自己裝（例如把外掛 git clone 進對應位置）。檢查方式是反過來從配置出發：把每個 config 會 source 或引用的外部東西列出來，逐一確認 script 有沒有負責把它裝上。</p>
<p><strong>可部分執行</strong>的結構：拆成 function，允許只跑某一段。除錯時只想重新 deploy 配置、不想重裝套件，直接呼叫 <code>deploy_configs</code> 就好。進一步可以把每段拆成獨立 script（<code>scripts/install-packages.sh</code>、<code>scripts/deploy-configs.sh</code>），bootstrap 入口只是依序呼叫它們。</p>
<h2 id="套件清單管理">套件清單管理</h2>
<p>dotfile repo 管的是「配置」，但配置的前提是軟體已安裝。沒有附帶套件清單的 dotfile repo 是不完整的重建指令——你 clone 下來卻不知道該先裝什麼。</p>
<h3 id="macosbrewfile">macOS：Brewfile</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Brewfile</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">tap</span> <span class="s2">&#34;homebrew/cask-fonts&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># CLI 工具</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;git&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;neovim&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;tmux&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;stow&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;ripgrep&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;fd&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;fzf&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">brew</span> <span class="s2">&#34;zsh&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># GUI app</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">cask</span> <span class="s2">&#34;wezterm&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">cask</span> <span class="s2">&#34;rectangle&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">cask</span> <span class="s2">&#34;font-jetbrains-mono-nerd-font&#34;</span></span></span></code></pre></div><p><code>brew bundle dump</code> 從當前系統產生 Brewfile、<code>brew bundle</code> 照 Brewfile 安裝。Brewfile 區分三種來源：<code>brew</code>（CLI formula）、<code>cask</code>（GUI app）、<code>tap</code>（第三方 repo）。把 Brewfile 放在 dotfile repo 根目錄，bootstrap script 用 <code>brew bundle --file=./Brewfile</code> 安裝。</p>
<h3 id="arch-linuxpackagestxt">Arch Linux：packages.txt</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 匯出已安裝的 explicitly installed 套件</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pacman -Qqe &gt; packages.txt
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># AUR 套件另外記</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">pacman -Qqem &gt; aur-packages.txt</span></span></code></pre></div><p><code>-Qqe</code> 只列出使用者主動安裝的套件（不含被依賴自動拉進來的），這是你實際需要管理的範圍。<code>-Qqem</code> 進一步篩出外部來源（AUR）。還原時用 <code>pacman -S --needed - &lt; packages.txt</code>，<code>--needed</code> 跳過已安裝的。</p>
<h3 id="ubuntudebian">Ubuntu/Debian</h3>
<p>apt 的匯出格式比較雜。務實做法是手動維護一份清單檔（<code>apt-packages.txt</code>），每行一個套件名，用 <code>xargs -a apt-packages.txt sudo apt install -y</code> 安裝。比起 <code>apt list --installed</code> 的完整匯出（包含大量系統依賴），手動維護的清單更乾淨、更容易讀懂。</p>
<h3 id="為什麼套件清單要進-repo">為什麼套件清單要進 repo</h3>
<p>一個常見的失敗模式：dotfile repo 裡有完整的 neovim 配置，clone 到新機器後發現 neovim 沒裝、ripgrep 沒裝、字型沒裝，配置跑起來全是 error。套件清單跟配置檔放在同一個 repo，bootstrap script 才能先裝套件再 deploy 配置，形成完整的重建鏈路。</p>
]]></content:encoded></item><item><title>Dotfile 跟 Infra IaC 的平行關係</title><link>https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/dotfile-iac-parallel/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/dotfile-iac-parallel/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/infra/" data-link-title="Infra 基礎設施建置指南" data-link-desc="從零循序漸進把雲端基礎設施做起來 — IaC、身分憑證、網路地基、環境分離、核心服務、可觀測性、自動化 review 與治理習慣，含怎麼在組織內推動">Infra 基礎設施建置指南&lt;/a>教的是用 Terraform 或 OpenTofu 把雲端資源（VPC、IAM role、EC2 instance）寫成代碼，讓基礎設施可重現、可 review、可回滾。Dotfile 做的事在概念上完全平行：把個人工作環境（shell、editor、terminal、window manager）寫成代碼，達成同樣的可重現性。&lt;/p>
&lt;h2 id="共用的核心原則">共用的核心原則&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>宣告式&lt;/strong>：描述「環境應該長什麼樣」，而非「操作了哪些步驟」。Terraform 宣告「要有一個 VPC、CIDR 是 10.0.0.0/16」；dotfile 宣告「zsh 的 prompt 格式是這樣、alias ll 對應 ls -la」。&lt;/li>
&lt;li>&lt;strong>版控下的變更歷史&lt;/strong>：誰改了什麼、什麼時候改的、為什麼改，都在 Git log 裡。環境出問題時可以回溯到「上一次正常的狀態」是哪個 commit。&lt;/li>
&lt;li>&lt;strong>可 review&lt;/strong>：改了一個 shell function，diff 清楚可讀。跟在 terminal 裡直接 export 一個變數、下次重開就忘了相比，版控下的改動有跡可循。&lt;/li>
&lt;/ul>
&lt;h2 id="差異">差異&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Infra IaC&lt;/th>
 &lt;th>Dotfile&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>管理對象&lt;/td>
 &lt;td>組織的雲端資源&lt;/td>
 &lt;td>個人的工作桌面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>State 管理&lt;/td>
 &lt;td>Remote backend + lock 機制（防並行衝突）&lt;/td>
 &lt;td>通常只用 Git，沒有額外 state file&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>生效方式&lt;/td>
 &lt;td>&lt;code>terraform plan&lt;/code> → &lt;code>terraform apply&lt;/code> 兩步&lt;/td>
 &lt;td>多數改完 source 即生效，或重開 terminal 生效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>影響範圍&lt;/td>
 &lt;td>改錯可能影響 production 服務&lt;/td>
 &lt;td>改錯最多影響自己的工作環境&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>協作需求&lt;/td>
 &lt;td>團隊共用、需要 PR review&lt;/td>
 &lt;td>通常個人維護，PR review 是可選的&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這個平行不只是比喻。&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/" data-link-title="模組九：從個人到團隊" data-link-desc="個人 dotfile 管理的思想要延伸到團隊開發環境標準化時回來讀 — devcontainer、nix、商業環境配置管理">從個人到團隊&lt;/a>會教怎麼把 dotfile 的思想正式擴展到團隊環境——devcontainer 把「開發環境應該長什麼樣」寫成宣告式配置，讓新人 clone repo 就能拿到一致的開發環境，這正是 IaC 思想從組織 infra 往個人工作桌面延伸的具體產物。&lt;/p>
&lt;h2 id="dotfile-是重建指令不是備份">Dotfile 是重建指令，不是備份&lt;/h2>
&lt;p>這是最重要的心智模型區分。Dotfile repo 的目標不是「把舊電腦的所有檔案搬到新電腦」（那是備份工具的工作），而是「一份能在空白機器上重建工作環境的指令集」。&lt;/p>
&lt;p>這個思維跟 Docker 的哲學一致：Docker image 透過 Dockerfile「描述如何重建」環境，而不是「對一台跑著的伺服器拍快照」。Dotfile repo 也是——它記錄的是「你的環境應該長什麼樣」的宣告，不是「你的機器上現在有什麼」的快照。&lt;/p>
&lt;p>這個區分決定了 repo 裡該放什麼：&lt;/p>
&lt;ul>
&lt;li>放進去的：&lt;strong>宣告式的配置檔&lt;/strong>（shell config、editor config、WM config）、&lt;strong>套件清單&lt;/strong>（Brewfile、pacman list）、&lt;strong>安裝腳本&lt;/strong>（&lt;code>install.sh&lt;/code>，用來在新機器上自動化部署流程）。&lt;/li>
&lt;li>不放的：&lt;strong>暫存狀態&lt;/strong>（shell history、undo file、session file）、&lt;strong>generated 產物&lt;/strong>（plugin 的 compiled cache）、&lt;strong>大型二進位檔&lt;/strong>（字型檔案可以用套件管理器裝，不用放 repo）。&lt;/li>
&lt;/ul>
&lt;p>維持「重建指令」的純度，repo 才能保持輕量、diff 可讀、跨機器部署不會帶進不必要的狀態。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/infra/" data-link-title="Infra 基礎設施建置指南" data-link-desc="從零循序漸進把雲端基礎設施做起來 — IaC、身分憑證、網路地基、環境分離、核心服務、可觀測性、自動化 review 與治理習慣，含怎麼在組織內推動">Infra 基礎設施建置指南</a>教的是用 Terraform 或 OpenTofu 把雲端資源（VPC、IAM role、EC2 instance）寫成代碼，讓基礎設施可重現、可 review、可回滾。Dotfile 做的事在概念上完全平行：把個人工作環境（shell、editor、terminal、window manager）寫成代碼，達成同樣的可重現性。</p>
<h2 id="共用的核心原則">共用的核心原則</h2>
<ul>
<li><strong>宣告式</strong>：描述「環境應該長什麼樣」，而非「操作了哪些步驟」。Terraform 宣告「要有一個 VPC、CIDR 是 10.0.0.0/16」；dotfile 宣告「zsh 的 prompt 格式是這樣、alias ll 對應 ls -la」。</li>
<li><strong>版控下的變更歷史</strong>：誰改了什麼、什麼時候改的、為什麼改，都在 Git log 裡。環境出問題時可以回溯到「上一次正常的狀態」是哪個 commit。</li>
<li><strong>可 review</strong>：改了一個 shell function，diff 清楚可讀。跟在 terminal 裡直接 export 一個變數、下次重開就忘了相比，版控下的改動有跡可循。</li>
</ul>
<h2 id="差異">差異</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Infra IaC</th>
          <th>Dotfile</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>管理對象</td>
          <td>組織的雲端資源</td>
          <td>個人的工作桌面</td>
      </tr>
      <tr>
          <td>State 管理</td>
          <td>Remote backend + lock 機制（防並行衝突）</td>
          <td>通常只用 Git，沒有額外 state file</td>
      </tr>
      <tr>
          <td>生效方式</td>
          <td><code>terraform plan</code> → <code>terraform apply</code> 兩步</td>
          <td>多數改完 source 即生效，或重開 terminal 生效</td>
      </tr>
      <tr>
          <td>影響範圍</td>
          <td>改錯可能影響 production 服務</td>
          <td>改錯最多影響自己的工作環境</td>
      </tr>
      <tr>
          <td>協作需求</td>
          <td>團隊共用、需要 PR review</td>
          <td>通常個人維護，PR review 是可選的</td>
      </tr>
  </tbody>
</table>
<p>這個平行不只是比喻。<a href="/blog/linux/dotfile/09-team-environment/" data-link-title="模組九：從個人到團隊" data-link-desc="個人 dotfile 管理的思想要延伸到團隊開發環境標準化時回來讀 — devcontainer、nix、商業環境配置管理">從個人到團隊</a>會教怎麼把 dotfile 的思想正式擴展到團隊環境——devcontainer 把「開發環境應該長什麼樣」寫成宣告式配置，讓新人 clone repo 就能拿到一致的開發環境，這正是 IaC 思想從組織 infra 往個人工作桌面延伸的具體產物。</p>
<h2 id="dotfile-是重建指令不是備份">Dotfile 是重建指令，不是備份</h2>
<p>這是最重要的心智模型區分。Dotfile repo 的目標不是「把舊電腦的所有檔案搬到新電腦」（那是備份工具的工作），而是「一份能在空白機器上重建工作環境的指令集」。</p>
<p>這個思維跟 Docker 的哲學一致：Docker image 透過 Dockerfile「描述如何重建」環境，而不是「對一台跑著的伺服器拍快照」。Dotfile repo 也是——它記錄的是「你的環境應該長什麼樣」的宣告，不是「你的機器上現在有什麼」的快照。</p>
<p>這個區分決定了 repo 裡該放什麼：</p>
<ul>
<li>放進去的：<strong>宣告式的配置檔</strong>（shell config、editor config、WM config）、<strong>套件清單</strong>（Brewfile、pacman list）、<strong>安裝腳本</strong>（<code>install.sh</code>，用來在新機器上自動化部署流程）。</li>
<li>不放的：<strong>暫存狀態</strong>（shell history、undo file、session file）、<strong>generated 產物</strong>（plugin 的 compiled cache）、<strong>大型二進位檔</strong>（字型檔案可以用套件管理器裝，不用放 repo）。</li>
</ul>
<p>維持「重建指令」的純度，repo 才能保持輕量、diff 可讀、跨機器部署不會帶進不必要的狀態。</p>
]]></content:encoded></item><item><title>GNU Stow</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/gnu-stow/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/gnu-stow/</guid><description>&lt;p>GNU Stow 是一個 symlink farm manager，原本設計給軟體安裝用（把 &lt;code>/usr/local/stow/program/&lt;/code> 下的檔案 symlink 到 &lt;code>/usr/local/&lt;/code>），在 dotfile 管理場景被借來做「把 repo 裡的配置檔 symlink 到家目錄」。&lt;/p>
&lt;h2 id="核心規則">核心規則&lt;/h2>
&lt;p>Stow 的核心規則只有一條：&lt;strong>package 目錄內的路徑結構，就是安裝後相對於目標目錄的路徑結構&lt;/strong>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">~/dotfiles/zsh/.zshrc → ~/.zshrc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">~/dotfiles/nvim/.config/nvim/ → ~/.config/nvim/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">~/dotfiles/git/.gitconfig → ~/.gitconfig&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個頂層目錄（&lt;code>zsh/&lt;/code>、&lt;code>nvim/&lt;/code>、&lt;code>git/&lt;/code>）是一個 stow package，可以獨立安裝或移除。&lt;/p>
&lt;h2 id="常用指令">常用指令&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/dotfiles
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 安裝：在目標目錄（預設 $HOME 的上一層，通常用 --target）建立 symlink&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">stow zsh &lt;span class="c1"># 安裝 zsh package&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">stow zsh git nvim tmux &lt;span class="c1"># 批次安裝多個 package&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">stow */ &lt;span class="c1"># 安裝所有 package&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 移除：刪除該 package 建立的 symlink&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">stow -D nvim
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># 重新安裝（移除 + 安裝）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">stow -R zsh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1"># 收養：如果目標位置已有檔案，--adopt 把它移進 repo 再建 symlink&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">stow --adopt zsh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>--adopt&lt;/code> 是首次把現有配置納入 dotfile 管理時的關鍵操作——它把家目錄的既有檔案「收養」進 repo（移動過去），然後建 symlink。之後 &lt;code>git diff&lt;/code> 就能看到 repo 版本跟原版的差異。&lt;/p>
&lt;h2 id="folding-與-unfolding">Folding 與 Unfolding&lt;/h2>
&lt;p>Stow 會自動判斷要 symlink 整個目錄還是逐一 symlink 檔案：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Folding&lt;/strong>：目標目錄不存在、或目錄內所有檔案都由同一個 package 管理 → symlink 整個目錄&lt;/li>
&lt;li>&lt;strong>Unfolding&lt;/strong>：目標目錄已有其他來源的檔案 → 展開成逐檔 symlink，保留既有檔案不受影響&lt;/li>
&lt;/ul>
&lt;p>這個機制讓多個 package 可以共存於同一個目標目錄（如 &lt;code>~/.config/&lt;/code>）。&lt;/p>
&lt;h2 id="限制">限制&lt;/h2>
&lt;ul>
&lt;li>只管 symlink 映射，不管套件安裝（套件由 Brewfile 或 packages.txt 處理）&lt;/li>
&lt;li>不管 file permission——需要 0600 的 secret 檔靠 symlink 繼承來源權限，無法在部署時自動 chmod&lt;/li>
&lt;li>沒有 template 機制——同一份配置在不同機器要不同內容時，需要在配置檔內用 shell 的 OS 判斷處理&lt;/li>
&lt;/ul>
&lt;p>完整選型比較見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/management-strategies/" data-link-title="管理策略與選型" data-link-desc="要選 dotfile 管理工具時回來讀 — bare repo、stow、chezmoi 的適用場景與選型判讀">管理策略與選型&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>GNU Stow 是一個 symlink farm manager，原本設計給軟體安裝用（把 <code>/usr/local/stow/program/</code> 下的檔案 symlink 到 <code>/usr/local/</code>），在 dotfile 管理場景被借來做「把 repo 裡的配置檔 symlink 到家目錄」。</p>
<h2 id="核心規則">核心規則</h2>
<p>Stow 的核心規則只有一條：<strong>package 目錄內的路徑結構，就是安裝後相對於目標目錄的路徑結構</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">~/dotfiles/zsh/.zshrc          → ~/.zshrc
</span></span><span class="line"><span class="ln">2</span><span class="cl">~/dotfiles/nvim/.config/nvim/  → ~/.config/nvim/
</span></span><span class="line"><span class="ln">3</span><span class="cl">~/dotfiles/git/.gitconfig      → ~/.gitconfig</span></span></code></pre></div><p>每個頂層目錄（<code>zsh/</code>、<code>nvim/</code>、<code>git/</code>）是一個 stow package，可以獨立安裝或移除。</p>
<h2 id="常用指令">常用指令</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nb">cd</span> ~/dotfiles
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 安裝：在目標目錄（預設 $HOME 的上一層，通常用 --target）建立 symlink</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">stow zsh                <span class="c1"># 安裝 zsh package</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">stow zsh git nvim tmux  <span class="c1"># 批次安裝多個 package</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">stow */                 <span class="c1"># 安裝所有 package</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 移除：刪除該 package 建立的 symlink</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">stow -D nvim
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 重新安裝（移除 + 安裝）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">stow -R zsh
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 收養：如果目標位置已有檔案，--adopt 把它移進 repo 再建 symlink</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">stow --adopt zsh</span></span></code></pre></div><p><code>--adopt</code> 是首次把現有配置納入 dotfile 管理時的關鍵操作——它把家目錄的既有檔案「收養」進 repo（移動過去），然後建 symlink。之後 <code>git diff</code> 就能看到 repo 版本跟原版的差異。</p>
<h2 id="folding-與-unfolding">Folding 與 Unfolding</h2>
<p>Stow 會自動判斷要 symlink 整個目錄還是逐一 symlink 檔案：</p>
<ul>
<li><strong>Folding</strong>：目標目錄不存在、或目錄內所有檔案都由同一個 package 管理 → symlink 整個目錄</li>
<li><strong>Unfolding</strong>：目標目錄已有其他來源的檔案 → 展開成逐檔 symlink，保留既有檔案不受影響</li>
</ul>
<p>這個機制讓多個 package 可以共存於同一個目標目錄（如 <code>~/.config/</code>）。</p>
<h2 id="限制">限制</h2>
<ul>
<li>只管 symlink 映射，不管套件安裝（套件由 Brewfile 或 packages.txt 處理）</li>
<li>不管 file permission——需要 0600 的 secret 檔靠 symlink 繼承來源權限，無法在部署時自動 chmod</li>
<li>沒有 template 機制——同一份配置在不同機器要不同內容時，需要在配置檔內用 shell 的 OS 判斷處理</li>
</ul>
<p>完整選型比較見<a href="/blog/linux/dotfile/01-dotfile-management/management-strategies/" data-link-title="管理策略與選型" data-link-desc="要選 dotfile 管理工具時回來讀 — bare repo、stow、chezmoi 的適用場景與選型判讀">管理策略與選型</a>。</p>
]]></content:encoded></item><item><title>Hyprland 核心配置</title><link>https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/hyprland-core-config/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/hyprland-core-config/</guid><description>&lt;p>Hyprland v0.55+ 使用 Lua 作為配置語言。Lua 語法基礎見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/lua-scripting-language/" data-link-title="Lua 腳本語言" data-link-desc="在 Hyprland 或 Neovim 配置檔遇到 Lua 語法看不懂時回來讀 — 配置檔需要的最小 Lua 知識">Lua 腳本語言&lt;/a>。&lt;/p>
&lt;h2 id="配置檔位置與格式">配置檔位置與格式&lt;/h2>
&lt;p>Hyprland v0.55 起，配置格式從 hyprlang（&lt;code>.conf&lt;/code>）遷移到 &lt;strong>Lua&lt;/strong>（&lt;code>.lua&lt;/code>）。主配置檔是 &lt;code>~/.config/hypr/hyprland.lua&lt;/code>。如果 &lt;code>.lua&lt;/code> 不存在但 &lt;code>.conf&lt;/code> 存在，Hyprland 會回退讀取舊格式，但 hyprlang 已標記為棄用，後續版本會移除支援。&lt;/p>
&lt;p>修改後即時生效（不需要重新登入或重啟）。第一次啟動時如果沒有配置檔，Hyprland 會自動產生範例配置。即使配置有語法錯誤，緊急快捷鍵（SUPER+Q 開 terminal、SUPER+M 離開）仍然可用。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>[VM 可測試]&lt;/strong> 配置檔格式和載入行為在 VM 裡跟實機完全相同。&lt;/p>&lt;/blockquote>
&lt;h3 id="模組化拆分">模組化拆分&lt;/h3>
&lt;p>用 Lua 原生的 &lt;code>require()&lt;/code> 拆分配置：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- ~/.config/hypr/hyprland.lua — 主入口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;monitors&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- 載入 monitors.lua&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;keybinds&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- 載入 keybinds.lua&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="n">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;rules&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- 載入 rules.lua&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="n">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;autostart&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- 載入 autostart.lua&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="n">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;appearance&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- 載入 appearance.lua&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="n">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;env&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- 載入 env.lua&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>require(&amp;quot;monitors&amp;quot;)&lt;/code> 會從同目錄載入 &lt;code>monitors.lua&lt;/code>。拆分的理由是職責分離——改 keybind 不用在千行配置裡找位置，每個子檔案可以獨立 diff 和 rollback。&lt;/p>
&lt;h3 id="從舊格式遷移">從舊格式遷移&lt;/h3>
&lt;p>Hyprland 官方提供遷移工具：&lt;code>hyprlang2lua&lt;/code>（Go，也有瀏覽器版）和 &lt;code>hypr-migrate&lt;/code>。如果你是從零開始，直接用 Lua 格式。&lt;/p>
&lt;h2 id="monitor-設定">Monitor 設定&lt;/h2>
&lt;p>Monitor 設定是硬體相關的核心配置，每台機器都不同。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- ~/.config/hypr/monitors.lua&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n">hl.config&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="n">monitor&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- 語法：&amp;#34;name, resolution@refreshrate, position, scale&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;DP-1, 2560x1440@144, 0x0, 1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;HDMI-A-1, 1920x1080@60, 2560x0, 1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- 筆電內建螢幕&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;eDP-1, preferred, auto, 1.5&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- 預設規則（未明確列出的螢幕）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;, preferred, auto, 1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- 鏡像模式（DP-2 鏡像 DP-1 的畫面）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- &amp;#34;DP-2, preferred, auto, 1, mirror, DP-1&amp;#34;,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="p">})&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>position&lt;/code> 決定多螢幕的空間排列——&lt;code>0x0&lt;/code> 是左上角原點，&lt;code>2560x0&lt;/code> 表示在第一個螢幕的右邊。&lt;code>scale&lt;/code> 處理 HiDPI 顯示（1.5 表示 150% 縮放）。&lt;/p>
&lt;p>查詢可用的 monitor 名稱：&lt;code>hyprctl monitors&lt;/code>&lt;/p>
&lt;p>其他選項：&lt;/p>
&lt;ul>
&lt;li>&lt;code>transform, 1&lt;/code> — 旋轉（0=正常、1=90 度、2=180 度、3=270 度）&lt;/li>
&lt;li>&lt;code>vrr, 1&lt;/code> — 可變更新率（1=開啟、2=僅全螢幕）&lt;/li>
&lt;li>&lt;code>bitdepth, 10&lt;/code> — 10-bit 色彩&lt;/li>
&lt;/ul>
&lt;h3 id="筆電合蓋行為">筆電合蓋行為&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 合蓋時關閉內建螢幕、開蓋時恢復&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">-- bindl = 即使輸入鎖定也能觸發（合蓋時需要）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n">hl.bindl&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;switch:on:Lid Switch&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;exec&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;hyprctl keyword monitor &amp;#39;eDP-1, disable&amp;#39;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n">hl.bindl&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;switch:off:Lid Switch&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;exec&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;hyprctl keyword monitor &amp;#39;eDP-1, preferred, auto, 1&amp;#39;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>[需實機測試]&lt;/strong> Monitor 位置排列、HiDPI 縮放、VRR、旋轉、多螢幕熱插拔、合蓋行為。VM 通常只有一個虛擬螢幕。&lt;/p></description><content:encoded><![CDATA[<p>Hyprland v0.55+ 使用 Lua 作為配置語言。Lua 語法基礎見 <a href="/blog/linux/dotfile/knowledge-cards/lua-scripting-language/" data-link-title="Lua 腳本語言" data-link-desc="在 Hyprland 或 Neovim 配置檔遇到 Lua 語法看不懂時回來讀 — 配置檔需要的最小 Lua 知識">Lua 腳本語言</a>。</p>
<h2 id="配置檔位置與格式">配置檔位置與格式</h2>
<p>Hyprland v0.55 起，配置格式從 hyprlang（<code>.conf</code>）遷移到 <strong>Lua</strong>（<code>.lua</code>）。主配置檔是 <code>~/.config/hypr/hyprland.lua</code>。如果 <code>.lua</code> 不存在但 <code>.conf</code> 存在，Hyprland 會回退讀取舊格式，但 hyprlang 已標記為棄用，後續版本會移除支援。</p>
<p>修改後即時生效（不需要重新登入或重啟）。第一次啟動時如果沒有配置檔，Hyprland 會自動產生範例配置。即使配置有語法錯誤，緊急快捷鍵（SUPER+Q 開 terminal、SUPER+M 離開）仍然可用。</p>
<blockquote>
<p><strong>[VM 可測試]</strong> 配置檔格式和載入行為在 VM 裡跟實機完全相同。</p></blockquote>
<h3 id="模組化拆分">模組化拆分</h3>
<p>用 Lua 原生的 <code>require()</code> 拆分配置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- ~/.config/hypr/hyprland.lua — 主入口</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;monitors&#34;</span><span class="p">)</span>      <span class="c1">-- 載入 monitors.lua</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;keybinds&#34;</span><span class="p">)</span>      <span class="c1">-- 載入 keybinds.lua</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;rules&#34;</span><span class="p">)</span>         <span class="c1">-- 載入 rules.lua</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;autostart&#34;</span><span class="p">)</span>     <span class="c1">-- 載入 autostart.lua</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;appearance&#34;</span><span class="p">)</span>    <span class="c1">-- 載入 appearance.lua</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">require</span><span class="p">(</span><span class="s2">&#34;env&#34;</span><span class="p">)</span>           <span class="c1">-- 載入 env.lua</span></span></span></code></pre></div><p><code>require(&quot;monitors&quot;)</code> 會從同目錄載入 <code>monitors.lua</code>。拆分的理由是職責分離——改 keybind 不用在千行配置裡找位置，每個子檔案可以獨立 diff 和 rollback。</p>
<h3 id="從舊格式遷移">從舊格式遷移</h3>
<p>Hyprland 官方提供遷移工具：<code>hyprlang2lua</code>（Go，也有瀏覽器版）和 <code>hypr-migrate</code>。如果你是從零開始，直接用 Lua 格式。</p>
<h2 id="monitor-設定">Monitor 設定</h2>
<p>Monitor 設定是硬體相關的核心配置，每台機器都不同。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- ~/.config/hypr/monitors.lua</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">monitor</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="c1">-- 語法：&#34;name, resolution@refreshrate, position, scale&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="s2">&#34;DP-1, 2560x1440@144, 0x0, 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="s2">&#34;HDMI-A-1, 1920x1080@60, 2560x0, 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="c1">-- 筆電內建螢幕</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="s2">&#34;eDP-1, preferred, auto, 1.5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="c1">-- 預設規則（未明確列出的螢幕）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="s2">&#34;, preferred, auto, 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="c1">-- 鏡像模式（DP-2 鏡像 DP-1 的畫面）</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="c1">-- &#34;DP-2, preferred, auto, 1, mirror, DP-1&#34;,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p><code>position</code> 決定多螢幕的空間排列——<code>0x0</code> 是左上角原點，<code>2560x0</code> 表示在第一個螢幕的右邊。<code>scale</code> 處理 HiDPI 顯示（1.5 表示 150% 縮放）。</p>
<p>查詢可用的 monitor 名稱：<code>hyprctl monitors</code></p>
<p>其他選項：</p>
<ul>
<li><code>transform, 1</code> — 旋轉（0=正常、1=90 度、2=180 度、3=270 度）</li>
<li><code>vrr, 1</code> — 可變更新率（1=開啟、2=僅全螢幕）</li>
<li><code>bitdepth, 10</code> — 10-bit 色彩</li>
</ul>
<h3 id="筆電合蓋行為">筆電合蓋行為</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 合蓋時關閉內建螢幕、開蓋時恢復</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">-- bindl = 即使輸入鎖定也能觸發（合蓋時需要）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">hl.bindl</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;switch:on:Lid Switch&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;hyprctl keyword monitor &#39;eDP-1, disable&#39;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">hl.bindl</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;switch:off:Lid Switch&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;hyprctl keyword monitor &#39;eDP-1, preferred, auto, 1&#39;&#34;</span><span class="p">)</span></span></span></code></pre></div><blockquote>
<p><strong>[需實機測試]</strong> Monitor 位置排列、HiDPI 縮放、VRR、旋轉、多螢幕熱插拔、合蓋行為。VM 通常只有一個虛擬螢幕。</p></blockquote>
<h3 id="dotfile-管理策略">Dotfile 管理策略</h3>
<p>Monitor 設定是典型的「機器專屬」配置。如果用 chezmoi，可以用 template 依機器名稱切換；如果用 stow，可以把 <code>monitors.lua</code> 排除在 Git 外、每台機器手動寫。</p>
<h2 id="輸入裝置配置">輸入裝置配置</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- ~/.config/hypr/hyprland.lua 或拆到獨立檔案</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">input</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">kb_layout</span> <span class="o">=</span> <span class="s2">&#34;us&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="c1">-- 多語系：kb_layout = &#34;us,de&#34;,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="c1">-- 切換：kb_options = &#34;grp:alt_shift_toggle&#34;,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="c1">-- Caps Lock 當 Escape：kb_options = &#34;caps:escape&#34;,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">kb_options</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="n">follow_mouse</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>         <span class="c1">-- 焦點跟隨滑鼠</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">sensitivity</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>          <span class="c1">-- -1.0 到 1.0（0 = 不修改）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">accel_profile</span> <span class="o">=</span> <span class="s2">&#34;flat&#34;</span><span class="p">,</span>   <span class="c1">-- flat 或 adaptive</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="n">repeat_rate</span> <span class="o">=</span> <span class="mi">25</span><span class="p">,</span>         <span class="c1">-- 長按時每秒重複幾次</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="n">repeat_delay</span> <span class="o">=</span> <span class="mi">600</span><span class="p">,</span>       <span class="c1">-- 按住多久開始重複（毫秒）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">touchpad</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="n">natural_scroll</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">            <span class="n">disable_while_typing</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">            <span class="p">[</span><span class="s2">&#34;tap-to-click&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">            <span class="n">drag_lock</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">            <span class="n">scroll_factor</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>可用的 XKB options 查詢：<code>/usr/share/X11/xkb/rules/evdev.lst</code> 或 <code>localectl list-x11-keymap-options</code>。</p>
<blockquote>
<p><strong>[VM 可測試]</strong> 鍵盤 layout、repeat rate。<br>
<strong>[需實機測試]</strong> 觸控板設定、手勢、滑鼠靈敏度/加速曲線、多鍵盤配置。</p></blockquote>
<h2 id="keybind-設計">Keybind 設計</h2>
<h3 id="修飾鍵modifiersuper-是哪顆鍵">修飾鍵（Modifier）：<code>SUPER</code> 是哪顆鍵</h3>
<p>keybind 由「修飾鍵 + 按鍵」組成，Hyprland 配置裡最常當主修飾鍵的是 <code>SUPER</code>。<code>SUPER</code> 是 X11/Wayland 對 <strong>Meta 鍵</strong>的稱呼，在不同鍵盤上是不同的實體鍵：</p>
<ul>
<li><strong>PC 鍵盤</strong>：<strong>Windows 鍵</strong>（⊞，通常在 Ctrl 與 Alt 之間）。</li>
<li><strong>Mac 鍵盤</strong>：<strong>Command 鍵（⌘）</strong>。</li>
</ul>
<p>其他常見修飾鍵：<code>SHIFT</code>、<code>CTRL</code>（Control）、<code>ALT</code>（Mac 上是 Option ⌥）。組合寫法用空格串，例如 <code>&quot;SUPER SHIFT&quot;</code>。選 <code>SUPER</code> 當主修飾鍵是慣例——它幾乎不跟應用程式本身的快捷鍵衝突（應用多用 Ctrl / Alt）。</p>
<p>在 Mac 上跑虛擬機時，實體 ⌘ 會對應到 guest 的 <code>SUPER</code>，但 macOS 會先攔截部分 ⌘ 組合——這層 VM 專屬的坑見 <a href="../hyprland-vm-setup/">VM 環境設定</a>。</p>
<h3 id="bind-類型">Bind 類型</h3>
<p>Hyprland 提供多種 bind 函式，處理不同互動模式：</p>
<table>
  <thead>
      <tr>
          <th>函式</th>
          <th>行為</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>hl.bind()</code></td>
          <td>按一次觸發</td>
          <td>多數快捷鍵</td>
      </tr>
      <tr>
          <td><code>hl.binde()</code></td>
          <td>長按連續觸發</td>
          <td>調整視窗大小、音量</td>
      </tr>
      <tr>
          <td><code>hl.bindm()</code></td>
          <td>滑鼠綁定</td>
          <td>拖曳移動 / 調整視窗</td>
      </tr>
      <tr>
          <td><code>hl.bindr()</code></td>
          <td>放開時觸發</td>
          <td>切換模式</td>
      </tr>
      <tr>
          <td><code>hl.bindl()</code></td>
          <td>輸入鎖定也觸發</td>
          <td>合蓋開關、鎖屏時的媒體鍵</td>
      </tr>
      <tr>
          <td><code>hl.bindel()</code></td>
          <td>連續 + 鎖定</td>
          <td>鎖屏時的音量鍵（長按連續、鎖定可用）</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><strong>[VM 可測試]</strong> 所有 keybind 邏輯和配置語法。</p></blockquote>
<h3 id="基本操作">基本操作</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- ~/.config/hypr/keybinds.lua</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">-- 基本操作</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;Return&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;kitty&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;Q&#34;</span><span class="p">,</span> <span class="s2">&#34;killactive&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;D&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;wofi --show drun&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;F&#34;</span><span class="p">,</span> <span class="s2">&#34;fullscreen&#34;</span><span class="p">,</span> <span class="s2">&#34;0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER SHIFT&#34;</span><span class="p">,</span> <span class="s2">&#34;Space&#34;</span><span class="p">,</span> <span class="s2">&#34;togglefloating&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">-- 焦點移動（vim 風格）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;H&#34;</span><span class="p">,</span> <span class="s2">&#34;movefocus&#34;</span><span class="p">,</span> <span class="s2">&#34;l&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;J&#34;</span><span class="p">,</span> <span class="s2">&#34;movefocus&#34;</span><span class="p">,</span> <span class="s2">&#34;d&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;K&#34;</span><span class="p">,</span> <span class="s2">&#34;movefocus&#34;</span><span class="p">,</span> <span class="s2">&#34;u&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;L&#34;</span><span class="p">,</span> <span class="s2">&#34;movefocus&#34;</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1">-- 視窗移動</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER SHIFT&#34;</span><span class="p">,</span> <span class="s2">&#34;H&#34;</span><span class="p">,</span> <span class="s2">&#34;movewindow&#34;</span><span class="p">,</span> <span class="s2">&#34;l&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER SHIFT&#34;</span><span class="p">,</span> <span class="s2">&#34;J&#34;</span><span class="p">,</span> <span class="s2">&#34;movewindow&#34;</span><span class="p">,</span> <span class="s2">&#34;d&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER SHIFT&#34;</span><span class="p">,</span> <span class="s2">&#34;K&#34;</span><span class="p">,</span> <span class="s2">&#34;movewindow&#34;</span><span class="p">,</span> <span class="s2">&#34;u&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER SHIFT&#34;</span><span class="p">,</span> <span class="s2">&#34;L&#34;</span><span class="p">,</span> <span class="s2">&#34;movewindow&#34;</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1">-- 視窗大小調整（長按連續觸發）</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="n">hl.binde</span><span class="p">(</span><span class="s2">&#34;SUPER CTRL&#34;</span><span class="p">,</span> <span class="s2">&#34;H&#34;</span><span class="p">,</span> <span class="s2">&#34;resizeactive&#34;</span><span class="p">,</span> <span class="s2">&#34;-20 0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="n">hl.binde</span><span class="p">(</span><span class="s2">&#34;SUPER CTRL&#34;</span><span class="p">,</span> <span class="s2">&#34;J&#34;</span><span class="p">,</span> <span class="s2">&#34;resizeactive&#34;</span><span class="p">,</span> <span class="s2">&#34;0 20&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="n">hl.binde</span><span class="p">(</span><span class="s2">&#34;SUPER CTRL&#34;</span><span class="p">,</span> <span class="s2">&#34;K&#34;</span><span class="p">,</span> <span class="s2">&#34;resizeactive&#34;</span><span class="p">,</span> <span class="s2">&#34;0 -20&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="n">hl.binde</span><span class="p">(</span><span class="s2">&#34;SUPER CTRL&#34;</span><span class="p">,</span> <span class="s2">&#34;L&#34;</span><span class="p">,</span> <span class="s2">&#34;resizeactive&#34;</span><span class="p">,</span> <span class="s2">&#34;20 0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1">-- 滑鼠綁定：SUPER + 左鍵拖曳移動、右鍵拖曳調整大小</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="n">hl.bindm</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;mouse:272&#34;</span><span class="p">,</span> <span class="s2">&#34;movewindow&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="n">hl.bindm</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;mouse:273&#34;</span><span class="p">,</span> <span class="s2">&#34;resizewindow&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="c1">-- Workspace 切換</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;1&#34;</span><span class="p">,</span> <span class="s2">&#34;workspace&#34;</span><span class="p">,</span> <span class="s2">&#34;1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;2&#34;</span><span class="p">,</span> <span class="s2">&#34;workspace&#34;</span><span class="p">,</span> <span class="s2">&#34;2&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;3&#34;</span><span class="p">,</span> <span class="s2">&#34;workspace&#34;</span><span class="p">,</span> <span class="s2">&#34;3&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;4&#34;</span><span class="p">,</span> <span class="s2">&#34;workspace&#34;</span><span class="p">,</span> <span class="s2">&#34;4&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;5&#34;</span><span class="p">,</span> <span class="s2">&#34;workspace&#34;</span><span class="p">,</span> <span class="s2">&#34;5&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;6&#34;</span><span class="p">,</span> <span class="s2">&#34;workspace&#34;</span><span class="p">,</span> <span class="s2">&#34;6&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;7&#34;</span><span class="p">,</span> <span class="s2">&#34;workspace&#34;</span><span class="p">,</span> <span class="s2">&#34;7&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;8&#34;</span><span class="p">,</span> <span class="s2">&#34;workspace&#34;</span><span class="p">,</span> <span class="s2">&#34;8&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;9&#34;</span><span class="p">,</span> <span class="s2">&#34;workspace&#34;</span><span class="p">,</span> <span class="s2">&#34;9&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="c1">-- 把視窗送到指定 workspace</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER SHIFT&#34;</span><span class="p">,</span> <span class="s2">&#34;1&#34;</span><span class="p">,</span> <span class="s2">&#34;movetoworkspace&#34;</span><span class="p">,</span> <span class="s2">&#34;1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER SHIFT&#34;</span><span class="p">,</span> <span class="s2">&#34;2&#34;</span><span class="p">,</span> <span class="s2">&#34;movetoworkspace&#34;</span><span class="p">,</span> <span class="s2">&#34;2&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER SHIFT&#34;</span><span class="p">,</span> <span class="s2">&#34;3&#34;</span><span class="p">,</span> <span class="s2">&#34;movetoworkspace&#34;</span><span class="p">,</span> <span class="s2">&#34;3&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl"><span class="c1">-- 以此類推</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">
</span></span><span class="line"><span class="ln">49</span><span class="cl"><span class="c1">-- 螢幕截圖</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;Print&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;grimblast copy area&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;Print&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;grimblast copy output&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">
</span></span><span class="line"><span class="ln">53</span><span class="cl"><span class="c1">-- 多螢幕焦點切換</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;comma&#34;</span><span class="p">,</span> <span class="s2">&#34;focusmonitor&#34;</span><span class="p">,</span> <span class="s2">&#34;l&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;period&#34;</span><span class="p">,</span> <span class="s2">&#34;focusmonitor&#34;</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER SHIFT&#34;</span><span class="p">,</span> <span class="s2">&#34;comma&#34;</span><span class="p">,</span> <span class="s2">&#34;movewindow&#34;</span><span class="p">,</span> <span class="s2">&#34;mon:l&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER SHIFT&#34;</span><span class="p">,</span> <span class="s2">&#34;period&#34;</span><span class="p">,</span> <span class="s2">&#34;movewindow&#34;</span><span class="p">,</span> <span class="s2">&#34;mon:r&#34;</span><span class="p">)</span></span></span></code></pre></div><h3 id="設計原則">設計原則</h3>
<ul>
<li><strong>SUPER</strong>（Windows/Command 鍵）當 modifier，避免跟應用程式快捷鍵衝突</li>
<li><strong>方向操作</strong>統一用 vim 的 HJKL，降低記憶負擔</li>
<li><strong>modifier 分層</strong>：SUPER 是焦點、SUPER+SHIFT 是移動、SUPER+CTRL 是調整大小</li>
<li>常用操作（開 terminal、關視窗、切 workspace）放在最順手的位置</li>
</ul>
<h3 id="submap模態快捷鍵">Submap：模態快捷鍵</h3>
<p>Submap 類似 vim 的模式切換——進入某個 submap 後，快捷鍵的意義改變，直到離開。適合需要連續操作的場景（如調整大小）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 按 SUPER+R 進入 resize 模式</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;R&#34;</span><span class="p">,</span> <span class="s2">&#34;submap&#34;</span><span class="p">,</span> <span class="s2">&#34;resize&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">-- 定義 resize 模式的快捷鍵</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">hl.submap</span><span class="p">(</span><span class="s2">&#34;resize&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">hl.binde</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;H&#34;</span><span class="p">,</span> <span class="s2">&#34;resizeactive&#34;</span><span class="p">,</span> <span class="s2">&#34;-20 0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">hl.binde</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;L&#34;</span><span class="p">,</span> <span class="s2">&#34;resizeactive&#34;</span><span class="p">,</span> <span class="s2">&#34;20 0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">hl.binde</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;K&#34;</span><span class="p">,</span> <span class="s2">&#34;resizeactive&#34;</span><span class="p">,</span> <span class="s2">&#34;0 -20&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">hl.binde</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;J&#34;</span><span class="p">,</span> <span class="s2">&#34;resizeactive&#34;</span><span class="p">,</span> <span class="s2">&#34;0 20&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;escape&#34;</span><span class="p">,</span> <span class="s2">&#34;submap&#34;</span><span class="p">,</span> <span class="s2">&#34;reset&#34;</span><span class="p">)</span>   <span class="c1">-- Escape 離開</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">hl.submap</span><span class="p">(</span><span class="s2">&#34;reset&#34;</span><span class="p">)</span></span></span></code></pre></div><p>進入 resize 模式後，直接按 HJKL（不需要 modifier）就能持續調整大小，按 Escape 回到正常模式。</p>
<h3 id="媒體鍵與硬體鍵">媒體鍵與硬體鍵</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 音量（鎖屏時也能用、長按連續觸發）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">hl.bindel</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;XF86AudioRaiseVolume&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">hl.bindel</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;XF86AudioLowerVolume&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">hl.bindl</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;XF86AudioMute&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">hl.bindl</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;XF86AudioMicMute&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">-- 亮度</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">hl.bindel</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;XF86MonBrightnessUp&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;brightnessctl s 10%+&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">hl.bindel</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;XF86MonBrightnessDown&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;brightnessctl s 10%-&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">-- 媒體播放</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">hl.bindl</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;XF86AudioPlay&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;playerctl play-pause&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">hl.bindl</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;XF86AudioNext&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;playerctl next&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">hl.bindl</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;XF86AudioPrev&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;playerctl previous&#34;</span><span class="p">)</span></span></span></code></pre></div><blockquote>
<p><strong>[需實機測試]</strong> 媒體鍵（XF86Audio*）、亮度鍵（XF86MonBrightness*）、合蓋開關——這些依賴實體硬體送出正確的 keycode。VM 鍵盤通常沒有這些鍵。</p></blockquote>
<h2 id="環境變數">環境變數</h2>
<p>環境變數在 <code>hl.config()</code> 的 <code>env</code> 區段設定，影響 Hyprland 自身和其下執行的應用程式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- ~/.config/hypr/env.lua</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">env</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="c1">-- 游標主題</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="s2">&#34;XCURSOR_SIZE, 24&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="s2">&#34;XCURSOR_THEME, Bibata-Modern-Classic&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="s2">&#34;HYPRCURSOR_SIZE, 24&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="s2">&#34;HYPRCURSOR_THEME, Bibata-Modern-Classic&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="c1">-- Qt 應用程式</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="s2">&#34;QT_AUTO_SCREEN_SCALE_FACTOR, 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="s2">&#34;QT_QPA_PLATFORM, wayland;xcb&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="s2">&#34;QT_QPA_PLATFORMTHEME, qt5ct&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="s2">&#34;QT_WAYLAND_DISABLE_WINDOWDECORATION, 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="c1">-- GTK 應用程式</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="s2">&#34;GDK_BACKEND, wayland,x11,*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="c1">-- XDG session</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="s2">&#34;XDG_CURRENT_DESKTOP, Hyprland&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="s2">&#34;XDG_SESSION_TYPE, wayland&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="s2">&#34;XDG_SESSION_DESKTOP, Hyprland&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="c1">-- Electron 應用程式（VS Code, Discord 等）</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="s2">&#34;ELECTRON_OZONE_PLATFORM_HINT, auto&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="c1">-- Firefox</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="s2">&#34;MOZ_ENABLE_WAYLAND, 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><h3 id="nvidia-專用環境變數">NVIDIA 專用環境變數</h3>
<p>如果使用 NVIDIA 顯卡，需要額外設定（AMD 和 Intel 不需要）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 只在 NVIDIA 機器上加這些</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">env</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="s2">&#34;LIBVA_DRIVER_NAME, nvidia&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="s2">&#34;GBM_BACKEND, nvidia-drm&#34;</span><span class="p">,</span>             <span class="c1">-- Firefox 若崩潰就移除這行</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="s2">&#34;__GLX_VENDOR_LIBRARY_NAME, nvidia&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="s2">&#34;NVD_BACKEND, direct&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">cursor</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">no_hardware_cursors</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>             <span class="c1">-- NVIDIA 常見的游標問題修正</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="n">allow_dumb_copy</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><blockquote>
<p><strong>[VM 不適用]</strong> NVIDIA 環境變數和驅動設定在 VM 裡無意義——VM 使用 virtio-gpu 或軟體渲染。這些設定只在實體 NVIDIA 機器上測試。</p></blockquote>
<h3 id="vm-專用環境變數">VM 專用環境變數</h3>
<p>在 VM（UTM/QEMU）裡跑 Hyprland 需要額外的回退設定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 只在 VM 裡加，實機要移除</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">env</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="s2">&#34;WLR_RENDERER_ALLOW_SOFTWARE, 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="s2">&#34;LIBGL_ALWAYS_SOFTWARE, 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="s2">&#34;WLR_NO_HARDWARE_CURSORS, 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><blockquote>
<p><strong>[已驗證]</strong> Hyprland 0.55（Aquamarine 渲染後端）+ UTM virtio-gpu-gl-pci 實測：使用 GPU 加速時不需要 <code>WLR_RENDERER</code> 或 <code>LIBGL_ALWAYS_SOFTWARE</code>，Hyprland 自動走 VirGL/Venus 硬體路徑。<code>WLR_NO_HARDWARE_CURSORS</code> 仍需要（virtio-gpu 不支援硬體 cursor）。完整 VM 設定見 <a href="/blog/linux/dotfile/05-hyprland-config/hyprland-vm-setup/" data-link-title="Hyprland VM 環境設定與測試矩陣" data-link-desc="要在 VM 裡測試 Hyprland 配置、或判斷某個設定該在 VM 還是實機驗證時回來讀">VM 環境設定與測試矩陣</a>。</p></blockquote>
]]></content:encoded></item><item><title>macOS 視窗管理工具鏈</title><link>https://tarrragon.github.io/blog/linux/dotfile/04-window-management/macos-window-tools/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/04-window-management/macos-window-tools/</guid><description>&lt;p>macOS 的視窗系統由 WindowServer 控制，第三方工具能做的主要是「排列邏輯」——決定視窗的位置和大小。視覺效果（動畫、模糊、圓角）由系統控制，第三方工具改不了。這是跟 Linux tiling WM 最大的差異。&lt;/p>
&lt;h2 id="macos-原生-window-tilingmacos-15">macOS 原生 Window Tiling（macOS 15+）&lt;/h2>
&lt;p>macOS Sequoia（15，2024 年 9 月）內建了 window tiling 功能：鍵盤快捷鍵把視窗貼到螢幕的半邊或四分之一、拖拉到邊緣自動貼齊（edge snap）、相鄰視窗可以組成 tile group 一起調整比例。&lt;/p>
&lt;p>原生 tiling 的邊界：沒有多工作區管理、快捷鍵自訂空間有限（只能用系統偏好設定裡的固定選項）、不支援自動平鋪（仍然是手動觸發的 snap，不會在開新視窗時自動重排）。&lt;/p>
&lt;p>如果「貼到半邊 + 邊緣吸附」就足夠，原生功能免安裝即可使用。以下第三方工具解決的是原生功能做不到的事：更多排列選項（Rectangle）、自動平鋪（Amethyst）、完整的鍵盤工作流加多工作區（AeroSpace / yabai）。&lt;/p>
&lt;h2 id="rectangle">Rectangle&lt;/h2>
&lt;p>免費、開源。用快捷鍵把視窗貼到螢幕的半邊、三分之一、角落。不是自動平鋪——每個視窗都要你主動下指令。安裝後開箱即用，學習成本最低。&lt;/p>
&lt;p>適合的情境：只需要快速排版、不想花時間學新操作邏輯、偶爾分割就滿足需求。&lt;/p>
&lt;p>配置檔位置：&lt;code>~/Library/Preferences/com.knollsoft.Rectangle.plist&lt;/code>（macOS plist 格式，不太適合手動編輯，通常用 GUI 設定）。&lt;/p>
&lt;h2 id="amethyst">Amethyst&lt;/h2>
&lt;p>自動平鋪，安裝後視窗就會自動排列。提供多種 layout（tall, wide, fullscreen, column 等）可以用快捷鍵切換。設定比 Rectangle 多但比 yabai 少，是「想要自動平鋪但不想深度折騰」的選擇。&lt;/p>
&lt;p>配置檔：&lt;code>~/.amethyst.yml&lt;/code>，YAML 格式，可以版控。&lt;/p>
&lt;h2 id="aerospace">AeroSpace&lt;/h2>
&lt;p>近年最受歡迎的選擇。核心優勢是&lt;strong>不需要關閉 SIP&lt;/strong>（System Integrity Protection）——它用自己實作的虛擬工作區概念，不依賴 macOS 原生的 Spaces，因此繞過了很多系統層的限制。&lt;/p>
&lt;p>配置是純文字的 TOML 檔 &lt;code>~/.aerospace.toml&lt;/code>，改完即時生效。工作區模型靈活，多螢幕支援被普遍認為比 yabai 穩定。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c"># ~/.aerospace.toml 片段&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="nx">after-startup-command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;workspace 1&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">gaps&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="nx">inner&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">horizontal&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nx">inner&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">vertical&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="nx">outer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">left&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="nx">outer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">right&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">mode&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">main&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">binding&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="nx">alt-h&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s1">&amp;#39;focus left&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nx">alt-j&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s1">&amp;#39;focus down&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="nx">alt-k&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s1">&amp;#39;focus up&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="nx">alt-l&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s1">&amp;#39;focus right&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="nx">alt-shift-h&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s1">&amp;#39;move left&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="nx">alt-shift-j&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s1">&amp;#39;move down&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="nx">alt-shift-k&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s1">&amp;#39;move up&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="nx">alt-shift-l&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s1">&amp;#39;move right&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="nx">alt-1&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s1">&amp;#39;workspace 1&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="nx">alt-2&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s1">&amp;#39;workspace 2&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="nx">alt-3&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s1">&amp;#39;workspace 3&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>配置結構直覺：gaps 控制視窗間距、binding 定義快捷鍵對應的動作。整份檔案進 dotfile repo 就能跨機器還原操作習慣。&lt;/p>
&lt;h2 id="yabai--skhd">yabai + skhd&lt;/h2>
&lt;p>功能最完整的 macOS tiling WM。yabai 負責視窗管理，skhd 負責快捷鍵綁定。支援 BSP（binary space partitioning）樹狀分割——每次開新視窗都是把現有空間二分，形成一棵樹，你可以操作樹的節點來旋轉、交換、調整比例。&lt;/p>
&lt;p>代價是部分進階功能（某些視窗操作、取消動畫）需要部分關閉 SIP。對某些人這是門檻，對另一些人不是問題。&lt;/p>
&lt;p>配置檔是 shell script：&lt;code>.yabairc&lt;/code>（yabai 設定）和 &lt;code>.skhdrc&lt;/code>（快捷鍵設定），進 dotfile repo 管理。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># .yabairc 片段&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">yabai -m config layout bsp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">yabai -m config window_gap &lt;span class="m">10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">yabai -m config top_padding &lt;span class="m">10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">yabai -m config bottom_padding &lt;span class="m">10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 某些 app 不適合平鋪，設為浮動&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">yabai -m rule --add &lt;span class="nv">app&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;System Preferences&amp;#34;&lt;/span> &lt;span class="nv">manage&lt;/span>&lt;span class="o">=&lt;/span>off
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">yabai -m rule --add &lt;span class="nv">app&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Calculator&amp;#34;&lt;/span> &lt;span class="nv">manage&lt;/span>&lt;span class="o">=&lt;/span>off
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">yabai -m rule --add &lt;span class="nv">app&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Finder&amp;#34;&lt;/span> &lt;span class="nv">title&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Info&amp;#34;&lt;/span> &lt;span class="nv">manage&lt;/span>&lt;span class="o">=&lt;/span>off&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="選型判讀">選型判讀&lt;/h2>
&lt;p>選工具的判準不是「哪個最強」，而是「你願意花多少時間、想要多少控制權」。&lt;/p>
&lt;p>只需要快速排視窗、不想改工作習慣，Rectangle 足夠。想要自動平鋪但學習曲線要短，Amethyst 是進入點。想要完整的平鋪工作流、多工作區管理、純文字配置、又不想動系統安全設定，AeroSpace 是目前多數人推薦的首選。想要最大的控制權、願意處理 SIP 和更複雜的配置，yabai 給你最多彈性。&lt;/p>
&lt;p>從 Rectangle 跳到 AeroSpace 或 yabai 是一次操作思維的轉換——從「我指定每個視窗去哪」變成「我操作版面結構、WM 負責排列」。這個轉換需要一兩週的適應期，適應期內效率會暫時下降。&lt;/p></description><content:encoded><![CDATA[<p>macOS 的視窗系統由 WindowServer 控制，第三方工具能做的主要是「排列邏輯」——決定視窗的位置和大小。視覺效果（動畫、模糊、圓角）由系統控制，第三方工具改不了。這是跟 Linux tiling WM 最大的差異。</p>
<h2 id="macos-原生-window-tilingmacos-15">macOS 原生 Window Tiling（macOS 15+）</h2>
<p>macOS Sequoia（15，2024 年 9 月）內建了 window tiling 功能：鍵盤快捷鍵把視窗貼到螢幕的半邊或四分之一、拖拉到邊緣自動貼齊（edge snap）、相鄰視窗可以組成 tile group 一起調整比例。</p>
<p>原生 tiling 的邊界：沒有多工作區管理、快捷鍵自訂空間有限（只能用系統偏好設定裡的固定選項）、不支援自動平鋪（仍然是手動觸發的 snap，不會在開新視窗時自動重排）。</p>
<p>如果「貼到半邊 + 邊緣吸附」就足夠，原生功能免安裝即可使用。以下第三方工具解決的是原生功能做不到的事：更多排列選項（Rectangle）、自動平鋪（Amethyst）、完整的鍵盤工作流加多工作區（AeroSpace / yabai）。</p>
<h2 id="rectangle">Rectangle</h2>
<p>免費、開源。用快捷鍵把視窗貼到螢幕的半邊、三分之一、角落。不是自動平鋪——每個視窗都要你主動下指令。安裝後開箱即用，學習成本最低。</p>
<p>適合的情境：只需要快速排版、不想花時間學新操作邏輯、偶爾分割就滿足需求。</p>
<p>配置檔位置：<code>~/Library/Preferences/com.knollsoft.Rectangle.plist</code>（macOS plist 格式，不太適合手動編輯，通常用 GUI 設定）。</p>
<h2 id="amethyst">Amethyst</h2>
<p>自動平鋪，安裝後視窗就會自動排列。提供多種 layout（tall, wide, fullscreen, column 等）可以用快捷鍵切換。設定比 Rectangle 多但比 yabai 少，是「想要自動平鋪但不想深度折騰」的選擇。</p>
<p>配置檔：<code>~/.amethyst.yml</code>，YAML 格式，可以版控。</p>
<h2 id="aerospace">AeroSpace</h2>
<p>近年最受歡迎的選擇。核心優勢是<strong>不需要關閉 SIP</strong>（System Integrity Protection）——它用自己實作的虛擬工作區概念，不依賴 macOS 原生的 Spaces，因此繞過了很多系統層的限制。</p>
<p>配置是純文字的 TOML 檔 <code>~/.aerospace.toml</code>，改完即時生效。工作區模型靈活，多螢幕支援被普遍認為比 yabai 穩定。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># ~/.aerospace.toml 片段</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nx">after-startup-command</span> <span class="p">=</span> <span class="p">[</span><span class="s1">&#39;workspace 1&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">[</span><span class="nx">gaps</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nx">inner</span><span class="p">.</span><span class="nx">horizontal</span> <span class="p">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nx">inner</span><span class="p">.</span><span class="nx">vertical</span> <span class="p">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nx">outer</span><span class="p">.</span><span class="nx">left</span> <span class="p">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">outer</span><span class="p">.</span><span class="nx">right</span> <span class="p">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">[</span><span class="nx">mode</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">binding</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nx">alt-h</span> <span class="p">=</span> <span class="s1">&#39;focus left&#39;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nx">alt-j</span> <span class="p">=</span> <span class="s1">&#39;focus down&#39;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nx">alt-k</span> <span class="p">=</span> <span class="s1">&#39;focus up&#39;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nx">alt-l</span> <span class="p">=</span> <span class="s1">&#39;focus right&#39;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nx">alt-shift-h</span> <span class="p">=</span> <span class="s1">&#39;move left&#39;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nx">alt-shift-j</span> <span class="p">=</span> <span class="s1">&#39;move down&#39;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nx">alt-shift-k</span> <span class="p">=</span> <span class="s1">&#39;move up&#39;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="nx">alt-shift-l</span> <span class="p">=</span> <span class="s1">&#39;move right&#39;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nx">alt-1</span> <span class="p">=</span> <span class="s1">&#39;workspace 1&#39;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="nx">alt-2</span> <span class="p">=</span> <span class="s1">&#39;workspace 2&#39;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="nx">alt-3</span> <span class="p">=</span> <span class="s1">&#39;workspace 3&#39;</span></span></span></code></pre></div><p>配置結構直覺：gaps 控制視窗間距、binding 定義快捷鍵對應的動作。整份檔案進 dotfile repo 就能跨機器還原操作習慣。</p>
<h2 id="yabai--skhd">yabai + skhd</h2>
<p>功能最完整的 macOS tiling WM。yabai 負責視窗管理，skhd 負責快捷鍵綁定。支援 BSP（binary space partitioning）樹狀分割——每次開新視窗都是把現有空間二分，形成一棵樹，你可以操作樹的節點來旋轉、交換、調整比例。</p>
<p>代價是部分進階功能（某些視窗操作、取消動畫）需要部分關閉 SIP。對某些人這是門檻，對另一些人不是問題。</p>
<p>配置檔是 shell script：<code>.yabairc</code>（yabai 設定）和 <code>.skhdrc</code>（快捷鍵設定），進 dotfile repo 管理。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># .yabairc 片段</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">yabai -m config layout bsp
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">yabai -m config window_gap <span class="m">10</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">yabai -m config top_padding <span class="m">10</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">yabai -m config bottom_padding <span class="m">10</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 某些 app 不適合平鋪，設為浮動</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">yabai -m rule --add <span class="nv">app</span><span class="o">=</span><span class="s2">&#34;System Preferences&#34;</span> <span class="nv">manage</span><span class="o">=</span>off
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">yabai -m rule --add <span class="nv">app</span><span class="o">=</span><span class="s2">&#34;Calculator&#34;</span> <span class="nv">manage</span><span class="o">=</span>off
</span></span><span class="line"><span class="ln">10</span><span class="cl">yabai -m rule --add <span class="nv">app</span><span class="o">=</span><span class="s2">&#34;Finder&#34;</span> <span class="nv">title</span><span class="o">=</span><span class="s2">&#34;Info&#34;</span> <span class="nv">manage</span><span class="o">=</span>off</span></span></code></pre></div><h2 id="選型判讀">選型判讀</h2>
<p>選工具的判準不是「哪個最強」，而是「你願意花多少時間、想要多少控制權」。</p>
<p>只需要快速排視窗、不想改工作習慣，Rectangle 足夠。想要自動平鋪但學習曲線要短，Amethyst 是進入點。想要完整的平鋪工作流、多工作區管理、純文字配置、又不想動系統安全設定，AeroSpace 是目前多數人推薦的首選。想要最大的控制權、願意處理 SIP 和更複雜的配置，yabai 給你最多彈性。</p>
<p>從 Rectangle 跳到 AeroSpace 或 yabai 是一次操作思維的轉換——從「我指定每個視窗去哪」變成「我操作版面結構、WM 負責排列」。這個轉換需要一兩週的適應期，適應期內效率會暫時下降。</p>
]]></content:encoded></item><item><title>Multiplexer：tmux vs zellij</title><link>https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/multiplexer-tmux-zellij/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/multiplexer-tmux-zellij/</guid><description>&lt;p>Multiplexer 在一個終端機視窗裡切分多個 pane、管理多個 session、SSH 斷線後保持 session 存活。&lt;/p>
&lt;h2 id="tmux">tmux&lt;/h2>
&lt;p>tmux 是最成熟、生態最廣的選擇。配置在 &lt;code>~/.config/tmux/tmux.conf&lt;/code>（新版）或 &lt;code>~/.tmux.conf&lt;/code>（傳統位置）。&lt;/p>
&lt;h3 id="核心配置項目">核心配置項目&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># prefix key（預設是 Ctrl-b，很多人改成 Ctrl-a）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">unbind C-b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="nb">set&lt;/span> -g prefix C-a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="nb">bind&lt;/span> C-a send-prefix
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 分割 pane 的快捷鍵（預設不直覺，改成 | 和 -）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="nb">bind&lt;/span> &lt;span class="p">|&lt;/span> split-window -h -c &lt;span class="s2">&amp;#34;#{pane_current_path}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="nb">bind&lt;/span> - split-window -v -c &lt;span class="s2">&amp;#34;#{pane_current_path}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># 用 vim 風格的 hjkl 切換 pane&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="nb">bind&lt;/span> h &lt;span class="k">select&lt;/span>-pane -L
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nb">bind&lt;/span> j &lt;span class="k">select&lt;/span>-pane -D
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="nb">bind&lt;/span> k &lt;span class="k">select&lt;/span>-pane -U
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="nb">bind&lt;/span> l &lt;span class="k">select&lt;/span>-pane -R
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1"># 啟用滑鼠支援&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="nb">set&lt;/span> -g mouse on
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="c1"># 256 色支援&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="nb">set&lt;/span> -g default-terminal &lt;span class="s2">&amp;#34;tmux-256color&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="nb">set&lt;/span> -ag terminal-overrides &lt;span class="s2">&amp;#34;,xterm-256color:RGB&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="c1"># status bar 位置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="nb">set&lt;/span> -g status-position top&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="tmux-plugin">tmux plugin&lt;/h3>
&lt;p>用 TPM（Tmux Plugin Manager）管理，常用：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>tmux-sensible&lt;/strong>：合理的預設值&lt;/li>
&lt;li>&lt;strong>tmux-resurrect&lt;/strong>：重開機後還原 session 佈局&lt;/li>
&lt;li>&lt;strong>tmux-continuum&lt;/strong>：自動儲存 session&lt;/li>
&lt;/ul>
&lt;h2 id="zellij">zellij&lt;/h2>
&lt;p>zellij 是較新的替代品，Rust 寫的，內建佈局系統、tab 命名、浮動 pane。配置在 &lt;code>~/.config/zellij/config.kdl&lt;/code>（KDL 格式）。&lt;/p>
&lt;p>跟 tmux 的主要差異：&lt;/p>
&lt;ul>
&lt;li>開箱即用的 UI 提示（底部顯示可用快捷鍵），學習曲線較低&lt;/li>
&lt;li>佈局用 KDL 宣告式描述，比 tmux 的 script 式設定更容易管理&lt;/li>
&lt;li>Plugin 系統用 WASM，跟 tmux 的 bash script 式 plugin 不同&lt;/li>
&lt;li>生態較新、plugin 和整合沒有 tmux 多&lt;/li>
&lt;/ul>
&lt;h2 id="選型判讀">選型判讀&lt;/h2>
&lt;p>已經熟 tmux 的人通常沒有強烈理由遷移；從零開始的人 zellij 的上手成本更低。&lt;/p>
&lt;h2 id="深入">深入&lt;/h2>
&lt;p>這篇是多工器的概覽（在終端機生態裡的定位、tmux 與 zellij 的取捨）。把它們當「遠端工作工具」深入用——session 持久化的核心概念、遠端斷線接回、瀏覽器連遠端 session——見工具選單的深度頁：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/tmux-persistence-and-basics/" data-link-title="tmux 基礎：遠端 session 持久化與基本操作" data-link-desc="tmux 終端機多工器的遠端使用核心：detach/reattach 讓 session 脫離連線生命週期、prefix key 與 window/pane 操作、手機友善的快捷鍵調校，以及 tmux 與 zellij 的選型對照。">tmux 持久化與基礎&lt;/a>——session 持久化怎麼保住遠端工作。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/zellij-pane/" data-link-title="Zellij 多終端機操作指南" data-link-desc="Zellij pane 的佈局查看、內容讀取、大小調整等 CLI 操作方式，適合搭配 AI 工具使用。">zellij 分頁與 pane&lt;/a>——內建佈局的操作深入。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/zellij-remote-web-client/" data-link-title="Zellij Web Client 外網連線教學" data-link-desc="讓他人透過瀏覽器連線到指定的 Zellij session，包含 SSL 憑證申請、防火牆設定、Token 管理等完整步驟。">zellij 遠端 web 客戶端&lt;/a>——從瀏覽器連遠端 session。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/tools/remote/connection-and-sync-tools/" data-link-title="遠端連線與同步工具選型：連得穩、斷得起、檔案一致" data-link-desc="遠端工作要挑連線與檔案同步工具、在 ssh/mosh/autossh 之間、或 rsync/sshfs/mutagen 之間拿不定、想知道各自解哪個問題與代價時回來讀">遠端連線與同步工具選型&lt;/a>——多工器之外的連線（mosh/autossh）與同步（rsync/sshfs/mutagen）。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Multiplexer 在一個終端機視窗裡切分多個 pane、管理多個 session、SSH 斷線後保持 session 存活。</p>
<h2 id="tmux">tmux</h2>
<p>tmux 是最成熟、生態最廣的選擇。配置在 <code>~/.config/tmux/tmux.conf</code>（新版）或 <code>~/.tmux.conf</code>（傳統位置）。</p>
<h3 id="核心配置項目">核心配置項目</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># prefix key（預設是 Ctrl-b，很多人改成 Ctrl-a）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">unbind C-b
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nb">set</span> -g prefix C-a
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nb">bind</span> C-a send-prefix
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 分割 pane 的快捷鍵（預設不直覺，改成 | 和 -）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nb">bind</span> <span class="p">|</span> split-window -h -c <span class="s2">&#34;#{pane_current_path}&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nb">bind</span> - split-window -v -c <span class="s2">&#34;#{pane_current_path}&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 用 vim 風格的 hjkl 切換 pane</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">bind</span> h <span class="k">select</span>-pane -L
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nb">bind</span> j <span class="k">select</span>-pane -D
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nb">bind</span> k <span class="k">select</span>-pane -U
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nb">bind</span> l <span class="k">select</span>-pane -R
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 啟用滑鼠支援</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nb">set</span> -g mouse on
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 256 色支援</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="nb">set</span> -g default-terminal <span class="s2">&#34;tmux-256color&#34;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="nb">set</span> -ag terminal-overrides <span class="s2">&#34;,xterm-256color:RGB&#34;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># status bar 位置</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="nb">set</span> -g status-position top</span></span></code></pre></div><h3 id="tmux-plugin">tmux plugin</h3>
<p>用 TPM（Tmux Plugin Manager）管理，常用：</p>
<ul>
<li><strong>tmux-sensible</strong>：合理的預設值</li>
<li><strong>tmux-resurrect</strong>：重開機後還原 session 佈局</li>
<li><strong>tmux-continuum</strong>：自動儲存 session</li>
</ul>
<h2 id="zellij">zellij</h2>
<p>zellij 是較新的替代品，Rust 寫的，內建佈局系統、tab 命名、浮動 pane。配置在 <code>~/.config/zellij/config.kdl</code>（KDL 格式）。</p>
<p>跟 tmux 的主要差異：</p>
<ul>
<li>開箱即用的 UI 提示（底部顯示可用快捷鍵），學習曲線較低</li>
<li>佈局用 KDL 宣告式描述，比 tmux 的 script 式設定更容易管理</li>
<li>Plugin 系統用 WASM，跟 tmux 的 bash script 式 plugin 不同</li>
<li>生態較新、plugin 和整合沒有 tmux 多</li>
</ul>
<h2 id="選型判讀">選型判讀</h2>
<p>已經熟 tmux 的人通常沒有強烈理由遷移；從零開始的人 zellij 的上手成本更低。</p>
<h2 id="深入">深入</h2>
<p>這篇是多工器的概覽（在終端機生態裡的定位、tmux 與 zellij 的取捨）。把它們當「遠端工作工具」深入用——session 持久化的核心概念、遠端斷線接回、瀏覽器連遠端 session——見工具選單的深度頁：</p>
<ul>
<li><a href="/blog/linux/tools/cli/tmux-persistence-and-basics/" data-link-title="tmux 基礎：遠端 session 持久化與基本操作" data-link-desc="tmux 終端機多工器的遠端使用核心：detach/reattach 讓 session 脫離連線生命週期、prefix key 與 window/pane 操作、手機友善的快捷鍵調校，以及 tmux 與 zellij 的選型對照。">tmux 持久化與基礎</a>——session 持久化怎麼保住遠端工作。</li>
<li><a href="/blog/linux/tools/cli/zellij-pane/" data-link-title="Zellij 多終端機操作指南" data-link-desc="Zellij pane 的佈局查看、內容讀取、大小調整等 CLI 操作方式，適合搭配 AI 工具使用。">zellij 分頁與 pane</a>——內建佈局的操作深入。</li>
<li><a href="/blog/linux/tools/cli/zellij-remote-web-client/" data-link-title="Zellij Web Client 外網連線教學" data-link-desc="讓他人透過瀏覽器連線到指定的 Zellij session，包含 SSL 憑證申請、防火牆設定、Token 管理等完整步驟。">zellij 遠端 web 客戶端</a>——從瀏覽器連遠端 session。</li>
<li><a href="/blog/linux/tools/remote/connection-and-sync-tools/" data-link-title="遠端連線與同步工具選型：連得穩、斷得起、檔案一致" data-link-desc="遠端工作要挑連線與檔案同步工具、在 ssh/mosh/autossh 之間、或 rsync/sshfs/mutagen 之間拿不定、想知道各自解哪個問題與代價時回來讀">遠端連線與同步工具選型</a>——多工器之外的連線（mosh/autossh）與同步（rsync/sshfs/mutagen）。</li>
</ul>
]]></content:encoded></item><item><title>PATH、Plugin 與 Prompt</title><link>https://tarrragon.github.io/blog/linux/dotfile/02-shell-config/path-plugin-prompt/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/02-shell-config/path-plugin-prompt/</guid><description>&lt;p>PATH、plugin manager 和 prompt 是 shell 配置裡「每個開發者都會碰到、但容易放任不管」的三個區域。&lt;/p>
&lt;h2 id="path-管理">PATH 管理&lt;/h2>
&lt;p>PATH 是最容易腐化的環境變數——每裝一個工具就加一條，最後 PATH 變成一長串看不懂的路徑，順序還會互相影響。&lt;/p>
&lt;p>管理原則：&lt;/p>
&lt;ul>
&lt;li>PATH 設定集中在一個地方（&lt;code>.zshenv&lt;/code> 或 &lt;code>env.zsh&lt;/code>），不散落在多個檔案&lt;/li>
&lt;li>新增前先想：這個路徑是所有機器都需要、還是特定機器才需要？共用的進 env.zsh，特定的進 local.zsh&lt;/li>
&lt;li>用 &lt;code>typeset -U PATH&lt;/code> (Zsh) 自動去除重複項目，避免多次 source 導致 PATH 不斷加長&lt;/li>
&lt;/ul>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.config/zsh/env.zsh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">typeset&lt;/span> -U PATH &lt;span class="c1"># 去重&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 自己的 script&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.local/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="plugin-manager-選型">Plugin Manager 選型&lt;/h2>
&lt;p>Zsh plugin manager 的選擇很多，差異主要在載入速度和功能豐富度：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>無 plugin manager&lt;/strong>：直接 git clone plugin 到某個目錄，手動 source。最簡單、最透明、但更新要自己管&lt;/li>
&lt;li>&lt;strong>zinit&lt;/strong>（原 zplugin）：載入速度最快（turbo mode 延遲載入）、功能最多、但配置語法學習曲線高&lt;/li>
&lt;li>&lt;strong>antidote&lt;/strong>：宣告式（一個 &lt;code>.zsh_plugins.txt&lt;/code> 列出所有 plugin），概念簡單&lt;/li>
&lt;li>&lt;strong>sheldon&lt;/strong>：Rust 寫的、速度快、設定用 TOML&lt;/li>
&lt;/ul>
&lt;p>常用 plugin：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>zsh-autosuggestions&lt;/strong>：根據歷史指令自動補全建議（灰色字，按右箭頭接受）&lt;/li>
&lt;li>&lt;strong>zsh-syntax-highlighting&lt;/strong>：指令行即時語法高亮&lt;/li>
&lt;li>&lt;strong>zsh-completions&lt;/strong>：額外的 tab 補全定義&lt;/li>
&lt;/ul>
&lt;h2 id="prompt-設計">Prompt 設計&lt;/h2>
&lt;p>Prompt 是每次按 Enter 都會看到的東西，值得花時間設計但不需要複雜。&lt;/p>
&lt;p>基本款（不用框架）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 顯示目錄 + git branch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">autoload -Uz vcs_info
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">precmd&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span> vcs_info &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">zstyle &lt;span class="s1">&amp;#39;:vcs_info:git:*&amp;#39;&lt;/span> formats &lt;span class="s1">&amp;#39; (%b)&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nv">PROMPT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;%F{blue}%~%f%F{green}${vcs_info_msg_0_}%f %# &amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>框架款：Starship（跨 shell、用 TOML 設定、Rust 寫的速度快）是目前最常被推薦的 prompt 工具。它的配置進 &lt;code>~/.config/starship.toml&lt;/code>，也是 dotfile 的一部分。&lt;/p>
&lt;h2 id="dotfile-結構對應">Dotfile 結構對應&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">管理工具與目錄結構&lt;/a>裡的 stow 目錄結構，shell 配置的對應：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">~/dotfiles/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">└── zsh/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> ├── .zshenv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> ├── .zshrc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> └── .config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> └── zsh/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> ├── aliases.zsh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> ├── functions.zsh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> ├── plugins.zsh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> ├── prompt.zsh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> ├── tools.zsh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> └── env.zsh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>stow zsh&lt;/code> 會在家目錄建立 &lt;code>.zshenv&lt;/code> 和 &lt;code>.zshrc&lt;/code> 的 symlink，在 &lt;code>.config/zsh/&lt;/code> 下建立各模組檔案的 symlink。&lt;code>local.zsh&lt;/code> 不在 repo 裡，各機器自己建。&lt;/p></description><content:encoded><![CDATA[<p>PATH、plugin manager 和 prompt 是 shell 配置裡「每個開發者都會碰到、但容易放任不管」的三個區域。</p>
<h2 id="path-管理">PATH 管理</h2>
<p>PATH 是最容易腐化的環境變數——每裝一個工具就加一條，最後 PATH 變成一長串看不懂的路徑，順序還會互相影響。</p>
<p>管理原則：</p>
<ul>
<li>PATH 設定集中在一個地方（<code>.zshenv</code> 或 <code>env.zsh</code>），不散落在多個檔案</li>
<li>新增前先想：這個路徑是所有機器都需要、還是特定機器才需要？共用的進 env.zsh，特定的進 local.zsh</li>
<li>用 <code>typeset -U PATH</code> (Zsh) 自動去除重複項目，避免多次 source 導致 PATH 不斷加長</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># ~/.config/zsh/env.zsh</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">typeset</span> -U PATH  <span class="c1"># 去重</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 自己的 script</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span></span></span></code></pre></div><h2 id="plugin-manager-選型">Plugin Manager 選型</h2>
<p>Zsh plugin manager 的選擇很多，差異主要在載入速度和功能豐富度：</p>
<ul>
<li><strong>無 plugin manager</strong>：直接 git clone plugin 到某個目錄，手動 source。最簡單、最透明、但更新要自己管</li>
<li><strong>zinit</strong>（原 zplugin）：載入速度最快（turbo mode 延遲載入）、功能最多、但配置語法學習曲線高</li>
<li><strong>antidote</strong>：宣告式（一個 <code>.zsh_plugins.txt</code> 列出所有 plugin），概念簡單</li>
<li><strong>sheldon</strong>：Rust 寫的、速度快、設定用 TOML</li>
</ul>
<p>常用 plugin：</p>
<ul>
<li><strong>zsh-autosuggestions</strong>：根據歷史指令自動補全建議（灰色字，按右箭頭接受）</li>
<li><strong>zsh-syntax-highlighting</strong>：指令行即時語法高亮</li>
<li><strong>zsh-completions</strong>：額外的 tab 補全定義</li>
</ul>
<h2 id="prompt-設計">Prompt 設計</h2>
<p>Prompt 是每次按 Enter 都會看到的東西，值得花時間設計但不需要複雜。</p>
<p>基本款（不用框架）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 顯示目錄 + git branch</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">autoload -Uz vcs_info
</span></span><span class="line"><span class="ln">3</span><span class="cl">precmd<span class="o">()</span> <span class="o">{</span> vcs_info <span class="o">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">zstyle <span class="s1">&#39;:vcs_info:git:*&#39;</span> formats <span class="s1">&#39; (%b)&#39;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nv">PROMPT</span><span class="o">=</span><span class="s1">&#39;%F{blue}%~%f%F{green}${vcs_info_msg_0_}%f %# &#39;</span></span></span></code></pre></div><p>框架款：Starship（跨 shell、用 TOML 設定、Rust 寫的速度快）是目前最常被推薦的 prompt 工具。它的配置進 <code>~/.config/starship.toml</code>，也是 dotfile 的一部分。</p>
<h2 id="dotfile-結構對應">Dotfile 結構對應</h2>
<p><a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">管理工具與目錄結構</a>裡的 stow 目錄結構，shell 配置的對應：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">~/dotfiles/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">└── zsh/
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    ├── .zshenv
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    ├── .zshrc
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    └── .config/
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        └── zsh/
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            ├── aliases.zsh
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            ├── functions.zsh
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            ├── plugins.zsh
</span></span><span class="line"><span class="ln">10</span><span class="cl">            ├── prompt.zsh
</span></span><span class="line"><span class="ln">11</span><span class="cl">            ├── tools.zsh
</span></span><span class="line"><span class="ln">12</span><span class="cl">            └── env.zsh</span></span></code></pre></div><p><code>stow zsh</code> 會在家目錄建立 <code>.zshenv</code> 和 <code>.zshrc</code> 的 symlink，在 <code>.config/zsh/</code> 下建立各模組檔案的 symlink。<code>local.zsh</code> 不在 repo 裡，各機器自己建。</p>
]]></content:encoded></item><item><title>配色系統、鎖屏與 GTK 主題</title><link>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/color-system-theming/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/color-system-theming/</guid><description>&lt;h2 id="hyprlock鎖屏">Hyprlock：鎖屏&lt;/h2>
&lt;p>Hyprlock 是 Hyprland 配套的鎖屏工具，支援模糊背景、自定義佈局：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.config/hypr/hyprlock.conf&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">background &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nv">monitor&lt;/span> &lt;span class="o">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nv">path&lt;/span> &lt;span class="o">=&lt;/span> screenshot
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nv">blur_passes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nv">blur_size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">8&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">input-field &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nv">monitor&lt;/span> &lt;span class="o">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nv">size&lt;/span> &lt;span class="o">=&lt;/span> 250, &lt;span class="m">50&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nv">outline_thickness&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="nv">outer_color&lt;/span> &lt;span class="o">=&lt;/span> rgba&lt;span class="o">(&lt;/span>137, 180, 250, 1&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nv">inner_color&lt;/span> &lt;span class="o">=&lt;/span> rgba&lt;span class="o">(&lt;/span>30, 30, 46, 1&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="nv">font_color&lt;/span> &lt;span class="o">=&lt;/span> rgba&lt;span class="o">(&lt;/span>205, 214, 244, 1&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="nv">placeholder_text&lt;/span> &lt;span class="o">=&lt;/span> &amp;lt;i&amp;gt;Password...&amp;lt;/i&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="nv">fade_on_empty&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="nv">position&lt;/span> &lt;span class="o">=&lt;/span> 0, -50
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="nv">halign&lt;/span> &lt;span class="o">=&lt;/span> center
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="nv">valign&lt;/span> &lt;span class="o">=&lt;/span> center
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">label &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl"> &lt;span class="nv">monitor&lt;/span> &lt;span class="o">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="nv">text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nv">$TIME&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="nv">font_size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">64&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> &lt;span class="nv">font_family&lt;/span> &lt;span class="o">=&lt;/span> MesloLGS Nerd Font
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl"> &lt;span class="nv">color&lt;/span> &lt;span class="o">=&lt;/span> rgba&lt;span class="o">(&lt;/span>205, 214, 244, 1&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl"> &lt;span class="nv">position&lt;/span> &lt;span class="o">=&lt;/span> 0, &lt;span class="m">80&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl"> &lt;span class="nv">halign&lt;/span> &lt;span class="o">=&lt;/span> center
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl"> &lt;span class="nv">valign&lt;/span> &lt;span class="o">=&lt;/span> center
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl">&lt;span class="o">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>鎖屏一旦啟動，桌面的「鎖定」狀態由 compositor 透過 Wayland 的 ext-session-lock 協議持有，殺掉鎖屏 process 不等於解鎖。鎖屏的安全模型與測試時的注意事項見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock&lt;/a>。&lt;/p>
&lt;h2 id="配色系統的統一管理">配色系統的統一管理&lt;/h2>
&lt;p>Rice 的視覺品質取決於配色一致性。散亂的色碼（waybar 用一套、wofi 用另一套、mako 又一套）是桌面看起來「雜」的最常見原因。&lt;/p>
&lt;p>管理方式：&lt;/p>
&lt;p>&lt;strong>選定一套配色方案&lt;/strong>。Catppuccin、Tokyo Night、Gruvbox、Nord 是目前最多 Linux ricer 使用的方案，每一套都有完整的色彩定義和各工具的預設配置。&lt;/p>
&lt;p>&lt;strong>建立配色變數檔&lt;/strong>。把色碼集中到一個檔案，其他配置引用它：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.config/hypr/colors.conf&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="nv">$rosewater&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>f5e0dc&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="nv">$flamingo&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>f2cdcd&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="nv">$pink&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>f5c2e7&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="nv">$mauve&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>cba6f7&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nv">$red&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>f38ba8&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="nv">$maroon&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>eba0ac&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="nv">$peach&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>fab387&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="nv">$yellow&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>f9e2af&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="nv">$green&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>a6e3a1&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="nv">$teal&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>94e2d5&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nv">$sky&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>89dceb&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="nv">$sapphire&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>74c7ec&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="nv">$blue&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>89b4fa&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="nv">$lavender&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>b4befe&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="nv">$text&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>cdd6f4&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="nv">$subtext1&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>bac2de&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="nv">$overlay0&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>6c7086&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="nv">$surface0&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>313244&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="nv">$base&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>1e1e2e&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="nv">$mantle&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>181825&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="nv">$crust&lt;/span> &lt;span class="o">=&lt;/span> rgb&lt;span class="o">(&lt;/span>11111b&lt;span class="o">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Hyprland 的 &lt;code>source&lt;/code> 可以引用這些變數。Waybar 和 Wofi 的 CSS 無法直接引用 Hyprland 變數，但可以用 build script 或 template 工具（如 pywal、flavours）從一份主設定產生各工具的配色。&lt;/p>
&lt;p>&lt;strong>換配色方案&lt;/strong>時需要改的檔案清單：&lt;/p>
&lt;ul>
&lt;li>Hyprland appearance.conf（邊框、陰影顏色）&lt;/li>
&lt;li>Waybar style.css&lt;/li>
&lt;li>Wofi/Rofi style.css&lt;/li>
&lt;li>Mako config&lt;/li>
&lt;li>Hyprlock config&lt;/li>
&lt;li>Terminal emulator 配色&lt;/li>
&lt;li>Neovim colorscheme&lt;/li>
&lt;li>GTK theme（影響 GUI 應用程式的外觀）&lt;/li>
&lt;/ul>
&lt;p>把這個清單記在 dotfile repo 的 README 裡，換主題時有對照不會漏改。&lt;/p></description><content:encoded><![CDATA[<h2 id="hyprlock鎖屏">Hyprlock：鎖屏</h2>
<p>Hyprlock 是 Hyprland 配套的鎖屏工具，支援模糊背景、自定義佈局：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># ~/.config/hypr/hyprlock.conf</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">background <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nv">monitor</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nv">path</span> <span class="o">=</span> screenshot
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nv">blur_passes</span> <span class="o">=</span> <span class="m">3</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nv">blur_size</span> <span class="o">=</span> <span class="m">8</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">input-field <span class="o">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nv">monitor</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nv">size</span> <span class="o">=</span> 250, <span class="m">50</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nv">outline_thickness</span> <span class="o">=</span> <span class="m">2</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nv">outer_color</span> <span class="o">=</span> rgba<span class="o">(</span>137, 180, 250, 1<span class="o">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nv">inner_color</span> <span class="o">=</span> rgba<span class="o">(</span>30, 30, 46, 1<span class="o">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nv">font_color</span> <span class="o">=</span> rgba<span class="o">(</span>205, 214, 244, 1<span class="o">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nv">placeholder_text</span> <span class="o">=</span> &lt;i&gt;Password...&lt;/i&gt;
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nv">fade_on_empty</span> <span class="o">=</span> <span class="nb">true</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="nv">position</span> <span class="o">=</span> 0, -50
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nv">halign</span> <span class="o">=</span> center
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="nv">valign</span> <span class="o">=</span> center
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">label <span class="o">{</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="nv">monitor</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="nv">text</span> <span class="o">=</span> <span class="nv">$TIME</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="nv">font_size</span> <span class="o">=</span> <span class="m">64</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="nv">font_family</span> <span class="o">=</span> MesloLGS Nerd Font
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="nv">color</span> <span class="o">=</span> rgba<span class="o">(</span>205, 214, 244, 1<span class="o">)</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="nv">position</span> <span class="o">=</span> 0, <span class="m">80</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="nv">halign</span> <span class="o">=</span> center
</span></span><span class="line"><span class="ln">32</span><span class="cl">    <span class="nv">valign</span> <span class="o">=</span> center
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="o">}</span></span></span></code></pre></div><p>鎖屏一旦啟動，桌面的「鎖定」狀態由 compositor 透過 Wayland 的 ext-session-lock 協議持有，殺掉鎖屏 process 不等於解鎖。鎖屏的安全模型與測試時的注意事項見 <a href="/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock</a>。</p>
<h2 id="配色系統的統一管理">配色系統的統一管理</h2>
<p>Rice 的視覺品質取決於配色一致性。散亂的色碼（waybar 用一套、wofi 用另一套、mako 又一套）是桌面看起來「雜」的最常見原因。</p>
<p>管理方式：</p>
<p><strong>選定一套配色方案</strong>。Catppuccin、Tokyo Night、Gruvbox、Nord 是目前最多 Linux ricer 使用的方案，每一套都有完整的色彩定義和各工具的預設配置。</p>
<p><strong>建立配色變數檔</strong>。把色碼集中到一個檔案，其他配置引用它：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># ~/.config/hypr/colors.conf</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nv">$rosewater</span> <span class="o">=</span> rgb<span class="o">(</span>f5e0dc<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nv">$flamingo</span>  <span class="o">=</span> rgb<span class="o">(</span>f2cdcd<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">$pink</span>      <span class="o">=</span> rgb<span class="o">(</span>f5c2e7<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nv">$mauve</span>     <span class="o">=</span> rgb<span class="o">(</span>cba6f7<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nv">$red</span>       <span class="o">=</span> rgb<span class="o">(</span>f38ba8<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nv">$maroon</span>    <span class="o">=</span> rgb<span class="o">(</span>eba0ac<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nv">$peach</span>     <span class="o">=</span> rgb<span class="o">(</span>fab387<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nv">$yellow</span>    <span class="o">=</span> rgb<span class="o">(</span>f9e2af<span class="o">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nv">$green</span>     <span class="o">=</span> rgb<span class="o">(</span>a6e3a1<span class="o">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nv">$teal</span>      <span class="o">=</span> rgb<span class="o">(</span>94e2d5<span class="o">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nv">$sky</span>       <span class="o">=</span> rgb<span class="o">(</span>89dceb<span class="o">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nv">$sapphire</span>  <span class="o">=</span> rgb<span class="o">(</span>74c7ec<span class="o">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nv">$blue</span>      <span class="o">=</span> rgb<span class="o">(</span>89b4fa<span class="o">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nv">$lavender</span>  <span class="o">=</span> rgb<span class="o">(</span>b4befe<span class="o">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nv">$text</span>      <span class="o">=</span> rgb<span class="o">(</span>cdd6f4<span class="o">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nv">$subtext1</span>  <span class="o">=</span> rgb<span class="o">(</span>bac2de<span class="o">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="nv">$overlay0</span>  <span class="o">=</span> rgb<span class="o">(</span>6c7086<span class="o">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nv">$surface0</span>  <span class="o">=</span> rgb<span class="o">(</span>313244<span class="o">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="nv">$base</span>      <span class="o">=</span> rgb<span class="o">(</span>1e1e2e<span class="o">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="nv">$mantle</span>    <span class="o">=</span> rgb<span class="o">(</span>181825<span class="o">)</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="nv">$crust</span>     <span class="o">=</span> rgb<span class="o">(</span>11111b<span class="o">)</span></span></span></code></pre></div><p>Hyprland 的 <code>source</code> 可以引用這些變數。Waybar 和 Wofi 的 CSS 無法直接引用 Hyprland 變數，但可以用 build script 或 template 工具（如 pywal、flavours）從一份主設定產生各工具的配色。</p>
<p><strong>換配色方案</strong>時需要改的檔案清單：</p>
<ul>
<li>Hyprland appearance.conf（邊框、陰影顏色）</li>
<li>Waybar style.css</li>
<li>Wofi/Rofi style.css</li>
<li>Mako config</li>
<li>Hyprlock config</li>
<li>Terminal emulator 配色</li>
<li>Neovim colorscheme</li>
<li>GTK theme（影響 GUI 應用程式的外觀）</li>
</ul>
<p>把這個清單記在 dotfile repo 的 README 裡，換主題時有對照不會漏改。</p>
<h2 id="gtk--qt-主題">GTK / Qt 主題</h2>
<p>Linux 的 GUI 應用程式分兩大陣營：GTK（GNOME 系）和 Qt（KDE 系）。平鋪式 WM 不自帶主題引擎，需要手動設定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># GTK 設定</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># ~/.config/gtk-3.0/settings.ini</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="o">[</span>Settings<span class="o">]</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">gtk-theme-name<span class="o">=</span>Catppuccin-Mocha-Standard-Blue-Dark
</span></span><span class="line"><span class="ln">5</span><span class="cl">gtk-icon-theme-name<span class="o">=</span>Papirus-Dark
</span></span><span class="line"><span class="ln">6</span><span class="cl">gtk-font-name<span class="o">=</span>Noto Sans CJK TC <span class="m">11</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">gtk-cursor-theme-name<span class="o">=</span>Bibata-Modern-Classic</span></span></code></pre></div><p>Qt 應用程式用 <code>qt5ct</code> / <code>qt6ct</code> 設定，或用 Kvantum 主題引擎統一風格。</p>
<h2 id="dotfile-結構對應">Dotfile 結構對應</h2>
<p>Rice 涉及的工具最多——waybar、wofi、mako、hyprlock、GTK 各自是獨立的 stow package。邊界切在「一個工具一個 package」，換主題時才能按需更新而不影響其他工具。<code>themes/</code> 放共用的配色變數，不屬於任何單一工具：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">~/dotfiles/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── waybar/
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│       └── waybar/
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│           ├── config.jsonc
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│           └── style.css
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">├── wofi/
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">│       └── wofi/
</span></span><span class="line"><span class="ln">10</span><span class="cl">│           ├── config
</span></span><span class="line"><span class="ln">11</span><span class="cl">│           └── style.css
</span></span><span class="line"><span class="ln">12</span><span class="cl">├── mako/
</span></span><span class="line"><span class="ln">13</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln">14</span><span class="cl">│       └── mako/
</span></span><span class="line"><span class="ln">15</span><span class="cl">│           └── config
</span></span><span class="line"><span class="ln">16</span><span class="cl">├── hyprlock/
</span></span><span class="line"><span class="ln">17</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln">18</span><span class="cl">│       └── hypr/
</span></span><span class="line"><span class="ln">19</span><span class="cl">│           └── hyprlock.conf
</span></span><span class="line"><span class="ln">20</span><span class="cl">├── gtk/
</span></span><span class="line"><span class="ln">21</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln">22</span><span class="cl">│       └── gtk-3.0/
</span></span><span class="line"><span class="ln">23</span><span class="cl">│           └── settings.ini
</span></span><span class="line"><span class="ln">24</span><span class="cl">└── themes/
</span></span><span class="line"><span class="ln">25</span><span class="cl">    └── colors.conf          # 集中配色定義</span></span></code></pre></div><h2 id="rice-的投資報酬判讀">Rice 的投資報酬判讀</h2>
<p>Rice 可以投入的時間沒有上限。務實的分界線：</p>
<ul>
<li><strong>功能性配置</strong>（waybar 顯示正確資訊、wofi 能搜到 app、通知會跳出來）：投入一到兩小時，這是桌面可用的前提</li>
<li><strong>視覺統一</strong>（全域配色一致、字型統一、圓角/間距協調）：投入半天到一天，這是「好看」跟「雜亂」的分界</li>
<li><strong>精雕細節</strong>（自定義動畫曲線、pixel-perfect 對齊、自製 widget）：時間無底洞，看個人興趣</li>
</ul>
<p>前兩層是值得做的——它們改善每天使用的體驗。第三層是嗜好領域，跟「把車改到完美」是同一種動力，不需要理性上的正當性。</p>
]]></content:encoded></item><item><title>商業環境的開發環境配置管理</title><link>https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/commercial-environment/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/commercial-environment/</guid><description>&lt;p>在企業環境裡，「開發環境標準化」的需求更加尖銳——安全政策、合規要求、軟體授權、機器數量（數十到數千台）都放大了管理複雜度。&lt;/p>
&lt;h2 id="常見做法">常見做法&lt;/h2>
&lt;h3 id="最低限度readme--onboarding-文件">最低限度：README + onboarding 文件&lt;/h3>
&lt;p>專案 repo 裡寫一份 &lt;code>CONTRIBUTING.md&lt;/code> 或 wiki 頁面，列出環境需求和設定步驟。新人照著做。成本最低但最容易過時——文件跟實際環境的漂移很常見，沒有自動化驗證機制時尤其如此。&lt;/p>
&lt;h3 id="中間層腳本化--ci-驗證">中間層：腳本化 + CI 驗證&lt;/h3>
&lt;p>把環境設定寫成 bootstrap script（同 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">Bootstrap Script 設計&lt;/a>），新人跑一次就好。CI 裡用相同的 script 或 Docker image 確保環境一致。比文件可靠，但 script 本身的維護和跨 OS 相容性是挑戰。&lt;/p>
&lt;p>Runtime 版本管理可以先從 &lt;strong>mise&lt;/strong>（前身 rtx）或 &lt;strong>asdf&lt;/strong> 開始：專案 repo 裡放一份 &lt;code>.tool-versions&lt;/code>（或 mise 的 &lt;code>mise.toml&lt;/code>），定義 Node/Ruby/Python/Go 的版本號，團隊成員跑 &lt;code>mise install&lt;/code> 就對齊。這比完整 devcontainer 輕量、比純 README 可靠，適合「只需要統一 runtime 版本、不需要容器化整個環境」的小團隊。它的邊界是只管語言版本——系統套件、服務依賴（PostgreSQL、Redis）、OS 層差異不在它的守備範圍。&lt;/p>
&lt;h3 id="成熟層devcontainer--nix--標準化-vm-image">成熟層：Devcontainer / Nix / 標準化 VM image&lt;/h3>
&lt;p>環境定義進專案 repo（devcontainer.json 或 flake.nix），每個開發者的環境從同一份定義產生。新人 onboarding 從「照文件設定半天」變成「打開專案等五分鐘」。&lt;/p>
&lt;h3 id="企業層受管裝置--mdm--內部套件-registry">企業層：受管裝置 + MDM + 內部套件 registry&lt;/h3>
&lt;p>大企業用 MDM（Mobile Device Management，企業裝置管理）控制開發機的安全基線，內部 registry 管理核准的套件版本，開發環境的「自由度」受限於安全政策。個人 dotfile 在這個層級仍然有效——它管的是「政策允許範圍內的個人偏好」。&lt;/p>
&lt;h2 id="跟-infra-的銜接">跟 Infra 的銜接&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">Dotfile 心智模型&lt;/a>把 dotfile 定位為「個人的環境 as code」、跟 Infra 的 IaC 平行。這裡的銜接點是：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Infra IaC&lt;/strong> 管雲端資源（VPC、EC2、RDS）&lt;/li>
&lt;li>&lt;strong>CI/CD pipeline&lt;/strong> 管建置和部署流程&lt;/li>
&lt;li>&lt;strong>Devcontainer / Nix&lt;/strong> 管開發環境定義&lt;/li>
&lt;li>&lt;strong>個人 Dotfile&lt;/strong> 管開發者的操作偏好&lt;/li>
&lt;/ul>
&lt;p>四層從組織到個人、從基礎設施到桌面，各自版控、各自演進，但共用「環境狀態用代碼描述」的思想。&lt;/p>
&lt;h2 id="判讀什麼時候該從個人-dotfile-往上走">判讀：什麼時候該從個人 Dotfile 往上走&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>建議動作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>新人 onboarding 環境設定要花半天以上&lt;/td>
 &lt;td>先寫 bootstrap script、再評估 devcontainer&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「在我電腦上能跑」的問題每月出現一次以上&lt;/td>
 &lt;td>把 runtime 版本和系統依賴定義進專案 repo&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CI 環境跟本機行為不一致&lt;/td>
 &lt;td>統一 CI 和本機的基底環境（Docker image 或 Nix）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>團隊超過五人、OS 組合超過兩種&lt;/td>
 &lt;td>devcontainer 或 Nix 的投資報酬率開始正向&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>企業有安全合規要求（核准軟體、版本鎖定）&lt;/td>
 &lt;td>需要受管環境 + 內部 registry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>向管理層提案標準化時，量化基準有助說服力：手動 onboarding 通常要半天到一天（找文件 + 裝套件 + 跑設定 + 除錯差異）。導入 devcontainer 後要區分兩個階段：初次 build（拉 base image + 安裝 feature + 跑 postCreateCommand）取決於 image 大小和網路速度，企業 proxy 或 VPN 環境下通常 20-60 分鐘；但之後的 reopen（image 已在本機 cache）只需 1-5 分鐘。日常體驗是後者——第一次 build 是一次性成本，後續每次打開專案都在分鐘級。初始投入大約一到兩個工作天（寫 devcontainer.json + 測試 + 文件化），之後維護成本隨專案依賴更新而遞增、但遠低於每次 onboarding 的重複成本。具體數字要從團隊自己的 onboarding 紀錄和 DORA 指標取得——「上一個新人花了幾天才送出第一個 PR」是最直接的 baseline。&lt;/p></description><content:encoded><![CDATA[<p>在企業環境裡，「開發環境標準化」的需求更加尖銳——安全政策、合規要求、軟體授權、機器數量（數十到數千台）都放大了管理複雜度。</p>
<h2 id="常見做法">常見做法</h2>
<h3 id="最低限度readme--onboarding-文件">最低限度：README + onboarding 文件</h3>
<p>專案 repo 裡寫一份 <code>CONTRIBUTING.md</code> 或 wiki 頁面，列出環境需求和設定步驟。新人照著做。成本最低但最容易過時——文件跟實際環境的漂移很常見，沒有自動化驗證機制時尤其如此。</p>
<h3 id="中間層腳本化--ci-驗證">中間層：腳本化 + CI 驗證</h3>
<p>把環境設定寫成 bootstrap script（同 <a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">Bootstrap Script 設計</a>），新人跑一次就好。CI 裡用相同的 script 或 Docker image 確保環境一致。比文件可靠，但 script 本身的維護和跨 OS 相容性是挑戰。</p>
<p>Runtime 版本管理可以先從 <strong>mise</strong>（前身 rtx）或 <strong>asdf</strong> 開始：專案 repo 裡放一份 <code>.tool-versions</code>（或 mise 的 <code>mise.toml</code>），定義 Node/Ruby/Python/Go 的版本號，團隊成員跑 <code>mise install</code> 就對齊。這比完整 devcontainer 輕量、比純 README 可靠，適合「只需要統一 runtime 版本、不需要容器化整個環境」的小團隊。它的邊界是只管語言版本——系統套件、服務依賴（PostgreSQL、Redis）、OS 層差異不在它的守備範圍。</p>
<h3 id="成熟層devcontainer--nix--標準化-vm-image">成熟層：Devcontainer / Nix / 標準化 VM image</h3>
<p>環境定義進專案 repo（devcontainer.json 或 flake.nix），每個開發者的環境從同一份定義產生。新人 onboarding 從「照文件設定半天」變成「打開專案等五分鐘」。</p>
<h3 id="企業層受管裝置--mdm--內部套件-registry">企業層：受管裝置 + MDM + 內部套件 registry</h3>
<p>大企業用 MDM（Mobile Device Management，企業裝置管理）控制開發機的安全基線，內部 registry 管理核准的套件版本，開發環境的「自由度」受限於安全政策。個人 dotfile 在這個層級仍然有效——它管的是「政策允許範圍內的個人偏好」。</p>
<h2 id="跟-infra-的銜接">跟 Infra 的銜接</h2>
<p><a href="/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">Dotfile 心智模型</a>把 dotfile 定位為「個人的環境 as code」、跟 Infra 的 IaC 平行。這裡的銜接點是：</p>
<ul>
<li><strong>Infra IaC</strong> 管雲端資源（VPC、EC2、RDS）</li>
<li><strong>CI/CD pipeline</strong> 管建置和部署流程</li>
<li><strong>Devcontainer / Nix</strong> 管開發環境定義</li>
<li><strong>個人 Dotfile</strong> 管開發者的操作偏好</li>
</ul>
<p>四層從組織到個人、從基礎設施到桌面，各自版控、各自演進，但共用「環境狀態用代碼描述」的思想。</p>
<h2 id="判讀什麼時候該從個人-dotfile-往上走">判讀：什麼時候該從個人 Dotfile 往上走</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>建議動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新人 onboarding 環境設定要花半天以上</td>
          <td>先寫 bootstrap script、再評估 devcontainer</td>
      </tr>
      <tr>
          <td>「在我電腦上能跑」的問題每月出現一次以上</td>
          <td>把 runtime 版本和系統依賴定義進專案 repo</td>
      </tr>
      <tr>
          <td>CI 環境跟本機行為不一致</td>
          <td>統一 CI 和本機的基底環境（Docker image 或 Nix）</td>
      </tr>
      <tr>
          <td>團隊超過五人、OS 組合超過兩種</td>
          <td>devcontainer 或 Nix 的投資報酬率開始正向</td>
      </tr>
      <tr>
          <td>企業有安全合規要求（核准軟體、版本鎖定）</td>
          <td>需要受管環境 + 內部 registry</td>
      </tr>
  </tbody>
</table>
<p>向管理層提案標準化時，量化基準有助說服力：手動 onboarding 通常要半天到一天（找文件 + 裝套件 + 跑設定 + 除錯差異）。導入 devcontainer 後要區分兩個階段：初次 build（拉 base image + 安裝 feature + 跑 postCreateCommand）取決於 image 大小和網路速度，企業 proxy 或 VPN 環境下通常 20-60 分鐘；但之後的 reopen（image 已在本機 cache）只需 1-5 分鐘。日常體驗是後者——第一次 build 是一次性成本，後續每次打開專案都在分鐘級。初始投入大約一到兩個工作天（寫 devcontainer.json + 測試 + 文件化），之後維護成本隨專案依賴更新而遞增、但遠低於每次 onboarding 的重複成本。具體數字要從團隊自己的 onboarding 紀錄和 DORA 指標取得——「上一個新人花了幾天才送出第一個 PR」是最直接的 baseline。</p>
<p>個人 dotfile 是起點，不是終點。當環境一致性的需求從「一個人的舒適」擴展到「團隊的生產力」，就是往上走的時機。</p>
]]></content:encoded></item><item><title>跨平台共用一個 Repo</title><link>https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/cross-platform-one-repo/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/cross-platform-one-repo/</guid><description>&lt;p>macOS 跟 Linux 可以用同一個 dotfile repo。不需要 fork 成兩個 repo——兩個 repo 的同步成本會隨時間膨脹，改了 git config 或 neovim 設定要在兩邊各 commit 一次，忘了同步就漂移。&lt;/p>
&lt;p>本文的做法基於 GNU Stow——將 dotfile repo 的檔案透過 symlink 對應到家目錄的工具（完整說明見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/management-strategies/" data-link-title="管理策略與選型" data-link-desc="要選 dotfile 管理工具時回來讀 — bare repo、stow、chezmoi 的適用場景與選型判讀">管理策略與選型&lt;/a>）。一個 repo 跨平台的做法是三層分離，每層處理不同粒度的差異：&lt;/p>
&lt;h2 id="第一層stow-選擇性安裝">第一層：stow 選擇性安裝&lt;/h2>
&lt;p>Stow 的 package 機制天然支持跨平台。repo 裡同時放 macOS 和 Linux 的配置，安裝時只 stow 該 OS 需要的 package：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># macOS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">stow zsh git zellij btop broot
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># Linux desktop&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">stow zsh git zellij btop broot hyprland waybar wofi mako&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>hyprland、waybar 這些目錄放在 repo 裡但 macOS 不 stow，不會有副作用。bootstrap script 裡用 &lt;code>uname&lt;/code> 自動決定要 stow 哪些 package：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nv">PACKAGES&lt;/span>&lt;span class="o">=(&lt;/span>zsh git zellij btop broot&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname -s&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;Linux&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> pkg in hyprland waybar wofi mako hyprlock&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="o">[[&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$pkg&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nv">PACKAGES&lt;/span>&lt;span class="o">+=(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$pkg&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="k">fi&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="第二層配置檔內的-os-分流">第二層：配置檔內的 OS 分流&lt;/h2>
&lt;p>同一份配置檔裡，用 &lt;code>uname&lt;/code> 區分 macOS 和 Linux 的差異。適合處理 PATH、工具初始化路徑這類「同一個用途、不同 OS 路徑」的狀況：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># path.zsh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="nb">typeset&lt;/span> -U PATH
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.local/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="c1"># 共用&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;Darwin&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="c1"># Homebrew Ruby、FVM、Android SDK（macOS 路徑）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="o">[[&lt;/span> -d &lt;span class="s2">&amp;#34;/opt/homebrew/opt/ruby/bin&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/opt/homebrew/opt/ruby/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/fvm/default/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/Library/Android/sdk/emulator&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;Linux&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="c1"># Android SDK（Linux 路徑）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="o">[[&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/Android/Sdk/emulator&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/Android/Sdk/emulator&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="k">fi&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># tools.zsh — autojump 的 source 路徑在兩個 OS 不同&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;Darwin&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="o">[&lt;/span> -f /opt/homebrew/etc/profile.d/autojump.sh &lt;span class="o">]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> . /opt/homebrew/etc/profile.d/autojump.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="o">[&lt;/span> -f /usr/share/autojump/autojump.sh &lt;span class="o">]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> . /usr/share/autojump/autojump.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="k">fi&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個路徑加 &lt;code>[[ -d ]]&lt;/code> 或 &lt;code>[ -f ]&lt;/code> 存在性檢查——即使在正確的 OS 上，如果該工具沒裝也不會報錯。&lt;/p>
&lt;h2 id="第三層localzsh-放機器專屬設定">第三層：local.zsh 放機器專屬設定&lt;/h2>
&lt;p>不是「macOS 的設定」而是「這台特定機器的設定」——工作專案 alias、公司 VPN proxy、只有這台機器需要的 PATH——放在 &lt;code>~/.config/zsh/local.zsh&lt;/code>，不進 Git。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># .zshrc 裡的載入方式&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="o">[[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.config/zsh/local.zsh&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">source&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/.config/zsh/local.zsh&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.config/zsh/local.zsh（不在 repo 裡，每台機器自己建）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> unimall-dev&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;cd ~/project/unimall_shop &amp;amp;&amp;amp; ./scripts/run_commands.sh dev&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">alias&lt;/span> unipos-dev&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;cd ~/project/unipos &amp;amp;&amp;amp; ./scripts/run_commands.sh dev&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Repo 裡放一份 &lt;code>local.zsh.example&lt;/code> 作為範本，&lt;code>.gitignore&lt;/code> 裡排除 &lt;code>local.zsh&lt;/code> 本身。&lt;/p></description><content:encoded><![CDATA[<p>macOS 跟 Linux 可以用同一個 dotfile repo。不需要 fork 成兩個 repo——兩個 repo 的同步成本會隨時間膨脹，改了 git config 或 neovim 設定要在兩邊各 commit 一次，忘了同步就漂移。</p>
<p>本文的做法基於 GNU Stow——將 dotfile repo 的檔案透過 symlink 對應到家目錄的工具（完整說明見<a href="/blog/linux/dotfile/01-dotfile-management/management-strategies/" data-link-title="管理策略與選型" data-link-desc="要選 dotfile 管理工具時回來讀 — bare repo、stow、chezmoi 的適用場景與選型判讀">管理策略與選型</a>）。一個 repo 跨平台的做法是三層分離，每層處理不同粒度的差異：</p>
<h2 id="第一層stow-選擇性安裝">第一層：stow 選擇性安裝</h2>
<p>Stow 的 package 機制天然支持跨平台。repo 裡同時放 macOS 和 Linux 的配置，安裝時只 stow 該 OS 需要的 package：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># macOS</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">stow zsh git zellij btop broot
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Linux desktop</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">stow zsh git zellij btop broot hyprland waybar wofi mako</span></span></code></pre></div><p>hyprland、waybar 這些目錄放在 repo 裡但 macOS 不 stow，不會有副作用。bootstrap script 裡用 <code>uname</code> 自動決定要 stow 哪些 package：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nv">PACKAGES</span><span class="o">=(</span>zsh git zellij btop broot<span class="o">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="k">$(</span>uname -s<span class="k">)</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;Linux&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">for</span> pkg in hyprland waybar wofi mako hyprlock<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="o">[[</span> -d <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nv">PACKAGES</span><span class="o">+=(</span><span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">done</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="k">fi</span></span></span></code></pre></div><h2 id="第二層配置檔內的-os-分流">第二層：配置檔內的 OS 分流</h2>
<p>同一份配置檔裡，用 <code>uname</code> 區分 macOS 和 Linux 的差異。適合處理 PATH、工具初始化路徑這類「同一個用途、不同 OS 路徑」的狀況：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># path.zsh</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nb">typeset</span> -U PATH
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>    <span class="c1"># 共用</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="k">$(</span>uname<span class="k">)</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;Darwin&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="c1"># Homebrew Ruby、FVM、Android SDK（macOS 路徑）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="o">[[</span> -d <span class="s2">&#34;/opt/homebrew/opt/ruby/bin&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;/opt/homebrew/opt/ruby/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/fvm/default/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$PATH</span><span class="s2">:</span><span class="nv">$HOME</span><span class="s2">/Library/Android/sdk/emulator&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="k">$(</span>uname<span class="k">)</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;Linux&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="c1"># Android SDK（Linux 路徑）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="o">[[</span> -d <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/Android/Sdk/emulator&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$PATH</span><span class="s2">:</span><span class="nv">$HOME</span><span class="s2">/Android/Sdk/emulator&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">fi</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># tools.zsh — autojump 的 source 路徑在兩個 OS 不同</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="k">$(</span>uname<span class="k">)</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;Darwin&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="o">[</span> -f /opt/homebrew/etc/profile.d/autojump.sh <span class="o">]</span> <span class="o">&amp;&amp;</span> . /opt/homebrew/etc/profile.d/autojump.sh
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="o">[</span> -f /usr/share/autojump/autojump.sh <span class="o">]</span> <span class="o">&amp;&amp;</span> . /usr/share/autojump/autojump.sh
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">fi</span></span></span></code></pre></div><p>每個路徑加 <code>[[ -d ]]</code> 或 <code>[ -f ]</code> 存在性檢查——即使在正確的 OS 上，如果該工具沒裝也不會報錯。</p>
<h2 id="第三層localzsh-放機器專屬設定">第三層：local.zsh 放機器專屬設定</h2>
<p>不是「macOS 的設定」而是「這台特定機器的設定」——工作專案 alias、公司 VPN proxy、只有這台機器需要的 PATH——放在 <code>~/.config/zsh/local.zsh</code>，不進 Git。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># .zshrc 裡的載入方式</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">[[</span> -f <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/zsh/local.zsh&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">source</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/zsh/local.zsh&#34;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># ~/.config/zsh/local.zsh（不在 repo 裡，每台機器自己建）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">alias</span> unimall-dev<span class="o">=</span><span class="s1">&#39;cd ~/project/unimall_shop &amp;&amp; ./scripts/run_commands.sh dev&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">alias</span> unipos-dev<span class="o">=</span><span class="s1">&#39;cd ~/project/unipos &amp;&amp; ./scripts/run_commands.sh dev&#39;</span></span></span></code></pre></div><p>Repo 裡放一份 <code>local.zsh.example</code> 作為範本，<code>.gitignore</code> 裡排除 <code>local.zsh</code> 本身。</p>
<h2 id="套件清單也分-os">套件清單也分 OS</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">~/dotfiles/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── Brewfile              # macOS: brew bundle dump 產生
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── packages-arch.txt     # Arch Linux: pacman -Qqe &gt; packages-arch.txt
</span></span><span class="line"><span class="ln">4</span><span class="cl">└── ...</span></span></code></pre></div><p>Bootstrap script 依 OS 讀對應的清單安裝套件。</p>
<h2 id="stow-跨平台的邊界">Stow 跨平台的邊界</h2>
<p>這個三層模型適合「配置檔本身大部分通用、差異只在 PATH 和少數工具路徑」的狀況——也就是多數開發者的實際情境。判斷是否需要遷移到 chezmoi 的訊號：</p>
<ul>
<li>同一份配置檔裡超過一半的行數是 OS 分流 → chezmoi template 更乾淨</li>
<li>需要在配置檔裡注入 secret（API key、token） → chezmoi 的 secret 管理是必要功能</li>
<li>管理的機器超過三種 OS/角色組合 → template 的條件判斷比 shell if-else 更可維護</li>
</ul>
<p>多數情況下 stow + uname + local.zsh 就足夠。</p>
]]></content:encoded></item><item><title>模組二：Shell 配置</title><link>https://tarrragon.github.io/blog/linux/dotfile/02-shell-config/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/02-shell-config/</guid><description>&lt;p>Shell 配置是 dotfile 管理裡最基礎也最常失控的一層。&lt;code>.zshrc&lt;/code> 或 &lt;code>.bashrc&lt;/code> 通常是開發者第一個開始客製的檔案，也是最容易長成數百行無結構巨檔的對象。這個模組教的是怎麼把 shell 配置拆成可維護的模組化結構。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/02-shell-config/zsh-modular-config/" data-link-title="Zsh 模組化配置" data-link-desc=".zshrc 長到數百行不敢動時回來讀">Zsh 模組化配置&lt;/a>&lt;/td>
 &lt;td>zsh/bash 載入順序、.zshrc 只做 source 的拆分結構、各模組的職責&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/02-shell-config/path-plugin-prompt/" data-link-title="PATH、Plugin 與 Prompt" data-link-desc="PATH 越來越長不知道怎麼管、要選 zsh plugin manager、或想設計 prompt 時回來讀">PATH、Plugin 與 Prompt&lt;/a>&lt;/td>
 &lt;td>PATH 管理原則、plugin manager 選型、prompt 設計、dotfile 目錄結構對應&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/management-strategies/" data-link-title="管理策略與選型" data-link-desc="要選 dotfile 管理工具時回來讀 — bare repo、stow、chezmoi 的適用場景與選型判讀">模組一：管理策略&lt;/a>：stow 的 package 概念怎麼對應 shell 配置的目錄結構&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/cross-platform-one-repo/" data-link-title="跨平台共用一個 Repo" data-link-desc="macOS 跟 Linux 要共用同一個 dotfile repo、不想維護兩份時回來讀">模組一：跨平台共用一個 Repo&lt;/a>：path.zsh 和 tools.zsh 裡的 OS 分流做法&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">模組三：終端機與編輯器&lt;/a>：terminal emulator 的配色跟 shell prompt 的配色協調&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Shell 配置是 dotfile 管理裡最基礎也最常失控的一層。<code>.zshrc</code> 或 <code>.bashrc</code> 通常是開發者第一個開始客製的檔案，也是最容易長成數百行無結構巨檔的對象。這個模組教的是怎麼把 shell 配置拆成可維護的模組化結構。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/02-shell-config/zsh-modular-config/" data-link-title="Zsh 模組化配置" data-link-desc=".zshrc 長到數百行不敢動時回來讀">Zsh 模組化配置</a></td>
          <td>zsh/bash 載入順序、.zshrc 只做 source 的拆分結構、各模組的職責</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/02-shell-config/path-plugin-prompt/" data-link-title="PATH、Plugin 與 Prompt" data-link-desc="PATH 越來越長不知道怎麼管、要選 zsh plugin manager、或想設計 prompt 時回來讀">PATH、Plugin 與 Prompt</a></td>
          <td>PATH 管理原則、plugin manager 選型、prompt 設計、dotfile 目錄結構對應</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/linux/dotfile/01-dotfile-management/management-strategies/" data-link-title="管理策略與選型" data-link-desc="要選 dotfile 管理工具時回來讀 — bare repo、stow、chezmoi 的適用場景與選型判讀">模組一：管理策略</a>：stow 的 package 概念怎麼對應 shell 配置的目錄結構</li>
<li>→ <a href="/blog/linux/dotfile/01-dotfile-management/cross-platform-one-repo/" data-link-title="跨平台共用一個 Repo" data-link-desc="macOS 跟 Linux 要共用同一個 dotfile repo、不想維護兩份時回來讀">模組一：跨平台共用一個 Repo</a>：path.zsh 和 tools.zsh 裡的 OS 分流做法</li>
<li>→ <a href="/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">模組三：終端機與編輯器</a>：terminal emulator 的配色跟 shell prompt 的配色協調</li>
</ul>
]]></content:encoded></item><item><title>最小安裝後的工具驗證與補足</title><link>https://tarrragon.github.io/blog/linux/install/minimal-install-verify/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/minimal-install-verify/</guid><description>&lt;p>最小化安裝給你的是一台能開機的系統，但「能開機」跟「能用」之間隔著一組「大家都假設存在」但其實沒被裝進去的工具。最小安裝（多數發行版的 &lt;code>base&lt;/code> 之類的套件組）刻意只裝開機與基本運作所需的東西，把工具的選擇權留給你。代價是許多你以為理所當然會在的指令——&lt;code>sudo&lt;/code>、&lt;code>which&lt;/code>、&lt;code>rsync&lt;/code>——一個都沒有。驗證它們在不在，比假設它們在安全。&lt;/p>
&lt;p>這層落差最常在你跑自動化腳本時引爆。一支 bootstrap script 的第一行可能就是 &lt;code>sudo pacman -S ...&lt;/code>，在一台連 &lt;code>sudo&lt;/code> 都沒有的機器上，它連第一步都跨不過去。所以裝好系統後、跑任何自動化之前，先過一輪工具驗證，把缺的補上。&lt;/p>
&lt;h2 id="sudo先有雞還是先有蛋">sudo：先有雞還是先有蛋&lt;/h2>
&lt;p>&lt;code>sudo&lt;/code> 是最容易被假設存在、卻最常缺席的工具，而且它的缺席有一個結構性的麻煩：補它的動作本身需要 root 權限。最小安裝通常不含 sudo。某些安裝程式（如本例的 archboot）即使你勾了「把這個使用者設為管理員」，那個動作也往往只是把使用者加進 &lt;code>wheel&lt;/code> 群組，並沒有真的裝上 sudo、也沒有啟用 sudoers 裡 wheel 群組的授權行。結果就是使用者「名義上是管理員」，但系統裡並沒有 sudo 這支指令。&lt;/p>
&lt;p>這形成一個先有雞還是先有蛋的關卡：bootstrap script 要靠 sudo 來裝套件，但 sudo 自己得先存在。它的解法不能是「把 sudo 寫進套件清單」——那份清單正是靠 sudo 來安裝的。sudo 只能是「跑 bootstrap 之前的前置」，用 root 身分手動補上（&lt;code>su -&lt;/code> 成為 root、&lt;code>echo &amp;gt; 檔案&lt;/code> 重導向、&lt;code>chmod&lt;/code> 設權限這些基礎操作不熟的話，見 &lt;a href="../basic-operations/">安裝過程用到的基礎操作&lt;/a>）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">su - &lt;span class="c1"># 切到 root（輸入 root 密碼）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">pacman -S --needed sudo &lt;span class="c1"># root 身分裝 sudo，不需要 sudo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;%wheel ALL=(ALL:ALL) ALL&amp;#39;&lt;/span> &amp;gt; /etc/sudoers.d/10-wheel &lt;span class="c1"># 啟用 wheel 群組授權&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">chmod &lt;span class="m">440&lt;/span> /etc/sudoers.d/10-wheel
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">visudo -c &lt;span class="c1"># 驗證 sudoers 語法，印 parsed OK 才安全&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">exit&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>切回一般使用者後用 &lt;code>sudo -v&lt;/code> 確認——能輸入密碼、沒報「不在 sudoers 檔」就成。這一步揭示一條通則：凡是 bootstrap 自身要依賴的工具，都不能由 bootstrap 來裝，必須當成前置先備好。&lt;code>sudo&lt;/code> 是這類前置最典型的一個。&lt;/p>
&lt;p>上面的指令以 Arch 的 &lt;code>pacman&lt;/code> 為例。Fedora 用 &lt;code>dnf&lt;/code>、Debian/Ubuntu 用 &lt;code>apt&lt;/code>；而 Debian 系的桌面與伺服器映像多半預設就裝了 sudo、也設好了授權，這個缺口主要出現在刻意精簡的 minimal 安裝。換句話說「sudo 是前置」這條判讀軸跨發行版成立，但「你這台到底缺不缺」要靠驗證、不是假設。&lt;/p>
&lt;h2 id="which腳本裡的隱形地雷">which：腳本裡的隱形地雷&lt;/h2>
&lt;p>&lt;code>which&lt;/code> 是另一個最小系統常缺、卻被腳本大量引用的指令，它的缺席會以一種隱晦的方式讓腳本出錯。很多腳本用 &lt;code>$(which zsh)&lt;/code> 之類的寫法取一支程式的完整路徑；在沒有 &lt;code>which&lt;/code> 的系統上，這個命令替換會吐出空字串，而下游拿到空字串的指令可能不會立刻報「找不到 which」，而是報一個看似無關的錯。實測中就遇過 &lt;code>chsh -s &amp;quot;$(which zsh)&amp;quot;&lt;/code> 因為 &lt;code>which&lt;/code> 不存在而變成 &lt;code>chsh -s &amp;quot;&amp;quot;&lt;/code>，最後報的是 &lt;code>chsh: shell must be a full path name&lt;/code>——錯誤訊息完全沒提到真正的元兇。&lt;/p>
&lt;p>正確的做法是用 &lt;code>command -v&lt;/code> 取代 &lt;code>which&lt;/code>。&lt;code>command -v&lt;/code> 是 POSIX 規範的 shell 內建，不依賴任何外部套件，在最小系統上一定存在。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">command&lt;/span> -v zsh &lt;span class="c1"># 印出 /usr/bin/zsh；找不到則回傳非零、不印東西&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這條判讀對你自己寫的腳本是「把 &lt;code>which&lt;/code> 全換成 &lt;code>command -v&lt;/code>」，對別人的腳本是「在缺 &lt;code>which&lt;/code> 的系統上，先補 &lt;code>which&lt;/code> 套件或改腳本」。它跟 sudo 的差別在於：&lt;code>which&lt;/code> 的缺席會悄悄製造一個誤導性的下游錯誤，而不是當場大聲報錯，所以更值得在驗證階段主動排掉。&lt;/p>
&lt;h2 id="其他常缺的工具">其他常缺的工具&lt;/h2>
&lt;p>除了 sudo 與 which，最小系統還常缺幾類在自動化裡會用到的工具，各有各的補法。它們不像 sudo 是硬前置，但缺了會在特定步驟卡住。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>缺了會怎樣&lt;/th>
 &lt;th>補法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>rsync&lt;/code>&lt;/td>
 &lt;td>從本機同步 dotfile 進機器時 &lt;code>rsync: command not found&lt;/code>&lt;/td>
 &lt;td>進套件清單；急用時改用 &lt;code>tar&lt;/code> over SSH 過渡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>ca-certificates&lt;/code>&lt;/td>
 &lt;td>HTTPS / 任何 TLS 連線在憑證驗證直接失敗（沒有信任根）&lt;/td>
 &lt;td>進套件清單；它是下一篇 HTTPS clone 的隱性前置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>hostname&lt;/code>&lt;/td>
 &lt;td>某些腳本呼叫 &lt;code>hostname&lt;/code> 取主機名時失敗&lt;/td>
 &lt;td>補 &lt;code>inetutils&lt;/code>，或改用 &lt;code>hostnamectl&lt;/code> / 讀 &lt;code>/etc/hostname&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>編譯工具鏈&lt;/td>
 &lt;td>從原始碼或社群套件庫編譯時缺 &lt;code>gcc&lt;/code> / &lt;code>make&lt;/code>&lt;/td>
 &lt;td>補發行版的開發工具組（如 &lt;code>base-devel&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>rsync&lt;/code> 的缺席要特別點出，因為它常被當成理所當然的傳輸工具。最小系統沒有它時，第一次把檔案弄進機器可以用兩邊都有的 &lt;code>tar&lt;/code> 搭配 SSH 過渡：&lt;/p></description><content:encoded><![CDATA[<p>最小化安裝給你的是一台能開機的系統，但「能開機」跟「能用」之間隔著一組「大家都假設存在」但其實沒被裝進去的工具。最小安裝（多數發行版的 <code>base</code> 之類的套件組）刻意只裝開機與基本運作所需的東西，把工具的選擇權留給你。代價是許多你以為理所當然會在的指令——<code>sudo</code>、<code>which</code>、<code>rsync</code>——一個都沒有。驗證它們在不在，比假設它們在安全。</p>
<p>這層落差最常在你跑自動化腳本時引爆。一支 bootstrap script 的第一行可能就是 <code>sudo pacman -S ...</code>，在一台連 <code>sudo</code> 都沒有的機器上，它連第一步都跨不過去。所以裝好系統後、跑任何自動化之前，先過一輪工具驗證，把缺的補上。</p>
<h2 id="sudo先有雞還是先有蛋">sudo：先有雞還是先有蛋</h2>
<p><code>sudo</code> 是最容易被假設存在、卻最常缺席的工具，而且它的缺席有一個結構性的麻煩：補它的動作本身需要 root 權限。最小安裝通常不含 sudo。某些安裝程式（如本例的 archboot）即使你勾了「把這個使用者設為管理員」，那個動作也往往只是把使用者加進 <code>wheel</code> 群組，並沒有真的裝上 sudo、也沒有啟用 sudoers 裡 wheel 群組的授權行。結果就是使用者「名義上是管理員」，但系統裡並沒有 sudo 這支指令。</p>
<p>這形成一個先有雞還是先有蛋的關卡：bootstrap script 要靠 sudo 來裝套件，但 sudo 自己得先存在。它的解法不能是「把 sudo 寫進套件清單」——那份清單正是靠 sudo 來安裝的。sudo 只能是「跑 bootstrap 之前的前置」，用 root 身分手動補上（<code>su -</code> 成為 root、<code>echo &gt; 檔案</code> 重導向、<code>chmod</code> 設權限這些基礎操作不熟的話，見 <a href="../basic-operations/">安裝過程用到的基礎操作</a>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">su -                                          <span class="c1"># 切到 root（輸入 root 密碼）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pacman -S --needed sudo                        <span class="c1"># root 身分裝 sudo，不需要 sudo</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;%wheel ALL=(ALL:ALL) ALL&#39;</span> &gt; /etc/sudoers.d/10-wheel   <span class="c1"># 啟用 wheel 群組授權</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">chmod <span class="m">440</span> /etc/sudoers.d/10-wheel
</span></span><span class="line"><span class="ln">5</span><span class="cl">visudo -c                                      <span class="c1"># 驗證 sudoers 語法，印 parsed OK 才安全</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">exit</span></span></code></pre></div><p>切回一般使用者後用 <code>sudo -v</code> 確認——能輸入密碼、沒報「不在 sudoers 檔」就成。這一步揭示一條通則：凡是 bootstrap 自身要依賴的工具，都不能由 bootstrap 來裝，必須當成前置先備好。<code>sudo</code> 是這類前置最典型的一個。</p>
<p>上面的指令以 Arch 的 <code>pacman</code> 為例。Fedora 用 <code>dnf</code>、Debian/Ubuntu 用 <code>apt</code>；而 Debian 系的桌面與伺服器映像多半預設就裝了 sudo、也設好了授權，這個缺口主要出現在刻意精簡的 minimal 安裝。換句話說「sudo 是前置」這條判讀軸跨發行版成立，但「你這台到底缺不缺」要靠驗證、不是假設。</p>
<h2 id="which腳本裡的隱形地雷">which：腳本裡的隱形地雷</h2>
<p><code>which</code> 是另一個最小系統常缺、卻被腳本大量引用的指令，它的缺席會以一種隱晦的方式讓腳本出錯。很多腳本用 <code>$(which zsh)</code> 之類的寫法取一支程式的完整路徑；在沒有 <code>which</code> 的系統上，這個命令替換會吐出空字串，而下游拿到空字串的指令可能不會立刻報「找不到 which」，而是報一個看似無關的錯。實測中就遇過 <code>chsh -s &quot;$(which zsh)&quot;</code> 因為 <code>which</code> 不存在而變成 <code>chsh -s &quot;&quot;</code>，最後報的是 <code>chsh: shell must be a full path name</code>——錯誤訊息完全沒提到真正的元兇。</p>
<p>正確的做法是用 <code>command -v</code> 取代 <code>which</code>。<code>command -v</code> 是 POSIX 規範的 shell 內建，不依賴任何外部套件，在最小系統上一定存在。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">command</span> -v zsh        <span class="c1"># 印出 /usr/bin/zsh；找不到則回傳非零、不印東西</span></span></span></code></pre></div><p>這條判讀對你自己寫的腳本是「把 <code>which</code> 全換成 <code>command -v</code>」，對別人的腳本是「在缺 <code>which</code> 的系統上，先補 <code>which</code> 套件或改腳本」。它跟 sudo 的差別在於：<code>which</code> 的缺席會悄悄製造一個誤導性的下游錯誤，而不是當場大聲報錯，所以更值得在驗證階段主動排掉。</p>
<h2 id="其他常缺的工具">其他常缺的工具</h2>
<p>除了 sudo 與 which，最小系統還常缺幾類在自動化裡會用到的工具，各有各的補法。它們不像 sudo 是硬前置，但缺了會在特定步驟卡住。</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>缺了會怎樣</th>
          <th>補法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>rsync</code></td>
          <td>從本機同步 dotfile 進機器時 <code>rsync: command not found</code></td>
          <td>進套件清單；急用時改用 <code>tar</code> over SSH 過渡</td>
      </tr>
      <tr>
          <td><code>ca-certificates</code></td>
          <td>HTTPS / 任何 TLS 連線在憑證驗證直接失敗（沒有信任根）</td>
          <td>進套件清單；它是下一篇 HTTPS clone 的隱性前置</td>
      </tr>
      <tr>
          <td><code>hostname</code></td>
          <td>某些腳本呼叫 <code>hostname</code> 取主機名時失敗</td>
          <td>補 <code>inetutils</code>，或改用 <code>hostnamectl</code> / 讀 <code>/etc/hostname</code></td>
      </tr>
      <tr>
          <td>編譯工具鏈</td>
          <td>從原始碼或社群套件庫編譯時缺 <code>gcc</code> / <code>make</code></td>
          <td>補發行版的開發工具組（如 <code>base-devel</code>）</td>
      </tr>
  </tbody>
</table>
<p><code>rsync</code> 的缺席要特別點出，因為它常被當成理所當然的傳輸工具。最小系統沒有它時，第一次把檔案弄進機器可以用兩邊都有的 <code>tar</code> 搭配 SSH 過渡：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">tar czf - --exclude <span class="s1">&#39;.git&#39;</span> . <span class="p">|</span> ssh user@host <span class="s1">&#39;mkdir -p ~/dest &amp;&amp; tar xzf - -C ~/dest&#39;</span></span></span></code></pre></div><p>這條的好處是不依賴目標機有 rsync；缺點是它每次都傳全部、沒有 rsync 的增量。在反覆同步的工作流裡，值得早點把 rsync 補進套件清單換取增量傳輸。</p>
<p><code>ca-certificates</code> 最容易在下一步咬人。最小系統可能沒有 CA 信任根，這時任何 HTTPS 連線——包括下一篇主推的「公開 repo 用 HTTPS clone」——會在 TLS 憑證驗證直接失敗，而錯誤訊息常指向 SSL handshake 而非「缺信任根」，容易誤判成網路問題。打算走 HTTPS 取得 dotfile 的機器，先確認 <code>ca-certificates</code> 在。<code>git</code> 與 <code>curl</code> 同理：它們是 bootstrap 取得程式碼的基本工具，下面的驗證迴圈也會檢查，最小系統若沒有要一併補。</p>
<p>剩下兩項的缺席各有觸發時機。<code>hostname</code> 只在腳本明確呼叫它取主機名時才會浮現缺失，而用 <code>hostnamectl</code> 或直接讀 <code>/etc/hostname</code> 可以繞過，所以它常被當成「補了省事、不補也有替代」的軟缺口。編譯工具鏈則是在你要從原始碼或社群套件庫編譯時才需要——純跑預編譯套件的機器可以不裝，但只要你的 dotfile 流程會編譯任何東西（例如從社群套件庫裝桌面元件），它就得進清單。</p>
<h2 id="系統性的驗證">系統性的驗證</h2>
<p>裝好系統後先跑一輪集中驗證、把缺口一次盤出來，比等腳本跑到一半才逐一踩雷省事。驗證的對象是「你接下來的流程會用到、但最小系統可能沒有」的工具。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">for</span> cmd in sudo git curl rsync tar zsh<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">if</span> <span class="nb">command</span> -v <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</span> &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;OK   </span><span class="nv">$cmd</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;缺   </span><span class="nv">$cmd</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>這段刻意用 <code>command -v</code> 來檢查（而不是 <code>which</code>），因為要檢查的對象之一正是「外部工具在不在」，用一個一定存在的內建來檢查才不會自己先掛掉。盤出來的缺口分兩類處理：bootstrap 自身依賴的（如 sudo）當前置手動補；其餘的（如 rsync、編譯工具）進套件清單，交給 bootstrap 一起裝。</p>
<h2 id="跟-bootstrap-套件清單的界線">跟 Bootstrap 套件清單的界線</h2>
<p>這篇的驗證跟 <a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八的 bootstrap script 設計</a> 是兩件互補的事，界線在「假設」上。bootstrap script 的套件清單假設一個前提：機器已經有能力執行安裝（有 sudo、有 package manager、清單裡的東西都能被裝上）。這篇處理的正是那個前提成立之前的階段——最小系統到底有沒有滿足那些假設，缺的補上，讓 bootstrap 的假設變成事實。</p>
<p>換句話說，套件清單回答「這台機器最終要有哪些套件」，工具驗證回答「這台機器現在夠不夠資格開始跑那份清單」。把兩者分清楚，才不會把 sudo 這種前置誤塞進靠 sudo 安裝的清單裡。</p>
<h2 id="下一步">下一步</h2>
<p>工具補齊、機器有能力執行安裝之後，你還困在一個地方：擠在機器的主控台手打。怎麼從舒適的本機終端機操作它，以及還沒有 SSH key 時怎麼把 dotfile 弄進去，<a href="../ssh-keyless-bootstrap/">外部連入、SSH key 與無 key 的 bootstrap 路徑</a> 處理這兩件事。</p>
]]></content:encoded></item><item><title>日誌判讀與診斷工具</title><link>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/</guid><description>&lt;p>恢復操作解決的是「怎麼讓桌面回來」，日誌判讀解決的是「為什麼會壞掉」。前者是急救，後者是找病因。如果同一個問題反覆出現，只做急救不找根因會一直繞圈。&lt;/p>
&lt;h2 id="journalctl系統日誌的主要入口">journalctl：系統日誌的主要入口&lt;/h2>
&lt;p>systemd 的日誌系統（journal）集中收錄所有 service、kernel、user session 的 log。&lt;code>journalctl&lt;/code> 是查詢這些日誌的指令。&lt;/p>
&lt;h3 id="基本用法">基本用法&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 本次開機的所有日誌&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">journalctl -b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 本次開機的錯誤以上等級（err + crit + alert + emerg）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">journalctl -b -p err
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 本次開機，只看最後 50 行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">journalctl -b -n &lt;span class="m">50&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># 上一次開機的日誌（如果問題發生在上次開機、這次重開後想查）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">journalctl -b -1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"># 即時跟蹤新 log（類似 tail -f）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">journalctl -f&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="過濾特定來源">過濾特定來源&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只看 Hyprland 相關&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">journalctl -b &lt;span class="p">|&lt;/span> grep -i hypr
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只看特定 systemd user unit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">journalctl --user -u waybar -b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只看 kernel 訊息（等同 dmesg）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">journalctl -b -k
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只看某個 process 的 log（用 PID）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">journalctl &lt;span class="nv">_PID&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">12345&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="時間範圍過濾">時間範圍過濾&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 最近 10 分鐘的 log&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">journalctl --since &lt;span class="s2">&amp;#34;10 min ago&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 指定時間區間&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">journalctl --since &lt;span class="s2">&amp;#34;2026-06-30 14:00&amp;#34;&lt;/span> --until &lt;span class="s2">&amp;#34;2026-06-30 14:30&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="dmesgkernel-層訊息">dmesg：Kernel 層訊息&lt;/h2>
&lt;p>&lt;code>dmesg&lt;/code> 顯示 kernel ring buffer 的內容——硬體偵測、driver 載入、硬體錯誤這些 kernel 層面的事件。排查 GPU driver 問題、USB 裝置問題、磁碟錯誤時需要看這裡。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 所有 kernel 訊息（帶時間戳記）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">dmesg -T
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只看錯誤和警告&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">dmesg -T --level&lt;span class="o">=&lt;/span>err,warn
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># GPU 相關（NVIDIA）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">dmesg -T &lt;span class="p">|&lt;/span> grep -i nvidia
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># GPU 相關（AMD）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">dmesg -T &lt;span class="p">|&lt;/span> grep -i amdgpu
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"># USB 相關（鍵盤滑鼠突然不回應時看這裡）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">dmesg -T &lt;span class="p">|&lt;/span> grep -i usb&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>GPU driver 問題在 dmesg 裡的嚴重度差異很大：&lt;/p>
&lt;p>一般 GPU hang（driver 嘗試自動恢復）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">[ 123.456] nvidia-modeset: ERROR: ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">[ 123.789] NVRM: Xid (PCI:0000:01:00): 79, pid=1234, ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">[ 124.000] amdgpu: GPU reset begin!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">[ 124.500] amdgpu: GPU reset succeeded&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>NVIDIA 的 &lt;code>Xid&lt;/code> 錯誤代碼表示不同類型的 GPU 錯誤。常見的 &lt;code>Xid 79&lt;/code> 是 GPU fallback，&lt;code>Xid 31&lt;/code> 是 GPU setup failure。完整代碼表可在 NVIDIA 官方文件搜尋「Xid Errors」。&lt;/p></description><content:encoded><![CDATA[<p>恢復操作解決的是「怎麼讓桌面回來」，日誌判讀解決的是「為什麼會壞掉」。前者是急救，後者是找病因。如果同一個問題反覆出現，只做急救不找根因會一直繞圈。</p>
<h2 id="journalctl系統日誌的主要入口">journalctl：系統日誌的主要入口</h2>
<p>systemd 的日誌系統（journal）集中收錄所有 service、kernel、user session 的 log。<code>journalctl</code> 是查詢這些日誌的指令。</p>
<h3 id="基本用法">基本用法</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 本次開機的所有日誌</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">journalctl -b
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 本次開機的錯誤以上等級（err + crit + alert + emerg）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">journalctl -b -p err
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 本次開機，只看最後 50 行</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">journalctl -b -n <span class="m">50</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 上一次開機的日誌（如果問題發生在上次開機、這次重開後想查）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">journalctl -b -1
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 即時跟蹤新 log（類似 tail -f）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">journalctl -f</span></span></code></pre></div><h3 id="過濾特定來源">過濾特定來源</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 只看 Hyprland 相關</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">journalctl -b <span class="p">|</span> grep -i hypr
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 只看特定 systemd user unit</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">journalctl --user -u waybar -b
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 只看 kernel 訊息（等同 dmesg）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">journalctl -b -k
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 只看某個 process 的 log（用 PID）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">journalctl <span class="nv">_PID</span><span class="o">=</span><span class="m">12345</span></span></span></code></pre></div><h3 id="時間範圍過濾">時間範圍過濾</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 最近 10 分鐘的 log</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">journalctl --since <span class="s2">&#34;10 min ago&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 指定時間區間</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">journalctl --since <span class="s2">&#34;2026-06-30 14:00&#34;</span> --until <span class="s2">&#34;2026-06-30 14:30&#34;</span></span></span></code></pre></div><h2 id="dmesgkernel-層訊息">dmesg：Kernel 層訊息</h2>
<p><code>dmesg</code> 顯示 kernel ring buffer 的內容——硬體偵測、driver 載入、硬體錯誤這些 kernel 層面的事件。排查 GPU driver 問題、USB 裝置問題、磁碟錯誤時需要看這裡。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 所有 kernel 訊息（帶時間戳記）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">dmesg -T
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 只看錯誤和警告</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">dmesg -T --level<span class="o">=</span>err,warn
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># GPU 相關（NVIDIA）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">dmesg -T <span class="p">|</span> grep -i nvidia
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># GPU 相關（AMD）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">dmesg -T <span class="p">|</span> grep -i amdgpu
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># USB 相關（鍵盤滑鼠突然不回應時看這裡）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">dmesg -T <span class="p">|</span> grep -i usb</span></span></code></pre></div><p>GPU driver 問題在 dmesg 裡的嚴重度差異很大：</p>
<p>一般 GPU hang（driver 嘗試自動恢復）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[  123.456] nvidia-modeset: ERROR: ...
</span></span><span class="line"><span class="ln">2</span><span class="cl">[  123.789] NVRM: Xid (PCI:0000:01:00): 79, pid=1234, ...
</span></span><span class="line"><span class="ln">3</span><span class="cl">[  124.000] amdgpu: GPU reset begin!
</span></span><span class="line"><span class="ln">4</span><span class="cl">[  124.500] amdgpu: GPU reset succeeded</span></span></code></pre></div><p>NVIDIA 的 <code>Xid</code> 錯誤代碼表示不同類型的 GPU 錯誤。常見的 <code>Xid 79</code> 是 GPU fallback，<code>Xid 31</code> 是 GPU setup failure。完整代碼表可在 NVIDIA 官方文件搜尋「Xid Errors」。</p>
<p>硬體層級故障（嚴重，可能需要檢查硬體）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[  123.789] NVRM: Xid (PCI:0000:01:00): 79, pid=1234, GPU has fallen off the bus</span></span></code></pre></div><p><code>GPU has fallen off the bus</code> 表示 GPU 跟主機板的 PCIe 連線完全中斷。偶發一次可能是 driver 問題，反覆出現通常是硬體故障（PCIe 供電不足、顯卡接觸不良、過熱）。</p>
<h2 id="hyprctlhyprland-的-runtime-狀態查詢">hyprctl：Hyprland 的 Runtime 狀態查詢</h2>
<p><code>hyprctl</code> 是 Hyprland 提供的命令列控制工具，可以在 compositor 運行中查詢狀態和執行操作。只有在 Hyprland 正在跑的時候才能使用。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 目前所有視窗的資訊</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">hyprctl clients
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 目前的 monitor 設定</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">hyprctl monitors
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 目前的 workspace 資訊</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">hyprctl workspaces
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># Hyprland 版本和 build 資訊</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">hyprctl version
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 重新載入 config（不重啟 compositor）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">hyprctl reload
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 查看上一次 config reload 是否有錯誤</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">hyprctl systeminfo</span></span></code></pre></div><p><code>hyprctl reload</code> 是測試 config 變更的安全方式。如果 config 有語法錯誤，reload 會報錯但 compositor 繼續用舊 config 跑，不會崩潰。</p>
<h2 id="systemctlservice-狀態管理">systemctl：Service 狀態管理</h2>
<p>桌面環境的工具（waybar、mako 等）如果用 systemd user unit 管理，可以用 <code>systemctl --user</code> 查看狀態和重啟。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 查看某個 user service 的狀態</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">systemctl --user status waybar
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 輸出範例：</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># waybar.service - Highly customizable Wayland bar</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">#    Loaded: loaded (/usr/lib/systemd/user/waybar.service; enabled)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">#    Active: active (running) since Mon 2026-06-30 10:00:00 CST</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">#    Main PID: 1234 (waybar)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 重啟</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">systemctl --user restart waybar
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 看最近的 log</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">systemctl --user status waybar -n <span class="m">20</span></span></span></code></pre></div><p>如果這些工具不是 systemd unit（在 Hyprland config 裡用 <code>exec-once</code> 啟動的），就不能用 <code>systemctl</code> 管理。改用 <code>pgrep</code> 和 <code>kill</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pgrep waybar      <span class="c1"># 查看是否在跑</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">killall waybar    <span class="c1"># 停止</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">waybar <span class="p">&amp;</span>          <span class="c1"># 背景啟動</span></span></span></code></pre></div><h2 id="即時資源監控">即時資源監控</h2>
<p>排查效能問題和記憶體耗盡時，需要看即時的系統資源使用情況。</p>
<p><strong>htop</strong>：互動式 process 監控。按 <code>M</code> 可以按記憶體用量排序，按 <code>P</code> 按 CPU 排序。找到佔用異常的 process 後按 <code>F9</code> 可以直接 kill。</p>
<p><strong>btop</strong>：功能更豐富的替代品，顯示 CPU、記憶體、磁碟、網路的即時使用情況，圖形化介面比 htop 直觀。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo pacman -S btop    <span class="c1"># Arch</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo apt install btop  <span class="c1"># Debian/Ubuntu</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 執行</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">btop</span></span></code></pre></div><p><strong>nvidia-smi</strong>：NVIDIA GPU 的專屬監控工具。顯示 GPU 使用率、記憶體、溫度、跑在上面的 process。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 一次性查看</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">nvidia-smi
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 持續監控（每 2 秒更新）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">nvidia-smi -l <span class="m">2</span></span></span></code></pre></div><h2 id="常見-log-pattern-速查">常見 Log Pattern 速查</h2>
<table>
  <thead>
      <tr>
          <th>Pattern</th>
          <th>出處</th>
          <th>代表什麼</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Out of memory: Killed process</code></td>
          <td>journalctl / dmesg</td>
          <td>OOM Killer 殺了某個 process</td>
          <td>檢查被殺的 process 名稱、設定 swap 或 systemd-oomd</td>
      </tr>
      <tr>
          <td><code>GPU has fallen off the bus</code></td>
          <td>dmesg</td>
          <td>NVIDIA GPU 完全失聯</td>
          <td>檢查 PCIe 供電、更新 driver、檢查硬體</td>
      </tr>
      <tr>
          <td><code>Xid ... pid=</code></td>
          <td>dmesg</td>
          <td>NVIDIA GPU 錯誤（Xid 編號對應不同類型的錯誤）</td>
          <td>查 NVIDIA 的 Xid 錯誤代碼表</td>
      </tr>
      <tr>
          <td><code>GPU reset begin</code></td>
          <td>dmesg</td>
          <td>AMD GPU driver 嘗試 reset GPU</td>
          <td>通常會自動恢復，頻繁出現代表 driver 或硬體問題</td>
      </tr>
      <tr>
          <td><code>segfault at</code></td>
          <td>journalctl</td>
          <td>某個 process segfault（記憶體存取違規）</td>
          <td>記下 process 名稱，搜尋該軟體的已知 bug</td>
      </tr>
      <tr>
          <td><code>Failed to start</code></td>
          <td>systemctl status</td>
          <td>systemd unit 啟動失敗</td>
          <td>看完整的 status 輸出和 journalctl log 找原因</td>
      </tr>
      <tr>
          <td><code>config error</code> / <code>parse error</code></td>
          <td>各工具自身的 log</td>
          <td>Config 檔語法錯誤</td>
          <td>檢查最近改過的 config 檔</td>
      </tr>
  </tbody>
</table>
<h2 id="排查流程">排查流程</h2>
<p>這篇是 Hyprland 桌面的具體日誌工具；背後「先讀權威狀態、不靠肉眼猜」的通用診斷心法（每種問題的權威來源、四步流程），見 <a href="/blog/linux/debug/diagnosis-read-authoritative-state/" data-link-title="診斷心法：讀權威狀態，不靠肉眼猜表象" data-link-desc="Linux 上一個現象看起來像 A 卻可能是 B、想建立一套先讀權威狀態再下判斷的除錯紀律、避免看畫面就猜而猜錯時回來讀">Linux 除錯與診斷：診斷心法</a>。遇到桌面環境問題時的判讀順序：</p>
<ol>
<li>
<p><strong>判斷影響範圍</strong>：只有一個視窗壞了、某個工具壞了、整個桌面壞了、還是系統完全不回應？影響範圍決定要看哪一層的 log。</p>
</li>
<li>
<p><strong>看 journalctl</strong>：<code>journalctl -b -p err</code> 先看本次開機有沒有錯誤等級的訊息。大部分 userspace 的問題（compositor crash、工具 crash）會出現在這裡。</p>
</li>
<li>
<p><strong>看 dmesg</strong>：如果 journalctl 沒有明顯線索、或症狀跟硬體有關（畫面凍結、USB 不回應），<code>dmesg -T --level=err,warn</code> 看 kernel 層有沒有硬體或 driver 錯誤。</p>
</li>
<li>
<p><strong>查特定工具的狀態</strong>：<code>systemctl --user status &lt;tool&gt;</code> 或 <code>pgrep &lt;tool&gt;</code> 確認工具是否還活著。如果死了，看它最後的 log 訊息。</p>
</li>
<li>
<p><strong>即時監控</strong>：如果問題是漸進式的（越來越慢、偶爾卡頓），開 <code>btop</code> 或 <code>htop</code> 觀察 CPU 和記憶體的即時趨勢，找出佔用異常的 process。</p>
</li>
</ol>
<h2 id="找到問題後的下一步">找到問題後的下一步</h2>
<p>判讀完 log 確認問題類型後，行動路徑依問題性質分流：</p>
<ul>
<li><strong>Config 錯誤</strong>：直接修 config，用 <code>hyprctl reload</code> 或重啟工具驗證。操作步驟見<a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作</a>。</li>
<li><strong>軟體 bug</strong>（segfault、特定操作觸發 crash）：到該軟體的 issue tracker（通常在 GitHub）搜尋錯誤訊息。Hyprland 的 issue tracker 在 <code>github.com/hyprwm/Hyprland</code>。回報 bug 時附上 <code>hyprctl systeminfo</code> 的輸出和相關的 journalctl log。</li>
<li><strong>GPU driver 問題</strong>：NVIDIA 用戶檢查是否有更新的 driver 版本（<code>pacman -Syu nvidia</code>）。AMD 用戶的 driver 跟 kernel 綁定，更新 kernel 就更新 driver（<code>pacman -Syu linux</code>）。</li>
<li><strong>硬體故障</strong>（<code>GPU has fallen off the bus</code> 反覆出現）：軟體層面無法解決，需要檢查硬體（PCIe 插槽接觸、供電、溫度）。</li>
</ul>
]]></content:encoded></item><item><title>環境建置的操作順序</title><link>https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/</guid><description>&lt;p>Dotfile 教學模組按主題組織（shell、終端機、視窗管理），適合理解各層概念。但第一次建環境時需要的是另一種順序——&lt;strong>按依賴關係排列的操作清單&lt;/strong>，因為有些步驟是後續步驟的前提。&lt;/p>
&lt;p>SSH key 是典型例子：管理工具和 shell 配置的知識在模組一和模組二，但實際操作時 SSH key 比這兩者都早——因為 &lt;code>git clone git@github.com:...&lt;/code> 本身就需要 SSH key。如果照模組順序走，到模組二才發現 dotfile repo clone 不下來。&lt;/p>
&lt;p>這篇是路線圖，告訴你每一步做什麼、為什麼這個順序、以及去哪個模組看具體操作。&lt;/p>
&lt;h2 id="階段一基礎設施後續所有步驟的前提">階段一：基礎設施（後續所有步驟的前提）&lt;/h2>
&lt;p>這些步驟在任何配置之前完成，因為它們是 Git、遠端存取、dotfile clone 的前提。&lt;/p>
&lt;h3 id="1-安裝作業系統--建立使用者帳號">1. 安裝作業系統 + 建立使用者帳號&lt;/h3>
&lt;p>macOS：開箱即用。Linux：選發行版（Arch 如果要用 Hyprland）、完成安裝、建立非 root 使用者。安裝程式每個選項該怎麼判讀、裝完最小系統缺哪些必要工具（連 &lt;code>sudo&lt;/code> 都可能沒有），見 &lt;a href="https://tarrragon.github.io/blog/linux/install/install-option-decisions/" data-link-title="Linux 安裝選項判讀" data-link-desc="在 Linux 安裝程式面對 locale、網路、磁碟分割、檔案系統、bootloader 等選項、需要判斷依據而非靠預設值硬選時回來讀">Linux 安裝選項判讀&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/linux/install/minimal-install-verify/" data-link-title="最小安裝後的工具驗證與補足" data-link-desc="最小化安裝的 Linux 裝完發現連 sudo 或 which 都沒有、bootstrap 腳本第一行就炸、需要先確認系統缺哪些必要工具再補時回來讀">最小安裝後的工具驗證與補足&lt;/a>——這一步是整個系列展開最深、卻最常被一句帶過的地基。&lt;/p>
&lt;h3 id="2-生成-ssh-key-pair">2. 生成 SSH key pair&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ssh-keygen -t ed25519 -C &lt;span class="s2">&amp;#34;your-email@example.com&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>為什麼這麼早做：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Git 操作&lt;/strong>：GitHub / GitLab 的 SSH 認證需要 public key。dotfile repo 通常用 SSH URL（&lt;code>git@github.com:...&lt;/code>），clone 前要先把 key 部署到 GitHub。用 HTTPS URL 可以繞過 SSH key，但長期來看 SSH key 是更省事的認證方式。還沒有 key、或想用 HTTPS / PAT 把 dotfile 弄進一台新機器的幾種路徑，見 &lt;a href="https://tarrragon.github.io/blog/linux/install/ssh-keyless-bootstrap/" data-link-title="外部連入、SSH key 與無 key 的 bootstrap 路徑" data-link-desc="要從本機終端機操作新裝好的 Linux 機器、設 SSH key 免密碼、或還沒有 key 就想把 dotfile 弄進機器跑 install.sh 時回來讀">外部連入、SSH key 與無 key 的 bootstrap 路徑&lt;/a>。&lt;/li>
&lt;li>&lt;strong>遠端救援&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">模組七&lt;/a>的場景三（GPU hang）依賴 SSH 作為桌面凍結時的救生通道。key 提前設好，出問題時才有路可走。&lt;/li>
&lt;li>&lt;strong>跨機器操作&lt;/strong>：筆電連桌機、桌機連 VM、VS Code Remote SSH——都靠這把 key。&lt;/li>
&lt;/ul>
&lt;h3 id="3-部署-public-key">3. 部署 public key&lt;/h3>
&lt;p>把 &lt;code>~/.ssh/id_ed25519.pub&lt;/code> 加到需要的服務：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 加到 GitHub&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">cat ~/.ssh/id_ed25519.pub
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 複製輸出，貼到 GitHub → Settings → SSH and GPG keys → New SSH key&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 加到另一台機器（可選，用於跨機器 SSH）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">ssh-copy-id user@target-machine&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="4-package-manager--git">4. Package manager + Git&lt;/h3>
&lt;p>macOS：先裝 Homebrew（macOS 的套件管理器，後續安裝 stow、tmux 等工具都靠它），再裝 Git：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 安裝 Homebrew&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">/bin/bash -c &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 安裝 Git（或用 xcode-select --install，會一併裝 Apple 的 Git）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">brew install git&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Arch：&lt;code>pacman&lt;/code> 隨 OS 安裝已可用，直接裝 Git：&lt;code>pacman -S git&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<p>Dotfile 教學模組按主題組織（shell、終端機、視窗管理），適合理解各層概念。但第一次建環境時需要的是另一種順序——<strong>按依賴關係排列的操作清單</strong>，因為有些步驟是後續步驟的前提。</p>
<p>SSH key 是典型例子：管理工具和 shell 配置的知識在模組一和模組二，但實際操作時 SSH key 比這兩者都早——因為 <code>git clone git@github.com:...</code> 本身就需要 SSH key。如果照模組順序走，到模組二才發現 dotfile repo clone 不下來。</p>
<p>這篇是路線圖，告訴你每一步做什麼、為什麼這個順序、以及去哪個模組看具體操作。</p>
<h2 id="階段一基礎設施後續所有步驟的前提">階段一：基礎設施（後續所有步驟的前提）</h2>
<p>這些步驟在任何配置之前完成，因為它們是 Git、遠端存取、dotfile clone 的前提。</p>
<h3 id="1-安裝作業系統--建立使用者帳號">1. 安裝作業系統 + 建立使用者帳號</h3>
<p>macOS：開箱即用。Linux：選發行版（Arch 如果要用 Hyprland）、完成安裝、建立非 root 使用者。安裝程式每個選項該怎麼判讀、裝完最小系統缺哪些必要工具（連 <code>sudo</code> 都可能沒有），見 <a href="/blog/linux/install/install-option-decisions/" data-link-title="Linux 安裝選項判讀" data-link-desc="在 Linux 安裝程式面對 locale、網路、磁碟分割、檔案系統、bootloader 等選項、需要判斷依據而非靠預設值硬選時回來讀">Linux 安裝選項判讀</a> 與 <a href="/blog/linux/install/minimal-install-verify/" data-link-title="最小安裝後的工具驗證與補足" data-link-desc="最小化安裝的 Linux 裝完發現連 sudo 或 which 都沒有、bootstrap 腳本第一行就炸、需要先確認系統缺哪些必要工具再補時回來讀">最小安裝後的工具驗證與補足</a>——這一步是整個系列展開最深、卻最常被一句帶過的地基。</p>
<h3 id="2-生成-ssh-key-pair">2. 生成 SSH key pair</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh-keygen -t ed25519 -C <span class="s2">&#34;your-email@example.com&#34;</span></span></span></code></pre></div><p>為什麼這麼早做：</p>
<ul>
<li><strong>Git 操作</strong>：GitHub / GitLab 的 SSH 認證需要 public key。dotfile repo 通常用 SSH URL（<code>git@github.com:...</code>），clone 前要先把 key 部署到 GitHub。用 HTTPS URL 可以繞過 SSH key，但長期來看 SSH key 是更省事的認證方式。還沒有 key、或想用 HTTPS / PAT 把 dotfile 弄進一台新機器的幾種路徑，見 <a href="/blog/linux/install/ssh-keyless-bootstrap/" data-link-title="外部連入、SSH key 與無 key 的 bootstrap 路徑" data-link-desc="要從本機終端機操作新裝好的 Linux 機器、設 SSH key 免密碼、或還沒有 key 就想把 dotfile 弄進機器跑 install.sh 時回來讀">外部連入、SSH key 與無 key 的 bootstrap 路徑</a>。</li>
<li><strong>遠端救援</strong>：<a href="/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">模組七</a>的場景三（GPU hang）依賴 SSH 作為桌面凍結時的救生通道。key 提前設好，出問題時才有路可走。</li>
<li><strong>跨機器操作</strong>：筆電連桌機、桌機連 VM、VS Code Remote SSH——都靠這把 key。</li>
</ul>
<h3 id="3-部署-public-key">3. 部署 public key</h3>
<p>把 <code>~/.ssh/id_ed25519.pub</code> 加到需要的服務：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 加到 GitHub</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">cat ~/.ssh/id_ed25519.pub
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 複製輸出，貼到 GitHub → Settings → SSH and GPG keys → New SSH key</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 加到另一台機器（可選，用於跨機器 SSH）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">ssh-copy-id user@target-machine</span></span></code></pre></div><h3 id="4-package-manager--git">4. Package manager + Git</h3>
<p>macOS：先裝 Homebrew（macOS 的套件管理器，後續安裝 stow、tmux 等工具都靠它），再裝 Git：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 安裝 Homebrew</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">/bin/bash -c <span class="s2">&#34;</span><span class="k">$(</span>curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 安裝 Git（或用 xcode-select --install，會一併裝 Apple 的 Git）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">brew install git</span></span></code></pre></div><p>Arch：<code>pacman</code> 隨 OS 安裝已可用，直接裝 Git：<code>pacman -S git</code>。</p>
<h3 id="5-clone-dotfile-repo">5. Clone dotfile repo</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git clone git@github.com:yourname/dotfiles.git ~/dotfiles</span></span></code></pre></div><p>如果是第一次建 dotfile repo（還沒有 repo），先建一個空的再開始往裡面加配置。具體做法見<a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構</a>。</p>
<h2 id="階段二shell-與終端機日常操作的基礎">階段二：Shell 與終端機（日常操作的基礎）</h2>
<p>Shell 是所有操作的介面，終端機是 shell 的容器。這兩層配置好，後續的安裝、設定、除錯效率會高很多。</p>
<h3 id="6-安裝管理工具stow--chezmoi">6. 安裝管理工具（stow / chezmoi）</h3>
<p>把 dotfile repo 裡的配置 symlink 到正確位置。具體選型和操作見<a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一</a>。</p>
<h3 id="7-shell-配置zshrc--bashrc">7. Shell 配置（.zshrc / .bashrc）</h3>
<p>模組化拆分、PATH 設定、alias、prompt。做完這一步，終端機操作才順手。見<a href="/blog/linux/dotfile/02-shell-config/" data-link-title="模組二：Shell 配置" data-link-desc="shell 配置檔長成一坨不敢動時回來讀 — .zshrc/.bashrc 的結構化拆分、alias/function/PATH 的模組化設計">模組二：Shell 配置</a>。</p>
<h3 id="8-終端機--編輯器">8. 終端機 + 編輯器</h3>
<p>Terminal emulator 選型、tmux/zellij、neovim 基礎配置。見<a href="/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">模組三：終端機與編輯器</a>。</p>
<p>macOS 用戶到階段二完成後就有一個完整的工作環境。下一步依序讀<a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一</a>（管理工具選型）、<a href="/blog/linux/dotfile/02-shell-config/" data-link-title="模組二：Shell 配置" data-link-desc="shell 配置檔長成一坨不敢動時回來讀 — .zshrc/.bashrc 的結構化拆分、alias/function/PATH 的模組化設計">模組二</a>（shell 配置）、<a href="/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">模組三</a>（終端機），然後跳到階段四的 bootstrap script。階段三是 Linux 桌面環境的設定，macOS 用戶跳過。</p>
<h2 id="階段三桌面環境linux-限定">階段三：桌面環境（Linux 限定）</h2>
<p>macOS 用戶到階段二就有一個完整的工作環境了。以下步驟是 Linux 桌面環境的設定，macOS 用戶可以跳到階段四。</p>
<h3 id="9-視窗管理器">9. 視窗管理器</h3>
<p>平鋪式 vs 浮動式的選型，Hyprland 安裝和核心配置。見<a href="/blog/linux/dotfile/04-window-management/" data-link-title="模組四：視窗管理與平鋪式工作流" data-link-desc="同時開多個視窗時的排列策略 — 手動貼齊跟自動平鋪的差距在哪、macOS 和 Linux 各有哪些工具、多螢幕怎麼處理、什麼情境值得從浮動切換到平鋪">模組四</a>和<a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五</a>。</p>
<h3 id="10-桌面配套工具--rice">10. 桌面配套工具 + Rice</h3>
<p>waybar、wofi、mako、配色系統。見<a href="/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計</a>。</p>
<h3 id="11-啟用-ssh-server--預防措施">11. 啟用 SSH server + 預防措施</h3>
<p>桌面環境可用之後，設定遠端救援通道和預防性配置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># SSH server（出問題時可以從另一台機器救援）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">sudo systemctl <span class="nb">enable</span> sshd
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">sudo systemctl start sshd
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 停用密碼登入（確保 SSH key 已設好）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 編輯 /etc/ssh/sshd_config：PasswordAuthentication no</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># swap（OOM 緩衝）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">sudo fallocate -l 4G /swapfile
</span></span><span class="line"><span class="ln">10</span><span class="cl">sudo chmod <span class="m">600</span> /swapfile <span class="o">&amp;&amp;</span> sudo mkswap /swapfile <span class="o">&amp;&amp;</span> sudo swapon /swapfile
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># systemd-oomd</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">sudo systemctl <span class="nb">enable</span> systemd-oomd</span></span></code></pre></div><p>為什麼放在桌面設定之後：SSH server 和 swap 是預防措施，桌面能用了才有東西要保護。但 SSH key pair（階段一步驟 2-3）要提前做——key pair 是認證基礎設施，server 只是把門打開。</p>
<p>詳細的故障場景和預防措施見<a href="/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">模組七：桌面環境維護與故障排除</a>。</p>
<h2 id="階段四同步與可攜性">階段四：同步與可攜性</h2>
<p>環境建好之後，確保這份配置能搬到下一台機器。</p>
<h3 id="12-bootstrap-script">12. Bootstrap script</h3>
<p>把階段一到三的操作自動化成 script，下次換機器跑一次就好。見<a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建</a>。</p>
<h3 id="13-secret-管理">13. Secret 管理</h3>
<p>哪些東西該進 repo、哪些要排除（SSH private key、API token、密碼）。同樣見<a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八</a>。</p>
<h2 id="依賴關係速查">依賴關係速查</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">OS 安裝
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  └─ SSH key pair ← 後續所有 Git / SSH 操作的前提
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">       └─ Git 安裝
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">            └─ dotfile repo clone
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                 └─ 管理工具（stow link）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                      ├─ Shell 配置
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                      ├─ 終端機 + 編輯器
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                      └─ 桌面環境（Linux，macOS 到此為止 → 直接跳階段四）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                           └─ SSH server + 預防措施
</span></span><span class="line"><span class="ln">10</span><span class="cl">                                └─ Bootstrap script 自動化</span></span></code></pre></div><h2 id="可以亂序的步驟">可以亂序的步驟</h2>
<p>依賴圖裡<strong>同一層級的步驟</strong>可以調換順序。具體來說：</p>
<ul>
<li>Shell 配置、終端機配置、編輯器配置三者互不依賴，先做哪個都行</li>
<li>視窗管理器和桌面配套工具可以交替設定（先裝 Hyprland 再裝 waybar，或反過來）</li>
<li>swap 和 SSH server 互不依賴，先做哪個都行</li>
</ul>
<p>跨層級的依賴必須按順序：SSH key 是 clone repo 的前提，repo 是 stow link 的前提，stow link 是 shell 配置生效的前提。</p>
]]></content:encoded></item><item><title>Caelestia 總覽：預組裝的 Hyprland 桌面 Shell</title><link>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/caelestia-overview/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/caelestia-overview/</guid><description>&lt;p>Caelestia 是基於 Quickshell 框架的 Hyprland 桌面 shell。它把狀態列、啟動器、通知、鎖屏、桌布管理這些在平鋪式桌面上需要各自挑選和配置的元件，整合成一套設計一致的成品。本質上是一套高品質的 dotfiles/rice，不是一個穩定的桌面環境產品。&lt;/p>
&lt;h2 id="caelestia-提供的元件">Caelestia 提供的元件&lt;/h2>
&lt;p>手動拼裝的桌面需要 6-8 個獨立工具各自配置。Caelestia 把這些元件用 Quickshell 統一實作：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>元件&lt;/th>
 &lt;th>手動拼裝的對應物&lt;/th>
 &lt;th>Caelestia 的實作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>狀態列（Bar）&lt;/td>
 &lt;td>Waybar&lt;/td>
 &lt;td>工作區指示器、視窗標題、系統匣、時鐘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>啟動器（Launcher）&lt;/td>
 &lt;td>Wofi / Rofi&lt;/td>
 &lt;td>模糊搜尋應用程式 + 桌布選擇介面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>通知&lt;/td>
 &lt;td>Mako / Dunst&lt;/td>
 &lt;td>分組通知、過期控制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>鎖屏&lt;/td>
 &lt;td>Hyprlock&lt;/td>
 &lt;td>模糊背景、可選指紋認證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>OSD&lt;/td>
 &lt;td>自己用 script 拼&lt;/td>
 &lt;td>音量 / 亮度變更的螢幕顯示&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>桌布管理&lt;/td>
 &lt;td>Hyprpaper / Swww&lt;/td>
 &lt;td>每螢幕桌布、從桌布動態取色（Material Design 3）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dashboard&lt;/td>
 &lt;td>無對應（自己拼 widget）&lt;/td>
 &lt;td>媒體播放器（MPRIS）、CPU/GPU/RAM、天氣&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sidebar&lt;/td>
 &lt;td>無對應&lt;/td>
 &lt;td>WiFi / 藍牙 / 暗色模式快速開關&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Session menu&lt;/td>
 &lt;td>自己用 script 拼&lt;/td>
 &lt;td>登出 / 關機 / 重啟&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Audio visualizer&lt;/td>
 &lt;td>Cava（終端機）&lt;/td>
 &lt;td>桌面上的音訊視覺化&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="quickshell-框架">Quickshell 框架&lt;/h2>
&lt;p>Quickshell 是一個 Qt6/QML 的 shell framework，用來在 Wayland compositor 上渲染 UI 元件。它的角色是 Caelestia 跟 Hyprland 之間的中介層：&lt;/p>
&lt;ul>
&lt;li>用 QML（Qt 的宣告式 UI 語言）描述介面元件&lt;/li>
&lt;li>透過 &lt;code>wlr-layer-shell&lt;/code> 協議把元件渲染成 Wayland layer surface（這是狀態列、啟動器能「黏在」螢幕邊緣的原因）&lt;/li>
&lt;li>直接存取 Hyprland IPC，可以即時讀取工作區狀態、視窗資訊&lt;/li>
&lt;li>配置檔修改後自動 reload，不需要重啟&lt;/li>
&lt;/ul>
&lt;p>跟其他桌面 shell 框架的差異主要在底層渲染引擎和配置語言。&lt;/p>
&lt;h2 id="跟-ags--eww-的比較">跟 AGS / Eww 的比較&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>框架&lt;/th>
 &lt;th>語言&lt;/th>
 &lt;th>渲染引擎&lt;/th>
 &lt;th>優勢&lt;/th>
 &lt;th>劣勢&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Quickshell（Caelestia 用）&lt;/td>
 &lt;td>QML（Qt6）&lt;/td>
 &lt;td>Qt&lt;/td>
 &lt;td>即時視窗預覽、Hyprland IPC、auto-reload、豐富動畫&lt;/td>
 &lt;td>Qt theming 複雜、positioning 不直覺、部分服務不完整&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AGS / Astal&lt;/td>
 &lt;td>TypeScript / JS&lt;/td>
 &lt;td>GTK&lt;/td>
 &lt;td>完整的系統服務庫（Network、Bluetooth）、GObject、前端友善&lt;/td>
 &lt;td>GTK 在 Wayland layer surface 上的限制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Eww&lt;/td>
 &lt;td>Yuck（自定義）&lt;/td>
 &lt;td>GTK/Rust&lt;/td>
 &lt;td>輕量、配置語法簡單、生態成熟&lt;/td>
 &lt;td>功能不如前兩者豐富、Yuck 是自定義 DSL 要額外學&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選型判讀：想要開箱即用的華麗桌面 → Caelestia（Quickshell）。想要自己一個個拼、有前端經驗 → AGS。想要最簡單輕量 → Eww。想要完全控制每個細節 → 手動拼裝 waybar + wofi + mako。&lt;/p>
&lt;h2 id="跟手動拼裝的-trade-off">跟手動拼裝的 Trade-off&lt;/h2>
&lt;p>&lt;strong>Caelestia 的優勢&lt;/strong>是省去「挑工具 + 各自配置 + 統一視覺風格」的大量前期功夫。Material Design 3 的動態取色（從桌布自動提取配色方案套用到所有元件）是手動拼裝很難做到的。&lt;/p>
&lt;p>&lt;strong>代價&lt;/strong>是兩個：&lt;/p>
&lt;p>第一，鎖定在 Caelestia 的設計決策裡。你可以調配色、改模組顯示、換桌布，但桌面的整體結構和行為邏輯是 Caelestia 決定的。想要「狀態列放底部 + 用不同的 launcher」這種結構性改動，比手動拼裝困難得多。&lt;/p>
&lt;p>第二，穩定性風險。Caelestia 的 README 明確寫：「Configs, and internal tokens used in them, may be changed or removed without notice」。這表示一次更新可能靜默破壞你的自訂設定。這跟 Hyprland 本身的 breaking changes 疊加，等於你的桌面有兩層快速移動的依賴。&lt;/p></description><content:encoded><![CDATA[<p>Caelestia 是基於 Quickshell 框架的 Hyprland 桌面 shell。它把狀態列、啟動器、通知、鎖屏、桌布管理這些在平鋪式桌面上需要各自挑選和配置的元件，整合成一套設計一致的成品。本質上是一套高品質的 dotfiles/rice，不是一個穩定的桌面環境產品。</p>
<h2 id="caelestia-提供的元件">Caelestia 提供的元件</h2>
<p>手動拼裝的桌面需要 6-8 個獨立工具各自配置。Caelestia 把這些元件用 Quickshell 統一實作：</p>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>手動拼裝的對應物</th>
          <th>Caelestia 的實作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>狀態列（Bar）</td>
          <td>Waybar</td>
          <td>工作區指示器、視窗標題、系統匣、時鐘</td>
      </tr>
      <tr>
          <td>啟動器（Launcher）</td>
          <td>Wofi / Rofi</td>
          <td>模糊搜尋應用程式 + 桌布選擇介面</td>
      </tr>
      <tr>
          <td>通知</td>
          <td>Mako / Dunst</td>
          <td>分組通知、過期控制</td>
      </tr>
      <tr>
          <td>鎖屏</td>
          <td>Hyprlock</td>
          <td>模糊背景、可選指紋認證</td>
      </tr>
      <tr>
          <td>OSD</td>
          <td>自己用 script 拼</td>
          <td>音量 / 亮度變更的螢幕顯示</td>
      </tr>
      <tr>
          <td>桌布管理</td>
          <td>Hyprpaper / Swww</td>
          <td>每螢幕桌布、從桌布動態取色（Material Design 3）</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>無對應（自己拼 widget）</td>
          <td>媒體播放器（MPRIS）、CPU/GPU/RAM、天氣</td>
      </tr>
      <tr>
          <td>Sidebar</td>
          <td>無對應</td>
          <td>WiFi / 藍牙 / 暗色模式快速開關</td>
      </tr>
      <tr>
          <td>Session menu</td>
          <td>自己用 script 拼</td>
          <td>登出 / 關機 / 重啟</td>
      </tr>
      <tr>
          <td>Audio visualizer</td>
          <td>Cava（終端機）</td>
          <td>桌面上的音訊視覺化</td>
      </tr>
  </tbody>
</table>
<h2 id="quickshell-框架">Quickshell 框架</h2>
<p>Quickshell 是一個 Qt6/QML 的 shell framework，用來在 Wayland compositor 上渲染 UI 元件。它的角色是 Caelestia 跟 Hyprland 之間的中介層：</p>
<ul>
<li>用 QML（Qt 的宣告式 UI 語言）描述介面元件</li>
<li>透過 <code>wlr-layer-shell</code> 協議把元件渲染成 Wayland layer surface（這是狀態列、啟動器能「黏在」螢幕邊緣的原因）</li>
<li>直接存取 Hyprland IPC，可以即時讀取工作區狀態、視窗資訊</li>
<li>配置檔修改後自動 reload，不需要重啟</li>
</ul>
<p>跟其他桌面 shell 框架的差異主要在底層渲染引擎和配置語言。</p>
<h2 id="跟-ags--eww-的比較">跟 AGS / Eww 的比較</h2>
<table>
  <thead>
      <tr>
          <th>框架</th>
          <th>語言</th>
          <th>渲染引擎</th>
          <th>優勢</th>
          <th>劣勢</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Quickshell（Caelestia 用）</td>
          <td>QML（Qt6）</td>
          <td>Qt</td>
          <td>即時視窗預覽、Hyprland IPC、auto-reload、豐富動畫</td>
          <td>Qt theming 複雜、positioning 不直覺、部分服務不完整</td>
      </tr>
      <tr>
          <td>AGS / Astal</td>
          <td>TypeScript / JS</td>
          <td>GTK</td>
          <td>完整的系統服務庫（Network、Bluetooth）、GObject、前端友善</td>
          <td>GTK 在 Wayland layer surface 上的限制</td>
      </tr>
      <tr>
          <td>Eww</td>
          <td>Yuck（自定義）</td>
          <td>GTK/Rust</td>
          <td>輕量、配置語法簡單、生態成熟</td>
          <td>功能不如前兩者豐富、Yuck 是自定義 DSL 要額外學</td>
      </tr>
  </tbody>
</table>
<p>選型判讀：想要開箱即用的華麗桌面 → Caelestia（Quickshell）。想要自己一個個拼、有前端經驗 → AGS。想要最簡單輕量 → Eww。想要完全控制每個細節 → 手動拼裝 waybar + wofi + mako。</p>
<h2 id="跟手動拼裝的-trade-off">跟手動拼裝的 Trade-off</h2>
<p><strong>Caelestia 的優勢</strong>是省去「挑工具 + 各自配置 + 統一視覺風格」的大量前期功夫。Material Design 3 的動態取色（從桌布自動提取配色方案套用到所有元件）是手動拼裝很難做到的。</p>
<p><strong>代價</strong>是兩個：</p>
<p>第一，鎖定在 Caelestia 的設計決策裡。你可以調配色、改模組顯示、換桌布，但桌面的整體結構和行為邏輯是 Caelestia 決定的。想要「狀態列放底部 + 用不同的 launcher」這種結構性改動，比手動拼裝困難得多。</p>
<p>第二，穩定性風險。Caelestia 的 README 明確寫：「Configs, and internal tokens used in them, may be changed or removed without notice」。這表示一次更新可能靜默破壞你的自訂設定。這跟 Hyprland 本身的 breaking changes 疊加，等於你的桌面有兩層快速移動的依賴。</p>
<h2 id="三個-github-repo-的分工">三個 GitHub Repo 的分工</h2>
<table>
  <thead>
      <tr>
          <th>Repo</th>
          <th>角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>caelestia-dots/caelestia</code></td>
          <td>主 dotfiles — Hyprland config、應用程式配置</td>
      </tr>
      <tr>
          <td><code>caelestia-dots/shell</code></td>
          <td>UI 層 — Quickshell/QML 實作的所有桌面元件</td>
      </tr>
      <tr>
          <td><code>caelestia-dots/cli</code></td>
          <td>CLI 工具 — 安裝、主題切換、截圖、錄影等指令</td>
      </tr>
  </tbody>
</table>
<p>完整安裝（<code>caelestia install</code>）會同時部署三者。也可以只裝 shell（<code>caelestia-shell</code> AUR package），保留自己的 Hyprland config 和應用程式設定。</p>
<h2 id="定位認知">定位認知</h2>
<p>Caelestia 的本質是一套社群維護的 rice dotfiles，不是有 release cycle 和 backward compatibility 承諾的軟體產品。用它的心態應該接近「fork 別人的 dotfiles 來改」而不是「安裝一個桌面環境」。這個定位決定了它適合什麼人：享受折騰和客製化的人會從中得到很好的起點，想要穩定日用的人應該考慮 GNOME 或 KDE Plasma。</p>
]]></content:encoded></item><item><title>Linux Tiling WM 生態</title><link>https://tarrragon.github.io/blog/linux/dotfile/04-window-management/linux-tiling-wm/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/04-window-management/linux-tiling-wm/</guid><description>&lt;p>Linux 桌面的視窗管理跑在**顯示協議（display protocol）**上，目前有兩套。&lt;/p>
&lt;p>&lt;strong>X11&lt;/strong>（X Window System）是用了三十多年的傳統協議。成熟、穩定、所有工具都支援。設計上的根本問題是安全性——任何 X11 應用程式都能讀取其他視窗的內容和鍵盤輸入，這是協議層的限制，不是 bug。&lt;/p>
&lt;p>&lt;strong>Wayland&lt;/strong> 是設計來取代 X11 的新協議。每個應用程式只看得到自己的視窗、效能更好、對現代硬體的支援更完整。多數主流發行版已經把 Wayland 設為預設，但部分老舊應用程式和特殊需求（某些螢幕錄製、遠端桌面工具）的支援還在追趕中，這些情境會透過 XWayland（相容層）跑 X11 應用程式。&lt;/p>
&lt;p>新的 tiling WM 主要基於 Wayland 開發，X11 上的老牌（i3, bspwm, dwm）仍然活躍但不再是未來方向。&lt;/p>
&lt;h2 id="主流-tiling-wm">主流 Tiling WM&lt;/h2>
&lt;p>&lt;strong>i3（X11）/ sway（Wayland）&lt;/strong> 是社群最大、文件最齊全、行為最可預測的選擇。i3 跑在 X11 上，sway 是它的 Wayland 移植，配置格式幾乎相同。配置檔用自己的語法，直覺的 &lt;code>key = action&lt;/code> 對應。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl"># sway/config 片段
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">set $mod Mod4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">bindsym $mod+h focus left
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">bindsym $mod+j focus down
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">bindsym $mod+k focus up
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">bindsym $mod+l focus right
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">bindsym $mod+Shift+h move left
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">bindsym $mod+1 workspace number 1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">bindsym $mod+2 workspace number 2&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>穩定性是 i3/sway 最大的賣點——配置寫好之後很少因為更新而壞掉。適合想要可靠的平鋪工作流、不需要華麗視覺效果的人。&lt;/p>
&lt;p>&lt;strong>Hyprland（Wayland）&lt;/strong> 吸引的是在意桌面視覺品質的平鋪使用者——看 r/unixporn 的 Hyprland 截圖就知道，流暢的視窗切換動畫、圓角、視窗模糊、漸層邊框，這些傳統 tiling WM 社群視為不必要的裝飾，Hyprland 做成內建功能並且認真打磨。配置檔改完即時生效（hot reload），迭代調教的回饋循環很短。&lt;/p>
&lt;p>這份視覺投入換來的 trade-off 是穩定性。開發節奏快意味著偶爾會有 breaking changes——某次更新後配置語法改了（2026 年 4 月的 Lua 遷移就是一例）、某個選項改名或移除。把它當主力桌面，要有「更新後可能要回來調配置」的心理準備。&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">Hyprland 配置&lt;/a>會詳細講它的設定。&lt;/p>
&lt;p>&lt;strong>bspwm（X11）&lt;/strong> 是純粹的 BSP 樹狀分割。它只做一件事：管理視窗的樹狀結構。所有操作透過 &lt;code>bspc&lt;/code> 命令列工具驅動，快捷鍵綁定交給 sxhkd（一個獨立的快捷鍵 daemon）。UNIX 哲學——每個工具只做一件事，組合起來用。&lt;/p>
&lt;p>&lt;strong>dwm（X11）&lt;/strong> 極簡到配置要改 C 原始碼然後重新編譯。不是給「想要方便配置」的人用的，而是給「想要完全理解自己桌面每一行程式碼」的人用的。&lt;/p>
&lt;h2 id="拼裝式桌面的代價">拼裝式桌面的代價&lt;/h2>
&lt;p>Linux tiling WM 的桌面是你自己拼出來的：compositor（Hyprland/sway）負責視窗管理、狀態列（waybar）負責頂部資訊條、啟動器（rofi/wofi）負責 app 搜尋啟動、通知（mako/dunst）負責通知彈窗、鎖屏另外裝。每一塊是不同專案、不同作者。&lt;/p>
&lt;p>好處是每一塊都可以換。壞處是當某件事壞了，你要自己判斷是哪一層的問題。藍牙選單不能點？可能是狀態列的 module 設定錯、可能是 blueman 沒跑、可能是 D-Bus session 有問題。完整桌面環境（KDE/GNOME）幫你整合測試過了，拼裝式桌面沒有這層保障。&lt;/p>
&lt;h2 id="多螢幕的處理">多螢幕的處理&lt;/h2>
&lt;p>多螢幕是自動平鋪比較能展現價值的場景。每個螢幕是獨立的平鋪區域，視窗在哪個螢幕就在那個螢幕的範圍內平鋪。&lt;/p>
&lt;p>快捷鍵跨螢幕操作是標配功能：把焦點跳到另一個螢幕、把當前視窗丟到另一個螢幕（丟過去後自動融入那邊的平鋪佈局）。三螢幕時這個便利性比單螢幕更明顯——純滑鼠拖視窗跨螢幕需要的移動距離很長。&lt;/p>
&lt;p>工作區跟螢幕的綁定方式是各工具差異最大的地方。&lt;/p>
&lt;p>macOS 的 Spaces 是綁定螢幕的——每個螢幕有自己獨立的一組 Spaces。yabai 沿用這個行為，切換工作區時只影響當前螢幕。AeroSpace 用自己的虛擬工作區繞過 macOS 原生 Spaces 的限制，多螢幕操作被普遍認為更穩定、更直覺。&lt;/p>
&lt;p>Hyprland 的 workspace 可以動態指派到不同螢幕——workspace 3 現在在螢幕 A，你可以把它移到螢幕 B。這種模型彈性最大，但也需要清楚的心智模型來管理「哪個 workspace 在哪」。&lt;/p>
&lt;p>熱插拔螢幕（接上、拔掉外接螢幕）是各工具的常見痛點。拔掉螢幕時，該螢幕上的視窗要搬到剩餘螢幕重新平鋪；插上螢幕時，佈局要擴展。多數工具有對應設定（記住螢幕配置、自動還原佈局），但體驗不見得完美，偶爾需要手動整理。&lt;/p>
&lt;h2 id="dotfile-中的視窗管理配置">Dotfile 中的視窗管理配置&lt;/h2>
&lt;p>視窗管理是 dotfile 管理裡「桌面層」的核心。WM 配置檔決定了整個桌面的操作邏輯——快捷鍵怎麼綁、視窗間距多大、哪些 app 要浮動、工作區怎麼分配。&lt;/p>
&lt;p>macOS 工具的配置檔通常是一個檔案：AeroSpace 的 &lt;code>~/.aerospace.toml&lt;/code>、yabai 的 &lt;code>~/.yabairc&lt;/code> + &lt;code>~/.skhdrc&lt;/code>、Amethyst 的 &lt;code>~/.amethyst.yml&lt;/code>。把這些檔案放進 dotfile repo，換 Mac 時就能還原整套視窗管理行為。&lt;/p>
&lt;p>Linux tiling WM 的配置在 &lt;code>~/.config/&lt;/code> 下，通常是一個資料夾：Hyprland 的 &lt;code>~/.config/hypr/&lt;/code>、sway 的 &lt;code>~/.config/sway/&lt;/code>、i3 的 &lt;code>~/.config/i3/&lt;/code>。除了 WM 本身，狀態列（waybar 的 &lt;code>~/.config/waybar/&lt;/code>）、啟動器（rofi 的 &lt;code>~/.config/rofi/&lt;/code>）、通知（mako 的 &lt;code>~/.config/mako/&lt;/code>）等周邊元件的配置也各自有檔案。一套完整的 Linux 平鋪桌面，dotfile repo 裡可能會有十幾個配置目錄——這也是為什麼 Linux 桌面客製化社群那麼依賴 dotfile 管理工具（見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">管理工具與目錄結構&lt;/a>）。&lt;/p></description><content:encoded><![CDATA[<p>Linux 桌面的視窗管理跑在**顯示協議（display protocol）**上，目前有兩套。</p>
<p><strong>X11</strong>（X Window System）是用了三十多年的傳統協議。成熟、穩定、所有工具都支援。設計上的根本問題是安全性——任何 X11 應用程式都能讀取其他視窗的內容和鍵盤輸入，這是協議層的限制，不是 bug。</p>
<p><strong>Wayland</strong> 是設計來取代 X11 的新協議。每個應用程式只看得到自己的視窗、效能更好、對現代硬體的支援更完整。多數主流發行版已經把 Wayland 設為預設，但部分老舊應用程式和特殊需求（某些螢幕錄製、遠端桌面工具）的支援還在追趕中，這些情境會透過 XWayland（相容層）跑 X11 應用程式。</p>
<p>新的 tiling WM 主要基於 Wayland 開發，X11 上的老牌（i3, bspwm, dwm）仍然活躍但不再是未來方向。</p>
<h2 id="主流-tiling-wm">主流 Tiling WM</h2>
<p><strong>i3（X11）/ sway（Wayland）</strong> 是社群最大、文件最齊全、行為最可預測的選擇。i3 跑在 X11 上，sway 是它的 Wayland 移植，配置格式幾乎相同。配置檔用自己的語法，直覺的 <code>key = action</code> 對應。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl"># sway/config 片段
</span></span><span class="line"><span class="ln">2</span><span class="cl">set $mod Mod4
</span></span><span class="line"><span class="ln">3</span><span class="cl">bindsym $mod+h focus left
</span></span><span class="line"><span class="ln">4</span><span class="cl">bindsym $mod+j focus down
</span></span><span class="line"><span class="ln">5</span><span class="cl">bindsym $mod+k focus up
</span></span><span class="line"><span class="ln">6</span><span class="cl">bindsym $mod+l focus right
</span></span><span class="line"><span class="ln">7</span><span class="cl">bindsym $mod+Shift+h move left
</span></span><span class="line"><span class="ln">8</span><span class="cl">bindsym $mod+1 workspace number 1
</span></span><span class="line"><span class="ln">9</span><span class="cl">bindsym $mod+2 workspace number 2</span></span></code></pre></div><p>穩定性是 i3/sway 最大的賣點——配置寫好之後很少因為更新而壞掉。適合想要可靠的平鋪工作流、不需要華麗視覺效果的人。</p>
<p><strong>Hyprland（Wayland）</strong> 吸引的是在意桌面視覺品質的平鋪使用者——看 r/unixporn 的 Hyprland 截圖就知道，流暢的視窗切換動畫、圓角、視窗模糊、漸層邊框，這些傳統 tiling WM 社群視為不必要的裝飾，Hyprland 做成內建功能並且認真打磨。配置檔改完即時生效（hot reload），迭代調教的回饋循環很短。</p>
<p>這份視覺投入換來的 trade-off 是穩定性。開發節奏快意味著偶爾會有 breaking changes——某次更新後配置語法改了（2026 年 4 月的 Lua 遷移就是一例）、某個選項改名或移除。把它當主力桌面，要有「更新後可能要回來調配置」的心理準備。<a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">Hyprland 配置</a>會詳細講它的設定。</p>
<p><strong>bspwm（X11）</strong> 是純粹的 BSP 樹狀分割。它只做一件事：管理視窗的樹狀結構。所有操作透過 <code>bspc</code> 命令列工具驅動，快捷鍵綁定交給 sxhkd（一個獨立的快捷鍵 daemon）。UNIX 哲學——每個工具只做一件事，組合起來用。</p>
<p><strong>dwm（X11）</strong> 極簡到配置要改 C 原始碼然後重新編譯。不是給「想要方便配置」的人用的，而是給「想要完全理解自己桌面每一行程式碼」的人用的。</p>
<h2 id="拼裝式桌面的代價">拼裝式桌面的代價</h2>
<p>Linux tiling WM 的桌面是你自己拼出來的：compositor（Hyprland/sway）負責視窗管理、狀態列（waybar）負責頂部資訊條、啟動器（rofi/wofi）負責 app 搜尋啟動、通知（mako/dunst）負責通知彈窗、鎖屏另外裝。每一塊是不同專案、不同作者。</p>
<p>好處是每一塊都可以換。壞處是當某件事壞了，你要自己判斷是哪一層的問題。藍牙選單不能點？可能是狀態列的 module 設定錯、可能是 blueman 沒跑、可能是 D-Bus session 有問題。完整桌面環境（KDE/GNOME）幫你整合測試過了，拼裝式桌面沒有這層保障。</p>
<h2 id="多螢幕的處理">多螢幕的處理</h2>
<p>多螢幕是自動平鋪比較能展現價值的場景。每個螢幕是獨立的平鋪區域，視窗在哪個螢幕就在那個螢幕的範圍內平鋪。</p>
<p>快捷鍵跨螢幕操作是標配功能：把焦點跳到另一個螢幕、把當前視窗丟到另一個螢幕（丟過去後自動融入那邊的平鋪佈局）。三螢幕時這個便利性比單螢幕更明顯——純滑鼠拖視窗跨螢幕需要的移動距離很長。</p>
<p>工作區跟螢幕的綁定方式是各工具差異最大的地方。</p>
<p>macOS 的 Spaces 是綁定螢幕的——每個螢幕有自己獨立的一組 Spaces。yabai 沿用這個行為，切換工作區時只影響當前螢幕。AeroSpace 用自己的虛擬工作區繞過 macOS 原生 Spaces 的限制，多螢幕操作被普遍認為更穩定、更直覺。</p>
<p>Hyprland 的 workspace 可以動態指派到不同螢幕——workspace 3 現在在螢幕 A，你可以把它移到螢幕 B。這種模型彈性最大，但也需要清楚的心智模型來管理「哪個 workspace 在哪」。</p>
<p>熱插拔螢幕（接上、拔掉外接螢幕）是各工具的常見痛點。拔掉螢幕時，該螢幕上的視窗要搬到剩餘螢幕重新平鋪；插上螢幕時，佈局要擴展。多數工具有對應設定（記住螢幕配置、自動還原佈局），但體驗不見得完美，偶爾需要手動整理。</p>
<h2 id="dotfile-中的視窗管理配置">Dotfile 中的視窗管理配置</h2>
<p>視窗管理是 dotfile 管理裡「桌面層」的核心。WM 配置檔決定了整個桌面的操作邏輯——快捷鍵怎麼綁、視窗間距多大、哪些 app 要浮動、工作區怎麼分配。</p>
<p>macOS 工具的配置檔通常是一個檔案：AeroSpace 的 <code>~/.aerospace.toml</code>、yabai 的 <code>~/.yabairc</code> + <code>~/.skhdrc</code>、Amethyst 的 <code>~/.amethyst.yml</code>。把這些檔案放進 dotfile repo，換 Mac 時就能還原整套視窗管理行為。</p>
<p>Linux tiling WM 的配置在 <code>~/.config/</code> 下，通常是一個資料夾：Hyprland 的 <code>~/.config/hypr/</code>、sway 的 <code>~/.config/sway/</code>、i3 的 <code>~/.config/i3/</code>。除了 WM 本身，狀態列（waybar 的 <code>~/.config/waybar/</code>）、啟動器（rofi 的 <code>~/.config/rofi/</code>）、通知（mako 的 <code>~/.config/mako/</code>）等周邊元件的配置也各自有檔案。一套完整的 Linux 平鋪桌面，dotfile repo 裡可能會有十幾個配置目錄——這也是為什麼 Linux 桌面客製化社群那麼依賴 dotfile 管理工具（見<a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">管理工具與目錄結構</a>）。</p>
<p>跟螢幕硬體綁定的設定（螢幕解析度、縮放比、螢幕排列順序）通常也寫在 WM 配置裡。這部分在跨機器搬移 dotfile 時需要調整——同一份 Hyprland 配置裡的 <code>monitor</code> 設定，在筆電上是一個螢幕、在桌機上可能是三個。常見做法是把硬體相關設定拆到單獨檔案（如 <code>monitors.lua</code>），主配置用 <code>require</code> 引入，這樣跨機器時只需要替換這一個檔案。<a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">同步與環境重建</a>會講跨機器搬移時怎麼處理這類硬體差異。</p>
]]></content:encoded></item><item><title>Neovim 配置</title><link>https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/neovim-config/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/neovim-config/</guid><description>&lt;p>Neovim 的配置是 dotfile 裡最複雜的單一工具——plugin 生態龐大、整個配置系統基於 Lua。Lua 語法基礎見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/lua-scripting-language/" data-link-title="Lua 腳本語言" data-link-desc="在 Hyprland 或 Neovim 配置檔遇到 Lua 語法看不懂時回來讀 — 配置檔需要的最小 Lua 知識">Lua 腳本語言&lt;/a>。&lt;/p>
&lt;p>配置路徑：&lt;code>~/.config/nvim/&lt;/code>&lt;/p>
&lt;h2 id="配置結構">配置結構&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">~/.config/nvim/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">├── init.lua # 入口，source 其他模組
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">├── lua/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">│ ├── options.lua # 基本設定（行號、tab 寬度、搜尋行為）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">│ ├── keymaps.lua # 快捷鍵
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">│ ├── autocmds.lua # 自動指令
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">│ └── plugins/ # 各 plugin 的配置
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">│ ├── init.lua # plugin manager 載入
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">│ ├── lsp.lua # LSP 設定
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">│ ├── telescope.lua # fuzzy finder
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">│ ├── treesitter.lua
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">│ └── ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">└── lazy-lock.json # lazy.nvim 的 lockfile（要進 Git）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="是否該用預設配置包">是否該用預設配置包&lt;/h2>
&lt;p>LazyVim、NvChad、AstroNvim 這類預設配置包提供了一整套開箱即用的 neovim 設定。判讀：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>用預設配置包&lt;/strong>：想要快速可用的 IDE-like 體驗、不想花時間逐一選 plugin 和配置。Dotfile 裡放的是「對預設的覆寫」&lt;/li>
&lt;li>&lt;strong>自己從零組&lt;/strong>：想完全理解每一個 plugin 做什麼、容忍前期投入時間。Dotfile 裡放的是完整配置&lt;/li>
&lt;/ul>
&lt;p>兩種都是 dotfile 管理的合法對象。差異在出問題時的除錯路徑：自己組的知道每一行做什麼，預設配置包的要先理解它的分層才能改。&lt;/p>
&lt;h2 id="dotfile-結構對應">Dotfile 結構對應&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">~/dotfiles/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">├── alacritty/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">│ └── .config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">│ └── alacritty/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">│ └── alacritty.toml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">├── tmux/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">│ └── .config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">│ └── tmux/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">│ └── tmux.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">├── nvim/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">│ └── .config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">│ └── nvim/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">│ ├── init.lua
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">│ ├── lazy-lock.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">│ └── lua/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">│ └── ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">└── zellij/ # 如果用 zellij
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> └── .config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> └── zellij/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> └── config.kdl&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個工具是獨立的 stow package，可以在不同機器選擇性安裝。例如伺服器只 &lt;code>stow tmux nvim&lt;/code>、桌面機才加 &lt;code>stow alacritty&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<p>Neovim 的配置是 dotfile 裡最複雜的單一工具——plugin 生態龐大、整個配置系統基於 Lua。Lua 語法基礎見 <a href="/blog/linux/dotfile/knowledge-cards/lua-scripting-language/" data-link-title="Lua 腳本語言" data-link-desc="在 Hyprland 或 Neovim 配置檔遇到 Lua 語法看不懂時回來讀 — 配置檔需要的最小 Lua 知識">Lua 腳本語言</a>。</p>
<p>配置路徑：<code>~/.config/nvim/</code></p>
<h2 id="配置結構">配置結構</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">~/.config/nvim/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── init.lua              # 入口，source 其他模組
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">├── lua/
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│   ├── options.lua       # 基本設定（行號、tab 寬度、搜尋行為）
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│   ├── keymaps.lua       # 快捷鍵
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│   ├── autocmds.lua      # 自動指令
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│   └── plugins/          # 各 plugin 的配置
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">│       ├── init.lua      # plugin manager 載入
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">│       ├── lsp.lua       # LSP 設定
</span></span><span class="line"><span class="ln">10</span><span class="cl">│       ├── telescope.lua # fuzzy finder
</span></span><span class="line"><span class="ln">11</span><span class="cl">│       ├── treesitter.lua
</span></span><span class="line"><span class="ln">12</span><span class="cl">│       └── ...
</span></span><span class="line"><span class="ln">13</span><span class="cl">└── lazy-lock.json        # lazy.nvim 的 lockfile（要進 Git）</span></span></code></pre></div><h2 id="是否該用預設配置包">是否該用預設配置包</h2>
<p>LazyVim、NvChad、AstroNvim 這類預設配置包提供了一整套開箱即用的 neovim 設定。判讀：</p>
<ul>
<li><strong>用預設配置包</strong>：想要快速可用的 IDE-like 體驗、不想花時間逐一選 plugin 和配置。Dotfile 裡放的是「對預設的覆寫」</li>
<li><strong>自己從零組</strong>：想完全理解每一個 plugin 做什麼、容忍前期投入時間。Dotfile 裡放的是完整配置</li>
</ul>
<p>兩種都是 dotfile 管理的合法對象。差異在出問題時的除錯路徑：自己組的知道每一行做什麼，預設配置包的要先理解它的分層才能改。</p>
<h2 id="dotfile-結構對應">Dotfile 結構對應</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">~/dotfiles/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── alacritty/
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│       └── alacritty/
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│           └── alacritty.toml
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">├── tmux/
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">│       └── tmux/
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">│           └── tmux.conf
</span></span><span class="line"><span class="ln">10</span><span class="cl">├── nvim/
</span></span><span class="line"><span class="ln">11</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln">12</span><span class="cl">│       └── nvim/
</span></span><span class="line"><span class="ln">13</span><span class="cl">│           ├── init.lua
</span></span><span class="line"><span class="ln">14</span><span class="cl">│           ├── lazy-lock.json
</span></span><span class="line"><span class="ln">15</span><span class="cl">│           └── lua/
</span></span><span class="line"><span class="ln">16</span><span class="cl">│               └── ...
</span></span><span class="line"><span class="ln">17</span><span class="cl">└── zellij/                    # 如果用 zellij
</span></span><span class="line"><span class="ln">18</span><span class="cl">    └── .config/
</span></span><span class="line"><span class="ln">19</span><span class="cl">        └── zellij/
</span></span><span class="line"><span class="ln">20</span><span class="cl">            └── config.kdl</span></span></code></pre></div><p>每個工具是獨立的 stow package，可以在不同機器選擇性安裝。例如伺服器只 <code>stow tmux nvim</code>、桌面機才加 <code>stow alacritty</code>。</p>
]]></content:encoded></item><item><title>Rice（桌面視覺客製化）</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/rice/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/rice/</guid><description>&lt;p>Rice 在 Linux 桌面社群指的是&lt;strong>桌面視覺客製化&lt;/strong>——把系統外觀調教成個人化的美學呈現。動詞 ricing 是「正在美化桌面」，名詞 a rice 是「一套美化成果/配置」，做這件事的人叫 ricer。&lt;/p>
&lt;h2 id="詞源">詞源&lt;/h2>
&lt;p>最被廣泛接受的說法是源自汽車改裝文化的 &amp;ldquo;rice burner&amp;rdquo; / &amp;ldquo;ricer&amp;rdquo;——原本指對（通常是日系的）平價車裝上浮誇外觀套件（大尾翼、炫光、貼紙），看起來拉風但實際性能沒提升。後來 Linux 社群借用這個概念：把桌面打扮得花俏好看，本質也是「外觀的炫技」。&lt;/p>
&lt;p>也有人提出 &amp;ldquo;Race Inspired Cosmetic Enhancements&amp;rdquo; 的逆向縮寫，但普遍被認為是事後湊的解釋。&lt;/p>
&lt;p>在 Linux 圈裡，rice 的原始貶意和種族色彩已經淡化，變成中性甚至帶自豪的自稱——r/unixporn 社群就是圍繞 ricing 成果的分享運轉的。&lt;/p>
&lt;h2 id="rice-涵蓋的範圍">Rice 涵蓋的範圍&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>配色方案&lt;/strong>：Catppuccin、Tokyo Night、Gruvbox、Nord 等跨工具統一的色彩定義&lt;/li>
&lt;li>&lt;strong>狀態列&lt;/strong>：Waybar、Eww 的模組設計和 CSS 外觀&lt;/li>
&lt;li>&lt;strong>啟動器&lt;/strong>：Wofi、Rofi 的搜尋框外觀&lt;/li>
&lt;li>&lt;strong>通知&lt;/strong>：Mako、Dunst 的通知氣泡樣式&lt;/li>
&lt;li>&lt;strong>鎖屏&lt;/strong>：Hyprlock 的登入畫面設計&lt;/li>
&lt;li>&lt;strong>桌布&lt;/strong>：靜態桌布或動態桌布（Swww）&lt;/li>
&lt;li>&lt;strong>終端機配色&lt;/strong>：Alacritty / Kitty / Foot 的 ANSI 色碼&lt;/li>
&lt;li>&lt;strong>字型&lt;/strong>：Nerd Font 的 icon glyph&lt;/li>
&lt;/ul>
&lt;p>Caelestia 這類「desktop shell」專案把上述元件統一設計出貨，是「打包好的 rice」。手動逐一挑選和調教各元件是「DIY rice」。兩者的目標相同——視覺上協調、好看、符合個人美學。&lt;/p>
&lt;p>完整的 rice 配置實務見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">桌面 Rice 設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Rice 在 Linux 桌面社群指的是<strong>桌面視覺客製化</strong>——把系統外觀調教成個人化的美學呈現。動詞 ricing 是「正在美化桌面」，名詞 a rice 是「一套美化成果/配置」，做這件事的人叫 ricer。</p>
<h2 id="詞源">詞源</h2>
<p>最被廣泛接受的說法是源自汽車改裝文化的 &ldquo;rice burner&rdquo; / &ldquo;ricer&rdquo;——原本指對（通常是日系的）平價車裝上浮誇外觀套件（大尾翼、炫光、貼紙），看起來拉風但實際性能沒提升。後來 Linux 社群借用這個概念：把桌面打扮得花俏好看，本質也是「外觀的炫技」。</p>
<p>也有人提出 &ldquo;Race Inspired Cosmetic Enhancements&rdquo; 的逆向縮寫，但普遍被認為是事後湊的解釋。</p>
<p>在 Linux 圈裡，rice 的原始貶意和種族色彩已經淡化，變成中性甚至帶自豪的自稱——r/unixporn 社群就是圍繞 ricing 成果的分享運轉的。</p>
<h2 id="rice-涵蓋的範圍">Rice 涵蓋的範圍</h2>
<ul>
<li><strong>配色方案</strong>：Catppuccin、Tokyo Night、Gruvbox、Nord 等跨工具統一的色彩定義</li>
<li><strong>狀態列</strong>：Waybar、Eww 的模組設計和 CSS 外觀</li>
<li><strong>啟動器</strong>：Wofi、Rofi 的搜尋框外觀</li>
<li><strong>通知</strong>：Mako、Dunst 的通知氣泡樣式</li>
<li><strong>鎖屏</strong>：Hyprlock 的登入畫面設計</li>
<li><strong>桌布</strong>：靜態桌布或動態桌布（Swww）</li>
<li><strong>終端機配色</strong>：Alacritty / Kitty / Foot 的 ANSI 色碼</li>
<li><strong>字型</strong>：Nerd Font 的 icon glyph</li>
</ul>
<p>Caelestia 這類「desktop shell」專案把上述元件統一設計出貨，是「打包好的 rice」。手動逐一挑選和調教各元件是「DIY rice」。兩者的目標相同——視覺上協調、好看、符合個人美學。</p>
<p>完整的 rice 配置實務見<a href="/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">桌面 Rice 設計</a>。</p>
]]></content:encoded></item><item><title>Workspace、Window Rules 與外觀</title><link>https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/workspace-rules-appearance/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/workspace-rules-appearance/</guid><description>&lt;p>本篇的配置範例使用 Lua 語法（Hyprland v0.55+）。Lua 基礎見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/lua-scripting-language/" data-link-title="Lua 腳本語言" data-link-desc="在 Hyprland 或 Neovim 配置檔遇到 Lua 語法看不懂時回來讀 — 配置檔需要的最小 Lua 知識">Lua 腳本語言&lt;/a>。&lt;/p>
&lt;h2 id="workspace-設定">Workspace 設定&lt;/h2>
&lt;p>Workspace 是平鋪式桌面的空間管理單位。Hyprland 的 workspace 是動態的——不需要預先定義多少個，按了不存在的 workspace 編號就會自動建立。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 把特定 workspace 綁定到特定螢幕&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">hl.config&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="n">workspace&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;1, monitor:DP-1, default:true&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;2, monitor:DP-1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;3, monitor:DP-1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;7, monitor:HDMI-A-1, default:true&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;8, monitor:HDMI-A-1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;9, monitor:HDMI-A-1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">})&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>常見的使用模式：&lt;/p>
&lt;ul>
&lt;li>Workspace 1-3 放在主螢幕（寫程式用）&lt;/li>
&lt;li>Workspace 7-9 放在副螢幕（瀏覽器、通訊軟體、監控）&lt;/li>
&lt;li>用鍵盤瞬間切換（SUPER + 數字鍵），比 alt-tab 在一堆視窗裡找快&lt;/li>
&lt;/ul>
&lt;h3 id="per-workspace-layout-覆寫">Per-workspace layout 覆寫&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">hl.config&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="n">workspace&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;1, layoutopt:orientation:left&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">-- workspace 1 用左側 master&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;2, layoutopt:orientation:top&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">-- workspace 2 用頂部 master&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">})&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>[VM 可測試]&lt;/strong> Workspace 切換、綁定邏輯、per-workspace layout。&lt;br>
&lt;strong>[需實機測試]&lt;/strong> Workspace 綁定到特定實體螢幕的行為。&lt;/p>&lt;/blockquote>
&lt;h2 id="window-rules">Window Rules&lt;/h2>
&lt;p>Window rules 讓特定應用程式在開啟時自動套用設定——指定 workspace、強制浮動、設定大小等。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="n">hl.config&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="n">windowrule&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- 特定 app 自動送到指定 workspace&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;workspace 8, class:^(firefox)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;workspace 9, class:^(Slack)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- 設定面板、對話框等維持浮動（不進平鋪）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;float, class:^(pavucontrol)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;float, class:^(nm-connection-editor)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;float, title:^(Open File)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;float, title:^(Save As)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- Picture-in-picture 浮動 + 置頂 + 固定大小&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;float, title:^(Picture-in-Picture)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;pin, title:^(Picture-in-Picture)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;size 480 270, title:^(Picture-in-Picture)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;move 100%-490 50, title:^(Picture-in-Picture)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- 視窗透明度（active inactive）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;opacity 0.9, class:^(kitty)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;opacity 0.85 0.75, class:^(Code)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- 防止 Electron app 自動最大化&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;suppressevent maximize, class:.*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="c1">-- XWayland video bridge（螢幕分享用）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;opacity 0.0 override, class:^(xwaylandvideobridge)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;noanim, class:^(xwaylandvideobridge)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;noinitialfocus, class:^(xwaylandvideobridge)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;maxsize 1 1, class:^(xwaylandvideobridge)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;noblur, class:^(xwaylandvideobridge)$&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl">&lt;span class="p">})&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="查詢應用程式的-class-和-title">查詢應用程式的 class 和 title&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">hyprctl clients &lt;span class="c1"># 列出所有開啟的視窗（含 class、title、PID 等）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">hyprctl activewindow &lt;span class="c1"># 當前焦點視窗的資訊&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Window rules 用 regex 匹配 &lt;code>class&lt;/code> 和 &lt;code>title&lt;/code>。靜態 rules 匹配的是視窗開啟時的初始值（&lt;code>initialTitle&lt;/code> / &lt;code>initialClass&lt;/code>）。&lt;/p></description><content:encoded><![CDATA[<p>本篇的配置範例使用 Lua 語法（Hyprland v0.55+）。Lua 基礎見 <a href="/blog/linux/dotfile/knowledge-cards/lua-scripting-language/" data-link-title="Lua 腳本語言" data-link-desc="在 Hyprland 或 Neovim 配置檔遇到 Lua 語法看不懂時回來讀 — 配置檔需要的最小 Lua 知識">Lua 腳本語言</a>。</p>
<h2 id="workspace-設定">Workspace 設定</h2>
<p>Workspace 是平鋪式桌面的空間管理單位。Hyprland 的 workspace 是動態的——不需要預先定義多少個，按了不存在的 workspace 編號就會自動建立。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 把特定 workspace 綁定到特定螢幕</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">workspace</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="s2">&#34;1, monitor:DP-1, default:true&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="s2">&#34;2, monitor:DP-1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="s2">&#34;3, monitor:DP-1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="s2">&#34;7, monitor:HDMI-A-1, default:true&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="s2">&#34;8, monitor:HDMI-A-1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="s2">&#34;9, monitor:HDMI-A-1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>常見的使用模式：</p>
<ul>
<li>Workspace 1-3 放在主螢幕（寫程式用）</li>
<li>Workspace 7-9 放在副螢幕（瀏覽器、通訊軟體、監控）</li>
<li>用鍵盤瞬間切換（SUPER + 數字鍵），比 alt-tab 在一堆視窗裡找快</li>
</ul>
<h3 id="per-workspace-layout-覆寫">Per-workspace layout 覆寫</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">workspace</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="s2">&#34;1, layoutopt:orientation:left&#34;</span><span class="p">,</span>     <span class="c1">-- workspace 1 用左側 master</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="s2">&#34;2, layoutopt:orientation:top&#34;</span><span class="p">,</span>      <span class="c1">-- workspace 2 用頂部 master</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><blockquote>
<p><strong>[VM 可測試]</strong> Workspace 切換、綁定邏輯、per-workspace layout。<br>
<strong>[需實機測試]</strong> Workspace 綁定到特定實體螢幕的行為。</p></blockquote>
<h2 id="window-rules">Window Rules</h2>
<p>Window rules 讓特定應用程式在開啟時自動套用設定——指定 workspace、強制浮動、設定大小等。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">windowrule</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="c1">-- 特定 app 自動送到指定 workspace</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="s2">&#34;workspace 8, class:^(firefox)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="s2">&#34;workspace 9, class:^(Slack)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="c1">-- 設定面板、對話框等維持浮動（不進平鋪）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="s2">&#34;float, class:^(pavucontrol)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="s2">&#34;float, class:^(nm-connection-editor)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="s2">&#34;float, title:^(Open File)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="s2">&#34;float, title:^(Save As)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="c1">-- Picture-in-picture 浮動 + 置頂 + 固定大小</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="s2">&#34;float, title:^(Picture-in-Picture)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="s2">&#34;pin, title:^(Picture-in-Picture)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="s2">&#34;size 480 270, title:^(Picture-in-Picture)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="s2">&#34;move 100%-490 50, title:^(Picture-in-Picture)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="c1">-- 視窗透明度（active inactive）</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="s2">&#34;opacity 0.9, class:^(kitty)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="s2">&#34;opacity 0.85 0.75, class:^(Code)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="c1">-- 防止 Electron app 自動最大化</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="s2">&#34;suppressevent maximize, class:.*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="c1">-- XWayland video bridge（螢幕分享用）</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="s2">&#34;opacity 0.0 override, class:^(xwaylandvideobridge)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="s2">&#34;noanim, class:^(xwaylandvideobridge)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="s2">&#34;noinitialfocus, class:^(xwaylandvideobridge)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="s2">&#34;maxsize 1 1, class:^(xwaylandvideobridge)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="s2">&#34;noblur, class:^(xwaylandvideobridge)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><h3 id="查詢應用程式的-class-和-title">查詢應用程式的 class 和 title</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hyprctl clients          <span class="c1"># 列出所有開啟的視窗（含 class、title、PID 等）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">hyprctl activewindow     <span class="c1"># 當前焦點視窗的資訊</span></span></span></code></pre></div><p>Window rules 用 regex 匹配 <code>class</code> 和 <code>title</code>。靜態 rules 匹配的是視窗開啟時的初始值（<code>initialTitle</code> / <code>initialClass</code>）。</p>
<p>Window rules 是讓平鋪式桌面「不只是把所有東西硬塞格子」的關鍵——識別哪些 app 適合浮動、哪些適合指定位置，需要累積使用經驗。</p>
<blockquote>
<p><strong>[VM 可測試]</strong> Window rules 邏輯、workspace 指派、float 規則。查 class/title 也在 VM 裡能做。</p></blockquote>
<h2 id="layout-配置">Layout 配置</h2>
<h3 id="dwindle">Dwindle</h3>
<p>每個新視窗把當前區域一分為二（螺旋式切分），適合不固定視窗數量的工作流：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">dwindle</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">pseudotile</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>       <span class="c1">-- 允許 app 請求 pseudo-tiling（保留原始大小）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">preserve_split</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>   <span class="c1">-- 不自動改變分割方向</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">force_split</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>         <span class="c1">-- 0=跟隨滑鼠, 1=永遠左/上, 2=永遠右/下</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">smart_split</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="n">smart_resizing</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><h3 id="master">Master</h3>
<p>一個主區域 + 其餘視窗堆疊在側邊，適合「一個主力視窗 + 多個參考視窗」的模式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">master</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">new_on_top</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>       <span class="c1">-- 新視窗放 master 還是 stack</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">orientation</span> <span class="o">=</span> <span class="s2">&#34;left&#34;</span><span class="p">,</span>     <span class="c1">-- master 區域位置：left / right / top / bottom / center</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">mfact</span> <span class="o">=</span> <span class="mf">0.55</span><span class="p">,</span>             <span class="c1">-- master 區域佔螢幕的比例</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>在 <code>general</code> 裡設定預設 layout：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">general</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">layout</span> <span class="o">=</span> <span class="s2">&#34;dwindle&#34;</span><span class="p">,</span>       <span class="c1">-- 或 &#34;master&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><blockquote>
<p><strong>[VM 可測試]</strong> Layout 切換和參數調整的行為邏輯。</p></blockquote>
<h2 id="外觀設定">外觀設定</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">general</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">gaps_in</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">gaps_out</span> <span class="o">=</span> <span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">border_size</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="p">[</span><span class="s2">&#34;col.active_border&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;rgba(89b4faee) rgba(cba6f7ee) 45deg&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="p">[</span><span class="s2">&#34;col.inactive_border&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;rgba(313244aa)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">decoration</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">rounding</span> <span class="o">=</span> <span class="mi">8</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">blur</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="n">enabled</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="n">size</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>             <span class="c1">-- 模糊半徑（越高越模糊、越吃 GPU）</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="n">passes</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>           <span class="c1">-- 模糊次數（越多越平滑、越重）</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="n">vibrancy</span> <span class="o">=</span> <span class="mf">0.17</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="n">noise</span> <span class="o">=</span> <span class="mf">0.02</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">            <span class="n">new_optimizations</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="n">shadow</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">            <span class="n">enabled</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">            <span class="n">range</span> <span class="o">=</span> <span class="mi">15</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">            <span class="n">render_power</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>     <span class="c1">-- 1-4，衰減曲線</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">            <span class="n">color</span> <span class="o">=</span> <span class="s2">&#34;0xee1a1a2e&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="n">dim_inactive</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="n">dim_strength</span> <span class="o">=</span> <span class="mf">0.2</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="n">active_opacity</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="n">inactive_opacity</span> <span class="o">=</span> <span class="mf">0.95</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><blockquote>
<p><strong>[需實機測試]</strong> blur 的效能影響和視覺品質——這是 Hyprland 最吃 GPU 的功能。VM 裡建議 <code>blur = { enabled = false }</code> 避免卡頓。shadow 和 rounding 在軟體渲染下可以動但會很慢。</p></blockquote>
<h2 id="動畫設定">動畫設定</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 定義 bezier 曲線</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">hl.animation</span><span class="p">(</span><span class="s2">&#34;bezier&#34;</span><span class="p">,</span> <span class="s2">&#34;ease&#34;</span><span class="p">,</span> <span class="mf">0.25</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mf">0.25</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">hl.animation</span><span class="p">(</span><span class="s2">&#34;bezier&#34;</span><span class="p">,</span> <span class="s2">&#34;overshot&#34;</span><span class="p">,</span> <span class="mf">0.05</span><span class="p">,</span> <span class="mf">0.9</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mf">1.05</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">-- Spring 物理動畫（較新的替代方案）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">hl.animation</span><span class="p">(</span><span class="s2">&#34;spring&#34;</span><span class="p">,</span> <span class="s2">&#34;bouncy&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>   <span class="c1">-- damping, frequency, speed</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">-- 套用動畫</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">hl.animation</span><span class="p">(</span><span class="s2">&#34;animation&#34;</span><span class="p">,</span> <span class="s2">&#34;windows&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="s2">&#34;ease&#34;</span><span class="p">,</span> <span class="s2">&#34;slide&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">hl.animation</span><span class="p">(</span><span class="s2">&#34;animation&#34;</span><span class="p">,</span> <span class="s2">&#34;windowsOut&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="s2">&#34;ease&#34;</span><span class="p">,</span> <span class="s2">&#34;popin 80%&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">hl.animation</span><span class="p">(</span><span class="s2">&#34;animation&#34;</span><span class="p">,</span> <span class="s2">&#34;fade&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="s2">&#34;ease&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">hl.animation</span><span class="p">(</span><span class="s2">&#34;animation&#34;</span><span class="p">,</span> <span class="s2">&#34;workspaces&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="s2">&#34;ease&#34;</span><span class="p">,</span> <span class="s2">&#34;slide&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">-- 關閉特定動畫</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">hl.animation</span><span class="p">(</span><span class="s2">&#34;animation&#34;</span><span class="p">,</span> <span class="s2">&#34;windowsMove&#34;</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span></span></span></code></pre></div><p>VM 裡跑 Hyprland 時，建議完全關閉動畫以維持可用的操作速度：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">animations</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">enabled</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><blockquote>
<p><strong>[需實機測試]</strong> 動畫流暢度是 Hyprland 的核心賣點，只有實機能評估。VM 裡的卡頓是 GPU 加速不足造成的，不代表 Hyprland 本身的效能。</p></blockquote>
<h2 id="autostart">Autostart</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">exec_once</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="c1">-- 桌面元件</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="s2">&#34;waybar&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="s2">&#34;mako&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="s2">&#34;hyprpaper&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="c1">-- 系統服務</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="s2">&#34;/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="s2">&#34;wl-paste --type text --watch cliphist store&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="c1">-- Idle daemon（閒置鎖屏，鎖屏安全模型見 session-lock 卡）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="s2">&#34;hypridle&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p><code>exec_once</code> 只在 Hyprland 啟動時跑一次（config reload 不會重複執行）。如果需要每次 reload 都重跑某個指令，用 <code>exec</code>（但多數情況不需要）。</p>
<h2 id="plugin-系統">Plugin 系統</h2>
<p>Hyprland 用 <code>hyprpm</code>（Hyprland Plugin Manager）管理 plugin：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hyprpm update                                              <span class="c1"># 更新 plugin index</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">hyprpm add https://github.com/hyprwm/hyprland-plugins     <span class="c1"># 加入官方 plugin repo</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">hyprpm <span class="nb">enable</span> hyprexpo                                     <span class="c1"># 啟用 plugin</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">hyprpm disable hyprexpo                                    <span class="c1"># 停用 plugin</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">hyprpm list                                                <span class="c1"># 列出可用 plugin</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>Plugin</th>
          <th>功能</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>hyprexpo</strong></td>
          <td>workspace 鳥瞰（所有 workspace 縮圖一覽）</td>
      </tr>
      <tr>
          <td><strong>hy3</strong></td>
          <td>i3/sway 風格的手動 tiling layout（替代 dwindle）</td>
      </tr>
      <tr>
          <td><strong>hyprspace</strong></td>
          <td>類似 macOS Mission Control 的 workspace 切換動畫</td>
      </tr>
      <tr>
          <td><strong>hyprbars</strong></td>
          <td>視窗標題列（可自訂按鈕）</td>
      </tr>
      <tr>
          <td><strong>hyprtrails</strong></td>
          <td>游標拖尾特效</td>
      </tr>
  </tbody>
</table>
<p>Plugin 的配置寫在 hyprland.lua 裡，是 dotfile 的一部分。</p>
<blockquote>
<p><strong>[VM 可測試]</strong> 不依賴 GPU 的 plugin（hy3、hyprbars）。<br>
<strong>[需實機測試]</strong> 視覺特效類 plugin（hyprexpo、hyprspace、hyprtrails）。</p></blockquote>
<h2 id="dotfile-結構對應">Dotfile 結構對應</h2>
<p>Hyprland 的配置拆成多個 <code>.lua</code> 檔，全部放在同一個 stow package 裡。<code>monitors.lua</code> 是硬體相關的——跨機器搬移時可能要排除或用 template/local 機制處理（見<a href="/blog/linux/dotfile/01-dotfile-management/cross-platform-one-repo/" data-link-title="跨平台共用一個 Repo" data-link-desc="macOS 跟 Linux 要共用同一個 dotfile repo、不想維護兩份時回來讀">跨平台共用一個 Repo</a>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">~/dotfiles/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">└── hyprland/
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    └── .config/
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        └── hypr/
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            ├── hyprland.lua       # 主入口（只有 require 行）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            ├── monitors.lua       # 硬體相關、可能排除或 template
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            ├── keybinds.lua
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            ├── rules.lua
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            ├── autostart.lua
</span></span><span class="line"><span class="ln">10</span><span class="cl">            ├── appearance.lua
</span></span><span class="line"><span class="ln">11</span><span class="cl">            └── env.lua</span></span></code></pre></div><h2 id="穩定性與維護的務實面">穩定性與維護的務實面</h2>
<p>Hyprland 的開發節奏快、功能更新激進。v0.55 的 Lua 遷移就是一個典型案例——配置格式整個換掉，舊的 <code>.conf</code> 只會再支援一到兩個版本。配置檔的語法和可用選項會隨版本變動。</p>
<p>應對策略：</p>
<ul>
<li>更新前先看 Hyprland wiki 的 Configuring 頁面和 changelog</li>
<li>dotfile repo 的 commit message 記錄「因應 Hyprland vX.Y 改了什麼設定」</li>
<li>如果用 Arch 的 rolling release，<code>pacman -Syu</code> 前先確認 Hyprland 是否有 breaking change（Arch 社群通常會在論壇預警）</li>
<li>官方提供遷移工具（如 <code>hyprlang2lua</code>），格式變更時優先使用</li>
</ul>
<p>這是<a href="/blog/linux/dotfile/04-window-management/" data-link-title="模組四：視窗管理與平鋪式工作流" data-link-desc="同時開多個視窗時的排列策略 — 手動貼齊跟自動平鋪的差距在哪、macOS 和 Linux 各有哪些工具、多螢幕怎麼處理、什麼情境值得從浮動切換到平鋪">視窗管理與平鋪式工作流</a>提過的代價——把日常桌面建立在高速移動的專案上，持續的配置維護是實際成本。</p>
<h2 id="vm-與實機測試對照">VM 與實機測試對照</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>VM 可測試</th>
          <th>需實機測試</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>配置語法 / 結構</td>
          <td>可</td>
          <td></td>
      </tr>
      <tr>
          <td>Keybind 設計</td>
          <td>可</td>
          <td></td>
      </tr>
      <tr>
          <td>Window rules 邏輯</td>
          <td>可</td>
          <td></td>
      </tr>
      <tr>
          <td>Workspace 切換</td>
          <td>可</td>
          <td></td>
      </tr>
      <tr>
          <td>Layout 參數</td>
          <td>可</td>
          <td></td>
      </tr>
      <tr>
          <td>Autostart 順序</td>
          <td>可</td>
          <td></td>
      </tr>
      <tr>
          <td>Plugin 配置</td>
          <td>部分</td>
          <td>視覺類</td>
      </tr>
      <tr>
          <td>動畫流暢度</td>
          <td></td>
          <td>必要</td>
      </tr>
      <tr>
          <td>Blur 效能 / 品質</td>
          <td></td>
          <td>必要</td>
      </tr>
      <tr>
          <td>多螢幕排列</td>
          <td></td>
          <td>必要</td>
      </tr>
      <tr>
          <td>HiDPI 縮放</td>
          <td></td>
          <td>必要</td>
      </tr>
      <tr>
          <td>觸控板 / 手勢</td>
          <td></td>
          <td>必要</td>
      </tr>
      <tr>
          <td>媒體鍵 / 亮度鍵</td>
          <td></td>
          <td>必要</td>
      </tr>
      <tr>
          <td>NVIDIA 驅動設定</td>
          <td></td>
          <td>必要</td>
      </tr>
      <tr>
          <td>螢幕分享</td>
          <td></td>
          <td>必要</td>
      </tr>
      <tr>
          <td>休眠 / 喚醒</td>
          <td></td>
          <td>必要</td>
      </tr>
      <tr>
          <td>合蓋行為</td>
          <td></td>
          <td>必要</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>目錄結構、Git 工作流與常見陷阱</title><link>https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/directory-structure-workflow/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/directory-structure-workflow/</guid><description>&lt;p>不管用哪個工具，dotfile repo 的目錄結構都遵循同一個原則：每個工具（或 package）是一個頂層目錄，內部路徑反映安裝後在家目錄的相對位置。&lt;/p>
&lt;h2 id="目錄結構設計">目錄結構設計&lt;/h2>
&lt;p>以 stow 為例的標準結構：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">~/dotfiles/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">├── zsh/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">│ └── .zshrc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">├── git/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">│ ├── .gitconfig
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">│ └── .gitignore_global
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">├── ssh/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">│ └── .ssh/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">│ └── config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">├── nvim/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">│ └── .config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">│ └── nvim/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">│ ├── init.lua
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">│ └── lua/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">├── tmux/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">│ └── .config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">│ └── tmux/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">│ └── tmux.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">├── hyprland/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">│ └── .config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">│ └── hypr/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">│ └── hyprland.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">├── waybar/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">│ └── .config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">│ └── waybar/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">│ ├── config.jsonc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">│ └── style.css
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">├── scripts/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">│ └── install.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">├── Brewfile
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">├── packages.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl">├── .gitignore
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl">└── README.md&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這些設計選擇的理由：&lt;/p>
&lt;p>&lt;strong>每個工具一個頂層目錄&lt;/strong>。stow 的 package 概念讓你可以選擇性安裝——伺服器不需要 hyprland 和 waybar，只 stow 需要的 package。即使不用 stow，這個分法也讓 repo 結構清晰：看頂層目錄就知道管了哪些工具。&lt;/p>
&lt;p>&lt;strong>目錄內路徑映射安裝位置&lt;/strong>。&lt;code>nvim/.config/nvim/init.lua&lt;/code> 安裝後變成 &lt;code>~/.config/nvim/init.lua&lt;/code>。這個映射是 stow 的核心假設，但即使用 chezmoi 或 bare repo，維持同樣的思維讓目錄結構自解釋。&lt;/p>
&lt;p>&lt;strong>scripts/ 不是 stow package&lt;/strong>。&lt;code>scripts/install.sh&lt;/code> 是 bootstrap 用的安裝腳本，不應該被 stow 到家目錄。它放在 repo 裡是為了讓新機器還原時有一個入口點可以跑。&lt;/p>
&lt;p>&lt;strong>Brewfile / packages.txt 記錄套件清單&lt;/strong>。配置檔只告訴工具「怎麼用」，但前提是工具已安裝。&lt;code>Brewfile&lt;/code>（macOS 用 &lt;code>brew bundle&lt;/code>）和 &lt;code>packages.txt&lt;/code>（Linux 用套件管理器批次安裝）把「裝了什麼」也納入版控，讓新機器還原時不用靠記憶。&lt;/p>
&lt;p>&lt;strong>ssh/ 只放 config，不放私鑰&lt;/strong>。&lt;code>~/.ssh/config&lt;/code> 記錄 SSH 連線設定（Host alias、ProxyJump 等），是有版控價值的配置。私鑰（&lt;code>id_ed25519&lt;/code>、&lt;code>id_rsa&lt;/code>）和公鑰不應進 dotfile repo，即使 repo 是 private。私鑰用密碼管理器或機器本地生成。&lt;/p>
&lt;h2 id="git-工作流">Git 工作流&lt;/h2>
&lt;p>dotfile repo 的 Git 工作流比一般程式碼專案簡單，因為通常只有一個人在用，branch 和 PR 的需求低。&lt;/p>
&lt;p>&lt;strong>日常修改&lt;/strong>。直接編輯配置（symlink 透通到 repo 裡的實體檔案），然後 commit：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/dotfiles
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">git add zsh/.zshrc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;zsh: add fzf integration&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">git push&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>新增一個工具的配置&lt;/strong>。先在 dotfiles 建好目錄結構，把現有配置搬進去，建 symlink，然後 commit：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mkdir -p ~/dotfiles/alacritty/.config/alacritty
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">mv ~/.config/alacritty/alacritty.toml ~/dotfiles/alacritty/.config/alacritty/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/dotfiles &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> stow alacritty
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">git add alacritty/ &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> git commit -m &lt;span class="s2">&amp;#34;add alacritty config&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>新機器還原&lt;/strong>。整個流程應該能在幾分鐘內完成：&lt;/p></description><content:encoded><![CDATA[<p>不管用哪個工具，dotfile repo 的目錄結構都遵循同一個原則：每個工具（或 package）是一個頂層目錄，內部路徑反映安裝後在家目錄的相對位置。</p>
<h2 id="目錄結構設計">目錄結構設計</h2>
<p>以 stow 為例的標準結構：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">~/dotfiles/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── zsh/
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│   └── .zshrc
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">├── git/
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│   ├── .gitconfig
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│   └── .gitignore_global
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">├── ssh/
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">│   └── .ssh/
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">│       └── config
</span></span><span class="line"><span class="ln">10</span><span class="cl">├── nvim/
</span></span><span class="line"><span class="ln">11</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln">12</span><span class="cl">│       └── nvim/
</span></span><span class="line"><span class="ln">13</span><span class="cl">│           ├── init.lua
</span></span><span class="line"><span class="ln">14</span><span class="cl">│           └── lua/
</span></span><span class="line"><span class="ln">15</span><span class="cl">├── tmux/
</span></span><span class="line"><span class="ln">16</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln">17</span><span class="cl">│       └── tmux/
</span></span><span class="line"><span class="ln">18</span><span class="cl">│           └── tmux.conf
</span></span><span class="line"><span class="ln">19</span><span class="cl">├── hyprland/
</span></span><span class="line"><span class="ln">20</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln">21</span><span class="cl">│       └── hypr/
</span></span><span class="line"><span class="ln">22</span><span class="cl">│           └── hyprland.conf
</span></span><span class="line"><span class="ln">23</span><span class="cl">├── waybar/
</span></span><span class="line"><span class="ln">24</span><span class="cl">│   └── .config/
</span></span><span class="line"><span class="ln">25</span><span class="cl">│       └── waybar/
</span></span><span class="line"><span class="ln">26</span><span class="cl">│           ├── config.jsonc
</span></span><span class="line"><span class="ln">27</span><span class="cl">│           └── style.css
</span></span><span class="line"><span class="ln">28</span><span class="cl">├── scripts/
</span></span><span class="line"><span class="ln">29</span><span class="cl">│   └── install.sh
</span></span><span class="line"><span class="ln">30</span><span class="cl">├── Brewfile
</span></span><span class="line"><span class="ln">31</span><span class="cl">├── packages.txt
</span></span><span class="line"><span class="ln">32</span><span class="cl">├── .gitignore
</span></span><span class="line"><span class="ln">33</span><span class="cl">└── README.md</span></span></code></pre></div><p>這些設計選擇的理由：</p>
<p><strong>每個工具一個頂層目錄</strong>。stow 的 package 概念讓你可以選擇性安裝——伺服器不需要 hyprland 和 waybar，只 stow 需要的 package。即使不用 stow，這個分法也讓 repo 結構清晰：看頂層目錄就知道管了哪些工具。</p>
<p><strong>目錄內路徑映射安裝位置</strong>。<code>nvim/.config/nvim/init.lua</code> 安裝後變成 <code>~/.config/nvim/init.lua</code>。這個映射是 stow 的核心假設，但即使用 chezmoi 或 bare repo，維持同樣的思維讓目錄結構自解釋。</p>
<p><strong>scripts/ 不是 stow package</strong>。<code>scripts/install.sh</code> 是 bootstrap 用的安裝腳本，不應該被 stow 到家目錄。它放在 repo 裡是為了讓新機器還原時有一個入口點可以跑。</p>
<p><strong>Brewfile / packages.txt 記錄套件清單</strong>。配置檔只告訴工具「怎麼用」，但前提是工具已安裝。<code>Brewfile</code>（macOS 用 <code>brew bundle</code>）和 <code>packages.txt</code>（Linux 用套件管理器批次安裝）把「裝了什麼」也納入版控，讓新機器還原時不用靠記憶。</p>
<p><strong>ssh/ 只放 config，不放私鑰</strong>。<code>~/.ssh/config</code> 記錄 SSH 連線設定（Host alias、ProxyJump 等），是有版控價值的配置。私鑰（<code>id_ed25519</code>、<code>id_rsa</code>）和公鑰不應進 dotfile repo，即使 repo 是 private。私鑰用密碼管理器或機器本地生成。</p>
<h2 id="git-工作流">Git 工作流</h2>
<p>dotfile repo 的 Git 工作流比一般程式碼專案簡單，因為通常只有一個人在用，branch 和 PR 的需求低。</p>
<p><strong>日常修改</strong>。直接編輯配置（symlink 透通到 repo 裡的實體檔案），然後 commit：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">cd</span> ~/dotfiles
</span></span><span class="line"><span class="ln">2</span><span class="cl">git add zsh/.zshrc
</span></span><span class="line"><span class="ln">3</span><span class="cl">git commit -m <span class="s2">&#34;zsh: add fzf integration&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">git push</span></span></code></pre></div><p><strong>新增一個工具的配置</strong>。先在 dotfiles 建好目錄結構，把現有配置搬進去，建 symlink，然後 commit：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mkdir -p ~/dotfiles/alacritty/.config/alacritty
</span></span><span class="line"><span class="ln">2</span><span class="cl">mv ~/.config/alacritty/alacritty.toml ~/dotfiles/alacritty/.config/alacritty/
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">cd</span> ~/dotfiles <span class="o">&amp;&amp;</span> stow alacritty
</span></span><span class="line"><span class="ln">4</span><span class="cl">git add alacritty/ <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;add alacritty config&#34;</span></span></span></code></pre></div><p><strong>新機器還原</strong>。整個流程應該能在幾分鐘內完成：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 裝 Git 和 stow（通常是最先裝的兩個東西）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 2. clone</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">git clone git@github.com:you/dotfiles.git ~/dotfiles
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 3. 安裝套件</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nb">cd</span> ~/dotfiles
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">brew bundle          <span class="c1"># macOS</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 或</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">xargs sudo pacman -S &lt; packages.txt  <span class="c1"># Arch</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 4. 建 symlink</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">stow zsh git nvim tmux ssh
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 5. 重開 shell，配置生效</span></span></span></code></pre></div><h2 id="常見陷阱">常見陷阱</h2>
<p><strong>私鑰進 repo</strong>。把 <code>~/.ssh/</code> 整個目錄（含 <code>id_ed25519</code>）推上 GitHub 是最危險的錯誤。即使事後刪除，Git 歷史裡仍然留有私鑰。做法是只追蹤 <code>~/.ssh/config</code>，在 <code>.gitignore</code> 明確排除 <code>*.pem</code>、<code>id_*</code>。</p>
<p><strong>缺少 .gitignore</strong>。很多工具會在配置目錄產生 cache、compiled 檔案、session 狀態。nvim 的 <code>plugin/packer_compiled.lua</code>、zsh 的 <code>.zcompdump</code>、tmux 的 <code>resurrect/</code> 都不該進 repo。建 repo 時第一件事就是寫 <code>.gitignore</code>。</p>
<p><strong>symlink 衝突</strong>。<code>stow zsh</code> 時如果 <code>~/.zshrc</code> 已經存在且不是 symlink，stow 會拒絕操作。解法是先備份再安裝：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mv ~/.zshrc ~/.zshrc.bak
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> ~/dotfiles <span class="o">&amp;&amp;</span> stow zsh</span></span></code></pre></div><p><strong>路徑寫死</strong>。<code>.zshrc</code> 裡寫 <code>source /Users/mac-eric/.nvm/nvm.sh</code> 搬到 Linux 就壞了。改用 <code>$HOME</code>：<code>source &quot;$HOME/.nvm/nvm.sh&quot;</code>。配置檔裡每一處絕對路徑都是可攜性的隱患。</p>
<p><strong>整包 .config 放一個 package</strong>。把 <code>~/.config</code> 整個目錄當成一個 stow package 會喪失模組化的好處，而且衝突風險大幅增加。正確做法是每個工具拆開：nvim 一個、tmux 一個、hyprland 一個。</p>
]]></content:encoded></item><item><title>跨機器同步、Secret 管理與環境重建流程</title><link>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/sync-strategy-secret/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/sync-strategy-secret/</guid><description>&lt;h2 id="跨機器同步策略">跨機器同步策略&lt;/h2>
&lt;p>多台機器共用 dotfile repo 時，需要一套同步策略來處理「改了配置後怎麼讓其他機器也更新」。&lt;/p>
&lt;h3 id="git-pushpull手動">Git push/pull（手動）&lt;/h3>
&lt;p>最基本的做法：改了就 commit + push，另一台機器 pull + 重新 apply。優點是簡單、沒有額外依賴。缺點是容易忘記——在公司機器上改了一個 alias，回家忘記 push，隔天公司又改了一版，兩邊 diverge。&lt;/p>
&lt;p>適合只有一兩台機器、改動不頻繁的人。&lt;/p>
&lt;h3 id="自動同步">自動同步&lt;/h3>
&lt;p>chezmoi 內建 &lt;code>chezmoi update&lt;/code> 指令（pull + apply 一步完成），搭配 cron 或 systemd timer 定期執行：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.config/systemd/user/chezmoi-update.timer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="na">Description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">Update dotfiles daily&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="k">[Timer]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="na">OnCalendar&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">daily&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="na">Persistent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="k">[Install]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="na">WantedBy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">timers.target&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.config/systemd/user/chezmoi-update.service&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">Description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">chezmoi update&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">[Service]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="na">Type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">oneshot&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="na">ExecStart&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">/usr/bin/chezmoi update --no-tty&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>自動同步減少手動操作，但要注意衝突處理——如果兩台機器同時改了同一個檔案且都 push，後面那台的自動 pull 會遇到 merge conflict。實務上 dotfile 很少有真正的衝突（兩台機器同時改同一行的機率低），但偶爾發生時需要手動介入。&lt;/p>
&lt;h3 id="機器差異的處理">機器差異的處理&lt;/h3>
&lt;p>推薦的模式是 main branch 放所有共用配置，機器差異用條件判斷處理。&lt;/p>
&lt;p>用 shell 的 OS 判斷：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.zshrc&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname -s&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;Darwin&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/opt/homebrew/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nb">alias&lt;/span> &lt;span class="nv">ls&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;ls -G&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nb">alias&lt;/span> &lt;span class="nv">ls&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;ls --color=auto&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="k">fi&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用 chezmoi template（Go template 語法）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># chezmoi 管理的 .zshrc.tmpl&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="o">{{&lt;/span> &lt;span class="k">if&lt;/span> eq .chezmoi.os &lt;span class="s2">&amp;#34;darwin&amp;#34;&lt;/span> -&lt;span class="o">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/opt/homebrew/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="o">{{&lt;/span> end -&lt;span class="o">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="o">{{&lt;/span> &lt;span class="k">if&lt;/span> eq .chezmoi.hostname &lt;span class="s2">&amp;#34;work-laptop&amp;#34;&lt;/span> -&lt;span class="o">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">HTTP_PROXY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://proxy.corp:8080&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="o">{{&lt;/span> end -&lt;span class="o">}}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>chezmoi template 的優勢是條件判斷發生在 apply 階段，產出的檔案裡看不到 template 語法，乾淨且不依賴 shell 的 runtime 判斷。&lt;/p>
&lt;p>不推薦每台機器一個 branch 的做法。短期可行，長期一定 diverge——main 加了新配置，各 branch 要 rebase 或 merge，忘了就漂移。一份 main + template 條件判斷是長期可維護的結構。&lt;/p>
&lt;h2 id="secret-排除與管理">Secret 排除與管理&lt;/h2>
&lt;p>dotfile repo 通常是 public 或至少多人可見的。以下東西進了 repo 等於把鑰匙掛在門口：&lt;/p>
&lt;ul>
&lt;li>SSH 私鑰（&lt;code>~/.ssh/id_*&lt;/code>、&lt;code>*.pem&lt;/code>）&lt;/li>
&lt;li>API token、password、.env 檔案&lt;/li>
&lt;li>GPG 私鑰&lt;/li>
&lt;li>cloud provider 的 credential 檔案（&lt;code>~/.aws/credentials&lt;/code>、&lt;code>~/.config/gcloud/application_default_credentials.json&lt;/code>）&lt;/li>
&lt;li>browser profile 裡的 cookie / session&lt;/li>
&lt;/ul>
&lt;h3 id="gitignore-是第一道防線">.gitignore 是第一道防線&lt;/h3>





&lt;pre tabindex="0">&lt;code class="language-gitignore" data-lang="gitignore"># SSH 私鑰
*.pem
id_*
known_hosts
authorized_keys

# 環境變數
.env
.env.*

# Cloud credentials
credentials
application_default_credentials.json&lt;/code>&lt;/pre>&lt;p>但 .gitignore 只防「不小心 add」，不防「故意 add -f」。更重要的是建立習慣：repo 裡永遠只放「看到了也沒關係」的東西。&lt;/p>
&lt;h3 id="ssh-config-的特殊處理">SSH config 的特殊處理&lt;/h3>
&lt;p>&lt;code>~/.ssh/config&lt;/code>（host alias、ProxyJump 設定、port forwarding）本身不含 secret，可以進 repo——它記錄的是「連線要怎麼走」而不是「憑證是什麼」。但同一個 &lt;code>~/.ssh/&lt;/code> 目錄下的私鑰絕對排除。&lt;/p></description><content:encoded><![CDATA[<h2 id="跨機器同步策略">跨機器同步策略</h2>
<p>多台機器共用 dotfile repo 時，需要一套同步策略來處理「改了配置後怎麼讓其他機器也更新」。</p>
<h3 id="git-pushpull手動">Git push/pull（手動）</h3>
<p>最基本的做法：改了就 commit + push，另一台機器 pull + 重新 apply。優點是簡單、沒有額外依賴。缺點是容易忘記——在公司機器上改了一個 alias，回家忘記 push，隔天公司又改了一版，兩邊 diverge。</p>
<p>適合只有一兩台機器、改動不頻繁的人。</p>
<h3 id="自動同步">自動同步</h3>
<p>chezmoi 內建 <code>chezmoi update</code> 指令（pull + apply 一步完成），搭配 cron 或 systemd timer 定期執行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># ~/.config/systemd/user/chezmoi-update.timer</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Update dotfiles daily</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">[Timer]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">OnCalendar</span><span class="o">=</span><span class="s">daily</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">Persistent</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># ~/.config/systemd/user/chezmoi-update.service</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">chezmoi update</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/chezmoi update --no-tty</span></span></span></code></pre></div><p>自動同步減少手動操作，但要注意衝突處理——如果兩台機器同時改了同一個檔案且都 push，後面那台的自動 pull 會遇到 merge conflict。實務上 dotfile 很少有真正的衝突（兩台機器同時改同一行的機率低），但偶爾發生時需要手動介入。</p>
<h3 id="機器差異的處理">機器差異的處理</h3>
<p>推薦的模式是 main branch 放所有共用配置，機器差異用條件判斷處理。</p>
<p>用 shell 的 OS 判斷：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># ~/.zshrc</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="k">$(</span>uname -s<span class="k">)</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;Darwin&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;/opt/homebrew/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nb">alias</span> <span class="nv">ls</span><span class="o">=</span><span class="s2">&#34;ls -G&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nb">alias</span> <span class="nv">ls</span><span class="o">=</span><span class="s2">&#34;ls --color=auto&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="k">fi</span></span></span></code></pre></div><p>用 chezmoi template（Go template 語法）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># chezmoi 管理的 .zshrc.tmpl</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">{{</span> <span class="k">if</span> eq .chezmoi.os <span class="s2">&#34;darwin&#34;</span> -<span class="o">}}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;/opt/homebrew/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="o">{{</span> end -<span class="o">}}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="o">{{</span> <span class="k">if</span> eq .chezmoi.hostname <span class="s2">&#34;work-laptop&#34;</span> -<span class="o">}}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nb">export</span> <span class="nv">HTTP_PROXY</span><span class="o">=</span><span class="s2">&#34;http://proxy.corp:8080&#34;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="o">{{</span> end -<span class="o">}}</span></span></span></code></pre></div><p>chezmoi template 的優勢是條件判斷發生在 apply 階段，產出的檔案裡看不到 template 語法，乾淨且不依賴 shell 的 runtime 判斷。</p>
<p>不推薦每台機器一個 branch 的做法。短期可行，長期一定 diverge——main 加了新配置，各 branch 要 rebase 或 merge，忘了就漂移。一份 main + template 條件判斷是長期可維護的結構。</p>
<h2 id="secret-排除與管理">Secret 排除與管理</h2>
<p>dotfile repo 通常是 public 或至少多人可見的。以下東西進了 repo 等於把鑰匙掛在門口：</p>
<ul>
<li>SSH 私鑰（<code>~/.ssh/id_*</code>、<code>*.pem</code>）</li>
<li>API token、password、.env 檔案</li>
<li>GPG 私鑰</li>
<li>cloud provider 的 credential 檔案（<code>~/.aws/credentials</code>、<code>~/.config/gcloud/application_default_credentials.json</code>）</li>
<li>browser profile 裡的 cookie / session</li>
</ul>
<h3 id="gitignore-是第一道防線">.gitignore 是第一道防線</h3>





<pre tabindex="0"><code class="language-gitignore" data-lang="gitignore"># SSH 私鑰
*.pem
id_*
known_hosts
authorized_keys

# 環境變數
.env
.env.*

# Cloud credentials
credentials
application_default_credentials.json</code></pre><p>但 .gitignore 只防「不小心 add」，不防「故意 add -f」。更重要的是建立習慣：repo 裡永遠只放「看到了也沒關係」的東西。</p>
<h3 id="ssh-config-的特殊處理">SSH config 的特殊處理</h3>
<p><code>~/.ssh/config</code>（host alias、ProxyJump 設定、port forwarding）本身不含 secret，可以進 repo——它記錄的是「連線要怎麼走」而不是「憑證是什麼」。但同一個 <code>~/.ssh/</code> 目錄下的私鑰絕對排除。</p>
<p>stow 管理時的目錄結構範例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">dotfiles/
</span></span><span class="line"><span class="ln">2</span><span class="cl">└── ssh/
</span></span><span class="line"><span class="ln">3</span><span class="cl">    └── .ssh/
</span></span><span class="line"><span class="ln">4</span><span class="cl">        └── config        # 進 repo
</span></span><span class="line"><span class="ln">5</span><span class="cl">        # id_rsa 不放這裡
</span></span><span class="line"><span class="ln">6</span><span class="cl">        # known_hosts 不放這裡</span></span></code></pre></div><h3 id="三個層級的-secret-管理">三個層級的 secret 管理</h3>
<p><strong>層級一：手動</strong>。.gitignore 排除 secret 檔案，在 README 記錄「這些東西需要在新機器手動設定」。最低成本、對只有一兩台機器的人足夠。</p>
<p><strong>層級二：密碼管理器整合</strong>。chezmoi 支援從 1Password、Bitwarden、pass（Unix password manager）等拉取 secret：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl"># chezmoi template 語法
</span></span><span class="line"><span class="ln">2</span><span class="cl">{{ (onepasswordRead &#34;op://Personal/SSH Key/private key&#34;).value }}</span></span></code></pre></div><p>配置檔的 template 裡引用密碼管理器的條目，apply 時自動填入。secret 不在 repo 裡，但 repo 知道去哪拉。</p>
<p><strong>層級三：加密存放</strong>。用 age 或 sops 把 secret 加密後直接存在 repo 裡。解密需要對應的 key。chezmoi 原生支援 age 加密：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 加密一個檔案</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">chezmoi add --encrypt ~/.ssh/id_rsa
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># repo 裡看到的是加密後的內容</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">cat ~/.local/share/chezmoi/private_dot_ssh/id_rsa.age</span></span></code></pre></div><p>加密存放的好處是 secret 跟著 repo 走、不用另外設密碼管理器。風險是加密 key 本身變成唯一的依賴——丟了 key，加密的 secret 就拿不回來。</p>
<p>層級選擇取決於安全需求和便利需求的平衡。多數情況從層級一開始，覺得手動處理太煩再往上升級。</p>
<h2 id="環境重建的實際流程">環境重建的實際流程</h2>
<p>假設拿到一台全新的 Arch Linux 機器，要從零重建完整的 Hyprland 桌面環境。以下是 end-to-end 的步驟，對應 <a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">bootstrap script</a> 的每個階段。</p>
<h3 id="階段一最小可用環境">階段一：最小可用環境</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Arch 安裝完成後，base system 只有 bash 和基本工具</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo pacman -S git base-devel</span></span></code></pre></div><p>這是 bootstrap script 的唯一外部前提：有 Git 能 clone repo、有 base-devel 能編譯 AUR 套件。其他一切由 script 處理。</p>
<h3 id="階段二取得-dotfile-repo">階段二：取得 dotfile repo</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git clone https://github.com/you/dotfiles ~/dotfiles
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> ~/dotfiles</span></span></code></pre></div><p>如果 repo 是 private，這一步需要先設定 SSH key 或用 HTTPS + token。這是前面提到的 secret 雞生蛋問題——clone 含有 SSH config 的 repo 本身就需要 SSH key。解法通常是：第一次用 HTTPS clone，deploy 完 SSH config 後把 remote 改成 SSH。</p>
<h3 id="階段三執行-bootstrap">階段三：執行 bootstrap</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">./scripts/install.sh</span></span></code></pre></div><p>script 依序：安裝套件（Hyprland、waybar、rofi、wezterm、zsh、neovim、stow 等）、用 stow 部署配置到 <code>$HOME</code>、執行初始化（換 shell、安裝 neovim plugin）。</p>
<h3 id="階段四手動處理">階段四：手動處理</h3>
<p>bootstrap 處理不了（或不該處理）的部分：</p>
<ul>
<li><strong>SSH 私鑰</strong>：從備份或密碼管理器取回，放到 <code>~/.ssh/</code>，設定正確權限（<code>chmod 600</code>）</li>
<li><strong>Git 簽署用的 GPG key</strong>：如果有用 commit signing</li>
<li><strong>密碼管理器登入</strong>：如果 secret 管理用了層級二或三</li>
</ul>
<h3 id="階段五硬體相關調整">階段五：硬體相關調整</h3>
<p>Hyprland 的 monitor 設定（解析度、縮放、排列位置）跟實際接的螢幕有關，這部分配置每台機器都不同：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># ~/.config/hypr/hyprland.conf 的 monitor 段</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 這幾行在每台機器上都要調</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">monitor</span><span class="o">=</span><span class="s">DP-1, 2560x1440@144, 0x0, 1</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">monitor</span><span class="o">=</span><span class="s">HDMI-A-1, 1920x1080@60, 2560x0, 1</span></span></span></code></pre></div><p>處理方式有兩種：把 monitor 設定拆成獨立的 <code>monitor.conf</code>，主配置用 <code>source</code> 引入，<code>monitor.conf</code> 不進 repo（加進 .gitignore）、每台機器本地維護；或者用 chezmoi template 按 hostname 判斷。</p>
<p>顯卡驅動（Intel/AMD 通常自動、NVIDIA 需要額外安裝 <code>nvidia-dkms</code> 和設定環境變數）也是硬體相關的步驟，可以放在 bootstrap script 的 OS 判斷裡，但通常 Arch 安裝階段就已經處理。</p>
<h3 id="階段六驗證">階段六：驗證</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 登出 TTY，重新用 Hyprland 登入</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 或者直接在 TTY 執行</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">Hyprland</span></span></code></pre></div><p>登入後確認：視窗管理器正常運作、keybind 正確、狀態列出現、字型正確渲染、終端機配色正常。如果某個元件沒反應，通常是套件沒裝或配置路徑不對——回去檢查 bootstrap 的套件清單和 stow 的 symlink。</p>
<h3 id="時間預估">時間預估</h3>
<p>整個流程在網路順暢的情況下，大約 30 分鐘到 1 小時，取決於套件數量和下載速度。主要時間花在套件安裝（pacman 下載 + 編譯 AUR 套件）。配置 deploy 本身是秒級操作（stow 只建 symlink）。</p>
<p>對比沒有 dotfile 管理時的重建：邊想邊裝、裝了忘記某個工具的名稱、配置靠記憶手打、兩天後還在調某個快捷鍵為什麼不對。差距在「可預期 vs 碰運氣」。</p>
<h2 id="維護節奏">維護節奏</h2>
<p>環境重建能力需要持續維護，不是設定完就一勞永逸。</p>
<p>日常習慣：新裝一個工具時，順手更新套件清單（<code>brew bundle dump</code> 或手動加一行到 <code>packages.txt</code>）。改了一個配置後，commit + push。這個習慣的建立成本低，但需要刻意練幾週才會變成反射動作。</p>
<p>定期檢查：每隔幾個月在 VM 或 container 裡跑一次完整的 bootstrap，驗證 script 還能從零跑通。配置會演進、套件會改名或被取代、script 裡硬寫的路徑可能失效——定期驗證才能確保「這份重建指令真的能重建」，而不是一份過期的紀錄。</p>
]]></content:encoded></item><item><title>模組三：終端機與編輯器</title><link>https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/</guid><description>&lt;p>終端機生態的配置檔數量比 shell 更多、散落位置更廣。Terminal emulator、multiplexer（tmux/zellij）、editor（neovim/vim）各自有獨立的配置體系，加上字型、配色這些跨工具共用的視覺設定，整層的管理複雜度比 shell 配置高一個量級。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/terminal-emulator-config/" data-link-title="Terminal Emulator 配置" data-link-desc="選 terminal emulator 時需要比對配置格式和跨平台能力、或想把配色和字型統一管理時回來讀">Terminal Emulator 配置&lt;/a>&lt;/td>
 &lt;td>常見 terminal emulator 選型、配置判讀、配色與字型管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/multiplexer-tmux-zellij/" data-link-title="Multiplexer：tmux vs zellij" data-link-desc="在終端機裡切分 pane、管理多個 session、SSH 斷線後保持工作時回來讀 — tmux 和 zellij 的配置與選型">Multiplexer：tmux vs zellij&lt;/a>&lt;/td>
 &lt;td>tmux 和 zellij 的配置、plugin、選型判讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/neovim-config/" data-link-title="Neovim 配置" data-link-desc="neovim 配置該怎麼組織進 dotfile、要不要用 LazyVim 等預設配置包時回來讀">Neovim 配置&lt;/a>&lt;/td>
 &lt;td>neovim 配置結構、預設配置包判讀、dotfile 結構對應&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/02-shell-config/" data-link-title="模組二：Shell 配置" data-link-desc="shell 配置檔長成一坨不敢動時回來讀 — .zshrc/.bashrc 的結構化拆分、alias/function/PATH 的模組化設計">模組二：Shell 配置&lt;/a>：shell 是終端機工具的載體，配置拆分邏輯相通&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計&lt;/a>：配色系統的統一管理從 terminal 延伸到桌面&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>終端機生態的配置檔數量比 shell 更多、散落位置更廣。Terminal emulator、multiplexer（tmux/zellij）、editor（neovim/vim）各自有獨立的配置體系，加上字型、配色這些跨工具共用的視覺設定，整層的管理複雜度比 shell 配置高一個量級。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/03-terminal-ecosystem/terminal-emulator-config/" data-link-title="Terminal Emulator 配置" data-link-desc="選 terminal emulator 時需要比對配置格式和跨平台能力、或想把配色和字型統一管理時回來讀">Terminal Emulator 配置</a></td>
          <td>常見 terminal emulator 選型、配置判讀、配色與字型管理</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/03-terminal-ecosystem/multiplexer-tmux-zellij/" data-link-title="Multiplexer：tmux vs zellij" data-link-desc="在終端機裡切分 pane、管理多個 session、SSH 斷線後保持工作時回來讀 — tmux 和 zellij 的配置與選型">Multiplexer：tmux vs zellij</a></td>
          <td>tmux 和 zellij 的配置、plugin、選型判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/03-terminal-ecosystem/neovim-config/" data-link-title="Neovim 配置" data-link-desc="neovim 配置該怎麼組織進 dotfile、要不要用 LazyVim 等預設配置包時回來讀">Neovim 配置</a></td>
          <td>neovim 配置結構、預設配置包判讀、dotfile 結構對應</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/linux/dotfile/02-shell-config/" data-link-title="模組二：Shell 配置" data-link-desc="shell 配置檔長成一坨不敢動時回來讀 — .zshrc/.bashrc 的結構化拆分、alias/function/PATH 的模組化設計">模組二：Shell 配置</a>：shell 是終端機工具的載體，配置拆分邏輯相通</li>
<li>→ <a href="/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計</a>：配色系統的統一管理從 terminal 延伸到桌面</li>
</ul>
]]></content:encoded></item><item><title>Caelestia 安裝</title><link>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/caelestia-installation/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/caelestia-installation/</guid><description>&lt;p>Caelestia 的安裝有兩條路：用 CLI 工具一鍵部署完整 dotfiles，或只安裝 shell 元件保留自己的 Hyprland 配置。兩者的前提都是 Hyprland 已經安裝且能正常啟動。&lt;/p>
&lt;h2 id="前提條件">前提條件&lt;/h2>
&lt;ul>
&lt;li>Arch Linux（或 Arch 系發行版如 CachyOS、EndeavourOS）&lt;/li>
&lt;li>Hyprland 已安裝且能從 TTY 啟動（見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/hyprland-installation/" data-link-title="Hyprland 安裝與環境建置" data-link-desc="要在 Arch Linux 上從零安裝 Hyprland 桌面環境時回來讀">Hyprland 安裝指南&lt;/a>）&lt;/li>
&lt;li>AUR helper 已安裝（yay 或 paru）&lt;/li>
&lt;li>網路連線（安裝過程需要拉 AUR 套件和 Git repo）&lt;/li>
&lt;/ul>
&lt;h2 id="推薦方式cli-完整安裝">推薦方式：CLI 完整安裝&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">paru -S caelestia-cli
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">caelestia install&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>caelestia install&lt;/code> 做的事：&lt;/p>
&lt;ol>
&lt;li>從 GitHub clone Caelestia dotfiles repo&lt;/li>
&lt;li>安裝所有 runtime 依賴（透過 AUR helper）&lt;/li>
&lt;li>部署配置檔到 &lt;code>~/.config/&lt;/code> 對應位置&lt;/li>
&lt;li>設定 Hyprland 載入 Caelestia shell&lt;/li>
&lt;/ol>
&lt;p>安裝完成後重新啟動 Hyprland，Caelestia 會自動載入。&lt;/p>
&lt;p>&lt;strong>注意&lt;/strong>：&lt;code>caelestia install&lt;/code> 會覆寫你現有的 Hyprland 配置。如果你已經有自己的 hyprland.conf / hyprland.lua，先備份。安裝後可以透過 &lt;code>~/.config/caelestia/hypr-user.lua&lt;/code> 加入自訂設定。&lt;/p>
&lt;h2 id="shell-only-安裝">Shell-only 安裝&lt;/h2>
&lt;p>只裝 UI 元件，不動 Hyprland config 和其他應用程式設定：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">yay -S caelestia-shell&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>啟動方式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">caelestia shell -d &lt;span class="c1"># daemonized，背景執行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 或&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">qs -c caelestia &lt;span class="c1"># 透過 quickshell 直接啟動&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Shell 不會因為裝了就自動啟動——Hyprland 開機只執行 config 裡 &lt;code>exec-once&lt;/code> 列出的程式，漏列的結果是登入後桌面沒有 bar 也沒有通知（實測：手動啟動用了幾天、直到主機重開才暴露從沒進 autostart）。把啟動指令加進 &lt;code>hyprland.conf&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="na">exec-once&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">caelestia shell -d&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Caelestia 自帶 bar、通知、鎖屏與 launcher，跟獨立的 waybar / mako 互斥：通知的 D-Bus name（&lt;code>org.freedesktop.Notifications&lt;/code>）同一時間只有一個擁有者、bar 會疊兩條。設 Caelestia 為主 shell 時，把 waybar / mako 從 &lt;code>exec-once&lt;/code> 移除（註解掉可留作停用 Caelestia 時的 fallback）。&lt;/p>
&lt;h2 id="aur-套件一覽">AUR 套件一覽&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>套件&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>caelestia-shell&lt;/code>&lt;/td>
 &lt;td>穩定版 shell（UI 元件）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>caelestia-shell-git&lt;/code>&lt;/td>
 &lt;td>開發版 shell（最新功能，可能不穩定）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>caelestia-cli&lt;/code>&lt;/td>
 &lt;td>CLI 工具（安裝、主題切換、截圖、錄影等）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>caelestia-shell&lt;/code> 是 &lt;code>caelestia-cli&lt;/code> 的 &lt;strong>optional dependency&lt;/strong>（提供 shell 控制與截圖功能）——只裝 CLI 不會自動拉 shell，兩個都要就各自明講：&lt;code>yay -S caelestia-cli caelestia-shell&lt;/code>（實測 caelestia-cli 1.1.1 的 PKGBUILD，shell 列在 optdepends）。&lt;/p>
&lt;h2 id="手動-build">手動 Build&lt;/h2>
&lt;p>從原始碼 build shell（不使用 AUR）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$XDG_CONFIG_HOME&lt;/span>&lt;span class="s2">/quickshell&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">git clone https://github.com/caelestia-dots/shell.git caelestia
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> caelestia
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">cmake -B build -G Ninja -DCMAKE_BUILD_TYPE&lt;span class="o">=&lt;/span>Release -DCMAKE_INSTALL_PREFIX&lt;span class="o">=&lt;/span>/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">cmake --build build
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">sudo cmake --install build&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="runtime-依賴">Runtime 依賴&lt;/h2>
&lt;h3 id="shell-依賴">Shell 依賴&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>套件&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>quickshell-git&lt;/td>
 &lt;td>Quickshell 框架（穩定版不夠用）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ddcutil&lt;/td>
 &lt;td>外接螢幕亮度控制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>brightnessctl&lt;/td>
 &lt;td>筆電螢幕亮度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>libcava&lt;/td>
 &lt;td>音訊視覺化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>networkmanager&lt;/td>
 &lt;td>網路管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>lm-sensors&lt;/td>
 &lt;td>硬體溫度感測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>fish&lt;/td>
 &lt;td>Fish shell（部分功能依賴）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>aubio&lt;/td>
 &lt;td>音訊分析&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>libpipewire&lt;/td>
 &lt;td>PipeWire 音訊整合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>qt6-declarative&lt;/td>
 &lt;td>QML runtime&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>material-symbols (font)&lt;/td>
 &lt;td>Material Design icon 字型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>caskaydia-cove-nerd&lt;/td>
 &lt;td>Nerd Font&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>quickshell-git 是硬性需求&lt;/strong>。穩定版的 quickshell 缺少 Caelestia 需要的 API，安裝穩定版會導致 shell 無法啟動。&lt;/p></description><content:encoded><![CDATA[<p>Caelestia 的安裝有兩條路：用 CLI 工具一鍵部署完整 dotfiles，或只安裝 shell 元件保留自己的 Hyprland 配置。兩者的前提都是 Hyprland 已經安裝且能正常啟動。</p>
<h2 id="前提條件">前提條件</h2>
<ul>
<li>Arch Linux（或 Arch 系發行版如 CachyOS、EndeavourOS）</li>
<li>Hyprland 已安裝且能從 TTY 啟動（見 <a href="/blog/linux/dotfile/05-hyprland-config/hyprland-installation/" data-link-title="Hyprland 安裝與環境建置" data-link-desc="要在 Arch Linux 上從零安裝 Hyprland 桌面環境時回來讀">Hyprland 安裝指南</a>）</li>
<li>AUR helper 已安裝（yay 或 paru）</li>
<li>網路連線（安裝過程需要拉 AUR 套件和 Git repo）</li>
</ul>
<h2 id="推薦方式cli-完整安裝">推薦方式：CLI 完整安裝</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">paru -S caelestia-cli
</span></span><span class="line"><span class="ln">2</span><span class="cl">caelestia install</span></span></code></pre></div><p><code>caelestia install</code> 做的事：</p>
<ol>
<li>從 GitHub clone Caelestia dotfiles repo</li>
<li>安裝所有 runtime 依賴（透過 AUR helper）</li>
<li>部署配置檔到 <code>~/.config/</code> 對應位置</li>
<li>設定 Hyprland 載入 Caelestia shell</li>
</ol>
<p>安裝完成後重新啟動 Hyprland，Caelestia 會自動載入。</p>
<p><strong>注意</strong>：<code>caelestia install</code> 會覆寫你現有的 Hyprland 配置。如果你已經有自己的 hyprland.conf / hyprland.lua，先備份。安裝後可以透過 <code>~/.config/caelestia/hypr-user.lua</code> 加入自訂設定。</p>
<h2 id="shell-only-安裝">Shell-only 安裝</h2>
<p>只裝 UI 元件，不動 Hyprland config 和其他應用程式設定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">yay -S caelestia-shell</span></span></code></pre></div><p>啟動方式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">caelestia shell -d    <span class="c1"># daemonized，背景執行</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 或</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">qs -c caelestia       <span class="c1"># 透過 quickshell 直接啟動</span></span></span></code></pre></div><p>Shell 不會因為裝了就自動啟動——Hyprland 開機只執行 config 裡 <code>exec-once</code> 列出的程式，漏列的結果是登入後桌面沒有 bar 也沒有通知（實測：手動啟動用了幾天、直到主機重開才暴露從沒進 autostart）。把啟動指令加進 <code>hyprland.conf</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="na">exec-once</span> <span class="o">=</span> <span class="s">caelestia shell -d</span></span></span></code></pre></div><p>Caelestia 自帶 bar、通知、鎖屏與 launcher，跟獨立的 waybar / mako 互斥：通知的 D-Bus name（<code>org.freedesktop.Notifications</code>）同一時間只有一個擁有者、bar 會疊兩條。設 Caelestia 為主 shell 時，把 waybar / mako 從 <code>exec-once</code> 移除（註解掉可留作停用 Caelestia 時的 fallback）。</p>
<h2 id="aur-套件一覽">AUR 套件一覽</h2>
<table>
  <thead>
      <tr>
          <th>套件</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>caelestia-shell</code></td>
          <td>穩定版 shell（UI 元件）</td>
      </tr>
      <tr>
          <td><code>caelestia-shell-git</code></td>
          <td>開發版 shell（最新功能，可能不穩定）</td>
      </tr>
      <tr>
          <td><code>caelestia-cli</code></td>
          <td>CLI 工具（安裝、主題切換、截圖、錄影等）</td>
      </tr>
  </tbody>
</table>
<p><code>caelestia-shell</code> 是 <code>caelestia-cli</code> 的 <strong>optional dependency</strong>（提供 shell 控制與截圖功能）——只裝 CLI 不會自動拉 shell，兩個都要就各自明講：<code>yay -S caelestia-cli caelestia-shell</code>（實測 caelestia-cli 1.1.1 的 PKGBUILD，shell 列在 optdepends）。</p>
<h2 id="手動-build">手動 Build</h2>
<p>從原始碼 build shell（不使用 AUR）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">cd</span> <span class="s2">&#34;</span><span class="nv">$XDG_CONFIG_HOME</span><span class="s2">/quickshell&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git clone https://github.com/caelestia-dots/shell.git caelestia
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">cd</span> caelestia
</span></span><span class="line"><span class="ln">4</span><span class="cl">cmake -B build -G Ninja -DCMAKE_BUILD_TYPE<span class="o">=</span>Release -DCMAKE_INSTALL_PREFIX<span class="o">=</span>/
</span></span><span class="line"><span class="ln">5</span><span class="cl">cmake --build build
</span></span><span class="line"><span class="ln">6</span><span class="cl">sudo cmake --install build</span></span></code></pre></div><h2 id="runtime-依賴">Runtime 依賴</h2>
<h3 id="shell-依賴">Shell 依賴</h3>
<table>
  <thead>
      <tr>
          <th>套件</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>quickshell-git</td>
          <td>Quickshell 框架（穩定版不夠用）</td>
      </tr>
      <tr>
          <td>ddcutil</td>
          <td>外接螢幕亮度控制</td>
      </tr>
      <tr>
          <td>brightnessctl</td>
          <td>筆電螢幕亮度</td>
      </tr>
      <tr>
          <td>libcava</td>
          <td>音訊視覺化</td>
      </tr>
      <tr>
          <td>networkmanager</td>
          <td>網路管理</td>
      </tr>
      <tr>
          <td>lm-sensors</td>
          <td>硬體溫度感測</td>
      </tr>
      <tr>
          <td>fish</td>
          <td>Fish shell（部分功能依賴）</td>
      </tr>
      <tr>
          <td>aubio</td>
          <td>音訊分析</td>
      </tr>
      <tr>
          <td>libpipewire</td>
          <td>PipeWire 音訊整合</td>
      </tr>
      <tr>
          <td>qt6-declarative</td>
          <td>QML runtime</td>
      </tr>
      <tr>
          <td>material-symbols (font)</td>
          <td>Material Design icon 字型</td>
      </tr>
      <tr>
          <td>caskaydia-cove-nerd</td>
          <td>Nerd Font</td>
      </tr>
  </tbody>
</table>
<p><strong>quickshell-git 是硬性需求</strong>。穩定版的 quickshell 缺少 Caelestia 需要的 API，安裝穩定版會導致 shell 無法啟動。</p>
<h3 id="cli-依賴">CLI 依賴</h3>
<table>
  <thead>
      <tr>
          <th>套件</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>libnotify</td>
          <td>通知發送</td>
      </tr>
      <tr>
          <td>swappy</td>
          <td>截圖標註</td>
      </tr>
      <tr>
          <td>grim</td>
          <td>Wayland 截圖</td>
      </tr>
      <tr>
          <td>dart-sass</td>
          <td>SCSS 編譯</td>
      </tr>
      <tr>
          <td>wl-clipboard</td>
          <td>剪貼簿</td>
      </tr>
      <tr>
          <td>slurp</td>
          <td>區域選取</td>
      </tr>
      <tr>
          <td>gpu-screen-recorder</td>
          <td>螢幕錄影</td>
      </tr>
      <tr>
          <td>glib2</td>
          <td>GLib 工具</td>
      </tr>
      <tr>
          <td>cliphist</td>
          <td>剪貼簿歷史</td>
      </tr>
      <tr>
          <td>fuzzel</td>
          <td>模糊搜尋選單</td>
      </tr>
  </tbody>
</table>
<h2 id="登入管理器">登入管理器</h2>
<p>Caelestia 不含登入管理器。推薦用 greetd + tuigreet：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo pacman -S greetd greetd-tuigreet</span></span></code></pre></div><p><code>/etc/greetd/config.toml</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span><span class="nx">terminal</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">vt</span> <span class="p">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">[</span><span class="nx">default_session</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">command</span> <span class="p">=</span> <span class="s2">&#34;tuigreet --cmd Hyprland&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">user</span> <span class="p">=</span> <span class="s2">&#34;greeter&#34;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl <span class="nb">enable</span> greetd</span></span></code></pre></div><p>也可以不裝登入管理器，直接從 TTY 啟動：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 登入 TTY 後</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">Hyprland</span></span></code></pre></div><h2 id="full-dotfiles-管理範圍">Full Dotfiles 管理範圍</h2>
<p><code>caelestia install</code> 部署的完整 dotfiles 不只是 shell，還包括：</p>
<ul>
<li>Hyprland config（Lua 格式）</li>
<li>Firefox / Zen Browser 設定</li>
<li>VSCode / Zed 設定</li>
<li>Fish shell config</li>
<li>Foot terminal config</li>
<li>Starship prompt</li>
<li>Btop</li>
<li>Fastfetch</li>
<li>Thunar 檔案管理器</li>
</ul>
<p>這是 Caelestia 「一套 rice」的完整範圍。如果你只想用 shell 元件、保留自己的應用程式配置，用 shell-only 安裝。</p>
<h2 id="cli-常用指令">CLI 常用指令</h2>
<table>
  <thead>
      <tr>
          <th>指令</th>
          <th>功能</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>caelestia shell -d</code></td>
          <td>啟動 shell（背景）</td>
      </tr>
      <tr>
          <td><code>caelestia shell -s</code></td>
          <td>列出所有 IPC 指令</td>
      </tr>
      <tr>
          <td><code>caelestia install</code></td>
          <td>完整安裝 dotfiles</td>
      </tr>
      <tr>
          <td><code>caelestia update</code></td>
          <td>系統 + dotfiles 更新</td>
      </tr>
      <tr>
          <td><code>caelestia scheme set -n dynamic</code></td>
          <td>設定動態配色方案</td>
      </tr>
      <tr>
          <td><code>caelestia wallpaper -f &lt;path&gt;</code></td>
          <td>設定桌布</td>
      </tr>
      <tr>
          <td><code>caelestia screenshot</code></td>
          <td>截圖</td>
      </tr>
      <tr>
          <td><code>caelestia record</code></td>
          <td>螢幕錄影</td>
      </tr>
      <tr>
          <td><code>caelestia clipboard</code></td>
          <td>剪貼簿歷史</td>
      </tr>
      <tr>
          <td><code>caelestia emoji</code></td>
          <td>Emoji / glyph 選取器</td>
      </tr>
      <tr>
          <td><code>caelestia toggle</code></td>
          <td>切換特殊工作區</td>
      </tr>
      <tr>
          <td><code>caelestia resizer</code></td>
          <td>視窗 resize daemon</td>
      </tr>
  </tbody>
</table>
<h2 id="首次啟動常見問題">首次啟動常見問題</h2>
<p><strong>黑屏</strong>：通常是缺少 <code>xdg-desktop-portal-hyprland</code>。確認已安裝：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pacman -Q xdg-desktop-portal-hyprland</span></span></code></pre></div><p><strong>Shell 沒有載入</strong>：確認 quickshell-git（不是 quickshell 穩定版）已安裝，且 Hyprland 的 exec-once 有啟動 Caelestia。</p>
<p><strong>字型 icon 顯示為方塊</strong>：缺少 Material Symbols 和 Nerd Font。安裝：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">yay -S material-symbols ttf-caskaydia-cove-nerd</span></span></code></pre></div><h2 id="vm-測試-vs-實機測試">VM 測試 vs 實機測試</h2>
<p><strong>VM 可測試</strong>：安裝流程完整性、CLI 指令是否正常、配置檔結構和語法、啟動器功能、通知系統行為、配置 reload。</p>
<p><strong>需實機測試</strong>：動畫流暢度和幀率、blur 品質和效能影響、Material Design 3 動態取色品質、多螢幕佈局、daily-use 的回應速度和穩定性。</p>
<p>VM 中 Caelestia 的 blur、動畫、動態取色會極度降級或無法運作（軟體渲染沒有足夠的 GPU 加速）。VM 適合驗證「裝得起來、config 能讀」，不適合評估視覺效果和日常使用體驗。</p>
]]></content:encoded></item><item><title>Dotfile 工作環境配置指南</title><link>https://tarrragon.github.io/blog/linux/dotfile/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/</guid><description>&lt;p>開發者的工作環境是一組配置檔的集合：shell 怎麼設定、編輯器用什麼快捷鍵、終端機長什麼樣、視窗怎麼排列。這些配置檔在 Unix 系統上以 &lt;code>.&lt;/code> 開頭（隱藏檔），統稱 dotfile。&lt;/p>
&lt;p>dotfile 管理解決的核心問題是&lt;strong>環境可重現性&lt;/strong>。一台新機器、一次重灌、一個 VM，都應該能用一份 Git repo 還原成你熟悉的工作環境。不做管理的代價是：每次換機器都在重新手動設定，每次都少記一兩個東西，每次都花一兩天才回到順手的狀態。&lt;/p>
&lt;h2 id="和其他系列的關係">和其他系列的關係&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>系列&lt;/th>
 &lt;th>聚焦&lt;/th>
 &lt;th>和 Dotfile 的交集&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/" data-link-title="Infra 基礎設施建置指南" data-link-desc="從零循序漸進把雲端基礎設施做起來 — IaC、身分憑證、網路地基、環境分離、核心服務、可觀測性、自動化 review 與治理習慣，含怎麼在組織內推動">Infra&lt;/a>&lt;/td>
 &lt;td>雲端基礎設施地基（IaC、網路、身分、環境分離）&lt;/td>
 &lt;td>Infra 是組織的環境 as code，Dotfile 是個人的環境 as code，思想平行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/devops/" data-link-title="DevOps 實務指南" data-link-desc="負載平衡、水平擴展、流量管控、服務探活、容量規劃、高可用、突發流量、成本管理 — 服務營運的工程基礎">DevOps&lt;/a>&lt;/td>
 &lt;td>服務營運（負載、擴展、容量、成本）&lt;/td>
 &lt;td>DevOps 工程師的日常工具鏈（多終端機、SSH、log tail）正是 dotfile 高度客製的場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/" data-link-title="CLI 環境工具" data-link-desc="在純文字終端機下做事時要挑工具——日常指令的現代替代品（rg/fd/fzf），以及用文字字元做出圖形化操作介面的 TUI（監控、圖表、多工器、資料庫）——想知道各情境有哪些選擇時回來讀">CLI&lt;/a>&lt;/td>
 &lt;td>TUI 工具、多工器、檔案管理器&lt;/td>
 &lt;td>CLI 工具的配置檔是 dotfile 管理的核心對象&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring&lt;/a>&lt;/td>
 &lt;td>客戶端監控體系&lt;/td>
 &lt;td>Monitoring 有獨立的 hands-on 專案做實測，Dotfile 也會有 VM 實測專案&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Infra 教「組織的地基怎麼用 code 管理」，Dotfile 教「個人的工作桌面怎麼用 code 管理」。模組九把兩者接起來——當個人 dotfile 的思想擴展到團隊，就是 devcontainer 和標準化開發環境。&lt;/p>
&lt;h2 id="地基機器初始化">地基：機器初始化&lt;/h2>
&lt;p>編號模組都假設你已經有一台裝好 Linux、工具齊全、能從外部連入的機器。&lt;a href="https://tarrragon.github.io/blog/linux/install/" data-link-title="Linux 安裝與機器初始化" data-link-desc="在 VM 或新機器從零裝好 Linux、判讀安裝程式選項、驗證最小系統、或要從外部連入跑 bootstrap 時回來讀">Linux 安裝與機器初始化&lt;/a> 補的是那段被預設跳過的地基——OS 安裝選項怎麼判讀、最小系統缺哪些必要工具、怎麼連進去跑 bootstrap（含還沒有 SSH key 的情況），以及怎麼讓安裝腳本在失敗時可診斷。只要你換到全新環境（開 VM、租雲端主機、拿到空機器），就會先撞上這層。內容來自一次完整的 VM 實測，蒸餾成不綁特定發行版的判讀與決策。&lt;/p>
&lt;h2 id="教學模組">教學模組&lt;/h2>
&lt;p>模組編號反映學習路徑：先理解為什麼、再學怎麼管理、然後逐層處理 shell/終端機/視窗管理/視覺客製化，最後談維護排錯、同步可攜性和團隊化。模組四到七是 Linux 桌面環境的深度實作，之後會搭配 VM 專案做 hands-on 實測。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>模組&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>回答什麼問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">模組零：Dotfile 心智模型&lt;/a>&lt;/td>
 &lt;td>什麼是 dotfile、為什麼要管理、環境可重現性&lt;/td>
 &lt;td>為什麼不能每次手動設定就好&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構&lt;/a>&lt;/td>
 &lt;td>bare repo / stow / chezmoi、目錄設計、版控工作流&lt;/td>
 &lt;td>dotfile 怎麼用 Git 管、目錄該怎麼組織&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/02-shell-config/" data-link-title="模組二：Shell 配置" data-link-desc="shell 配置檔長成一坨不敢動時回來讀 — .zshrc/.bashrc 的結構化拆分、alias/function/PATH 的模組化設計">模組二：Shell 配置&lt;/a>&lt;/td>
 &lt;td>zsh/bash 結構化配置、模組化拆分、alias/function/PATH&lt;/td>
 &lt;td>.zshrc 該怎麼寫才不會長成一坨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">模組三：終端機與編輯器&lt;/a>&lt;/td>
 &lt;td>terminal emulator 選型、tmux/zellij、neovim 基礎&lt;/td>
 &lt;td>終端機生態的配置檔有哪些、怎麼管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/04-window-management/" data-link-title="模組四：視窗管理與平鋪式工作流" data-link-desc="同時開多個視窗時的排列策略 — 手動貼齊跟自動平鋪的差距在哪、macOS 和 Linux 各有哪些工具、多螢幕怎麼處理、什麼情境值得從浮動切換到平鋪">模組四：視窗管理與平鋪式工作流&lt;/a>&lt;/td>
 &lt;td>手動 vs 自動平鋪、macOS 工具鏈、Linux tiling WM&lt;/td>
 &lt;td>Rectangle 不夠用的時候該換什麼&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置&lt;/a>&lt;/td>
 &lt;td>Hyprland 安裝、核心設定、keybind、monitor、workspace&lt;/td>
 &lt;td>Hyprland 的配置檔怎麼寫、核心概念是什麼&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計&lt;/a>&lt;/td>
 &lt;td>狀態列、啟動器、通知、配色系統、desktop shell&lt;/td>
 &lt;td>桌面怎麼從「能用」變成「好看又好用」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">模組七：桌面環境維護與故障排除&lt;/a>&lt;/td>
 &lt;td>故障隔離模型、常見故障恢復、日誌判讀&lt;/td>
 &lt;td>compositor 掛了或桌面凍結時怎麼診斷和恢復&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建&lt;/a>&lt;/td>
 &lt;td>跨機器同步、install script、secret 排除、VM 對比&lt;/td>
 &lt;td>換機器時怎麼一鍵還原、哪些東西不該進 repo&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/" data-link-title="模組九：從個人到團隊" data-link-desc="個人 dotfile 管理的思想要延伸到團隊開發環境標準化時回來讀 — devcontainer、nix、商業環境配置管理">模組九：從個人到團隊&lt;/a>&lt;/td>
 &lt;td>devcontainer、nix、商業開發環境標準化&lt;/td>
 &lt;td>個人 dotfile 的思想怎麼延伸到團隊環境管理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="vm-實測專案">VM 實測專案&lt;/h2>
&lt;p>模組四到七的 Linux 桌面配置會搭配一個獨立的 VM 專案做 hands-on 實測（類似 &lt;a href="https://tarrragon.github.io/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring&lt;/a> 系列搭配 monitor 專案的關係）。教材負責概念與配置邏輯的說明，VM 專案負責實際安裝、調教、截圖驗證。VM 專案會在教材完成後另外建立。&lt;/p></description><content:encoded><![CDATA[<p>開發者的工作環境是一組配置檔的集合：shell 怎麼設定、編輯器用什麼快捷鍵、終端機長什麼樣、視窗怎麼排列。這些配置檔在 Unix 系統上以 <code>.</code> 開頭（隱藏檔），統稱 dotfile。</p>
<p>dotfile 管理解決的核心問題是<strong>環境可重現性</strong>。一台新機器、一次重灌、一個 VM，都應該能用一份 Git repo 還原成你熟悉的工作環境。不做管理的代價是：每次換機器都在重新手動設定，每次都少記一兩個東西，每次都花一兩天才回到順手的狀態。</p>
<h2 id="和其他系列的關係">和其他系列的關係</h2>
<table>
  <thead>
      <tr>
          <th>系列</th>
          <th>聚焦</th>
          <th>和 Dotfile 的交集</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/" data-link-title="Infra 基礎設施建置指南" data-link-desc="從零循序漸進把雲端基礎設施做起來 — IaC、身分憑證、網路地基、環境分離、核心服務、可觀測性、自動化 review 與治理習慣，含怎麼在組織內推動">Infra</a></td>
          <td>雲端基礎設施地基（IaC、網路、身分、環境分離）</td>
          <td>Infra 是組織的環境 as code，Dotfile 是個人的環境 as code，思想平行</td>
      </tr>
      <tr>
          <td><a href="/blog/devops/" data-link-title="DevOps 實務指南" data-link-desc="負載平衡、水平擴展、流量管控、服務探活、容量規劃、高可用、突發流量、成本管理 — 服務營運的工程基礎">DevOps</a></td>
          <td>服務營運（負載、擴展、容量、成本）</td>
          <td>DevOps 工程師的日常工具鏈（多終端機、SSH、log tail）正是 dotfile 高度客製的場景</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/tools/cli/" data-link-title="CLI 環境工具" data-link-desc="在純文字終端機下做事時要挑工具——日常指令的現代替代品（rg/fd/fzf），以及用文字字元做出圖形化操作介面的 TUI（監控、圖表、多工器、資料庫）——想知道各情境有哪些選擇時回來讀">CLI</a></td>
          <td>TUI 工具、多工器、檔案管理器</td>
          <td>CLI 工具的配置檔是 dotfile 管理的核心對象</td>
      </tr>
      <tr>
          <td><a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring</a></td>
          <td>客戶端監控體系</td>
          <td>Monitoring 有獨立的 hands-on 專案做實測，Dotfile 也會有 VM 實測專案</td>
      </tr>
  </tbody>
</table>
<p>Infra 教「組織的地基怎麼用 code 管理」，Dotfile 教「個人的工作桌面怎麼用 code 管理」。模組九把兩者接起來——當個人 dotfile 的思想擴展到團隊，就是 devcontainer 和標準化開發環境。</p>
<h2 id="地基機器初始化">地基：機器初始化</h2>
<p>編號模組都假設你已經有一台裝好 Linux、工具齊全、能從外部連入的機器。<a href="/blog/linux/install/" data-link-title="Linux 安裝與機器初始化" data-link-desc="在 VM 或新機器從零裝好 Linux、判讀安裝程式選項、驗證最小系統、或要從外部連入跑 bootstrap 時回來讀">Linux 安裝與機器初始化</a> 補的是那段被預設跳過的地基——OS 安裝選項怎麼判讀、最小系統缺哪些必要工具、怎麼連進去跑 bootstrap（含還沒有 SSH key 的情況），以及怎麼讓安裝腳本在失敗時可診斷。只要你換到全新環境（開 VM、租雲端主機、拿到空機器），就會先撞上這層。內容來自一次完整的 VM 實測，蒸餾成不綁特定發行版的判讀與決策。</p>
<h2 id="教學模組">教學模組</h2>
<p>模組編號反映學習路徑：先理解為什麼、再學怎麼管理、然後逐層處理 shell/終端機/視窗管理/視覺客製化，最後談維護排錯、同步可攜性和團隊化。模組四到七是 Linux 桌面環境的深度實作，之後會搭配 VM 專案做 hands-on 實測。</p>
<table>
  <thead>
      <tr>
          <th>模組</th>
          <th>主題</th>
          <th>回答什麼問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">模組零：Dotfile 心智模型</a></td>
          <td>什麼是 dotfile、為什麼要管理、環境可重現性</td>
          <td>為什麼不能每次手動設定就好</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構</a></td>
          <td>bare repo / stow / chezmoi、目錄設計、版控工作流</td>
          <td>dotfile 怎麼用 Git 管、目錄該怎麼組織</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/02-shell-config/" data-link-title="模組二：Shell 配置" data-link-desc="shell 配置檔長成一坨不敢動時回來讀 — .zshrc/.bashrc 的結構化拆分、alias/function/PATH 的模組化設計">模組二：Shell 配置</a></td>
          <td>zsh/bash 結構化配置、模組化拆分、alias/function/PATH</td>
          <td>.zshrc 該怎麼寫才不會長成一坨</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">模組三：終端機與編輯器</a></td>
          <td>terminal emulator 選型、tmux/zellij、neovim 基礎</td>
          <td>終端機生態的配置檔有哪些、怎麼管理</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/04-window-management/" data-link-title="模組四：視窗管理與平鋪式工作流" data-link-desc="同時開多個視窗時的排列策略 — 手動貼齊跟自動平鋪的差距在哪、macOS 和 Linux 各有哪些工具、多螢幕怎麼處理、什麼情境值得從浮動切換到平鋪">模組四：視窗管理與平鋪式工作流</a></td>
          <td>手動 vs 自動平鋪、macOS 工具鏈、Linux tiling WM</td>
          <td>Rectangle 不夠用的時候該換什麼</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置</a></td>
          <td>Hyprland 安裝、核心設定、keybind、monitor、workspace</td>
          <td>Hyprland 的配置檔怎麼寫、核心概念是什麼</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計</a></td>
          <td>狀態列、啟動器、通知、配色系統、desktop shell</td>
          <td>桌面怎麼從「能用」變成「好看又好用」</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">模組七：桌面環境維護與故障排除</a></td>
          <td>故障隔離模型、常見故障恢復、日誌判讀</td>
          <td>compositor 掛了或桌面凍結時怎麼診斷和恢復</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建</a></td>
          <td>跨機器同步、install script、secret 排除、VM 對比</td>
          <td>換機器時怎麼一鍵還原、哪些東西不該進 repo</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/09-team-environment/" data-link-title="模組九：從個人到團隊" data-link-desc="個人 dotfile 管理的思想要延伸到團隊開發環境標準化時回來讀 — devcontainer、nix、商業環境配置管理">模組九：從個人到團隊</a></td>
          <td>devcontainer、nix、商業開發環境標準化</td>
          <td>個人 dotfile 的思想怎麼延伸到團隊環境管理</td>
      </tr>
  </tbody>
</table>
<h2 id="vm-實測專案">VM 實測專案</h2>
<p>模組四到七的 Linux 桌面配置會搭配一個獨立的 VM 專案做 hands-on 實測（類似 <a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring</a> 系列搭配 monitor 專案的關係）。教材負責概念與配置邏輯的說明，VM 專案負責實際安裝、調教、截圖驗證。VM 專案會在教材完成後另外建立。</p>
]]></content:encoded></item><item><title>Hyprland VM 環境設定與測試矩陣</title><link>https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/hyprland-vm-setup/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/hyprland-vm-setup/</guid><description>&lt;p>VM 是 Hyprland 配置的演練場——用來驗證「配置邏輯對不對」，不是用來體驗「跑起來順不順」。GPU 加速在 VM 中受限，動畫和模糊效果會嚴重降級或無法使用，但配置檔語法、keybind 設計、window rules、workspace 邏輯都能在 VM 中完整測試。&lt;/p>
&lt;h2 id="utm-on-apple-silicon-設定">UTM on Apple Silicon 設定&lt;/h2>
&lt;p>Apple Silicon Mac 用 UTM（基於 QEMU）跑 ARM64 Linux VM：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CPU&lt;/strong>：UTM 使用 Apple Hypervisor.framework，ARM64 guest 接近原生速度&lt;/li>
&lt;li>&lt;strong>GPU&lt;/strong>：使用 &lt;code>virtio-gpu-gl-pci&lt;/code>（UTM v4.1+ 新建 Linux VM 的預設）。UTM v5.0.0+ 的 GitHub release 版支援 Venus driver（guest Mesa → host MoltenVK → Apple Metal 的 Vulkan 轉送路徑），這是目前最好的 GPU 加速方案&lt;/li>
&lt;li>&lt;strong>Linux ISO&lt;/strong>：Arch Linux ARM（archlinuxarm.org）或 Fedora aarch64&lt;/li>
&lt;li>&lt;strong>建議配置&lt;/strong>：4 CPU cores、4GB+ RAM、40GB+ disk&lt;/li>
&lt;/ul>
&lt;h3 id="utm-建-vm-的注意事項">UTM 建 VM 的注意事項&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>第一頁選 Virtualize，不是 Emulate&lt;/strong>——同架構（Apple Silicon 跑 ARM64 guest）兩條都是 QEMU，差在 Virtualize 走 hvf 硬體虛擬化（CPU 接近原生）、Emulate 走 TCG 純軟體模擬（CPU 慢一個數量級；實測 C++ 大型編譯一小時只跑 1/3）。Emulate 只在跨架構（ARM Mac 跑 x86_64 guest）才需要。判別現有 VM：guest 裡 &lt;code>lscpu&lt;/code> 的 Model name 是 &lt;code>-&lt;/code> 為直通、顯示具體型號（如 Cortex-A72）為模擬&lt;/li>
&lt;li>選 「Linux」 虛擬機類型，不是 「Other」&lt;/li>
&lt;li>Display Card 選 &lt;code>virtio-gpu-gl-pci&lt;/code>（有 3D 加速），不是 &lt;code>virtio-gpu-pci&lt;/code>（無加速）；Emulate 精靈預設給無加速的 &lt;code>virtio-gpu-pci&lt;/code>、Virtualize 精靈通常直接給對&lt;/li>
&lt;li>&lt;strong>VM 視窗可能停在序列視圖、圖形顯示是另一個視圖&lt;/strong>——Virtualize 精靈預設多附一個序列裝置，UTM 視窗開機後可能顯示的是序列 console（文字登入、guest 裡 &lt;code>who&lt;/code> 顯示登入在 &lt;code>pts/0&lt;/code>），跟 virtio-gpu 的圖形輸出是兩個獨立視圖。切換：VM 視窗工具列的顯示器下拉選單 → 選 &lt;code>Display 1 (virtio-gpu)&lt;/code>。判讀圖形裝置本身有沒有掛上看 guest 的 &lt;code>ls /dev/dri/&lt;/code>（有 &lt;code>card0&lt;/code> = 裝置在、只是視窗看錯視圖）。不想每次切就到 VM 設定移除序列裝置。compositor 要在圖形視圖那側的 VT 上啟動、序列 console 起不了 Hyprland&lt;/li>
&lt;li>如果用 App Store 版的 UTM（不含 Venus），只有基本的 virtio-gpu-gl 加速&lt;/li>
&lt;li>GitHub release 版的 UTM 支援 Venus/MoltenVK，效能更好但仍不及裸金屬&lt;/li>
&lt;li>&lt;strong>修飾鍵：Mac 的 ⌘ 對應 guest 的 &lt;code>SUPER&lt;/code>，但 macOS 會先攔截部分 ⌘ 組合&lt;/strong>——Hyprland 的 keybind 幾乎都以 &lt;code>SUPER&lt;/code>（Meta）當主修飾鍵（見 &lt;a href="../hyprland-core-config/">核心配置的修飾鍵段&lt;/a>），而 UTM 裡 Mac 的 Command ⌘ 通常就是那顆 &lt;code>SUPER&lt;/code>。問題是 macOS 自己會先吃掉某些 ⌘ 組合（⌘+Q 結束 app、⌘+Space Spotlight…），VM 收不到。判讀：&lt;code>SUPER&lt;/code> 綁定沒反應、但焦點視窗打字正常，多半是宿主層攔截、不是 Hyprland 配置錯。解法：先確認 VM 視窗有 focus；在 UTM 的鍵盤/輸入設定開「把系統快捷鍵送進 VM（capture input）」讓 ⌘ 組合進 guest。臨時繞過：需要重載配置這類動作，直接在已開的終端機下指令（如 &lt;code>source&lt;/code> / &lt;code>hyprctl reload&lt;/code>），不必卡在 ⌘ 鍵上&lt;/li>
&lt;/ul>
&lt;h2 id="vm-必要環境變數">VM 必要環境變數&lt;/h2>
&lt;p>在 Hyprland 配置裡加入以下環境變數（只在 VM 中使用，實機要移除）：&lt;/p></description><content:encoded><![CDATA[<p>VM 是 Hyprland 配置的演練場——用來驗證「配置邏輯對不對」，不是用來體驗「跑起來順不順」。GPU 加速在 VM 中受限，動畫和模糊效果會嚴重降級或無法使用，但配置檔語法、keybind 設計、window rules、workspace 邏輯都能在 VM 中完整測試。</p>
<h2 id="utm-on-apple-silicon-設定">UTM on Apple Silicon 設定</h2>
<p>Apple Silicon Mac 用 UTM（基於 QEMU）跑 ARM64 Linux VM：</p>
<ul>
<li><strong>CPU</strong>：UTM 使用 Apple Hypervisor.framework，ARM64 guest 接近原生速度</li>
<li><strong>GPU</strong>：使用 <code>virtio-gpu-gl-pci</code>（UTM v4.1+ 新建 Linux VM 的預設）。UTM v5.0.0+ 的 GitHub release 版支援 Venus driver（guest Mesa → host MoltenVK → Apple Metal 的 Vulkan 轉送路徑），這是目前最好的 GPU 加速方案</li>
<li><strong>Linux ISO</strong>：Arch Linux ARM（archlinuxarm.org）或 Fedora aarch64</li>
<li><strong>建議配置</strong>：4 CPU cores、4GB+ RAM、40GB+ disk</li>
</ul>
<h3 id="utm-建-vm-的注意事項">UTM 建 VM 的注意事項</h3>
<ul>
<li><strong>第一頁選 Virtualize，不是 Emulate</strong>——同架構（Apple Silicon 跑 ARM64 guest）兩條都是 QEMU，差在 Virtualize 走 hvf 硬體虛擬化（CPU 接近原生）、Emulate 走 TCG 純軟體模擬（CPU 慢一個數量級；實測 C++ 大型編譯一小時只跑 1/3）。Emulate 只在跨架構（ARM Mac 跑 x86_64 guest）才需要。判別現有 VM：guest 裡 <code>lscpu</code> 的 Model name 是 <code>-</code> 為直通、顯示具體型號（如 Cortex-A72）為模擬</li>
<li>選 「Linux」 虛擬機類型，不是 「Other」</li>
<li>Display Card 選 <code>virtio-gpu-gl-pci</code>（有 3D 加速），不是 <code>virtio-gpu-pci</code>（無加速）；Emulate 精靈預設給無加速的 <code>virtio-gpu-pci</code>、Virtualize 精靈通常直接給對</li>
<li><strong>VM 視窗可能停在序列視圖、圖形顯示是另一個視圖</strong>——Virtualize 精靈預設多附一個序列裝置，UTM 視窗開機後可能顯示的是序列 console（文字登入、guest 裡 <code>who</code> 顯示登入在 <code>pts/0</code>），跟 virtio-gpu 的圖形輸出是兩個獨立視圖。切換：VM 視窗工具列的顯示器下拉選單 → 選 <code>Display 1 (virtio-gpu)</code>。判讀圖形裝置本身有沒有掛上看 guest 的 <code>ls /dev/dri/</code>（有 <code>card0</code> = 裝置在、只是視窗看錯視圖）。不想每次切就到 VM 設定移除序列裝置。compositor 要在圖形視圖那側的 VT 上啟動、序列 console 起不了 Hyprland</li>
<li>如果用 App Store 版的 UTM（不含 Venus），只有基本的 virtio-gpu-gl 加速</li>
<li>GitHub release 版的 UTM 支援 Venus/MoltenVK，效能更好但仍不及裸金屬</li>
<li><strong>修飾鍵：Mac 的 ⌘ 對應 guest 的 <code>SUPER</code>，但 macOS 會先攔截部分 ⌘ 組合</strong>——Hyprland 的 keybind 幾乎都以 <code>SUPER</code>（Meta）當主修飾鍵（見 <a href="../hyprland-core-config/">核心配置的修飾鍵段</a>），而 UTM 裡 Mac 的 Command ⌘ 通常就是那顆 <code>SUPER</code>。問題是 macOS 自己會先吃掉某些 ⌘ 組合（⌘+Q 結束 app、⌘+Space Spotlight…），VM 收不到。判讀：<code>SUPER</code> 綁定沒反應、但焦點視窗打字正常，多半是宿主層攔截、不是 Hyprland 配置錯。解法：先確認 VM 視窗有 focus；在 UTM 的鍵盤/輸入設定開「把系統快捷鍵送進 VM（capture input）」讓 ⌘ 組合進 guest。臨時繞過：需要重載配置這類動作，直接在已開的終端機下指令（如 <code>source</code> / <code>hyprctl reload</code>），不必卡在 ⌘ 鍵上</li>
</ul>
<h2 id="vm-必要環境變數">VM 必要環境變數</h2>
<p>在 Hyprland 配置裡加入以下環境變數（只在 VM 中使用，實機要移除）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- VM-only environment variables</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">env</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s2">&#34;WLR_RENDERER_ALLOW_SOFTWARE, 1&#34;</span><span class="p">,</span>  <span class="c1">-- 允許軟體渲染 fallback</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s2">&#34;WLR_NO_HARDWARE_CURSORS, 1&#34;</span><span class="p">,</span>      <span class="c1">-- 停用硬體 cursor（VM 常見問題）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s2">&#34;LIBGL_ALWAYS_SOFTWARE, 1&#34;</span><span class="p">,</span>        <span class="c1">-- 強制 Mesa 軟體渲染</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>如果上述仍無法啟動（virtio-gpu vGPU passthrough 的情況）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">env</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s2">&#34;AQ_NO_KMS_REQUIREMENT, 1&#34;</span><span class="p">,</span>       <span class="c1">-- 繞過 KMS 需求</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s2">&#34;WLR_RENDERER, pixman&#34;</span><span class="p">,</span>            <span class="c1">-- 強制 pixman 軟體 renderer</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><blockquote>
<p><strong>[已驗證]</strong> Hyprland 0.55（Aquamarine）+ UTM virtio-gpu-gl-pci 實測：GPU 加速模式下 Hyprland 直接走 VirGL/Venus，不需要 <code>WLR_RENDERER</code> 或 <code>WLR_RENDERER_ALLOW_SOFTWARE</code>。<code>AQ_NO_KMS_REQUIREMENT</code> 仍有效。軟體渲染 fallback 路徑（<code>WLR_RENDERER=pixman</code>）未測試——有 GPU 加速時不需要走這條。</p></blockquote>
<h2 id="vm-中應該關閉的效果">VM 中應該關閉的效果</h2>
<p>軟體渲染下，視覺效果是最大的效能殺手。建議在 VM 配置中停用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">decoration</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">blur</span> <span class="o">=</span> <span class="p">{</span> <span class="n">enabled</span> <span class="o">=</span> <span class="kc">false</span> <span class="p">},</span>     <span class="c1">-- 模糊是 GPU 最重的效果</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">shadow</span> <span class="o">=</span> <span class="p">{</span> <span class="n">enabled</span> <span class="o">=</span> <span class="kc">false</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">rounding</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">animations</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">enabled</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>                <span class="c1">-- 或設定極簡動畫</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>這些效果關閉後，基本的平鋪操作（切換視窗、移動 workspace、開關 app）在 VM 中應該足夠流暢。</p>
<h2 id="效能預期">效能預期</h2>
<table>
  <thead>
      <tr>
          <th>功能</th>
          <th>軟體渲染</th>
          <th>virtio-gpu-gl</th>
          <th>裸金屬（實機）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>基本平鋪操作</td>
          <td>可用</td>
          <td>順暢</td>
          <td>順暢</td>
      </tr>
      <tr>
          <td>視窗動畫</td>
          <td>卡頓明顯</td>
          <td>勉強可接受</td>
          <td>流暢</td>
      </tr>
      <tr>
          <td>模糊/透明</td>
          <td>無法使用</td>
          <td>卡頓</td>
          <td>流暢</td>
      </tr>
      <tr>
          <td>Waybar + Wofi</td>
          <td>正常</td>
          <td>正常</td>
          <td>正常</td>
      </tr>
      <tr>
          <td>多 Workspace 切換</td>
          <td>正常</td>
          <td>正常</td>
          <td>正常</td>
      </tr>
      <tr>
          <td>Firefox 瀏覽</td>
          <td>明顯變慢</td>
          <td>可用</td>
          <td>正常</td>
      </tr>
  </tbody>
</table>
<p>VM 的價值在於驗證配置邏輯，不在於評估視覺體驗。如果在 VM 裡覺得「卡」，不代表 Hyprland 本身慢——多數情況是 VM 圖形加速的限制。</p>
<h2 id="sway-作為-vm-初步驗證工具">Sway 作為 VM 初步驗證工具</h2>
<p>如果 VM 中 Hyprland 跑得太吃力，可以先用 Sway 驗證 VM 的 Wayland 圖形棧是否正常：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo pacman -S sway foot
</span></span><span class="line"><span class="ln">2</span><span class="cl">sway</span></span></code></pre></div><p>Sway 比 Hyprland 輕量（基於 wlroots、沒有華麗動畫），如果 Sway 能跑，代表 VM 的 Wayland 環境是正常的，Hyprland 的問題只是效能不夠。如果連 Sway 都跑不動，要回去檢查 VM 的 GPU 設定。</p>
<h2 id="vm-vs-實機測試矩陣">VM vs 實機測試矩陣</h2>
<h3 id="vm-中可完整驗證">VM 中可完整驗證</h3>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>配置檔語法</td>
          <td>Lua config 是否解析正確、require 拆分是否正常</td>
      </tr>
      <tr>
          <td>Keybind 設計</td>
          <td>快捷鍵邏輯、submap、modifier 組合</td>
      </tr>
      <tr>
          <td>Window rules</td>
          <td>float / workspace assignment / opacity 規則是否生效</td>
      </tr>
      <tr>
          <td>Workspace 切換</td>
          <td>workspace 編號、切換邏輯</td>
      </tr>
      <tr>
          <td>Layout 選擇</td>
          <td>dwindle vs master 的行為差異</td>
      </tr>
      <tr>
          <td>Waybar 模組配置</td>
          <td>JSON config + CSS styling 是否正確顯示</td>
      </tr>
      <tr>
          <td>Wofi/Rofi 主題</td>
          <td>啟動器的功能和外觀設定</td>
      </tr>
      <tr>
          <td>Mako 通知樣式</td>
          <td>通知的位置、配色、timeout</td>
      </tr>
      <tr>
          <td>Hyprlock 佈局</td>
          <td>鎖屏的輸入框位置和文字配置</td>
      </tr>
      <tr>
          <td>Autostart 順序</td>
          <td>exec-once 的程式是否正確啟動</td>
      </tr>
      <tr>
          <td>環境變數</td>
          <td>XDG、Qt、GTK 等環境變數是否正確設定</td>
      </tr>
      <tr>
          <td>Stow 部署</td>
          <td>dotfile repo 的 stow 是否正確建立 symlink</td>
      </tr>
      <tr>
          <td>Bootstrap script</td>
          <td>install.sh 的完整流程（安裝套件 + deploy 配置）</td>
      </tr>
      <tr>
          <td>Caelestia CLI 指令</td>
          <td><code>caelestia shell</code>、<code>caelestia scheme</code> 等指令是否可執行</td>
      </tr>
  </tbody>
</table>
<h3 id="需要實機測試">需要實機測試</h3>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>為什麼 VM 不行</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多螢幕配置</td>
          <td>VM 通常只有一個虛擬顯示器</td>
      </tr>
      <tr>
          <td>HiDPI / fractional scaling</td>
          <td>虛擬顯示器不模擬真實解析度行為</td>
      </tr>
      <tr>
          <td>VRR / Adaptive Sync</td>
          <td>需要支援 VRR 的真實螢幕</td>
      </tr>
      <tr>
          <td>動畫流暢度</td>
          <td>VM 的 GPU 加速不足以評估真實效能</td>
      </tr>
      <tr>
          <td>模糊效果品質</td>
          <td>軟體渲染下模糊不可用或品質差</td>
      </tr>
      <tr>
          <td>觸控板 / 手勢</td>
          <td>VM 沒有觸控板裝置</td>
      </tr>
      <tr>
          <td>媒體鍵 / 亮度鍵</td>
          <td>需要實體鍵盤上的 XF86 keycodes</td>
      </tr>
      <tr>
          <td>NVIDIA 驅動設定</td>
          <td>VM 不走 NVIDIA 驅動，所有 NVIDIA 配置無法測試</td>
      </tr>
      <tr>
          <td>Screen sharing</td>
          <td>PipeWire + portal 的完整鏈路在 VM 中測試無意義</td>
      </tr>
      <tr>
          <td>Suspend / Resume</td>
          <td>虛擬機的 suspend 行為跟實機不同</td>
      </tr>
      <tr>
          <td>硬體 cursor 渲染</td>
          <td>VM 用軟體 cursor，無法測試硬體 cursor 問題</td>
      </tr>
      <tr>
          <td>藍牙 / WiFi 整合</td>
          <td>需要實際硬體</td>
      </tr>
      <tr>
          <td>電池 / 電源管理</td>
          <td>筆電專屬功能</td>
      </tr>
      <tr>
          <td>日常使用效能</td>
          <td>只有在實機跑一段時間才能評估「能不能當主力」</td>
      </tr>
  </tbody>
</table>
<h2 id="務實的-vm-使用策略">務實的 VM 使用策略</h2>
<p>VM 階段的目標是「把配置寫好、驗證邏輯、確認 bootstrap script 能跑」，不是「體驗 Hyprland 好不好用」。具體做法：</p>
<ol>
<li>在 VM 中完成 Arch Linux 安裝 + Hyprland 套件安裝（怎麼把那台 Linux 從 ISO 裝起來、安裝程式選項怎麼判讀、裝完怎麼驗工具與連入，見 <a href="/blog/linux/install/" data-link-title="Linux 安裝與機器初始化" data-link-desc="在 VM 或新機器從零裝好 Linux、判讀安裝程式選項、驗證最小系統、或要從外部連入跑 bootstrap 時回來讀">Linux 安裝與機器初始化</a>）</li>
<li>關閉所有視覺效果（blur / animation / shadow）</li>
<li>寫好完整的 Hyprland 配置（keybind / rules / workspace / autostart）</li>
<li>寫好 waybar / wofi / mako 配置</li>
<li>測試 stow 部署流程（從 dotfile repo clone → stow → 配置生效）</li>
<li>測試 bootstrap script（install.sh 從零到完整桌面）</li>
<li>把驗證過的配置 commit 進 dotfile repo</li>
</ol>
<p>到實機時，clone dotfile repo → 跑 install.sh → 補上硬體相關設定（monitor、GPU 驅動、觸控板）→ 打開視覺效果。VM 階段的工作在實機上幾乎不用重做。</p>
]]></content:encoded></item><item><title>Wayland 顯示協議：為什麼 Hyprland 不跑在 X11 上</title><link>https://tarrragon.github.io/blog/linux/dotfile/04-window-management/wayland-explainer/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/04-window-management/wayland-explainer/</guid><description>&lt;p>Wayland 是 Linux 圖形系統的顯示協議，定義了應用程式怎麼跟負責畫面合成的 compositor 溝通。Hyprland、Sway 這些 tiling WM 都是 Wayland compositor——理解 Wayland 的架構，才能理解這些工具為什麼存在、能做什麼、不能做什麼。&lt;/p>
&lt;h2 id="wayland-是協議不是軟體">Wayland 是協議，不是軟體&lt;/h2>
&lt;p>一個常見的誤解是把 Wayland 當成「一個程式」。Wayland 是一份協議規格（protocol specification），描述了 client（應用程式）和 compositor（負責合成畫面的東西）之間怎麼傳遞 buffer、怎麼處理輸入事件。&lt;/p>
&lt;p>每個 Wayland compositor 同時扮演三個角色：display server（管理顯示輸出）、window manager（管理視窗排列）、compositor（合成最終畫面）。X11 的世界裡這三個角色是分開的，Wayland 把它們統一成一個程式。這個「三合一」定位的濃縮版與它在故障排除中的責任邊界，見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/compositor/" data-link-title="Compositor（合成器）" data-link-desc="教材反覆出現 compositor / 合成器、想確認它到底負責什麼、跟 window manager 和桌面環境差在哪時讀 — Wayland 下把畫面合成與視窗管理合一的核心程式">Compositor 術語卡&lt;/a>。&lt;/p>
&lt;p>「Hyprland 是 Wayland compositor」這個說法的意思是：它不只是一個視窗管理器，它同時是整個圖形系統的核心。&lt;/p>
&lt;h2 id="x11-vs-wayland-架構差異">X11 vs Wayland 架構差異&lt;/h2>
&lt;h3 id="x11-的三角架構1984-年設計">X11 的三角架構（1984 年設計）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">應用程式 ──→ X Server（中央仲介）──→ Compositor ──→ 螢幕
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↑ ↑
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> └── 輸入事件 ──────────────┘&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>X Server 是一個龐大的中央仲介，負責接收所有應用程式的繪圖指令、管理輸入事件、再把畫面交給 compositor 做最終合成。Compositing 是後來才加上去的（Compiz、picom 這些都是外掛的 compositor），不是 X11 原始設計的一部分。&lt;/p>
&lt;h3 id="wayland-的直接模型2008-年設計">Wayland 的直接模型（2008 年設計）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">應用程式 ──→ Compositor（= display server + window manager）──→ 螢幕
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↑
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> └── 輸入事件&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>應用程式用 OpenGL/Vulkan 自己渲染畫面到 buffer，然後把 buffer 直接交給 compositor。沒有中間人。compositor 收到所有 client 的 buffer 後合成最終畫面輸出到螢幕。&lt;/p>
&lt;p>這個架構上的差異帶來三個實際影響：效能、安全、screen tearing。&lt;/p>
&lt;h2 id="安全性">安全性&lt;/h2>
&lt;p>X11 的安全模型有一個根本性的設計缺陷：任何連到同一個 X Server 的 client，都能監聽其他 client 的鍵盤輸入、擷取其他視窗的畫面、甚至注入假的輸入事件。這代表一個惡意程式可以輕易做到 keylogging——記錄你在其他視窗打的所有字，包括密碼。&lt;/p>
&lt;p>Wayland 從協議層就隔離了 client 之間的存取。每個應用程式只看得到自己的 buffer，無法存取其他視窗的內容或輸入。要做 screen capture 或 screen sharing，必須透過 xdg-desktop-portal 這個受控的中介機制，使用者會看到明確的授權提示。&lt;/p>
&lt;p>對 tiling WM 的使用者來說，這個安全差異特別有意義：平鋪式桌面通常同時開大量終端機視窗（有些跑 SSH、有些在 sudo 操作），X11 下任何一個視窗裡的惡意程式都能讀到其他視窗的鍵盤輸入。Wayland 阻斷了這條攻擊路徑。&lt;/p>
&lt;h2 id="效能與-screen-tearing">效能與 Screen Tearing&lt;/h2>
&lt;p>X11 的渲染路徑有多餘的記憶體複製——應用程式把繪圖指令送給 X Server，X Server 渲染後再交給 compositor，compositor 再合成。Wayland 省掉了中間那一步：應用程式直接渲染到 buffer、直接交給 compositor，記憶體複製更少。&lt;/p>
&lt;p>Screen tearing（畫面撕裂）在 X11 上是長年的老問題——需要 compositor + vsync 的各種 workaround。Wayland 在協議層就處理了 frame presentation 的時機，compositor 控制什麼時候把合成好的畫面送到螢幕，原生消除 tearing。這也是為什麼 Hyprland 的動畫能跑得流暢——直接 rendering + 原生 vsync 的組合，在 X11 上需要大量額外配置才能接近的效果。&lt;/p>
&lt;h2 id="xwaylandx11-相容層">XWayland：X11 相容層&lt;/h2>
&lt;p>很多應用程式仍然只支援 X11 協議。XWayland 是一個 X11 server，但它本身作為一個 Wayland client 運行——X11 應用程式連到 XWayland，以為自己在跟正常的 X Server 溝通；XWayland 把 X11 協議翻譯成 Wayland 協議，把 buffer 交給 compositor。&lt;/p></description><content:encoded><![CDATA[<p>Wayland 是 Linux 圖形系統的顯示協議，定義了應用程式怎麼跟負責畫面合成的 compositor 溝通。Hyprland、Sway 這些 tiling WM 都是 Wayland compositor——理解 Wayland 的架構，才能理解這些工具為什麼存在、能做什麼、不能做什麼。</p>
<h2 id="wayland-是協議不是軟體">Wayland 是協議，不是軟體</h2>
<p>一個常見的誤解是把 Wayland 當成「一個程式」。Wayland 是一份協議規格（protocol specification），描述了 client（應用程式）和 compositor（負責合成畫面的東西）之間怎麼傳遞 buffer、怎麼處理輸入事件。</p>
<p>每個 Wayland compositor 同時扮演三個角色：display server（管理顯示輸出）、window manager（管理視窗排列）、compositor（合成最終畫面）。X11 的世界裡這三個角色是分開的，Wayland 把它們統一成一個程式。這個「三合一」定位的濃縮版與它在故障排除中的責任邊界，見 <a href="/blog/linux/dotfile/knowledge-cards/compositor/" data-link-title="Compositor（合成器）" data-link-desc="教材反覆出現 compositor / 合成器、想確認它到底負責什麼、跟 window manager 和桌面環境差在哪時讀 — Wayland 下把畫面合成與視窗管理合一的核心程式">Compositor 術語卡</a>。</p>
<p>「Hyprland 是 Wayland compositor」這個說法的意思是：它不只是一個視窗管理器，它同時是整個圖形系統的核心。</p>
<h2 id="x11-vs-wayland-架構差異">X11 vs Wayland 架構差異</h2>
<h3 id="x11-的三角架構1984-年設計">X11 的三角架構（1984 年設計）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">應用程式 ──→ X Server（中央仲介）──→ Compositor ──→ 螢幕
</span></span><span class="line"><span class="ln">2</span><span class="cl">             ↑                         ↑
</span></span><span class="line"><span class="ln">3</span><span class="cl">             └── 輸入事件 ──────────────┘</span></span></code></pre></div><p>X Server 是一個龐大的中央仲介，負責接收所有應用程式的繪圖指令、管理輸入事件、再把畫面交給 compositor 做最終合成。Compositing 是後來才加上去的（Compiz、picom 這些都是外掛的 compositor），不是 X11 原始設計的一部分。</p>
<h3 id="wayland-的直接模型2008-年設計">Wayland 的直接模型（2008 年設計）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">應用程式 ──→ Compositor（= display server + window manager）──→ 螢幕
</span></span><span class="line"><span class="ln">2</span><span class="cl">             ↑
</span></span><span class="line"><span class="ln">3</span><span class="cl">             └── 輸入事件</span></span></code></pre></div><p>應用程式用 OpenGL/Vulkan 自己渲染畫面到 buffer，然後把 buffer 直接交給 compositor。沒有中間人。compositor 收到所有 client 的 buffer 後合成最終畫面輸出到螢幕。</p>
<p>這個架構上的差異帶來三個實際影響：效能、安全、screen tearing。</p>
<h2 id="安全性">安全性</h2>
<p>X11 的安全模型有一個根本性的設計缺陷：任何連到同一個 X Server 的 client，都能監聽其他 client 的鍵盤輸入、擷取其他視窗的畫面、甚至注入假的輸入事件。這代表一個惡意程式可以輕易做到 keylogging——記錄你在其他視窗打的所有字，包括密碼。</p>
<p>Wayland 從協議層就隔離了 client 之間的存取。每個應用程式只看得到自己的 buffer，無法存取其他視窗的內容或輸入。要做 screen capture 或 screen sharing，必須透過 xdg-desktop-portal 這個受控的中介機制，使用者會看到明確的授權提示。</p>
<p>對 tiling WM 的使用者來說，這個安全差異特別有意義：平鋪式桌面通常同時開大量終端機視窗（有些跑 SSH、有些在 sudo 操作），X11 下任何一個視窗裡的惡意程式都能讀到其他視窗的鍵盤輸入。Wayland 阻斷了這條攻擊路徑。</p>
<h2 id="效能與-screen-tearing">效能與 Screen Tearing</h2>
<p>X11 的渲染路徑有多餘的記憶體複製——應用程式把繪圖指令送給 X Server，X Server 渲染後再交給 compositor，compositor 再合成。Wayland 省掉了中間那一步：應用程式直接渲染到 buffer、直接交給 compositor，記憶體複製更少。</p>
<p>Screen tearing（畫面撕裂）在 X11 上是長年的老問題——需要 compositor + vsync 的各種 workaround。Wayland 在協議層就處理了 frame presentation 的時機，compositor 控制什麼時候把合成好的畫面送到螢幕，原生消除 tearing。這也是為什麼 Hyprland 的動畫能跑得流暢——直接 rendering + 原生 vsync 的組合，在 X11 上需要大量額外配置才能接近的效果。</p>
<h2 id="xwaylandx11-相容層">XWayland：X11 相容層</h2>
<p>很多應用程式仍然只支援 X11 協議。XWayland 是一個 X11 server，但它本身作為一個 Wayland client 運行——X11 應用程式連到 XWayland，以為自己在跟正常的 X Server 溝通；XWayland 把 X11 協議翻譯成 Wayland 協議，把 buffer 交給 compositor。</p>
<p>多數 Wayland compositor（包括 Hyprland）預設啟用 XWayland，X11 應用程式不需要任何設定就能運行。</p>
<h3 id="需要-xwayland-的常見應用">需要 XWayland 的常見應用</h3>
<ul>
<li>舊版 Electron 應用（新版可用 <code>--ozone-platform=wayland</code> 旗標切換到 Wayland 原生）</li>
<li>Java 應用程式（NetBeans 等）</li>
<li>舊的 GTK2 應用程式</li>
<li>部分 Wine/Proton 遊戲（但 XWayland 下的遊戲效能已經相當好）</li>
<li>未啟用 PGTK 的舊版 Emacs build（Emacs 29+ 的 PGTK 後端已原生支援 Wayland）</li>
</ul>
<h3 id="xwayland-的已知問題">XWayland 的已知問題</h3>
<p>最大的實務痛點是 <strong>HiDPI 非整數縮放</strong>。在 150%、125% 等非整數 scale 下，XWayland 應用程式可能出現模糊渲染，因為 XWayland 不支援 fractional scaling 協議。整數縮放（100%、200%）則沒有問題。</p>
<h2 id="關鍵-wayland-協議">關鍵 Wayland 協議</h2>
<h3 id="wlr-layer-shell">wlr-layer-shell</h3>
<p>Layer-shell 定義了 client 可以在螢幕的哪個「層」上建立畫面——background（最底）、bottom、top、overlay（最頂）。Status bar（waybar）、notification daemon（mako）、launcher（wofi）、wallpaper（hyprpaper）、lock screen（hyprlock）都透過 layer-shell 來佔據螢幕空間。</p>
<p>沒有 layer-shell 支援的 compositor，這些桌面元件就無法運作。這也是為什麼<a href="/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">桌面 Rice 設計</a>的所有工具都只能在支援 layer-shell 的 Wayland compositor 上跑——它是可組裝式桌面的基礎設施。</p>
<h3 id="xdg-desktop-portal">xdg-desktop-portal</h3>
<p>Portal 是一個 D-Bus 介面，讓被隔離的 Wayland 應用程式可以請求特權操作：screen sharing、檔案對話框、螢幕截圖。每個 compositor 提供自己的 portal backend：</p>
<ul>
<li>Hyprland：<code>xdg-desktop-portal-hyprland</code></li>
<li>GNOME：<code>xdg-desktop-portal-gnome</code></li>
<li>KDE：<code>xdg-desktop-portal-kde</code></li>
<li>Sway/wlroots 系：<code>xdg-desktop-portal-wlr</code></li>
</ul>
<p>Screen sharing 的完整鏈路是：<code>PipeWire</code> + <code>xdg-desktop-portal-hyprland</code>。OBS 的 PipeWire source、瀏覽器的畫面分享，都走這條路。</p>
<h2 id="wlroots-與-hyprland-的關係">wlroots 與 Hyprland 的關係</h2>
<p>wlroots 是一個模組化的 C library，提供 Wayland compositor 的共用基礎建設——輸入處理、輸出管理、協議實作。Sway、river、wayfire 這些 compositor 都基於 wlroots 開發，共享同一套底層程式碼。</p>
<p>Hyprland 最初也基於 wlroots，但後來 fork 出去自己維護底層。這讓 Hyprland 可以更快地加入新功能（動畫、模糊、漸層邊框），不需要等 wlroots 上游審核。代價是 Hyprland 的更新不再跟 wlroots 生態同步，偶爾會有相容性差異。</p>
<h2 id="2026-年的-wayland-採用現況">2026 年的 Wayland 採用現況</h2>
<p>Wayland 在 2026 年已經從「取代 X11 的方向」變成「Linux 桌面的預設」：</p>
<table>
  <thead>
      <tr>
          <th>桌面環境 / 發行版</th>
          <th>狀態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GNOME</td>
          <td>GNOME 49（2025）放棄 X11 session，計劃在 GNOME 50 永久移除</td>
      </tr>
      <tr>
          <td>KDE Plasma</td>
          <td>Plasma 6.0 起預設 Wayland，6.8（2026 末）將 Wayland-only</td>
      </tr>
      <tr>
          <td>Fedora</td>
          <td>Fedora 43 完全移除 X11</td>
      </tr>
      <tr>
          <td>Ubuntu</td>
          <td>Ubuntu 25.10 不再提供 X11 session</td>
      </tr>
      <tr>
          <td>整體趨勢</td>
          <td>主流發行版的預設 session 已切換到 Wayland，X11 session 逐步移除中</td>
      </tr>
      <tr>
          <td>NVIDIA 驅動</td>
          <td>driver 555+ 支援 GBM，590 系列（2025）修復了多數 HDR 和多螢幕問題</td>
      </tr>
  </tbody>
</table>
<p>X11 上的老牌 tiling WM（i3、bspwm、dwm）仍然活躍，但不再是新專案的主流方向。新的 tiling WM 幾乎都選擇 Wayland 作為基礎。</p>
<h2 id="x11-仍有優勢的場景">X11 仍有優勢的場景</h2>
<p>Wayland 並非在所有面向都超越 X11：</p>
<ul>
<li><strong>SSH X forwarding</strong>：X11 原生支援透過網路轉送 GUI 視窗（<code>ssh -X</code>）。Wayland 沒有對等機制，替代方案是 <code>waypipe</code>，但不是 drop-in replacement</li>
<li><strong>跨 compositor 的 GUI 自動化</strong>：<code>xdotool</code>、<code>wmctrl</code> 在 X11 上可以操控任何 WM 的視窗。Wayland 沒有跨 compositor 的通用對應方案——每個 compositor 提供自己的 IPC 工具。不過 Hyprland 的 <code>hyprctl</code> 在 Hyprland 環境內提供了比 xdotool 更豐富的能力：socket-based IPC、JSON 格式查詢所有視窗和工作區狀態、即時 dispatch 動作、event 監控（<code>hyprctl socket2</code>）。瓶頸在「跨 compositor 通用」，不在單一 compositor 內的功能深度</li>
<li><strong>VNC / 遠端桌面</strong>：X11 的網路透明性讓遠端桌面相對簡單。Wayland 需要 PipeWire + portal，設定複雜度更高</li>
<li><strong>Color management</strong>：專業色彩工作流（ICC profile 管理）在部分 Wayland compositor 上仍需手動配置</li>
</ul>
<p>這些場景如果是你的核心需求，目前 X11（或 X11-based 的 i3/sway 的 X11 版本）可能仍是更務實的選擇。</p>
<h2 id="為什麼-tiling-wm-使用者該關心-wayland">為什麼 Tiling WM 使用者該關心 Wayland</h2>
<p>平鋪式桌面是「自己從元件組裝桌面」的工作流。Wayland 的幾個特性讓這件事變得更好：</p>
<p><strong>layer-shell</strong> 給了 status bar、launcher、notification daemon 一個標準化的方式來佔據螢幕空間，不再需要各種 hack 讓這些元件「浮在視窗上面又不被平鋪規則管到」。</p>
<p><strong>安全隔離</strong>在多終端機場景特別有價值——平鋪式桌面通常同時開著 SSH session、密碼管理器、sudo 操作，Wayland 確保任何一個視窗裡的程式都無法偷看其他視窗的鍵盤輸入。</p>
<p><strong>直接 rendering</strong> 讓 Hyprland 能做到流暢的視窗動畫和即時模糊，這在 X11 上需要大量額外的 compositor 配置（picom + 各種 backend 選擇 + vsync 調校）才能接近。</p>
<p>從實務角度看：2026 年開始接觸 Linux tiling WM，選 Wayland-based 的工具（Hyprland、Sway）是順流而行。X11 不會立刻消失，但新功能、新工具、社群活力都在 Wayland 這一邊。</p>
]]></content:encoded></item><item><title>模組四：視窗管理與平鋪式工作流</title><link>https://tarrragon.github.io/blog/linux/dotfile/04-window-management/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/04-window-management/</guid><description>&lt;p>視窗管理器（window manager, WM）負責決定螢幕上的視窗怎麼排列、怎麼切換、怎麼調整大小。每個桌面環境都有一個 WM，差別在於它是讓你用滑鼠自己拖，還是按規則自動幫你排好。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/04-window-management/floating-vs-tiling/" data-link-title="浮動式 vs 平鋪式視窗管理" data-link-desc="在手動貼齊（Rectangle）跟自動平鋪之間猶豫、或想評估自己的工作型態適不適合平鋪式 WM 時回來讀">浮動式 vs 平鋪式&lt;/a>&lt;/td>
 &lt;td>兩種視窗管理模式的差異、手動貼齊 vs 自動平鋪的三層差距、適用判讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/04-window-management/macos-window-tools/" data-link-title="macOS 視窗管理工具鏈" data-link-desc="macOS 上想用鍵盤管理視窗、不確定該用哪個工具時回來讀">macOS 視窗管理工具鏈&lt;/a>&lt;/td>
 &lt;td>Rectangle / Amethyst / AeroSpace / yabai 選型判讀與配置範例&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/04-window-management/linux-tiling-wm/" data-link-title="Linux Tiling WM 生態" data-link-desc="要在 Linux 上選 tiling WM（i3/sway/Hyprland/bspwm）或理解 Wayland vs X11 差異時回來讀">Linux Tiling WM 生態&lt;/a>&lt;/td>
 &lt;td>主流 tiling WM 比較（i3/sway/Hyprland/bspwm/dwm）、多螢幕處理、dotfile 中的角色&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/04-window-management/wayland-explainer/" data-link-title="Wayland 顯示協議：為什麼 Hyprland 不跑在 X11 上" data-link-desc="想理解 Hyprland 底層的圖形架構、Wayland 跟 X11 的差異、XWayland 相容層、以及 2026 年 Wayland 已經是主流這件事時回來讀">Wayland 顯示協議&lt;/a>&lt;/td>
 &lt;td>Wayland 架構、跟 X11 的差異、XWayland 相容層、2026 採用現況、為什麼 tiling WM 選 Wayland&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>macOS 讀者的主線是前兩篇（浮動 vs 平鋪、macOS 工具鏈）。Linux Tiling WM 生態和 Wayland 顯示協議是想在 VM 或實機上體驗 Linux 桌面的選讀——macOS 上用 AeroSpace 或 yabai 的讀者可以直接跳到&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">同步與 Bootstrap&lt;/a>。&lt;/p>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置&lt;/a>：Hyprland 的完整配置實務&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計&lt;/a>：compositor 之上的狀態列、啟動器、通知、配色系統&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構&lt;/a>：WM 配置怎麼進 dotfile repo&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建&lt;/a>：跨機器搬移時硬體相關設定怎麼處理&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>視窗管理器（window manager, WM）負責決定螢幕上的視窗怎麼排列、怎麼切換、怎麼調整大小。每個桌面環境都有一個 WM，差別在於它是讓你用滑鼠自己拖，還是按規則自動幫你排好。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/04-window-management/floating-vs-tiling/" data-link-title="浮動式 vs 平鋪式視窗管理" data-link-desc="在手動貼齊（Rectangle）跟自動平鋪之間猶豫、或想評估自己的工作型態適不適合平鋪式 WM 時回來讀">浮動式 vs 平鋪式</a></td>
          <td>兩種視窗管理模式的差異、手動貼齊 vs 自動平鋪的三層差距、適用判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/04-window-management/macos-window-tools/" data-link-title="macOS 視窗管理工具鏈" data-link-desc="macOS 上想用鍵盤管理視窗、不確定該用哪個工具時回來讀">macOS 視窗管理工具鏈</a></td>
          <td>Rectangle / Amethyst / AeroSpace / yabai 選型判讀與配置範例</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/04-window-management/linux-tiling-wm/" data-link-title="Linux Tiling WM 生態" data-link-desc="要在 Linux 上選 tiling WM（i3/sway/Hyprland/bspwm）或理解 Wayland vs X11 差異時回來讀">Linux Tiling WM 生態</a></td>
          <td>主流 tiling WM 比較（i3/sway/Hyprland/bspwm/dwm）、多螢幕處理、dotfile 中的角色</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/04-window-management/wayland-explainer/" data-link-title="Wayland 顯示協議：為什麼 Hyprland 不跑在 X11 上" data-link-desc="想理解 Hyprland 底層的圖形架構、Wayland 跟 X11 的差異、XWayland 相容層、以及 2026 年 Wayland 已經是主流這件事時回來讀">Wayland 顯示協議</a></td>
          <td>Wayland 架構、跟 X11 的差異、XWayland 相容層、2026 採用現況、為什麼 tiling WM 選 Wayland</td>
      </tr>
  </tbody>
</table>
<p>macOS 讀者的主線是前兩篇（浮動 vs 平鋪、macOS 工具鏈）。Linux Tiling WM 生態和 Wayland 顯示協議是想在 VM 或實機上體驗 Linux 桌面的選讀——macOS 上用 AeroSpace 或 yabai 的讀者可以直接跳到<a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">同步與 Bootstrap</a>。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置</a>：Hyprland 的完整配置實務</li>
<li>→ <a href="/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計</a>：compositor 之上的狀態列、啟動器、通知、配色系統</li>
<li>→ <a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構</a>：WM 配置怎麼進 dotfile repo</li>
<li>→ <a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建</a>：跨機器搬移時硬體相關設定怎麼處理</li>
</ul>
]]></content:encoded></item><item><title>外部連入、SSH key 與無 key 的 bootstrap 路徑</title><link>https://tarrragon.github.io/blog/linux/install/ssh-keyless-bootstrap/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/ssh-keyless-bootstrap/</guid><description>&lt;p>操作一台新機器，從你本機的終端機透過 SSH 連進去是阻力最小的位置。直接在主控台操作有兩個實際的痛點：純文字的主控台（TTY 或虛擬機的序列 console）往往不能貼上，長指令只能手打、還容易掉字；畫面也通常擠、不能捲。把機器的 sshd 跑起來、從本機 SSH 進去之後，貼上、捲動、補全全部回到你熟悉的環境，而且這條路本身就貼近真實的遠端維運。&lt;/p>
&lt;p>這篇處理三件事：把 sshd 跑起來並從本機連入、設 SSH key 達到免密碼、以及一個容易被卡住的情境——你還沒有 SSH key 時，怎麼把 dotfile 弄進機器、跑完基礎安裝。&lt;/p>
&lt;h2 id="啟用-sshd-並從本機連入">啟用 sshd 並從本機連入&lt;/h2>
&lt;p>讓機器能被 SSH 連入只需要兩步：裝 SSH 伺服器、啟動它的服務。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">pacman -S openssh &lt;span class="c1"># 剛裝好的系統套件資料庫是新的，-S 不必先 -Sy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">systemctl &lt;span class="nb">enable&lt;/span> --now sshd &lt;span class="c1"># enable 開機自啟、--now 立刻啟動&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>指令以 Arch 為例。換發行版時套件管理器不同（Fedora &lt;code>dnf&lt;/code>、Debian/Ubuntu &lt;code>apt&lt;/code>），服務名也可能不同——Debian 系的 OpenSSH 服務叫 &lt;code>ssh&lt;/code> 不是 &lt;code>sshd&lt;/code>，那邊要 &lt;code>systemctl enable --now ssh&lt;/code>。&lt;/p>
&lt;p>從本機連的時候用一般使用者、不要用 root：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ssh user@&amp;lt;機器 IP&amp;gt; &lt;span class="c1"># IP 來自機器上的 ip -brief a&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用一般使用者是因為多數發行版的 sshd 預設擋 root 密碼登入（&lt;code>PermitRootLogin prohibit-password&lt;/code>）——root 只能用 key、不能用密碼。這個預設是好的安全姿態，順著它走、用你裝系統時建的一般使用者連即可。連進去之後，後續所有需要長指令、需要貼上的操作都在這個 session 裡做，不再回主控台手打。&lt;/p>
&lt;p>這裡啟用 sshd 是為了 bootstrap 期間從本機連入操作，跟 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">操作順序指引&lt;/a> 後段把 sshd 當「桌面就緒後的常駐遠端救援通道」是兩個不同的時間點與目的——同一個 &lt;code>systemctl enable sshd&lt;/code> 動作，這裡是為了「現在好操作」，那裡是為了「之後好救援」。&lt;/p>
&lt;h2 id="ssh-key-免密碼">SSH key 免密碼&lt;/h2>
&lt;p>每次連線都打密碼很快會變成阻力，尤其當你要反覆同步檔案或跑自動化時。SSH key 讓本機免密碼連入，做法是生一把金鑰、把公鑰放進機器、本機用私鑰認證。&lt;/p>
&lt;p>生 key 時建議生一把專用的、不要佔用本機的預設金鑰槽，並在 SSH 設定裡給它一個好記的別名：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ssh-keygen -t ed25519 -f ~/.ssh/vm_arch -N &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span> -C &lt;span class="s2">&amp;#34;vm_arch host-&amp;gt;target&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 在 ~/.ssh/config 加一段別名：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># Host vm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># HostName &amp;lt;機器 IP&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># User &amp;lt;你的使用者&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># IdentityFile ~/.ssh/vm_arch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># IdentitiesOnly yes&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>專用 key 的好處是它的權限範圍清楚——這把只給這台機器用，跟你其他身分的金鑰互不牽連。設好別名後，&lt;code>ssh vm&lt;/code> 就免密碼連入，後面的 &lt;code>rsync&lt;/code>、&lt;code>scp&lt;/code> 也跟著免密碼。&lt;/p>
&lt;p>把公鑰放進機器有兩條路。標準工具是 &lt;code>ssh-copy-id&lt;/code>，它會在本機跑、要你輸入一次目標機的密碼。另一條省一次切換的路是：當你已經用密碼連進機器、且這個 session 在真終端機裡（貼上可用），直接把公鑰內容貼進機器的 &lt;code>authorized_keys&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mkdir -p ~/.ssh &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> chmod &lt;span class="m">700&lt;/span> ~/.ssh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;ssh-ed25519 AAAA... 你的公鑰內容&amp;#34;&lt;/span> &amp;gt;&amp;gt; ~/.ssh/authorized_keys
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">chmod &lt;span class="m">600&lt;/span> ~/.ssh/authorized_keys&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩條路等價，選哪條看你當下在哪——還沒連上就用 &lt;code>ssh-copy-id&lt;/code>，已經連上就直接貼，少一次切換。&lt;/p>
&lt;h2 id="還沒有-ssh-key-時怎麼把-dotfile-弄進去">還沒有 SSH key 時，怎麼把 dotfile 弄進去&lt;/h2>
&lt;p>設 SSH key 是讓「之後」連線變方便，但 bootstrap 的第一步——把 dotfile repo 弄進機器——並不一定需要 key。常見的卡點是把「clone repo」跟「有 SSH key」綁在一起，但 clone 有不需要 key 的路徑。怎麼把 dotfile 弄進去，取決於這份 dotfile 放在哪。&lt;/p>
&lt;p>&lt;strong>repo 是公開的（在 GitHub 之類）&lt;/strong>：用 HTTPS clone，公開 repo 的唯讀 clone 不需要任何認證。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">git clone https://github.com/&amp;lt;帳號&amp;gt;/dotfiles ~/dotfiles
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/dotfiles &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> ./scripts/install.sh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這是最直接的路——機器只要能上網就能拉到 dotfile，完全繞過 key 的問題。clone URL 裡的帳號要對；用錯帳號（例如把 email handle 當成 GitHub 帳號）會 clone 失敗或抓到別的 repo，這類筆誤在只看 README 範例時很容易漏掉。SSH key 在這個情境只有「之後要從機器 push 回去」才需要，純粹跑部署用不到。&lt;/p></description><content:encoded><![CDATA[<p>操作一台新機器，從你本機的終端機透過 SSH 連進去是阻力最小的位置。直接在主控台操作有兩個實際的痛點：純文字的主控台（TTY 或虛擬機的序列 console）往往不能貼上，長指令只能手打、還容易掉字；畫面也通常擠、不能捲。把機器的 sshd 跑起來、從本機 SSH 進去之後，貼上、捲動、補全全部回到你熟悉的環境，而且這條路本身就貼近真實的遠端維運。</p>
<p>這篇處理三件事：把 sshd 跑起來並從本機連入、設 SSH key 達到免密碼、以及一個容易被卡住的情境——你還沒有 SSH key 時，怎麼把 dotfile 弄進機器、跑完基礎安裝。</p>
<h2 id="啟用-sshd-並從本機連入">啟用 sshd 並從本機連入</h2>
<p>讓機器能被 SSH 連入只需要兩步：裝 SSH 伺服器、啟動它的服務。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pacman -S openssh             <span class="c1"># 剛裝好的系統套件資料庫是新的，-S 不必先 -Sy</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl <span class="nb">enable</span> --now sshd   <span class="c1"># enable 開機自啟、--now 立刻啟動</span></span></span></code></pre></div><p>指令以 Arch 為例。換發行版時套件管理器不同（Fedora <code>dnf</code>、Debian/Ubuntu <code>apt</code>），服務名也可能不同——Debian 系的 OpenSSH 服務叫 <code>ssh</code> 不是 <code>sshd</code>，那邊要 <code>systemctl enable --now ssh</code>。</p>
<p>從本機連的時候用一般使用者、不要用 root：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh user@&lt;機器 IP&gt;            <span class="c1"># IP 來自機器上的 ip -brief a</span></span></span></code></pre></div><p>用一般使用者是因為多數發行版的 sshd 預設擋 root 密碼登入（<code>PermitRootLogin prohibit-password</code>）——root 只能用 key、不能用密碼。這個預設是好的安全姿態，順著它走、用你裝系統時建的一般使用者連即可。連進去之後，後續所有需要長指令、需要貼上的操作都在這個 session 裡做，不再回主控台手打。</p>
<p>這裡啟用 sshd 是為了 bootstrap 期間從本機連入操作，跟 <a href="/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">操作順序指引</a> 後段把 sshd 當「桌面就緒後的常駐遠端救援通道」是兩個不同的時間點與目的——同一個 <code>systemctl enable sshd</code> 動作，這裡是為了「現在好操作」，那裡是為了「之後好救援」。</p>
<h2 id="ssh-key-免密碼">SSH key 免密碼</h2>
<p>每次連線都打密碼很快會變成阻力，尤其當你要反覆同步檔案或跑自動化時。SSH key 讓本機免密碼連入，做法是生一把金鑰、把公鑰放進機器、本機用私鑰認證。</p>
<p>生 key 時建議生一把專用的、不要佔用本機的預設金鑰槽，並在 SSH 設定裡給它一個好記的別名：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh-keygen -t ed25519 -f ~/.ssh/vm_arch -N <span class="s2">&#34;&#34;</span> -C <span class="s2">&#34;vm_arch host-&gt;target&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 在 ~/.ssh/config 加一段別名：</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">#   Host vm</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">#       HostName &lt;機器 IP&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">#       User &lt;你的使用者&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">#       IdentityFile ~/.ssh/vm_arch</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">#       IdentitiesOnly yes</span></span></span></code></pre></div><p>專用 key 的好處是它的權限範圍清楚——這把只給這台機器用，跟你其他身分的金鑰互不牽連。設好別名後，<code>ssh vm</code> 就免密碼連入，後面的 <code>rsync</code>、<code>scp</code> 也跟著免密碼。</p>
<p>把公鑰放進機器有兩條路。標準工具是 <code>ssh-copy-id</code>，它會在本機跑、要你輸入一次目標機的密碼。另一條省一次切換的路是：當你已經用密碼連進機器、且這個 session 在真終端機裡（貼上可用），直接把公鑰內容貼進機器的 <code>authorized_keys</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mkdir -p ~/.ssh <span class="o">&amp;&amp;</span> chmod <span class="m">700</span> ~/.ssh
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;ssh-ed25519 AAAA... 你的公鑰內容&#34;</span> &gt;&gt; ~/.ssh/authorized_keys
</span></span><span class="line"><span class="ln">3</span><span class="cl">chmod <span class="m">600</span> ~/.ssh/authorized_keys</span></span></code></pre></div><p>兩條路等價，選哪條看你當下在哪——還沒連上就用 <code>ssh-copy-id</code>，已經連上就直接貼，少一次切換。</p>
<h2 id="還沒有-ssh-key-時怎麼把-dotfile-弄進去">還沒有 SSH key 時，怎麼把 dotfile 弄進去</h2>
<p>設 SSH key 是讓「之後」連線變方便，但 bootstrap 的第一步——把 dotfile repo 弄進機器——並不一定需要 key。常見的卡點是把「clone repo」跟「有 SSH key」綁在一起，但 clone 有不需要 key 的路徑。怎麼把 dotfile 弄進去，取決於這份 dotfile 放在哪。</p>
<p><strong>repo 是公開的（在 GitHub 之類）</strong>：用 HTTPS clone，公開 repo 的唯讀 clone 不需要任何認證。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git clone https://github.com/&lt;帳號&gt;/dotfiles ~/dotfiles
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> ~/dotfiles <span class="o">&amp;&amp;</span> ./scripts/install.sh</span></span></code></pre></div><p>這是最直接的路——機器只要能上網就能拉到 dotfile，完全繞過 key 的問題。clone URL 裡的帳號要對；用錯帳號（例如把 email handle 當成 GitHub 帳號）會 clone 失敗或抓到別的 repo，這類筆誤在只看 README 範例時很容易漏掉。SSH key 在這個情境只有「之後要從機器 push 回去」才需要，純粹跑部署用不到。</p>
<p><strong>repo 是私有的、但機器能上網</strong>：機器可以直接 clone，用 GitHub Personal Access Token（PAT）走 HTTPS——這是私有 repo 免 SSH key 的標準解。clone 時把 PAT 當密碼填進認證，機器就拉得到，一樣不必在它上面設 SSH key。</p>
<p><strong>repo 還沒推到任何遠端、或機器離線</strong>：從本機把檔案傳進去。如果本機到機器的 SSH 已經能用（即使只是密碼登入），用 <code>tar</code> over SSH 一次傳進去（跟 <code>scp -r</code> 等價，差別只在 tar 能一次打包、又好控制要不要帶 <code>.git</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">tar czf - --exclude <span class="s1">&#39;.git&#39;</span> . <span class="p">|</span> ssh user@host <span class="s1">&#39;mkdir -p ~/dotfiles &amp;&amp; tar xzf - -C ~/dotfiles&#39;</span></span></span></code></pre></div><p>這條只需要兩邊都有的 <code>ssh</code> 跟 <code>tar</code>，不依賴目標機有 rsync。從 macOS 傳的時候要關掉 AppleDouble 中繼檔，否則會夾帶一堆 <code>._</code> 開頭的中繼檔到 Linux 上：在指令前加 <code>COPYFILE_DISABLE=1</code>。完全離線、連 SSH 都還沒通時，最後手段是把 repo 放進 USB、掛載到機器上複製出來。</p>
<p>把 dotfile 弄進去之後，跑它的 <code>install.sh</code> 完成基礎安裝。如果安裝腳本一開始就要用 sudo，記得 sudo 必須在工具驗證階段就備好——它是 <a href="../minimal-install-verify/">最小安裝後的工具驗證與補足</a> 的前置，bootstrap 自身補不了。</p>
<h2 id="換一台新機器或重裝時ssh-為什麼突然連不上">換一台新機器（或重裝）時，SSH 為什麼突然連不上</h2>
<p>SSH 的別名、金鑰、<code>known_hosts</code> 都是綁在「某一台特定機器」上的，所以當你重裝、或換一台新 VM，先前設好的 <code>ssh &lt;別名&gt;</code> 往往會以看似無關的錯誤失敗——那套設定是為舊機器建的，而重裝後是另一台機器：不同的 IP、不同的 SSH host key、還沒裝 sshd、<code>authorized_keys</code> 也是空的。判讀的起點是把重裝後的機器當成全新的一台，重做第一次連線的設定，而不是沿用舊別名。</p>
<p>失敗會以三種形式出現，各對應不同層、各有各的修法：</p>
<p><code>Permission denied (publickey)</code> 是認證被拒，代表 sshd 有在跑、連線有到（這是進度），卡在金鑰這關。常見於你用的別名設了 <code>IdentitiesOnly yes</code> 只送某一把 key，而新機器的 <code>authorized_keys</code> 還沒有它。修法是改用帳號加 IP 直連、走密碼，繞過那個鎖死金鑰的別名：<code>ssh user@&lt;新 IP&gt;</code>，密碼是「這次安裝」為該使用者設的（每次重裝各自獨立，不是舊機器那個）。連進去後再把公鑰貼回新機器的 <code>authorized_keys</code>、把別名的 <code>HostName</code> 更新成新 IP，免密碼才會恢復。</p>
<p><code>Host key verification failed</code>（或 <code>REMOTE HOST IDENTIFICATION HAS CHANGED</code>）發生在新機器剛好拿到跟舊機器一樣的 IP 時：你本機 <code>known_hosts</code> 存的是舊機器的 host key，SSH 偵測到同一個 IP 換了 key、當成可能的中間人攻擊而拒連。修法是刪掉那筆舊紀錄，再重連時接受新 key：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh-keygen -R &lt;IP&gt;       <span class="c1"># 刪掉該 IP 的舊 host key</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ssh-keygen -R &lt;別名&gt;     <span class="c1"># 有用別名的話一併刪</span></span></span></code></pre></div><p><code>Connection refused</code> 代表沒有 sshd 在監聽，也就是新機器還沒把 SSH server 起來。修法回到最開始——在新機器的 console 裝 openssh、啟動服務（見本篇開頭「啟用 sshd」），這一步在每台全新機器上都要重做。</p>
<p>三個症狀的共同根因是同一件事：SSH 的便利設定（別名、金鑰、host key 快取）綁的是機器身分、不會跟著「重裝」自動轉移。把它們當成「為某一台機器設好的」，換機器就重做第一次連線，能省下對著看似無關的錯誤瞎猜的時間。</p>
<h2 id="連入後可能遇到的兩個終端機問題">連入後可能遇到的兩個終端機問題</h2>
<p>SSH 連線本身通了之後，互動 shell 還可能因為終端機環境不對而出現「打字變亂碼、prompt 重繪錯位」。這類問題在你用現代終端機（如 Ghostty、Kitty）連進一台剛裝好的最小 Linux、又跑了 unicode 較重的 prompt（如 Powerlevel10k）時最容易出現，根源是兩個跟字元處理有關的終端機設定，跟你的 shell 配置無關。</p>
<p>第一個是 locale。macOS 的終端機 SSH 連線時常把 <code>LC_CTYPE=UTF-8</code> 送到遠端，但 <code>UTF-8</code> 不是合法的 Linux locale 名稱，Linux 收到後 fallback 成 <code>POSIX</code>/C locale——於是 shell 的行編輯器把輸入當單位元組處理，配上 unicode 字元的 prompt 就重繪成一個字母重複好幾次的累加亂碼。判讀方式是在遠端跑 <code>locale</code>，看 <code>LANG</code> 是不是空的、<code>LC_CTYPE</code> 是不是 <code>POSIX</code>。修法是在 shell 設定裡強制一個合法的 UTF-8 locale（前提是該 locale 已生成，見 <a href="../install-option-decisions/">安裝選項判讀</a> 的 locale 段）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">export</span> <span class="nv">LANG</span><span class="o">=</span>en_US.UTF-8
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">export</span> <span class="nv">LC_CTYPE</span><span class="o">=</span>en_US.UTF-8</span></span></code></pre></div><p>第二個是 terminfo。現代終端機會把 <code>TERM</code> 設成自己的值（Ghostty 是 <code>xterm-ghostty</code>、Kitty 是 <code>xterm-kitty</code>），而一台剛裝好的 Linux 的 terminfo 資料庫沒有這些條目，shell 的行編輯器做「清行重繪」時找不到對應的控制序列、就把畫面畫壞。判讀方式是在遠端 <code>echo $TERM</code> 看是哪個值、<code>toe | grep &lt;值&gt;</code> 看遠端認不認得。修法有兩條：把你終端機的 terminfo 灌進遠端（保留完整功能），或退而求其次強制一個遠端一定有的 <code>TERM</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 把本機終端機的 terminfo 灌進遠端的 ~/.terminfo（推薦）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">infocmp -x <span class="s2">&#34;</span><span class="nv">$TERM</span><span class="s2">&#34;</span> <span class="p">|</span> ssh remote <span class="s1">&#39;tic -x -&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 或：連線時強制遠端一定有的 TERM（功能略降，但保證能用）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">ssh -t remote <span class="s1">&#39;TERM=xterm-256color exec zsh -l&#39;</span></span></span></code></pre></div><p>這兩個問題的共同點是：它們在你裝了 unicode 較重的互動 shell 之後才浮現，而陽春的 shell（ASCII prompt）即使 locale 跟 terminfo 都不對也照樣能用。所以排查時，先確認是不是這層、而不是去懷疑剛裝的 shell 配置壞了。</p>
<h2 id="連入傳輸安裝的順序">連入、傳輸、安裝的順序</h2>
<p>這三件事有一個固定的先後，順序錯了會在中間卡住。先把 sshd 跑起來、從本機連入，取得一個能貼上、可捲動的 session；再把 dotfile 弄進機器（公開 repo 走 HTTPS clone、私有或本地走傳輸）；最後在機器上跑 install.sh 完成安裝。SSH key 是讓「連入」從每次打密碼變成免密碼的優化，可以在任何時候補，不是這條鏈的必要環節、也不是 bootstrap 的前置。</p>
<p><a href="/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">模組零的操作順序指引</a> 把「生成 SSH key、部署公鑰」列為標準流程的一環，那是預設你會建 key 的主路徑。這篇補的是它沒展開的另一面：當你手上還沒有 key、或這台機器的 dotfile 根本不需要 key 就能取得時，怎麼一樣把 bootstrap 跑完。</p>
<h2 id="下一步">下一步</h2>
<p>連入、傳輸、安裝都跑通之後，真正的考驗是當 install.sh 中途失敗時——而它遲早會撞到失敗——你能不能快速看出哪裡錯了。這取決於安裝腳本有沒有把可觀測性內建進去，<a href="../observable-bootstrap/">可除錯的 bootstrap</a> 談的就是怎麼內建。</p>
]]></content:encoded></item><item><title>Caelestia 配置</title><link>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/caelestia-configuration/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/caelestia-configuration/</guid><description>&lt;p>&lt;code>~/.config/caelestia/&lt;/code> 下有兩類檔案：使用者層的 JSON 配置（&lt;code>shell.json&lt;/code>、&lt;code>cli.json&lt;/code>）控制功能和外觀，token 層（&lt;code>shell-tokens.json&lt;/code>）控制細部視覺數值——間距、圓角、字型大小、動畫曲線。&lt;code>shell.json&lt;/code> 修改後 Quickshell 自動 reload、不需要重啟（VM 實測：跑著的實例會即時吃到檔案變更、驗證後重新序列化回檔案）。配色（scheme）的熱套用是另一個獨立的 watcher、有啟動時序前提，見下方配色段。&lt;/p>
&lt;h2 id="配置檔路徑">配置檔路徑&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>路徑&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>~/.config/caelestia/shell.json&lt;/code>&lt;/td>
 &lt;td>主配置（使用者自建，不會自動產生）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/.config/caelestia/shell-tokens.json&lt;/code>&lt;/td>
 &lt;td>進階視覺 token（rounding、spacing、padding、字型、動畫）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/.config/caelestia/monitors/&amp;lt;name&amp;gt;/shell.json&lt;/code>&lt;/td>
 &lt;td>每螢幕覆寫（螢幕名稱用 &lt;code>hyprctl monitors&lt;/code> 查）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/.config/caelestia/monitors/&amp;lt;name&amp;gt;/shell-tokens.json&lt;/code>&lt;/td>
 &lt;td>每螢幕 token 覆寫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/.config/caelestia/hypr-user.lua&lt;/code>&lt;/td>
 &lt;td>自定義 Hyprland 設定（Lua 格式）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/.config/caelestia/hypr-vars.lua&lt;/code>&lt;/td>
 &lt;td>Hyprland 變數覆寫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/.config/caelestia/cli.json&lt;/code>&lt;/td>
 &lt;td>CLI 工具配置（主題目標、icon theme、workspace toggle）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/.config/caelestia/templates/&lt;/code>&lt;/td>
 &lt;td>自定義色彩模板，語法 &lt;code>{{ primary.hex }}&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/.face&lt;/code>&lt;/td>
 &lt;td>個人頭像（Dashboard 用）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/Pictures/Wallpapers&lt;/code>&lt;/td>
 &lt;td>預設桌布目錄（可在 paths section 改）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>shell.json&lt;/code> 不會在安裝時自動產生——預設行為由 Caelestia 內建值決定。你只需要建立這個檔案、寫入你想覆寫的 section。未寫的 section 使用預設值。&lt;/p>
&lt;h2 id="shelljson-結構">shell.json 結構&lt;/h2>
&lt;p>shell.json 的 top-level section 對應桌面 shell 的各個元件和全域設定：&lt;/p>
&lt;h3 id="全域設定">全域設定&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Section&lt;/th>
 &lt;th>控制什麼&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>enabled&lt;/code>&lt;/td>
 &lt;td>各元件的啟用 / 停用主開關&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>appearance&lt;/code>&lt;/td>
 &lt;td>全域視覺：deformScale、rounding、spacing、padding、字型、動畫、透明度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>general&lt;/code>&lt;/td>
 &lt;td>Logo 圖片路徑、app 路徑、閒置逾時秒數、低電量警告門檻&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>services&lt;/code>&lt;/td>
 &lt;td>天氣服務、時間格式、GPU 類型（AMD/NVIDIA/Intel）、音訊後端&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>paths&lt;/code>&lt;/td>
 &lt;td>桌布目錄、歌詞目錄、assets 目錄的自定義路徑&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="元件設定">元件設定&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Section&lt;/th>
 &lt;th>控制什麼&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>bar&lt;/code>&lt;/td>
 &lt;td>狀態列：persistent toggle、工作區顯示、active window、tray、時鐘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>background&lt;/code>&lt;/td>
 &lt;td>桌布設定、桌面時鐘、音訊視覺化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>dashboard&lt;/code>&lt;/td>
 &lt;td>媒體播放器、效能指標（CPU/GPU/RAM）、天氣&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>launcher&lt;/code>&lt;/td>
 &lt;td>應用程式搜尋、動作列表、桌布選擇&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>lock&lt;/code>&lt;/td>
 &lt;td>鎖屏：指紋認證開關、logo 重新配色&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>notifs&lt;/code>&lt;/td>
 &lt;td>通知：過期時間、分組邏輯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>osd&lt;/code>&lt;/td>
 &lt;td>音量 / 亮度變更的螢幕顯示&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>session&lt;/code>&lt;/td>
 &lt;td>登出 / 關機 / 重啟選單&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>sidebar&lt;/code>&lt;/td>
 &lt;td>hover 行為、快速開關（WiFi、藍牙、暗色模式）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>nexus&lt;/code>&lt;/td>
 &lt;td>設定介面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>utilities&lt;/code>&lt;/td>
 &lt;td>Toast 通知、VPN 開關、其他快速操作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>border&lt;/code>&lt;/td>
 &lt;td>視窗邊框：thickness、rounding、smoothing&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="範例常見客製化">範例：常見客製化&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;bar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;persistent&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;workspaces&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;shown&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">9&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;clock&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;format&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;%H:%M&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;notifs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;expiration&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">8000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;paths&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;wallpapers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;~/Pictures/MyWallpapers&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;services&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;gpu&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;amd&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;format24h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Key 要對著實際版本驗&lt;/strong>：caelestia-shell 啟動時會改寫 shell.json、丟掉 schema 不認得的 key、只留有效子集（VM 實測 2.1.0：&lt;code>bar.clock.format&lt;/code>、&lt;code>notifs.expiration&lt;/code>、&lt;code>services.*&lt;/code> 都被丟掉、只有 &lt;code>bar.persistent&lt;/code> 與 &lt;code>workspaces.shown&lt;/code> 留下）。寫覆寫檔後啟動 shell、再回頭看檔案剩什麼，就是這個版本的有效 schema——被丟掉的 key 代表該功能在這版不存在或搬了位置，去 token 或別的 section 找。schema 的權威來源是安裝進來的 &lt;code>/usr/lib/qt6/qml/Caelestia/Config/caelestia-config.qmltypes&lt;/code>（config 編在 C++ plugin 裡、QML 原始碼查不到）。&lt;/p>
&lt;p>一個經 2.1.0 驗證的實用例——idle 鎖屏時間（預設 3 分鐘鎖、10 分鐘關螢幕）延長到 2 小時：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;general&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;idle&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;timeouts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;timeout&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">7200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;idleAction&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;lock&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="token-系統">Token 系統&lt;/h2>
&lt;p>&lt;code>shell-tokens.json&lt;/code> 控制的是比 &lt;code>shell.json&lt;/code> 更細粒度的視覺數值：&lt;/p></description><content:encoded><![CDATA[<p><code>~/.config/caelestia/</code> 下有兩類檔案：使用者層的 JSON 配置（<code>shell.json</code>、<code>cli.json</code>）控制功能和外觀，token 層（<code>shell-tokens.json</code>）控制細部視覺數值——間距、圓角、字型大小、動畫曲線。<code>shell.json</code> 修改後 Quickshell 自動 reload、不需要重啟（VM 實測：跑著的實例會即時吃到檔案變更、驗證後重新序列化回檔案）。配色（scheme）的熱套用是另一個獨立的 watcher、有啟動時序前提，見下方配色段。</p>
<h2 id="配置檔路徑">配置檔路徑</h2>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>~/.config/caelestia/shell.json</code></td>
          <td>主配置（使用者自建，不會自動產生）</td>
      </tr>
      <tr>
          <td><code>~/.config/caelestia/shell-tokens.json</code></td>
          <td>進階視覺 token（rounding、spacing、padding、字型、動畫）</td>
      </tr>
      <tr>
          <td><code>~/.config/caelestia/monitors/&lt;name&gt;/shell.json</code></td>
          <td>每螢幕覆寫（螢幕名稱用 <code>hyprctl monitors</code> 查）</td>
      </tr>
      <tr>
          <td><code>~/.config/caelestia/monitors/&lt;name&gt;/shell-tokens.json</code></td>
          <td>每螢幕 token 覆寫</td>
      </tr>
      <tr>
          <td><code>~/.config/caelestia/hypr-user.lua</code></td>
          <td>自定義 Hyprland 設定（Lua 格式）</td>
      </tr>
      <tr>
          <td><code>~/.config/caelestia/hypr-vars.lua</code></td>
          <td>Hyprland 變數覆寫</td>
      </tr>
      <tr>
          <td><code>~/.config/caelestia/cli.json</code></td>
          <td>CLI 工具配置（主題目標、icon theme、workspace toggle）</td>
      </tr>
      <tr>
          <td><code>~/.config/caelestia/templates/</code></td>
          <td>自定義色彩模板，語法 <code>{{ primary.hex }}</code></td>
      </tr>
      <tr>
          <td><code>~/.face</code></td>
          <td>個人頭像（Dashboard 用）</td>
      </tr>
      <tr>
          <td><code>~/Pictures/Wallpapers</code></td>
          <td>預設桌布目錄（可在 paths section 改）</td>
      </tr>
  </tbody>
</table>
<p><code>shell.json</code> 不會在安裝時自動產生——預設行為由 Caelestia 內建值決定。你只需要建立這個檔案、寫入你想覆寫的 section。未寫的 section 使用預設值。</p>
<h2 id="shelljson-結構">shell.json 結構</h2>
<p>shell.json 的 top-level section 對應桌面 shell 的各個元件和全域設定：</p>
<h3 id="全域設定">全域設定</h3>
<table>
  <thead>
      <tr>
          <th>Section</th>
          <th>控制什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>enabled</code></td>
          <td>各元件的啟用 / 停用主開關</td>
      </tr>
      <tr>
          <td><code>appearance</code></td>
          <td>全域視覺：deformScale、rounding、spacing、padding、字型、動畫、透明度</td>
      </tr>
      <tr>
          <td><code>general</code></td>
          <td>Logo 圖片路徑、app 路徑、閒置逾時秒數、低電量警告門檻</td>
      </tr>
      <tr>
          <td><code>services</code></td>
          <td>天氣服務、時間格式、GPU 類型（AMD/NVIDIA/Intel）、音訊後端</td>
      </tr>
      <tr>
          <td><code>paths</code></td>
          <td>桌布目錄、歌詞目錄、assets 目錄的自定義路徑</td>
      </tr>
  </tbody>
</table>
<h3 id="元件設定">元件設定</h3>
<table>
  <thead>
      <tr>
          <th>Section</th>
          <th>控制什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>bar</code></td>
          <td>狀態列：persistent toggle、工作區顯示、active window、tray、時鐘</td>
      </tr>
      <tr>
          <td><code>background</code></td>
          <td>桌布設定、桌面時鐘、音訊視覺化</td>
      </tr>
      <tr>
          <td><code>dashboard</code></td>
          <td>媒體播放器、效能指標（CPU/GPU/RAM）、天氣</td>
      </tr>
      <tr>
          <td><code>launcher</code></td>
          <td>應用程式搜尋、動作列表、桌布選擇</td>
      </tr>
      <tr>
          <td><code>lock</code></td>
          <td>鎖屏：指紋認證開關、logo 重新配色</td>
      </tr>
      <tr>
          <td><code>notifs</code></td>
          <td>通知：過期時間、分組邏輯</td>
      </tr>
      <tr>
          <td><code>osd</code></td>
          <td>音量 / 亮度變更的螢幕顯示</td>
      </tr>
      <tr>
          <td><code>session</code></td>
          <td>登出 / 關機 / 重啟選單</td>
      </tr>
      <tr>
          <td><code>sidebar</code></td>
          <td>hover 行為、快速開關（WiFi、藍牙、暗色模式）</td>
      </tr>
      <tr>
          <td><code>nexus</code></td>
          <td>設定介面</td>
      </tr>
      <tr>
          <td><code>utilities</code></td>
          <td>Toast 通知、VPN 開關、其他快速操作</td>
      </tr>
      <tr>
          <td><code>border</code></td>
          <td>視窗邊框：thickness、rounding、smoothing</td>
      </tr>
  </tbody>
</table>
<h3 id="範例常見客製化">範例：常見客製化</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nt">&#34;bar&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nt">&#34;persistent&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nt">&#34;workspaces&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="nt">&#34;shown&#34;</span><span class="p">:</span> <span class="mi">9</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nt">&#34;clock&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;%H:%M&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nt">&#34;notifs&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nt">&#34;expiration&#34;</span><span class="p">:</span> <span class="mi">8000</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nt">&#34;paths&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nt">&#34;wallpapers&#34;</span><span class="p">:</span> <span class="s2">&#34;~/Pictures/MyWallpapers&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nt">&#34;services&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nt">&#34;gpu&#34;</span><span class="p">:</span> <span class="s2">&#34;amd&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="nt">&#34;time&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">            <span class="nt">&#34;format24h&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>Key 要對著實際版本驗</strong>：caelestia-shell 啟動時會改寫 shell.json、丟掉 schema 不認得的 key、只留有效子集（VM 實測 2.1.0：<code>bar.clock.format</code>、<code>notifs.expiration</code>、<code>services.*</code> 都被丟掉、只有 <code>bar.persistent</code> 與 <code>workspaces.shown</code> 留下）。寫覆寫檔後啟動 shell、再回頭看檔案剩什麼，就是這個版本的有效 schema——被丟掉的 key 代表該功能在這版不存在或搬了位置，去 token 或別的 section 找。schema 的權威來源是安裝進來的 <code>/usr/lib/qt6/qml/Caelestia/Config/caelestia-config.qmltypes</code>（config 編在 C++ plugin 裡、QML 原始碼查不到）。</p>
<p>一個經 2.1.0 驗證的實用例——idle 鎖屏時間（預設 3 分鐘鎖、10 分鐘關螢幕）延長到 2 小時：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nt">&#34;general&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nt">&#34;idle&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">            <span class="nt">&#34;timeouts&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                    <span class="nt">&#34;timeout&#34;</span><span class="p">:</span> <span class="mi">7200</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                    <span class="nt">&#34;idleAction&#34;</span><span class="p">:</span> <span class="s2">&#34;lock&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="p">]</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="token-系統">Token 系統</h2>
<p><code>shell-tokens.json</code> 控制的是比 <code>shell.json</code> 更細粒度的視覺數值：</p>
<ul>
<li>每個元件的 rounding（圓角半徑）</li>
<li>各區域的 spacing 和 padding</li>
<li>各類別文字的 font size</li>
<li>動畫的 duration 和 easing curve</li>
<li>元件的固定尺寸</li>
</ul>
<p>官方的說法是：「Do NOT change any of these options if you do not know what you are doing.」這個警告的實際意義是——token 之間有隱含的依賴關係（某個 padding 值跟某個 rounding 值配合才好看），隨意改一個可能讓整體視覺走樣。而且 token 的名稱和結構可能在版本更新時變動，沒有向後相容承諾。</p>
<p>務實的做法是先不動 token，用 <code>shell.json</code> 做功能層面的客製化。等到你有明確的視覺需求（例如想把所有圓角改更大），再查 token 文件做精確調整。</p>
<h2 id="自定義-hyprland-設定">自定義 Hyprland 設定</h2>
<p>Caelestia 管理自己的 Hyprland 配置。你的自訂設定放在 <code>hypr-user.lua</code>（不是直接改 hyprland.lua）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- ~/.config/caelestia/hypr-user.lua</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">-- 額外的 keybind</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">hl.bind</span><span class="p">(</span><span class="s2">&#34;SUPER&#34;</span><span class="p">,</span> <span class="s2">&#34;Return&#34;</span><span class="p">,</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;foot&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">-- monitor 配置</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">monitor</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="s2">&#34;DP-1, 2560x1440@144, 0x0, 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="s2">&#34;HDMI-A-1, 1920x1080@60, 2560x0, 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">-- 額外的 window rules</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">hl.config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">windowrule</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="s2">&#34;workspace 8, class:^(firefox)$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p><strong>Hyprland Lua 格式</strong>：Caelestia 已經遷移到 Lua 配置（對應 Hyprland v0.55+ 的格式變更）。如果你手上有舊的 <code>hypr-user.conf</code>，需要轉換成 Lua。轉換工具：<code>hyprlang2lua</code>（Go）或 <code>hyprconf2lua</code>（Python pip）。</p>
<h2 id="配色與主題">配色與主題</h2>
<p>Caelestia 的配色系統用 Material Design 3 的動態取色——從桌布圖片自動提取色彩，產生一套 primary / secondary / surface / error 色系，套用到所有元件。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 設定桌布並觸發動態取色</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">caelestia wallpaper -f ~/Pictures/Wallpapers/mountain.jpg
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 手動切換配色方案</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">caelestia scheme <span class="nb">set</span> -n dynamic    <span class="c1"># 動態（從桌布取）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">caelestia scheme <span class="nb">set</span> -n catppuccin <span class="c1"># 如果有對應 template</span></span></span></code></pre></div><p>自定義配色模板放在 <code>~/.config/caelestia/templates/</code>，用 <code>{{ primary.hex }}</code>、<code>{{ surface.hex }}</code> 等變數。Caelestia 會在切換桌布時用新的色系填入這些變數，產生對應的配置檔。</p>
<p><strong>Scheme 熱套用的啟動時序前提</strong>（兩台 VM 各實測一次、行為一致）：shell 對 <code>~/.local/state/caelestia/scheme.json</code> 的 file watcher 只在<strong>啟動當下檔案已存在</strong>時建立。首次安裝、從未 <code>scheme set</code> 過的機器上，第一個 shell 實例啟動時 state 檔還不存在——之後下 <code>scheme set</code> 會寫入 state、CLI 回報也正確，但跑著的 UI 不變色。修法：<code>scheme set</code> 建好 state 檔後重啟一次 shell；那之後同一實例的配色切換都即時生效、不用再重啟。</p>
<p><strong>重啟 shell 用 <code>pkill -x qs</code></strong>：<code>caelestia shell -k</code> 可能靜默失敗（實測 CLI 1.1.1：指令回傳正常但 process 沒死、新起的實例偵測到舊實例就自行退出——看起來重啟了、實際同一個實例一直活著）。驗證重啟是否真的發生，比對 <code>ps -o pid,lstart -p $(pgrep -x qs)</code> 的 pid 與起始時間，別只看指令沒報錯。process 名是 <code>qs</code>（<code>/usr/bin/qs</code> 是 quickshell 的 symlink）、<code>pgrep caelestia</code> 找不到它。</p>
<h2 id="不要改-aur-安裝的檔案">不要改 AUR 安裝的檔案</h2>
<p>AUR package（<code>caelestia-shell</code>）安裝的檔案在系統路徑下，更新時會被覆蓋。所有客製化都應該放在 <code>~/.config/caelestia/</code>，Caelestia 會優先讀取使用者路徑的配置，沒有的才 fallback 到系統預設。</p>
<h2 id="已知問題">已知問題</h2>
<p><strong>Config 靜默破壞</strong>：Caelestia 的 token 名稱和配置結構可能在版本更新時變動，不會事先通知。更新後如果 shell 行為異常，先檢查 <code>shell-tokens.json</code> 裡的 key 是否還存在。</p>
<p><strong>Notification backlog</strong>：Shell 可能因為積累大量通知而變卡。清除：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">caelestia shell notifs clear</span></span></code></pre></div><p><strong>quickshell-git 必須</strong>：穩定版的 quickshell 缺少 Caelestia 需要的 API。如果裝了穩定版，shell 會啟動失敗或功能不完整。確認用的是 <code>quickshell-git</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pacman -Q quickshell-git</span></span></code></pre></div><p><strong>工作區切換卡頓</strong>：在某些 GPU / 驅動組合下報告過隨機卡頓。排查方向：關閉 VRR（<code>vrr = 0</code>）、減少 blur passes、檢查 GPU 驅動版本。</p>
<h2 id="dotfile-repo-結構對應">Dotfile Repo 結構對應</h2>
<p>Caelestia 的配置只追蹤覆寫用的檔案（<code>shell.json</code>、<code>cli.json</code>、<code>hypr-user.lua</code>），AUR package 安裝的原始檔案不進 repo：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">~/dotfiles/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">└── caelestia/
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    └── .config/
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        └── caelestia/
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            ├── shell.json
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            ├── shell-tokens.json   # 如果有自訂
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            ├── cli.json
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            ├── hypr-user.lua
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            ├── hypr-vars.lua       # 如果有自訂
</span></span><span class="line"><span class="ln">10</span><span class="cl">            └── templates/          # 如果有自訂配色模板</span></span></code></pre></div><p>monitor 專屬的覆寫（<code>monitors/&lt;name&gt;/</code>）是硬體相關的，跟 <a href="/blog/linux/dotfile/05-hyprland-config/hyprland-core-config/" data-link-title="Hyprland 核心配置" data-link-desc="Hyprland 的配置檔該怎麼組織、monitor 怎麼設定、keybind 怎麼設計、輸入裝置和環境變數怎麼配時回來讀">Hyprland 的 monitor 設定</a>一樣，可能需要排除在 Git 外或用 template/local 機制處理。</p>
<p><strong>Runtime 寫入會穿透 stow 的目錄 symlink 弄髒 repo</strong>：用 stow 部署時 <code>~/.config/caelestia</code> 常是折疊的目錄 symlink、指向 repo 裡的 package 目錄，而 caelestia-shell 會在執行期往自己的 config 目錄寫（啟動時建 <code>monitors/</code>、改寫 <code>shell.json</code>）——這些寫入直接落進 dotfiles repo、<code>git status</code> 變髒（btop 往 <code>themes/</code> 寫也是同一型）。配套：repo 的 <code>.gitignore</code> 把「app 會自己寫的路徑」列入（<code>caelestia/.config/caelestia/monitors/</code> 等），shell.json 被改寫後 diff 一下、把有效 schema 的版本 commit 回去。</p>
<h2 id="vm-測試-vs-實機測試">VM 測試 vs 實機測試</h2>
<blockquote>
<p><strong>[VM 可測試]</strong> shell.json 配置語法、各 section 的效果（bar 模組顯示、launcher 搜尋行為、通知過期邏輯）、CLI 指令執行、hypr-user.lua 載入、配色方案切換指令。</p></blockquote>
<blockquote>
<p><strong>[需實機測試]</strong> token 微調的視覺效果（間距和圓角的差異在軟體渲染下難以判讀）。動畫流暢度、blur 效能、動態取色品質、多螢幕佈局、日常穩定性等視覺與效能項目見 <a href="/blog/linux/dotfile/06-rice-design/caelestia-installation/" data-link-title="Caelestia 安裝" data-link-desc="要在 Arch Linux 上安裝 Caelestia 桌面 shell 時回來讀">Caelestia 安裝</a>的對應段落。</p></blockquote>
]]></content:encoded></item><item><title>模組五：Hyprland 配置</title><link>https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/</guid><description>&lt;p>Hyprland 是 Wayland 上的動態平鋪式 compositor，在 tiling WM 裡少見地重視視覺效果——流暢的視窗動畫、圓角、模糊、漸層邊框。這個模組教它的配置檔怎麼寫、核心概念是什麼、以及常見的設定場景。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/04-window-management/" data-link-title="模組四：視窗管理與平鋪式工作流" data-link-desc="同時開多個視窗時的排列策略 — 手動貼齊跟自動平鋪的差距在哪、macOS 和 Linux 各有哪些工具、多螢幕怎麼處理、什麼情境值得從浮動切換到平鋪">視窗管理與平鋪式工作流&lt;/a>講了平鋪概念和選型，這裡直接進入 Hyprland 的配置實務。之後的 VM 實測專案會用這些配置做實際安裝和驗證。&lt;/p>
&lt;p>macOS 讀者如果不打算在 Linux 或 VM 上跑 Hyprland，可以跳到&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">同步、Bootstrap 與環境重建&lt;/a>——bootstrap script 和 secret 管理的概念對 macOS 同樣適用。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/hyprland-installation/" data-link-title="Hyprland 安裝與環境建置" data-link-desc="要在 Arch Linux 上從零安裝 Hyprland 桌面環境時回來讀">安裝與環境建置&lt;/a>&lt;/td>
 &lt;td>Arch Linux 套件、GPU 驅動（AMD/Intel/NVIDIA 含完整設定）、配套工具、登入管理器、首次啟動除錯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/hyprland-core-config/" data-link-title="Hyprland 核心配置" data-link-desc="Hyprland 的配置檔該怎麼組織、monitor 怎麼設定、keybind 怎麼設計、輸入裝置和環境變數怎麼配時回來讀">Hyprland 核心配置&lt;/a>&lt;/td>
 &lt;td>配置檔位置與模組化拆分、monitor 設定、keybind 設計（含完整配置範例）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/workspace-rules-appearance/" data-link-title="Workspace、Window Rules 與外觀" data-link-desc="Hyprland 的 workspace 綁定螢幕、window rules 設定浮動例外、外觀動畫調教、layout 選型、autostart 和 plugin 管理時回來讀">Workspace、Window Rules 與外觀&lt;/a>&lt;/td>
 &lt;td>workspace 設定、window rules、外觀設定、兩種平鋪分割演算法（dwindle / master）、autostart、plugin、穩定性維護&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/hyprland-vm-setup/" data-link-title="Hyprland VM 環境設定與測試矩陣" data-link-desc="要在 VM 裡測試 Hyprland 配置、或判斷某個設定該在 VM 還是實機驗證時回來讀">VM 環境設定與測試矩陣&lt;/a>&lt;/td>
 &lt;td>UTM/QEMU 設定、VM 環境變數、效能預期、VM 可測試 vs 需實機測試的完整矩陣&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/04-window-management/" data-link-title="模組四：視窗管理與平鋪式工作流" data-link-desc="同時開多個視窗時的排列策略 — 手動貼齊跟自動平鋪的差距在哪、macOS 和 Linux 各有哪些工具、多螢幕怎麼處理、什麼情境值得從浮動切換到平鋪">模組四：視窗管理與自動平鋪&lt;/a>：平鋪式 WM 的概念和選型&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計&lt;/a>：compositor 之上的狀態列、啟動器、通知、配色系統&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">模組七：桌面環境維護與故障排除&lt;/a>：Hyprland crash、GPU hang、config 寫錯時的診斷和恢復&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建&lt;/a>：跨機器搬移時硬體相關設定怎麼處理&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/tools/gui/" data-link-title="圖形桌面工具" data-link-desc="有圖形環境時要挑桌面應用（檔案管理員、桌面環境與擴充）、需要判斷不同實作的相依足跡與桌面環境耦合、以及在最小化 WM 上加一個 GUI app 的取捨時回來讀">Linux 工具選單：圖形桌面工具&lt;/a>：在 Hyprland 下加圖形檔案管理員（Thunar / Nemo / PCManFM-Qt）的相依取捨&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Hyprland 是 Wayland 上的動態平鋪式 compositor，在 tiling WM 裡少見地重視視覺效果——流暢的視窗動畫、圓角、模糊、漸層邊框。這個模組教它的配置檔怎麼寫、核心概念是什麼、以及常見的設定場景。</p>
<p><a href="/blog/linux/dotfile/04-window-management/" data-link-title="模組四：視窗管理與平鋪式工作流" data-link-desc="同時開多個視窗時的排列策略 — 手動貼齊跟自動平鋪的差距在哪、macOS 和 Linux 各有哪些工具、多螢幕怎麼處理、什麼情境值得從浮動切換到平鋪">視窗管理與平鋪式工作流</a>講了平鋪概念和選型，這裡直接進入 Hyprland 的配置實務。之後的 VM 實測專案會用這些配置做實際安裝和驗證。</p>
<p>macOS 讀者如果不打算在 Linux 或 VM 上跑 Hyprland，可以跳到<a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">同步、Bootstrap 與環境重建</a>——bootstrap script 和 secret 管理的概念對 macOS 同樣適用。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/05-hyprland-config/hyprland-installation/" data-link-title="Hyprland 安裝與環境建置" data-link-desc="要在 Arch Linux 上從零安裝 Hyprland 桌面環境時回來讀">安裝與環境建置</a></td>
          <td>Arch Linux 套件、GPU 驅動（AMD/Intel/NVIDIA 含完整設定）、配套工具、登入管理器、首次啟動除錯</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/05-hyprland-config/hyprland-core-config/" data-link-title="Hyprland 核心配置" data-link-desc="Hyprland 的配置檔該怎麼組織、monitor 怎麼設定、keybind 怎麼設計、輸入裝置和環境變數怎麼配時回來讀">Hyprland 核心配置</a></td>
          <td>配置檔位置與模組化拆分、monitor 設定、keybind 設計（含完整配置範例）</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/05-hyprland-config/workspace-rules-appearance/" data-link-title="Workspace、Window Rules 與外觀" data-link-desc="Hyprland 的 workspace 綁定螢幕、window rules 設定浮動例外、外觀動畫調教、layout 選型、autostart 和 plugin 管理時回來讀">Workspace、Window Rules 與外觀</a></td>
          <td>workspace 設定、window rules、外觀設定、兩種平鋪分割演算法（dwindle / master）、autostart、plugin、穩定性維護</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/05-hyprland-config/hyprland-vm-setup/" data-link-title="Hyprland VM 環境設定與測試矩陣" data-link-desc="要在 VM 裡測試 Hyprland 配置、或判斷某個設定該在 VM 還是實機驗證時回來讀">VM 環境設定與測試矩陣</a></td>
          <td>UTM/QEMU 設定、VM 環境變數、效能預期、VM 可測試 vs 需實機測試的完整矩陣</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/linux/dotfile/04-window-management/" data-link-title="模組四：視窗管理與平鋪式工作流" data-link-desc="同時開多個視窗時的排列策略 — 手動貼齊跟自動平鋪的差距在哪、macOS 和 Linux 各有哪些工具、多螢幕怎麼處理、什麼情境值得從浮動切換到平鋪">模組四：視窗管理與自動平鋪</a>：平鋪式 WM 的概念和選型</li>
<li>→ <a href="/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計</a>：compositor 之上的狀態列、啟動器、通知、配色系統</li>
<li>→ <a href="/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">模組七：桌面環境維護與故障排除</a>：Hyprland crash、GPU hang、config 寫錯時的診斷和恢復</li>
<li>→ <a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建</a>：跨機器搬移時硬體相關設定怎麼處理</li>
<li>→ <a href="/blog/linux/tools/gui/" data-link-title="圖形桌面工具" data-link-desc="有圖形環境時要挑桌面應用（檔案管理員、桌面環境與擴充）、需要判斷不同實作的相依足跡與桌面環境耦合、以及在最小化 WM 上加一個 GUI app 的取捨時回來讀">Linux 工具選單：圖形桌面工具</a>：在 Hyprland 下加圖形檔案管理員（Thunar / Nemo / PCManFM-Qt）的相依取捨</li>
</ul>
]]></content:encoded></item><item><title>可除錯的 bootstrap：把可觀測性內建進安裝腳本</title><link>https://tarrragon.github.io/blog/linux/install/observable-bootstrap/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/observable-bootstrap/</guid><description>&lt;p>Bootstrap 腳本失敗是常態，所以它的設計目標之一應該是「失敗時可診斷」：把失敗當成會發生的事來設計，預先留好定位問題的痕跡。一支自動化安裝腳本要跨越的環境差異很多——機器缺某個工具、套件清單有筆誤、某個指令在這個發行版的行為跟預期不同——任何一處都可能讓它中斷。決定你是「三分鐘看出哪裡錯」還是「對著終端機捲半天瞎猜」的，是這支腳本有沒有在設計時就把可觀測性內建進去，跟運氣無關。&lt;/p>
&lt;p>可觀測性要事先設計，是因為失敗發生的當下，你能拿到的資訊就已經定型了。如果腳本只把輸出丟到終端機、失敗時只留下一句通用的錯誤，那當下你就只有那句話可看；如果它一路把帶時間戳的紀錄寫進檔案、失敗時主動印出出錯的位置，那同一個失敗就變得可定位。差別不在失敗本身，在失敗前你準備了什麼。如果你寫的是自己的 bootstrap（例如部署 dotfile 的那支 &lt;code>install.sh&lt;/code>），這層要在你第一次跑它之前就設計進去，而不是等它出事才回頭加；就算腳本不是你寫的、你只是來 debug 一次失敗，下一段「找程式自己的 log」一樣適用。&lt;/p>
&lt;h2 id="為什麼會瞎找">為什麼會瞎找&lt;/h2>
&lt;p>不可觀測的腳本失敗時，你手上只有終端機捲動過的那些輸出，而那往往不足以定位真正的原因。終端機的輸出是易逝的、會被後續輸出沖掉、多個來源的訊息交錯在一起；更麻煩的是，很多失敗的「表面錯誤」離「真正原因」隔了好幾層。一個指令因為前面某個變數是空的而失敗，但它報出來的錯可能完全沒提到那個空變數——你看著一個誤導性的症狀，往上游找不到源頭。&lt;/p>
&lt;p>破解這種瞎找的，常常是一份你一開始沒看的 log。很多程式在終端機只印一段摘要，卻同時把詳細的執行紀錄寫進一個 log 檔；當終端機的訊息不足以定位時，那份程式自己寫的 log 裡往往就有答案。除錯時養成「找程式自己的 log，而不是只盯著終端機捲動」的習慣，是把瞎找變成定位的關鍵一步——這也是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">模組七日誌判讀&lt;/a> 的核心。而對你自己寫的 bootstrap，你可以更進一步：在設計時就讓它產生這樣一份 log。&lt;/p>
&lt;h2 id="三個內建可觀測性的手法">三個內建可觀測性的手法&lt;/h2>
&lt;p>讓一支 bootstrap 腳本可診斷，有三個低成本、效果明顯的手法，它們合起來把「失敗了」變成「失敗在第幾行、哪個指令、什麼狀態」。&lt;/p>
&lt;h3 id="log-落地把全部輸出-tee-進帶時間戳的檔案">log 落地：把全部輸出 tee 進帶時間戳的檔案&lt;/h3>
&lt;p>第一個手法是讓腳本的全部輸出同時進終端機跟一個 log 檔，而不是只進終端機。終端機的捲動是易逝的，log 檔是持久的——可以事後 &lt;code>grep&lt;/code>、可以貼給別人看、可以比對前後兩次跑的差異。在 bash 裡，一行 &lt;code>exec&lt;/code> 就能把後續所有 stdout 與 stderr 都導去 &lt;code>tee&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nv">LOG_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">XDG_STATE_HOME&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="p">/.local/state&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/dotfiles&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">mkdir -p &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nv">LOG_FILE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_DIR&lt;/span>&lt;span class="s2">/install-&lt;/span>&lt;span class="k">$(&lt;/span>date +%Y%m%d-%H%M%S&lt;span class="k">)&lt;/span>&lt;span class="s2">.log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nb">exec&lt;/span> &amp;gt; &amp;gt;&lt;span class="o">(&lt;/span>tee -a &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>帶時間戳的檔名讓每次跑各留一份、不互相覆蓋，事後可以回溯「上一次成功跟這次失敗差在哪」。log 檔放在 &lt;code>XDG_STATE_HOME&lt;/code>（狀態資料的標準位置）底下，符合慣例、也不污染家目錄。&lt;/p>
&lt;h3 id="錯誤定位用-err-trap-印出出錯的行與指令">錯誤定位：用 ERR trap 印出出錯的行與指令&lt;/h3>
&lt;p>第二個手法是讓腳本在中斷的瞬間，主動報出「是哪一行、哪個指令、什麼結束碼」失敗的。配合 &lt;code>set -e&lt;/code>（出錯即停）的腳本，預設只會默默地停，不告訴你停在哪。加一個 &lt;code>ERR&lt;/code> trap，就能在 &lt;code>set -e&lt;/code> 中斷之前先印出定位資訊：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">set&lt;/span> -Eeuo pipefail &lt;span class="c1"># -E 讓 ERR trap 在函式/子 shell 也生效&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">trap&lt;/span> &lt;span class="s1">&amp;#39;log &amp;#34;ERROR line $LINENO: [$BASH_COMMAND] exit=$?&amp;#34;&amp;#39;&lt;/span> ERR&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>$LINENO&lt;/code> 是出錯的行號、&lt;code>$BASH_COMMAND&lt;/code> 是當下正在執行的那條指令、&lt;code>$?&lt;/code> 是它的結束碼。三者合起來，輸出會長這樣：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">[00:06:51] ERROR line 40: [sudo pacman -S --needed stow git zsh] exit=1&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>範例裡的 &lt;code>pacman&lt;/code> 換發行版會不同，這裡只是示意 trap 輸出的格式——手法本身（行號 + 指令 + 結束碼）跟發行版無關。這一行直接點名元兇。前面提過的那類「表面錯誤離真正原因隔好幾層」的情況——例如某個指令因為 &lt;code>which&lt;/code> 不存在而拿到空字串、最後報一個看似無關的錯——有了這行，你會直接看到是哪一行的哪條指令掛了，不必從誤導性的症狀往回猜。&lt;code>set -E&lt;/code>（&lt;code>-E&lt;/code> 旗標）是為了讓 trap 在函式跟子 shell 裡也照樣觸發，少了它，包在函式裡的錯誤會漏掉。&lt;/p>
&lt;h3 id="步驟標記用帶時間戳的-log-函式標出進度">步驟標記：用帶時間戳的 log 函式標出進度&lt;/h3>
&lt;p>第三個手法是在關鍵步驟前印一行帶時間戳的標記，讓你能看出腳本跑到哪、哪一步慢。一個極簡的 log 函式就夠：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">log&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;[%s] %s\n&amp;#39;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date +%H:%M:%S&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$*&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;install.sh start | OS=&lt;/span>&lt;span class="nv">$OS&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;Installing base packages...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;Stowing configs...&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>時間戳的價值在於它同時給你「進度」跟「效能」兩種資訊：失敗時，最後一行成功的 log 告訴你它跨過了哪些步驟、卡在哪一步之後；正常時，相鄰兩行的時間差告訴你哪一步耗時最久。這比沒有標記、只能從一堆套件下載輸出裡猜「現在到底在幹嘛」清楚得多。&lt;/p>
&lt;h2 id="失敗可診斷是設計目標">失敗可診斷是設計目標&lt;/h2>
&lt;p>把這三個手法合起來，一支原本「失敗時只留一句通用錯誤」的腳本，會變成「每次跑都留一份完整 log、失敗時直接點名第幾行哪個指令、過程中每步都有時間戳」。成本是腳本開頭多幾行，回報是把未來每一次除錯從瞎找變成定位。這層可觀測性是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八 bootstrap script 設計&lt;/a> 的延伸——那篇給安裝腳本的骨架與套件清單，這篇給它加上失敗時的診斷能力，兩篇處理的是同一支腳本的兩個層面。&lt;/p>
&lt;p>這是設計階段的決定，不是事後能補的。當一支沒有可觀測性的腳本在一台陌生機器上失敗，你沒辦法回到過去讓它記錄當時的狀態——資訊在失敗的瞬間就已經流失了。所以「失敗可診斷」要跟功能一起設計進去，把它當成 bootstrap 的基本屬性，而不是出事之後才想加的補丁。&lt;/p>
&lt;h2 id="回到系列">回到系列&lt;/h2>
&lt;p>這幾篇合起來，是把一台機器從「空的」帶到「能接收 dotfile、且部署過程可診斷」的完整地基：&lt;a href="../install-option-decisions/">安裝選項判讀&lt;/a> 處理 OS 怎麼裝、&lt;a href="../minimal-install-verify/">工具驗證與補足&lt;/a> 處理裝完缺什麼、&lt;a href="../ssh-keyless-bootstrap/">外部連入與無 key bootstrap&lt;/a> 處理怎麼連進去把 dotfile 弄進來，這一篇處理當部署失敗時怎麼快速看出原因。再往前一步，把這套地基用在無人值守的長任務上、讓機器在你離開後自己跑完工作，見 &lt;a href="../unattended-remote-work/">讓機器跑無人值守的長任務&lt;/a>——無人盯著的任務尤其依賴這篇談的可觀測性。地基打好，後面 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一到八&lt;/a> 的 dotfile 管理才有立足點。&lt;/p></description><content:encoded><![CDATA[<p>Bootstrap 腳本失敗是常態，所以它的設計目標之一應該是「失敗時可診斷」：把失敗當成會發生的事來設計，預先留好定位問題的痕跡。一支自動化安裝腳本要跨越的環境差異很多——機器缺某個工具、套件清單有筆誤、某個指令在這個發行版的行為跟預期不同——任何一處都可能讓它中斷。決定你是「三分鐘看出哪裡錯」還是「對著終端機捲半天瞎猜」的，是這支腳本有沒有在設計時就把可觀測性內建進去，跟運氣無關。</p>
<p>可觀測性要事先設計，是因為失敗發生的當下，你能拿到的資訊就已經定型了。如果腳本只把輸出丟到終端機、失敗時只留下一句通用的錯誤，那當下你就只有那句話可看；如果它一路把帶時間戳的紀錄寫進檔案、失敗時主動印出出錯的位置，那同一個失敗就變得可定位。差別不在失敗本身，在失敗前你準備了什麼。如果你寫的是自己的 bootstrap（例如部署 dotfile 的那支 <code>install.sh</code>），這層要在你第一次跑它之前就設計進去，而不是等它出事才回頭加；就算腳本不是你寫的、你只是來 debug 一次失敗，下一段「找程式自己的 log」一樣適用。</p>
<h2 id="為什麼會瞎找">為什麼會瞎找</h2>
<p>不可觀測的腳本失敗時，你手上只有終端機捲動過的那些輸出，而那往往不足以定位真正的原因。終端機的輸出是易逝的、會被後續輸出沖掉、多個來源的訊息交錯在一起；更麻煩的是，很多失敗的「表面錯誤」離「真正原因」隔了好幾層。一個指令因為前面某個變數是空的而失敗，但它報出來的錯可能完全沒提到那個空變數——你看著一個誤導性的症狀，往上游找不到源頭。</p>
<p>破解這種瞎找的，常常是一份你一開始沒看的 log。很多程式在終端機只印一段摘要，卻同時把詳細的執行紀錄寫進一個 log 檔；當終端機的訊息不足以定位時，那份程式自己寫的 log 裡往往就有答案。除錯時養成「找程式自己的 log，而不是只盯著終端機捲動」的習慣，是把瞎找變成定位的關鍵一步——這也是 <a href="/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">模組七日誌判讀</a> 的核心。而對你自己寫的 bootstrap，你可以更進一步：在設計時就讓它產生這樣一份 log。</p>
<h2 id="三個內建可觀測性的手法">三個內建可觀測性的手法</h2>
<p>讓一支 bootstrap 腳本可診斷，有三個低成本、效果明顯的手法，它們合起來把「失敗了」變成「失敗在第幾行、哪個指令、什麼狀態」。</p>
<h3 id="log-落地把全部輸出-tee-進帶時間戳的檔案">log 落地：把全部輸出 tee 進帶時間戳的檔案</h3>
<p>第一個手法是讓腳本的全部輸出同時進終端機跟一個 log 檔，而不是只進終端機。終端機的捲動是易逝的，log 檔是持久的——可以事後 <code>grep</code>、可以貼給別人看、可以比對前後兩次跑的差異。在 bash 裡，一行 <code>exec</code> 就能把後續所有 stdout 與 stderr 都導去 <code>tee</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nv">LOG_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">XDG_STATE_HOME</span><span class="k">:-</span><span class="nv">$HOME</span><span class="p">/.local/state</span><span class="si">}</span><span class="s2">/dotfiles&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$LOG_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nv">LOG_FILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$LOG_DIR</span><span class="s2">/install-</span><span class="k">$(</span>date +%Y%m%d-%H%M%S<span class="k">)</span><span class="s2">.log&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">exec</span> &gt; &gt;<span class="o">(</span>tee -a <span class="s2">&#34;</span><span class="nv">$LOG_FILE</span><span class="s2">&#34;</span><span class="o">)</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span></span></span></code></pre></div><p>帶時間戳的檔名讓每次跑各留一份、不互相覆蓋，事後可以回溯「上一次成功跟這次失敗差在哪」。log 檔放在 <code>XDG_STATE_HOME</code>（狀態資料的標準位置）底下，符合慣例、也不污染家目錄。</p>
<h3 id="錯誤定位用-err-trap-印出出錯的行與指令">錯誤定位：用 ERR trap 印出出錯的行與指令</h3>
<p>第二個手法是讓腳本在中斷的瞬間，主動報出「是哪一行、哪個指令、什麼結束碼」失敗的。配合 <code>set -e</code>（出錯即停）的腳本，預設只會默默地停，不告訴你停在哪。加一個 <code>ERR</code> trap，就能在 <code>set -e</code> 中斷之前先印出定位資訊：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">set</span> -Eeuo pipefail   <span class="c1"># -E 讓 ERR trap 在函式/子 shell 也生效</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">trap</span> <span class="s1">&#39;log &#34;ERROR line $LINENO: [$BASH_COMMAND] exit=$?&#34;&#39;</span> ERR</span></span></code></pre></div><p><code>$LINENO</code> 是出錯的行號、<code>$BASH_COMMAND</code> 是當下正在執行的那條指令、<code>$?</code> 是它的結束碼。三者合起來，輸出會長這樣：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[00:06:51] ERROR line 40: [sudo pacman -S --needed stow git zsh] exit=1</span></span></code></pre></div><p>範例裡的 <code>pacman</code> 換發行版會不同，這裡只是示意 trap 輸出的格式——手法本身（行號 + 指令 + 結束碼）跟發行版無關。這一行直接點名元兇。前面提過的那類「表面錯誤離真正原因隔好幾層」的情況——例如某個指令因為 <code>which</code> 不存在而拿到空字串、最後報一個看似無關的錯——有了這行，你會直接看到是哪一行的哪條指令掛了，不必從誤導性的症狀往回猜。<code>set -E</code>（<code>-E</code> 旗標）是為了讓 trap 在函式跟子 shell 裡也照樣觸發，少了它，包在函式裡的錯誤會漏掉。</p>
<h3 id="步驟標記用帶時間戳的-log-函式標出進度">步驟標記：用帶時間戳的 log 函式標出進度</h3>
<p>第三個手法是在關鍵步驟前印一行帶時間戳的標記，讓你能看出腳本跑到哪、哪一步慢。一個極簡的 log 函式就夠：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">log<span class="o">()</span> <span class="o">{</span> <span class="nb">printf</span> <span class="s1">&#39;[%s] %s\n&#39;</span> <span class="s2">&#34;</span><span class="k">$(</span>date +%H:%M:%S<span class="k">)</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$*</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">log <span class="s2">&#34;install.sh start | OS=</span><span class="nv">$OS</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">log <span class="s2">&#34;Installing base packages...&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">log <span class="s2">&#34;Stowing configs...&#34;</span></span></span></code></pre></div><p>時間戳的價值在於它同時給你「進度」跟「效能」兩種資訊：失敗時，最後一行成功的 log 告訴你它跨過了哪些步驟、卡在哪一步之後；正常時，相鄰兩行的時間差告訴你哪一步耗時最久。這比沒有標記、只能從一堆套件下載輸出裡猜「現在到底在幹嘛」清楚得多。</p>
<h2 id="失敗可診斷是設計目標">失敗可診斷是設計目標</h2>
<p>把這三個手法合起來，一支原本「失敗時只留一句通用錯誤」的腳本，會變成「每次跑都留一份完整 log、失敗時直接點名第幾行哪個指令、過程中每步都有時間戳」。成本是腳本開頭多幾行，回報是把未來每一次除錯從瞎找變成定位。這層可觀測性是 <a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八 bootstrap script 設計</a> 的延伸——那篇給安裝腳本的骨架與套件清單，這篇給它加上失敗時的診斷能力，兩篇處理的是同一支腳本的兩個層面。</p>
<p>這是設計階段的決定，不是事後能補的。當一支沒有可觀測性的腳本在一台陌生機器上失敗，你沒辦法回到過去讓它記錄當時的狀態——資訊在失敗的瞬間就已經流失了。所以「失敗可診斷」要跟功能一起設計進去，把它當成 bootstrap 的基本屬性，而不是出事之後才想加的補丁。</p>
<h2 id="回到系列">回到系列</h2>
<p>這幾篇合起來，是把一台機器從「空的」帶到「能接收 dotfile、且部署過程可診斷」的完整地基：<a href="../install-option-decisions/">安裝選項判讀</a> 處理 OS 怎麼裝、<a href="../minimal-install-verify/">工具驗證與補足</a> 處理裝完缺什麼、<a href="../ssh-keyless-bootstrap/">外部連入與無 key bootstrap</a> 處理怎麼連進去把 dotfile 弄進來，這一篇處理當部署失敗時怎麼快速看出原因。再往前一步，把這套地基用在無人值守的長任務上、讓機器在你離開後自己跑完工作，見 <a href="../unattended-remote-work/">讓機器跑無人值守的長任務</a>——無人盯著的任務尤其依賴這篇談的可觀測性。地基打好，後面 <a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一到八</a> 的 dotfile 管理才有立足點。</p>
]]></content:encoded></item><item><title>整合式 Shell vs 手動拼裝：實測足跡、失敗半徑與選型判準</title><link>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/integrated-shell-vs-manual-assembly/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/integrated-shell-vs-manual-assembly/</guid><description>&lt;p>整合式桌面 shell 與手動拼裝，是「一個大程式包辦整個桌面」與「多個小程式各司其職、由 compositor 黏起來」兩種架構。&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/caelestia-overview/" data-link-title="Caelestia 總覽：預組裝的 Hyprland 桌面 Shell" data-link-desc="考慮用 Caelestia 取代手動拼裝 waybar&amp;#43;wofi&amp;#43;mako、或評估預組裝桌面 shell 的 trade-off 時回來讀">Caelestia 總覽&lt;/a> 從概念層談過它的取捨（設計鎖定、穩定性風險）；這篇補上在同一台機器上實際跑過兩種之後量到的數據——資源足跡、失敗半徑、配色一致性——把「感覺整合比較方便」變成可以拿數字判斷的選型。&lt;/p>
&lt;p>這裡的數據來自一次 VM 實測：先手動拼裝一套 waybar + wofi + mako + hyprlock，再換成 Caelestia，量兩者的安裝大小、記憶體、config 結構與失敗行為。&lt;/p>
&lt;h2 id="資源足跡差約一個數量級">資源足跡：差約一個數量級&lt;/h2>
&lt;p>整合式 shell 把整個桌面畫在一個程式裡，這個程式通常是重量級的 UI runtime。Caelestia 建在 Quickshell（Qt6/QML）上，實測安裝足跡如下：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>項目&lt;/th>
 &lt;th>整合式（Caelestia）&lt;/th>
 &lt;th>手動拼裝（waybar+wofi+mako+hyprlock）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>安裝大小&lt;/td>
 &lt;td>約 230 MB（Quickshell 佔 213 MB）&lt;/td>
 &lt;td>約 4.5 MB（waybar 3 MB，其餘 KB 級）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>執行記憶體 RSS&lt;/td>
 &lt;td>單一 &lt;code>qs&lt;/code> 程式約 400 MB&lt;/td>
 &lt;td>waybar 約 53 MB + 通知/啟動器（小）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>差距的來源是 Quickshell 這個 UI 框架——那 213 MB 是 &lt;code>quickshell&lt;/code> 套件本身的安裝大小（一個建在 Qt6 上的 QML shell runtime），不是 Caelestia 的功能程式碼；而且 Qt6 的函式庫（&lt;code>qt6-declarative&lt;/code>、&lt;code>qt6-base&lt;/code> 等）還是它之上的額外相依，沒算進這 213 MB。手動拼裝的 waybar、wofi、mako 都是輕量的 wlroots/GTK 程式，加起來還不到 5 MB。&lt;/p>
&lt;p>這一軸在資源受限的機器上才會咬人：舊筆電、記憶體小的 VPS、或你本來就想把桌面壓到最輕。在一台記憶體充裕的桌機上，400 MB 對 60 MB 的差別多半感覺不到；在一台 2 GB RAM 的機器上，這就是「桌面吃掉五分之一記憶體」跟「幾乎不佔」的差別。&lt;/p>
&lt;h2 id="失敗半徑單點-vs-各自獨立">失敗半徑：單點 vs 各自獨立&lt;/h2>
&lt;p>整合式 shell 把狀態列、通知、鎖屏、啟動器畫在&lt;strong>同一個程式&lt;/strong>裡，所以這個程式崩潰時，這些東西會&lt;strong>一起消失&lt;/strong>。手動拼裝的每個元件是獨立行程，一個崩掉不影響其他——mako（通知）崩了，waybar（狀態列）還在。&lt;/p>
&lt;p>這不只是理論。這次 VM 實測就撞到一個具體案例：Caelestia 的鎖屏是由 Quickshell 主程式畫的，當這個持鎖的程式被中止時，Hyprland 依 &lt;code>ext-session-lock&lt;/code> 協議保持鎖定並顯示「lockscreen app died」的死局——狀態列、通知、鎖屏因為同源，一個環節出事就連帶整個桌面 UI。手動拼裝的 hyprlock 是獨立的鎖屏程式，它崩潰同樣會觸發那個死局，但你的狀態列與通知不會跟著沒。&lt;/p>
&lt;p>這一軸在穩定性敏感或無人值守的場景最關鍵。跑長時間無人盯著的任務時，「一個元件崩掉只損失那個元件」的隔離性，比「全部整合在一起」的一致性更值錢——因為沒人在旁邊立刻重啟。&lt;/p>
&lt;h2 id="配色一致性最容易被低估的一軸">配色一致性：最容易被低估的一軸&lt;/h2>
&lt;p>讓整個桌面配色一致，是整合式與手動拼裝差別最大、卻最常被忽略的地方。整合式 shell 因為所有元件在同一個程式裡，天生共用一套配色——Caelestia 的 dynamic scheme 從桌布抽一組 Material-3 palette，狀態列、通知、鎖屏、dashboard 全部同時套用，換張桌布整套 UI 跟著變。&lt;/p>
&lt;p>手動拼裝要達到同樣的一致，得自己解決一個跨程式的問題：每個元件用不同的設定格式與主題引擎，它們之間不會自動共享顏色。這次手動拼裝那套時就撞到這點——waybar 的 GTK CSS 引擎讀不到 Hyprland 的 &lt;code>$&lt;/code> 顏色變數，結果 waybar 的 &lt;code>style.css&lt;/code> 裡得&lt;strong>手抄一份跟 Hyprland &lt;code>colors.conf&lt;/code> 相同的 hex 色碼&lt;/strong>。換一次配色，就要在 waybar CSS、wofi CSS、mako config、hyprland colors 好幾個地方各改一遍。&lt;/p>
&lt;p>解這個手工問題的標準做法，是加一層&lt;strong>模板工具&lt;/strong>（matugen、pywal、wallust 之類）：從一張桌布或一套色票，自動生成每個元件的設定檔（例如 &lt;code>matugen/templates/rofi-colors.rasi&lt;/code> 就是給 rofi 用的顏色模板）。這等於是手動重建 Caelestia 內建的那套 dynamic theming pipeline。所以配色一致這件事的真正取捨是：Caelestia 開箱就有「換桌布全套跟著變」，手動拼裝要嘛手抄 hex、要嘛自己搭一條 templating pipeline。&lt;/p>
&lt;h2 id="config-結構">config 結構&lt;/h2>
&lt;p>配色一致的差別，也反映在 config 的形狀上。Caelestia 的使用者設定集中在一個 &lt;code>shell.json&lt;/code>（實測約 24 行就涵蓋狀態列、通知、idle 行為）。手動拼裝的設定散在各元件目錄、各用各的格式：waybar 的 &lt;code>config.jsonc&lt;/code> + &lt;code>style.css&lt;/code>、wofi 的 &lt;code>config&lt;/code> + &lt;code>style.css&lt;/code>、mako 的 &lt;code>config&lt;/code>、hypr 的數個 &lt;code>.conf&lt;/code>。集中的好處是好懂好改；散開的好處是每個元件可以獨立替換（把 waybar 換成 ironbar 不影響其他），代價是你要管更多檔案、更多格式。&lt;/p></description><content:encoded><![CDATA[<p>整合式桌面 shell 與手動拼裝，是「一個大程式包辦整個桌面」與「多個小程式各司其職、由 compositor 黏起來」兩種架構。<a href="/blog/linux/dotfile/06-rice-design/caelestia-overview/" data-link-title="Caelestia 總覽：預組裝的 Hyprland 桌面 Shell" data-link-desc="考慮用 Caelestia 取代手動拼裝 waybar&#43;wofi&#43;mako、或評估預組裝桌面 shell 的 trade-off 時回來讀">Caelestia 總覽</a> 從概念層談過它的取捨（設計鎖定、穩定性風險）；這篇補上在同一台機器上實際跑過兩種之後量到的數據——資源足跡、失敗半徑、配色一致性——把「感覺整合比較方便」變成可以拿數字判斷的選型。</p>
<p>這裡的數據來自一次 VM 實測：先手動拼裝一套 waybar + wofi + mako + hyprlock，再換成 Caelestia，量兩者的安裝大小、記憶體、config 結構與失敗行為。</p>
<h2 id="資源足跡差約一個數量級">資源足跡：差約一個數量級</h2>
<p>整合式 shell 把整個桌面畫在一個程式裡，這個程式通常是重量級的 UI runtime。Caelestia 建在 Quickshell（Qt6/QML）上，實測安裝足跡如下：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>整合式（Caelestia）</th>
          <th>手動拼裝（waybar+wofi+mako+hyprlock）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>安裝大小</td>
          <td>約 230 MB（Quickshell 佔 213 MB）</td>
          <td>約 4.5 MB（waybar 3 MB，其餘 KB 級）</td>
      </tr>
      <tr>
          <td>執行記憶體 RSS</td>
          <td>單一 <code>qs</code> 程式約 400 MB</td>
          <td>waybar 約 53 MB + 通知/啟動器（小）</td>
      </tr>
  </tbody>
</table>
<p>差距的來源是 Quickshell 這個 UI 框架——那 213 MB 是 <code>quickshell</code> 套件本身的安裝大小（一個建在 Qt6 上的 QML shell runtime），不是 Caelestia 的功能程式碼；而且 Qt6 的函式庫（<code>qt6-declarative</code>、<code>qt6-base</code> 等）還是它之上的額外相依，沒算進這 213 MB。手動拼裝的 waybar、wofi、mako 都是輕量的 wlroots/GTK 程式，加起來還不到 5 MB。</p>
<p>這一軸在資源受限的機器上才會咬人：舊筆電、記憶體小的 VPS、或你本來就想把桌面壓到最輕。在一台記憶體充裕的桌機上，400 MB 對 60 MB 的差別多半感覺不到；在一台 2 GB RAM 的機器上，這就是「桌面吃掉五分之一記憶體」跟「幾乎不佔」的差別。</p>
<h2 id="失敗半徑單點-vs-各自獨立">失敗半徑：單點 vs 各自獨立</h2>
<p>整合式 shell 把狀態列、通知、鎖屏、啟動器畫在<strong>同一個程式</strong>裡，所以這個程式崩潰時，這些東西會<strong>一起消失</strong>。手動拼裝的每個元件是獨立行程，一個崩掉不影響其他——mako（通知）崩了，waybar（狀態列）還在。</p>
<p>這不只是理論。這次 VM 實測就撞到一個具體案例：Caelestia 的鎖屏是由 Quickshell 主程式畫的，當這個持鎖的程式被中止時，Hyprland 依 <code>ext-session-lock</code> 協議保持鎖定並顯示「lockscreen app died」的死局——狀態列、通知、鎖屏因為同源，一個環節出事就連帶整個桌面 UI。手動拼裝的 hyprlock 是獨立的鎖屏程式，它崩潰同樣會觸發那個死局，但你的狀態列與通知不會跟著沒。</p>
<p>這一軸在穩定性敏感或無人值守的場景最關鍵。跑長時間無人盯著的任務時，「一個元件崩掉只損失那個元件」的隔離性，比「全部整合在一起」的一致性更值錢——因為沒人在旁邊立刻重啟。</p>
<h2 id="配色一致性最容易被低估的一軸">配色一致性：最容易被低估的一軸</h2>
<p>讓整個桌面配色一致，是整合式與手動拼裝差別最大、卻最常被忽略的地方。整合式 shell 因為所有元件在同一個程式裡，天生共用一套配色——Caelestia 的 dynamic scheme 從桌布抽一組 Material-3 palette，狀態列、通知、鎖屏、dashboard 全部同時套用，換張桌布整套 UI 跟著變。</p>
<p>手動拼裝要達到同樣的一致，得自己解決一個跨程式的問題：每個元件用不同的設定格式與主題引擎，它們之間不會自動共享顏色。這次手動拼裝那套時就撞到這點——waybar 的 GTK CSS 引擎讀不到 Hyprland 的 <code>$</code> 顏色變數，結果 waybar 的 <code>style.css</code> 裡得<strong>手抄一份跟 Hyprland <code>colors.conf</code> 相同的 hex 色碼</strong>。換一次配色，就要在 waybar CSS、wofi CSS、mako config、hyprland colors 好幾個地方各改一遍。</p>
<p>解這個手工問題的標準做法，是加一層<strong>模板工具</strong>（matugen、pywal、wallust 之類）：從一張桌布或一套色票，自動生成每個元件的設定檔（例如 <code>matugen/templates/rofi-colors.rasi</code> 就是給 rofi 用的顏色模板）。這等於是手動重建 Caelestia 內建的那套 dynamic theming pipeline。所以配色一致這件事的真正取捨是：Caelestia 開箱就有「換桌布全套跟著變」，手動拼裝要嘛手抄 hex、要嘛自己搭一條 templating pipeline。</p>
<h2 id="config-結構">config 結構</h2>
<p>配色一致的差別，也反映在 config 的形狀上。Caelestia 的使用者設定集中在一個 <code>shell.json</code>（實測約 24 行就涵蓋狀態列、通知、idle 行為）。手動拼裝的設定散在各元件目錄、各用各的格式：waybar 的 <code>config.jsonc</code> + <code>style.css</code>、wofi 的 <code>config</code> + <code>style.css</code>、mako 的 <code>config</code>、hypr 的數個 <code>.conf</code>。集中的好處是好懂好改；散開的好處是每個元件可以獨立替換（把 waybar 換成 ironbar 不影響其他），代價是你要管更多檔案、更多格式。</p>
<h2 id="選型判準">選型判準</h2>
<p>沒有一種在所有軸上都贏。依你的情境對照：</p>
<table>
  <thead>
      <tr>
          <th>你的情境</th>
          <th>偏向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資源受限（舊機、小 RAM VPS）</td>
          <td>手動拼裝（省下那 ~340 MB 記憶體）</td>
      </tr>
      <tr>
          <td>想要開箱即用、換桌布全套變色</td>
          <td>整合式（Caelestia 的 dynamic 原生就有）</td>
      </tr>
      <tr>
          <td>穩定性敏感、無人值守</td>
          <td>手動拼裝（元件獨立、失敗半徑小）</td>
      </tr>
      <tr>
          <td>想要結構性客製（狀態列位置、換 launcher）</td>
          <td>手動拼裝（整合式的結構是 shell 決定的）</td>
      </tr>
      <tr>
          <td>想少管檔案、快速有一套設計一致的成品</td>
          <td>整合式（一個 config、一套配色）</td>
      </tr>
      <tr>
          <td>已經在跑 templating 工具（matugen/pywal）</td>
          <td>手動拼裝（你已經有一致配色的機制、少了整合式的理由）</td>
      </tr>
  </tbody>
</table>
<h3 id="重新評估的訊號tripwire">重新評估的訊號（tripwire）</h3>
<p>選了之後，出現這些訊號時值得回頭重新評估：</p>
<ul>
<li>選了整合式，卻發現一直在跟它的設計決策對抗（想改的結構它不讓你改）——你要的其實是手動拼裝的自由度。</li>
<li>選了手動拼裝，卻發現配色維護（每次改色手抄多個檔案）吃掉大量時間——該加 templating 工具，或重新考慮整合式。</li>
<li>記憶體壓力浮現（整合式的 Qt runtime 在小機器上排擠其他程式）——往手動拼裝退。</li>
<li>整合式的一次更新靜默破壞了你的自訂設定（<a href="/blog/linux/dotfile/06-rice-design/caelestia-overview/" data-link-title="Caelestia 總覽：預組裝的 Hyprland 桌面 Shell" data-link-desc="考慮用 Caelestia 取代手動拼裝 waybar&#43;wofi&#43;mako、或評估預組裝桌面 shell 的 trade-off 時回來讀">Caelestia README 明言 config 可能無預警變動</a>）——評估這層快速移動的依賴值不值得。</li>
</ul>
<h2 id="下一步">下一步</h2>
<ul>
<li>整合式 shell 的概念定位、跟 AGS/Eww 的比較、三個 repo 的分工，見 <a href="/blog/linux/dotfile/06-rice-design/caelestia-overview/" data-link-title="Caelestia 總覽：預組裝的 Hyprland 桌面 Shell" data-link-desc="考慮用 Caelestia 取代手動拼裝 waybar&#43;wofi&#43;mako、或評估預組裝桌面 shell 的 trade-off 時回來讀">Caelestia 總覽</a>。</li>
<li>手動拼裝那幾個元件（狀態列、啟動器、通知）各自怎麼配置，見 <a href="/blog/linux/dotfile/06-rice-design/desktop-shell-components/" data-link-title="桌面 Shell 元件：狀態列、啟動器與通知" data-link-desc="Hyprland 桌面要拼哪些元件、各元件的配置檔怎麼寫時回來讀">桌面 Shell 元件</a>。</li>
<li>配色系統本身（不管哪條路線）怎麼設計，見 <a href="/blog/linux/dotfile/06-rice-design/color-system-theming/" data-link-title="配色系統、鎖屏與 GTK 主題" data-link-desc="桌面配色散亂看起來雜、或要換主題不知道該改哪些檔案時回來讀">配色系統、鎖屏與 GTK 主題</a>。</li>
</ul>
<p>這篇的足跡數字（安裝 230 MB vs 4.5 MB、RSS ~400 MB vs ~60 MB）與 lock-died 失敗案例，來自一次在 Apple Silicon UTM VM 上實際跑過兩種桌面棧的量測。</p>
]]></content:encoded></item><item><title>模組六：桌面 Rice 設計</title><link>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/</guid><description>&lt;p>Rice 在 Linux 桌面社群指的是桌面視覺客製化——把系統外觀調教成個人化的美學呈現。這個詞源自汽車改裝文化（Race Inspired Cosmetic Enhancements 的逆向縮寫），在 Linux 圈已轉為中性的圈內用語，r/unixporn 社群就是圍繞這件事運轉的。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">Hyprland 配置&lt;/a>教了 compositor（Wayland 下負責視窗排列和畫面合成的程式）本身的設定。這個模組處理的是 compositor 之上的「桌面 shell」層——狀態列、啟動器、通知、鎖屏、桌布、配色系統。做法有兩條路：自己從 Waybar + Wofi + Mako 等獨立工具一個個拼裝，或用 Caelestia 這類預組裝的桌面 shell 一次部署。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/desktop-shell-components/" data-link-title="桌面 Shell 元件：狀態列、啟動器與通知" data-link-desc="Hyprland 桌面要拼哪些元件、各元件的配置檔怎麼寫時回來讀">桌面 Shell 元件：狀態列、啟動器與通知&lt;/a>&lt;/td>
 &lt;td>拼裝式桌面的元件組成、Waybar 狀態列配置、Wofi/Rofi 啟動器、Mako 通知&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/color-system-theming/" data-link-title="配色系統、鎖屏與 GTK 主題" data-link-desc="桌面配色散亂看起來雜、或要換主題不知道該改哪些檔案時回來讀">配色系統、鎖屏與 GTK 主題&lt;/a>&lt;/td>
 &lt;td>Hyprlock 鎖屏配置、配色變數集中管理、GTK/Qt 主題統一、投資報酬判讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/caelestia-overview/" data-link-title="Caelestia 總覽：預組裝的 Hyprland 桌面 Shell" data-link-desc="考慮用 Caelestia 取代手動拼裝 waybar&amp;#43;wofi&amp;#43;mako、或評估預組裝桌面 shell 的 trade-off 時回來讀">Caelestia 總覽&lt;/a>&lt;/td>
 &lt;td>Quickshell 框架、提供的元件、跟手動拼裝和 AGS/Eww 的 trade-off、定位認知&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/caelestia-installation/" data-link-title="Caelestia 安裝" data-link-desc="要在 Arch Linux 上安裝 Caelestia 桌面 shell 時回來讀">Caelestia 安裝&lt;/a>&lt;/td>
 &lt;td>AUR 套件、CLI 安裝流程、依賴清單、登入管理器、CLI 指令、VM 測試範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/caelestia-configuration/" data-link-title="Caelestia 配置" data-link-desc="安裝完 Caelestia 後要客製化設定時回來讀">Caelestia 配置&lt;/a>&lt;/td>
 &lt;td>shell.json 結構、token 系統、hypr-user.lua、動態取色、已知問題、dotfile 結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/integrated-shell-vs-manual-assembly/" data-link-title="整合式 Shell vs 手動拼裝：實測足跡、失敗半徑與選型判準" data-link-desc="在整合式桌面 shell（如 Caelestia）與手動拼裝 waybar&amp;#43;wofi&amp;#43;mako 之間選型、需要實測的資源足跡、失敗半徑與配色一致性數據來判斷時回來讀">整合式 Shell vs 手動拼裝：實測取捨&lt;/a>&lt;/td>
 &lt;td>兩種桌面棧的實測足跡（安裝/記憶體）、失敗半徑、配色一致性機制與選型判準&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">模組三：終端機與編輯器&lt;/a>：Nerd Font 安裝是 Waybar icon 正常顯示的前提&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置&lt;/a>：compositor 的 appearance 設定（圓角、動畫）跟 rice 的視覺層互補&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構&lt;/a>：各 rice 元件的配置檔怎麼放進 stow 結構&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock&lt;/a>：鎖屏的安全模型——殺 process 不等於解鎖&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">字型的可用集合在 process 啟動時決定&lt;/a>：裝了字型但狀態列 / 通知還是豆腐時的判讀&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/fontconfig/" data-link-title="fontconfig — 字型搜尋、匹配與 fallback 服務" data-link-desc="不確定 fc-list / fc-match / fc-cache 各做什麼、或 fontconfig fallback 機制怎麼運作時回來讀">fontconfig&lt;/a>：字型搜尋、匹配與 fallback 機制&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Rice 在 Linux 桌面社群指的是桌面視覺客製化——把系統外觀調教成個人化的美學呈現。這個詞源自汽車改裝文化（Race Inspired Cosmetic Enhancements 的逆向縮寫），在 Linux 圈已轉為中性的圈內用語，r/unixporn 社群就是圍繞這件事運轉的。</p>
<p><a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">Hyprland 配置</a>教了 compositor（Wayland 下負責視窗排列和畫面合成的程式）本身的設定。這個模組處理的是 compositor 之上的「桌面 shell」層——狀態列、啟動器、通知、鎖屏、桌布、配色系統。做法有兩條路：自己從 Waybar + Wofi + Mako 等獨立工具一個個拼裝，或用 Caelestia 這類預組裝的桌面 shell 一次部署。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/06-rice-design/desktop-shell-components/" data-link-title="桌面 Shell 元件：狀態列、啟動器與通知" data-link-desc="Hyprland 桌面要拼哪些元件、各元件的配置檔怎麼寫時回來讀">桌面 Shell 元件：狀態列、啟動器與通知</a></td>
          <td>拼裝式桌面的元件組成、Waybar 狀態列配置、Wofi/Rofi 啟動器、Mako 通知</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/06-rice-design/color-system-theming/" data-link-title="配色系統、鎖屏與 GTK 主題" data-link-desc="桌面配色散亂看起來雜、或要換主題不知道該改哪些檔案時回來讀">配色系統、鎖屏與 GTK 主題</a></td>
          <td>Hyprlock 鎖屏配置、配色變數集中管理、GTK/Qt 主題統一、投資報酬判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/06-rice-design/caelestia-overview/" data-link-title="Caelestia 總覽：預組裝的 Hyprland 桌面 Shell" data-link-desc="考慮用 Caelestia 取代手動拼裝 waybar&#43;wofi&#43;mako、或評估預組裝桌面 shell 的 trade-off 時回來讀">Caelestia 總覽</a></td>
          <td>Quickshell 框架、提供的元件、跟手動拼裝和 AGS/Eww 的 trade-off、定位認知</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/06-rice-design/caelestia-installation/" data-link-title="Caelestia 安裝" data-link-desc="要在 Arch Linux 上安裝 Caelestia 桌面 shell 時回來讀">Caelestia 安裝</a></td>
          <td>AUR 套件、CLI 安裝流程、依賴清單、登入管理器、CLI 指令、VM 測試範圍</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/06-rice-design/caelestia-configuration/" data-link-title="Caelestia 配置" data-link-desc="安裝完 Caelestia 後要客製化設定時回來讀">Caelestia 配置</a></td>
          <td>shell.json 結構、token 系統、hypr-user.lua、動態取色、已知問題、dotfile 結構</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/06-rice-design/integrated-shell-vs-manual-assembly/" data-link-title="整合式 Shell vs 手動拼裝：實測足跡、失敗半徑與選型判準" data-link-desc="在整合式桌面 shell（如 Caelestia）與手動拼裝 waybar&#43;wofi&#43;mako 之間選型、需要實測的資源足跡、失敗半徑與配色一致性數據來判斷時回來讀">整合式 Shell vs 手動拼裝：實測取捨</a></td>
          <td>兩種桌面棧的實測足跡（安裝/記憶體）、失敗半徑、配色一致性機制與選型判準</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">模組三：終端機與編輯器</a>：Nerd Font 安裝是 Waybar icon 正常顯示的前提</li>
<li>→ <a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置</a>：compositor 的 appearance 設定（圓角、動畫）跟 rice 的視覺層互補</li>
<li>→ <a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構</a>：各 rice 元件的配置檔怎麼放進 stow 結構</li>
<li>→ <a href="/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock</a>：鎖屏的安全模型——殺 process 不等於解鎖</li>
<li>→ <a href="/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">字型的可用集合在 process 啟動時決定</a>：裝了字型但狀態列 / 通知還是豆腐時的判讀</li>
<li>→ <a href="/blog/linux/dotfile/knowledge-cards/fontconfig/" data-link-title="fontconfig — 字型搜尋、匹配與 fallback 服務" data-link-desc="不確定 fc-list / fc-match / fc-cache 各做什麼、或 fontconfig fallback 機制怎麼運作時回來讀">fontconfig</a>：字型搜尋、匹配與 fallback 機制</li>
</ul>
]]></content:encoded></item><item><title>讓機器跑無人值守的長任務</title><link>https://tarrragon.github.io/blog/linux/install/unattended-remote-work/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/unattended-remote-work/</guid><description>&lt;p>一台機器能被連入、能跑 bootstrap（把它從空機器設定成可用環境的安裝流程）之後，下一個層次是讓它在你不盯著的時候自己跑完一個長任務——一次耗時的編譯、一個批次作業、一個無人值守的 agent。能不能放著走人，取決於有沒有把三件會中斷無人值守執行的事先解決掉：互動提示、斷線即死、結果出不去。這三件是「讓任務能在無人時順利啟動並交付」的障礙；任務跑起來之後的資源耗盡、OOM、額度或憑證到期是另一條軸（執行期的持久性），最後一段會接到那裡。這篇逐一拆解這三個障礙與對應的解法，並說明它們共同的代價判讀——這些便利大多拿安全性換自主性，該不該開要看這台機器的爆炸半徑。&lt;/p>
&lt;p>底下用一個具體情境當例子：在一台用完即丟的測試 VM 上，讓 Claude Code 這類 agent 自己跑完一段工作、把成果推回 GitHub 給你早上 review。同一組障礙換成 overnight 編譯或 cron 批次也成立。&lt;/p>
&lt;h2 id="障礙一互動提示擋住自動執行">障礙一：互動提示擋住自動執行&lt;/h2>
&lt;p>無人值守的程序沒有人在鍵盤前，所以任何「停下來等你輸入」的提示都會讓它卡死，其中最常見的是 sudo 密碼。一個要裝套件、改系統設定的任務，跑到 &lt;code>sudo&lt;/code> 那行就停在密碼提示、永遠等不到輸入，整個任務卡在那裡直到你回來。&lt;/p>
&lt;p>解法是讓這台機器的 sudo 免密碼（NOPASSWD），但這是一個明確的安全取捨、不是預設該開的東西。設定方式是給 sudoers 加一條 NOPASSWD 規則：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>whoami&lt;span class="k">)&lt;/span>&lt;span class="s2"> ALL=(ALL:ALL) NOPASSWD: ALL&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> sudo tee /etc/sudoers.d/20-nopasswd &lt;span class="c1"># $(whoami) 會填入你的登入帳號&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">sudo chmod &lt;span class="m">440&lt;/span> /etc/sudoers.d/20-nopasswd&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>開了 NOPASSWD，等於放棄「sudo 密碼」這道在你被入侵或程序失控時的最後防線。判讀軸是這台機器的爆炸半徑——它持有哪些憑證、能觸及哪些系統，也就是最壞情況下會波及多大範圍。一台範圍受限、沒有任何真實憑證、出事就重建的測試 VM，放棄這道防線換取自動執行是划算的；一台共享主機、生產伺服器、或裝著真實憑證與資料的機器，不該為了方便開 NOPASSWD。關鍵是「可不可丟」不等於「爆炸半徑小」：一台用完即丟的 VM，一旦塞進能碰到生產系統或你帳號的憑證，爆炸半徑就不小了——看的不是機器本身，是它最壞情況能波及什麼。&lt;/p>
&lt;h2 id="障礙二ssh-斷線就把任務一起殺掉">障礙二：SSH 斷線就把任務一起殺掉&lt;/h2>
&lt;p>直接在 SSH session 裡跑的程序，會隨著 SSH 連線中斷而一起死掉——你闔上筆電、網路斷一下、或單純關掉終端機，正在跑的任務就沒了。對一個要跑好幾小時的無人值守任務，這條等於「你不能離開」，跟無人值守的目的矛盾。&lt;/p>
&lt;p>把任務搬進終端機多工器（zellij、tmux 這類，配置見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/multiplexer-tmux-zellij/" data-link-title="Multiplexer：tmux vs zellij" data-link-desc="在終端機裡切分 pane、管理多個 session、SSH 斷線後保持工作時回來讀 — tmux 和 zellij 的配置與選型">模組三&lt;/a>）就解決了。多工器的 session 活在那台機器上、獨立於你的 SSH 連線：你在多工器裡啟動任務、然後 detach（卸離），任務繼續在機器上跑，你這頭關掉 SSH 都不影響；之後再連回來 attach（接回）就能看它跑到哪。典型流程是連入機器、起多工器、在裡面啟動任務、detach、走人：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ssh user@host
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">zellij &lt;span class="c1"># 起多工器（tmux 同理）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">./run-my-long-task.sh &lt;span class="c1"># 在裡面啟動你的長任務（換成你的實際指令）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 然後 detach：zellij 預設 Ctrl+o 再按 d（tmux 是 Ctrl+b 再按 d）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 此時關掉 SSH 不影響任務，它在 host 上繼續跑&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 之後連回來看進度：再 ssh 進去，然後&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">zellij attach &lt;span class="c1"># tmux 是 tmux attach&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>判讀訊號是「這個任務跑完前，我會不會斷線」。只要會（過夜、跨小時、不穩的網路），就把它放進多工器；幾秒鐘就結束的指令不需要這層。&lt;/p>
&lt;h2 id="障礙三成果推不出去等於沒做">障礙三：成果推不出去，等於沒做&lt;/h2>
&lt;p>無人值守任務的產出留在那台機器上，你看不到——除非它能把結果送出去。最常見的形式是把改動 commit 後 push 回 git 遠端，你在別處 pull 來看。但 push 需要認證，而一台剛連入的機器通常還沒設好推送的憑證，於是任務做完了、commit 也建了，卻卡在 push 那步推不出去，你隔天連回來才發現結果根本沒送出去。&lt;/p>
&lt;p>先在這台機器上設好推送認證，這個障礙就消失。用 GitHub CLI 是直接的一條路，它認證後會一併把 git 的 credential helper（git 用來自動帶出認證、不必每次手打的機制）設好，後續 &lt;code>git push&lt;/code> 就能用——但 &lt;code>gh auth login&lt;/code> 本身是互動式的、要你在場完成一次，屬於離開前的人工前置：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">gh auth login &lt;span class="c1"># 選 HTTPS、完成認證、同意設定 git 認證&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>判讀軸是「這個任務的價值要怎麼回到你手上」。如果你打算從遠端（GitHub）看結果，那 push 認證就是必要前置——沒設好，整段工作就被困在機器裡。連帶的紀律是讓任務頻繁 commit 當檢查點、做完務必確認 push 成功：對一個你不在場的任務，「沒推出去」跟「沒做」對你是一樣的。機器若沒裝 &lt;code>gh&lt;/code>，也可以用 PAT 走 HTTPS，見 &lt;a href="../ssh-keyless-bootstrap/">外部連入篇&lt;/a> 的私有 repo 段。&lt;/p>
&lt;p>把 push 憑證設進這台機器，等於提高了它的爆炸半徑——它現在能動你的 repo 了。這會回頭讓障礙一的 NOPASSWD、以及下面 agent 段的權限放行更該謹慎：最壞情況從「弄壞這台機器」升級成「污染你的 repo」，而後者不是重建一台 VM 就能還原的。所以設了 push 憑證之後，要連帶重估前面那些「因為機器可丟所以放心」的取捨。&lt;/p>
&lt;h2 id="額外一層宿主暫停會連帶停掉任務">額外一層：宿主暫停會連帶停掉任務&lt;/h2>
&lt;p>當這台機器是跑在某個宿主上的虛擬機，還有一個容易忽略的中斷源：宿主睡著，VM 跟著暫停，裡面的無人值守任務也一起停。你以為它整夜在跑，回來發現它從你離開那刻就凍在那裡。判讀方式是想一下「這台機器的存在依賴什麼」——VM 依賴宿主醒著、雲端主機依賴帳單沒欠費。對 VM 的情況，離開前確保宿主不會自動睡眠（macOS 用 &lt;code>caffeinate&lt;/code>、Linux 宿主用 &lt;code>systemd-inhibit&lt;/code> 或停用 suspend、Windows 調電源設定，或直接關掉節能的自動睡眠）。&lt;/p>
&lt;h2 id="如果無人值守的工作者是-ai-agent">如果無人值守的工作者是 AI agent&lt;/h2>
&lt;p>當你放著跑的是一個 AI agent，除了上面三個障礙，還多一個它自己的互動提示要處理：agent 預設會在每個有風險的動作前停下來問你確認，而無人值守時沒人回答，它就卡住。對應的是 agent 的「跳過確認」模式（如 Claude Code 的權限放行旗標），讓它不停下來問。這跟 NOPASSWD 是同一類取捨、判讀軸也一樣：放給一個無人盯著的 agent 在一台範圍受限、用完即丟的機器上自主動作是可接受的；在一台有真實資料或共享的機器上不該這樣。降低風險的兩個做法是把 agent 的工作範圍用清楚的指引限定（只動哪些目錄、別碰系統其他地方），以及讓它在分支上做、產出交給你 review，而不是直接動到你會依賴的東西。&lt;/p></description><content:encoded><![CDATA[<p>一台機器能被連入、能跑 bootstrap（把它從空機器設定成可用環境的安裝流程）之後，下一個層次是讓它在你不盯著的時候自己跑完一個長任務——一次耗時的編譯、一個批次作業、一個無人值守的 agent。能不能放著走人，取決於有沒有把三件會中斷無人值守執行的事先解決掉：互動提示、斷線即死、結果出不去。這三件是「讓任務能在無人時順利啟動並交付」的障礙；任務跑起來之後的資源耗盡、OOM、額度或憑證到期是另一條軸（執行期的持久性），最後一段會接到那裡。這篇逐一拆解這三個障礙與對應的解法，並說明它們共同的代價判讀——這些便利大多拿安全性換自主性，該不該開要看這台機器的爆炸半徑。</p>
<p>底下用一個具體情境當例子：在一台用完即丟的測試 VM 上，讓 Claude Code 這類 agent 自己跑完一段工作、把成果推回 GitHub 給你早上 review。同一組障礙換成 overnight 編譯或 cron 批次也成立。</p>
<h2 id="障礙一互動提示擋住自動執行">障礙一：互動提示擋住自動執行</h2>
<p>無人值守的程序沒有人在鍵盤前，所以任何「停下來等你輸入」的提示都會讓它卡死，其中最常見的是 sudo 密碼。一個要裝套件、改系統設定的任務，跑到 <code>sudo</code> 那行就停在密碼提示、永遠等不到輸入，整個任務卡在那裡直到你回來。</p>
<p>解法是讓這台機器的 sudo 免密碼（NOPASSWD），但這是一個明確的安全取捨、不是預設該開的東西。設定方式是給 sudoers 加一條 NOPASSWD 規則：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="k">$(</span>whoami<span class="k">)</span><span class="s2"> ALL=(ALL:ALL) NOPASSWD: ALL&#34;</span> <span class="p">|</span> sudo tee /etc/sudoers.d/20-nopasswd  <span class="c1"># $(whoami) 會填入你的登入帳號</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo chmod <span class="m">440</span> /etc/sudoers.d/20-nopasswd</span></span></code></pre></div><p>開了 NOPASSWD，等於放棄「sudo 密碼」這道在你被入侵或程序失控時的最後防線。判讀軸是這台機器的爆炸半徑——它持有哪些憑證、能觸及哪些系統，也就是最壞情況下會波及多大範圍。一台範圍受限、沒有任何真實憑證、出事就重建的測試 VM，放棄這道防線換取自動執行是划算的；一台共享主機、生產伺服器、或裝著真實憑證與資料的機器，不該為了方便開 NOPASSWD。關鍵是「可不可丟」不等於「爆炸半徑小」：一台用完即丟的 VM，一旦塞進能碰到生產系統或你帳號的憑證，爆炸半徑就不小了——看的不是機器本身，是它最壞情況能波及什麼。</p>
<h2 id="障礙二ssh-斷線就把任務一起殺掉">障礙二：SSH 斷線就把任務一起殺掉</h2>
<p>直接在 SSH session 裡跑的程序，會隨著 SSH 連線中斷而一起死掉——你闔上筆電、網路斷一下、或單純關掉終端機，正在跑的任務就沒了。對一個要跑好幾小時的無人值守任務，這條等於「你不能離開」，跟無人值守的目的矛盾。</p>
<p>把任務搬進終端機多工器（zellij、tmux 這類，配置見 <a href="/blog/linux/dotfile/03-terminal-ecosystem/multiplexer-tmux-zellij/" data-link-title="Multiplexer：tmux vs zellij" data-link-desc="在終端機裡切分 pane、管理多個 session、SSH 斷線後保持工作時回來讀 — tmux 和 zellij 的配置與選型">模組三</a>）就解決了。多工器的 session 活在那台機器上、獨立於你的 SSH 連線：你在多工器裡啟動任務、然後 detach（卸離），任務繼續在機器上跑，你這頭關掉 SSH 都不影響；之後再連回來 attach（接回）就能看它跑到哪。典型流程是連入機器、起多工器、在裡面啟動任務、detach、走人：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh user@host
</span></span><span class="line"><span class="ln">2</span><span class="cl">zellij                       <span class="c1"># 起多工器（tmux 同理）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">./run-my-long-task.sh        <span class="c1"># 在裡面啟動你的長任務（換成你的實際指令）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 然後 detach：zellij 預設 Ctrl+o 再按 d（tmux 是 Ctrl+b 再按 d）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 此時關掉 SSH 不影響任務，它在 host 上繼續跑</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 之後連回來看進度：再 ssh 進去，然後</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">zellij attach                <span class="c1"># tmux 是 tmux attach</span></span></span></code></pre></div><p>判讀訊號是「這個任務跑完前，我會不會斷線」。只要會（過夜、跨小時、不穩的網路），就把它放進多工器；幾秒鐘就結束的指令不需要這層。</p>
<h2 id="障礙三成果推不出去等於沒做">障礙三：成果推不出去，等於沒做</h2>
<p>無人值守任務的產出留在那台機器上，你看不到——除非它能把結果送出去。最常見的形式是把改動 commit 後 push 回 git 遠端，你在別處 pull 來看。但 push 需要認證，而一台剛連入的機器通常還沒設好推送的憑證，於是任務做完了、commit 也建了，卻卡在 push 那步推不出去，你隔天連回來才發現結果根本沒送出去。</p>
<p>先在這台機器上設好推送認證，這個障礙就消失。用 GitHub CLI 是直接的一條路，它認證後會一併把 git 的 credential helper（git 用來自動帶出認證、不必每次手打的機制）設好，後續 <code>git push</code> 就能用——但 <code>gh auth login</code> 本身是互動式的、要你在場完成一次，屬於離開前的人工前置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">gh auth login    <span class="c1"># 選 HTTPS、完成認證、同意設定 git 認證</span></span></span></code></pre></div><p>判讀軸是「這個任務的價值要怎麼回到你手上」。如果你打算從遠端（GitHub）看結果，那 push 認證就是必要前置——沒設好，整段工作就被困在機器裡。連帶的紀律是讓任務頻繁 commit 當檢查點、做完務必確認 push 成功：對一個你不在場的任務，「沒推出去」跟「沒做」對你是一樣的。機器若沒裝 <code>gh</code>，也可以用 PAT 走 HTTPS，見 <a href="../ssh-keyless-bootstrap/">外部連入篇</a> 的私有 repo 段。</p>
<p>把 push 憑證設進這台機器，等於提高了它的爆炸半徑——它現在能動你的 repo 了。這會回頭讓障礙一的 NOPASSWD、以及下面 agent 段的權限放行更該謹慎：最壞情況從「弄壞這台機器」升級成「污染你的 repo」，而後者不是重建一台 VM 就能還原的。所以設了 push 憑證之後，要連帶重估前面那些「因為機器可丟所以放心」的取捨。</p>
<h2 id="額外一層宿主暫停會連帶停掉任務">額外一層：宿主暫停會連帶停掉任務</h2>
<p>當這台機器是跑在某個宿主上的虛擬機，還有一個容易忽略的中斷源：宿主睡著，VM 跟著暫停，裡面的無人值守任務也一起停。你以為它整夜在跑，回來發現它從你離開那刻就凍在那裡。判讀方式是想一下「這台機器的存在依賴什麼」——VM 依賴宿主醒著、雲端主機依賴帳單沒欠費。對 VM 的情況，離開前確保宿主不會自動睡眠（macOS 用 <code>caffeinate</code>、Linux 宿主用 <code>systemd-inhibit</code> 或停用 suspend、Windows 調電源設定，或直接關掉節能的自動睡眠）。</p>
<h2 id="如果無人值守的工作者是-ai-agent">如果無人值守的工作者是 AI agent</h2>
<p>當你放著跑的是一個 AI agent，除了上面三個障礙，還多一個它自己的互動提示要處理：agent 預設會在每個有風險的動作前停下來問你確認，而無人值守時沒人回答，它就卡住。對應的是 agent 的「跳過確認」模式（如 Claude Code 的權限放行旗標），讓它不停下來問。這跟 NOPASSWD 是同一類取捨、判讀軸也一樣：放給一個無人盯著的 agent 在一台範圍受限、用完即丟的機器上自主動作是可接受的；在一台有真實資料或共享的機器上不該這樣。降低風險的兩個做法是把 agent 的工作範圍用清楚的指引限定（只動哪些目錄、別碰系統其他地方），以及讓它在分支上做、產出交給你 review，而不是直接動到你會依賴的東西。</p>
<h2 id="下一步">下一步</h2>
<p>把這三到四個障礙解決掉，一台機器就能在你離開後自己跑完工作、把成果送回你手上。這篇是 <a href="../ssh-keyless-bootstrap/">外部連入</a>（怎麼連進去）的延伸——從「我連進去手動操作」進到「我設好讓它自己跑」。而要讓那個無人值守的任務在失敗時還留得下可診斷的痕跡，回到 <a href="../observable-bootstrap/">可除錯的 bootstrap</a> 的原則：無人盯著的任務尤其需要把可觀測性內建進去，因為你不在場、只能事後從 log 重建發生了什麼。</p>
]]></content:encoded></item><item><title>模組七：桌面環境維護與故障排除</title><link>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/</guid><description>&lt;p>模組零到六教的是怎麼建立桌面環境，這個模組教的是壞了怎麼修。&lt;/p>
&lt;p>Linux 桌面環境跟 Windows 在故障模型上有根本的結構差異。Windows 的藍屏（BSOD）是核心層崩潰，整台機器停擺；Linux 桌面環境的大部分故障只影響 userspace，系統核心不受波及。理解這個隔離邊界，是判斷「當下該做什麼」的前提。&lt;/p>
&lt;p>這個模組的閱讀方式跟其他模組不同。其他模組是線性學習——從頭讀到尾建立知識。這個模組是 reference——出問題時根據症狀查對應的恢復操作。第一篇建立概念模型，第二篇按場景查操作，第三篇教怎麼看日誌找根因。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/fault-isolation-model/" data-link-title="Linux 桌面的故障隔離模型" data-link-desc="從 Windows 轉過來想知道 Linux 桌面掛了會不會整台崩潰時讀 — kernel vs userspace 隔離、compositor 是 userspace process、TTY 救生通道與其限制">Linux 桌面的故障隔離模型&lt;/a>&lt;/td>
 &lt;td>kernel vs userspace 隔離、compositor 掛了不等於系統崩潰、TTY&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作&lt;/a>&lt;/td>
 &lt;td>compositor crash、工具掛了、GPU hang、OOM、config 錯誤、suspend/resume 異常的處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">日誌判讀與診斷工具&lt;/a>&lt;/td>
 &lt;td>journalctl、dmesg、hyprctl、systemctl 的使用與常見 pattern&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>-&amp;gt; &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置&lt;/a>：Hyprland 的配置結構和 hyprctl 指令&lt;/li>
&lt;li>-&amp;gt; &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計&lt;/a>：waybar / wofi / mako 等工具的配置位置&lt;/li>
&lt;li>-&amp;gt; &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建&lt;/a>：環境損壞到無法修復時的重建策略&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>模組零到六教的是怎麼建立桌面環境，這個模組教的是壞了怎麼修。</p>
<p>Linux 桌面環境跟 Windows 在故障模型上有根本的結構差異。Windows 的藍屏（BSOD）是核心層崩潰，整台機器停擺；Linux 桌面環境的大部分故障只影響 userspace，系統核心不受波及。理解這個隔離邊界，是判斷「當下該做什麼」的前提。</p>
<p>這個模組的閱讀方式跟其他模組不同。其他模組是線性學習——從頭讀到尾建立知識。這個模組是 reference——出問題時根據症狀查對應的恢復操作。第一篇建立概念模型，第二篇按場景查操作，第三篇教怎麼看日誌找根因。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/07-desktop-maintenance/fault-isolation-model/" data-link-title="Linux 桌面的故障隔離模型" data-link-desc="從 Windows 轉過來想知道 Linux 桌面掛了會不會整台崩潰時讀 — kernel vs userspace 隔離、compositor 是 userspace process、TTY 救生通道與其限制">Linux 桌面的故障隔離模型</a></td>
          <td>kernel vs userspace 隔離、compositor 掛了不等於系統崩潰、TTY</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作</a></td>
          <td>compositor crash、工具掛了、GPU hang、OOM、config 錯誤、suspend/resume 異常的處理</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">日誌判讀與診斷工具</a></td>
          <td>journalctl、dmesg、hyprctl、systemctl 的使用與常見 pattern</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>-&gt; <a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置</a>：Hyprland 的配置結構和 hyprctl 指令</li>
<li>-&gt; <a href="/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計</a>：waybar / wofi / mako 等工具的配置位置</li>
<li>-&gt; <a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建</a>：環境損壞到無法修復時的重建策略</li>
</ul>
]]></content:encoded></item><item><title>模組八：同步、Bootstrap 與環境重建</title><link>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/</guid><description>&lt;p>環境重建是 dotfile 管理的最終目的：拿到一台空白機器，能在可預期的時間內還原成你熟悉的工作環境。這件事有兩條根本不同的路線——「拍照」（VM 快照）和「重建指令」（dotfile + install script），選哪條決定了你之後所有的管理策略。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/snapshot-vs-rebuild/" data-link-title="拍照 vs 重建指令：環境重建的兩種思路" data-link-desc="猶豫該用 VM 快照還是 dotfile 重建來管理環境時回來讀">拍照 vs 重建指令：環境重建的兩種思路&lt;/a>&lt;/td>
 &lt;td>VM 快照和 dotfile 重建的本質差異、各自的守備範圍與場景判讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">Bootstrap Script 與套件清單管理&lt;/a>&lt;/td>
 &lt;td>install script 的冪等性設計、OS 分流、Brewfile / packages.txt 套件清單管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/sync-strategy-secret/" data-link-title="跨機器同步、Secret 管理與環境重建流程" data-link-desc="多台機器的 dotfile 怎麼同步、哪些東西不該進 repo 時回來讀">跨機器同步、Secret 管理與環境重建流程&lt;/a>&lt;/td>
 &lt;td>Git push/pull vs 自動同步、secret 三層級管理、從空白機器到完整工作環境的 end-to-end（範例用 Arch + Hyprland，macOS 同樣適用）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構&lt;/a>：stow / chezmoi 選型與跨平台三層模型&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置&lt;/a>：環境重建流程裡 monitor 設定的硬體相關調整&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/" data-link-title="模組九：從個人到團隊" data-link-desc="個人 dotfile 管理的思想要延伸到團隊開發環境標準化時回來讀 — devcontainer、nix、商業環境配置管理">模組九：從個人到團隊&lt;/a>：個人 bootstrap 的思想怎麼延伸到團隊 onboarding&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>環境重建是 dotfile 管理的最終目的：拿到一台空白機器，能在可預期的時間內還原成你熟悉的工作環境。這件事有兩條根本不同的路線——「拍照」（VM 快照）和「重建指令」（dotfile + install script），選哪條決定了你之後所有的管理策略。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/08-sync-bootstrap/snapshot-vs-rebuild/" data-link-title="拍照 vs 重建指令：環境重建的兩種思路" data-link-desc="猶豫該用 VM 快照還是 dotfile 重建來管理環境時回來讀">拍照 vs 重建指令：環境重建的兩種思路</a></td>
          <td>VM 快照和 dotfile 重建的本質差異、各自的守備範圍與場景判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">Bootstrap Script 與套件清單管理</a></td>
          <td>install script 的冪等性設計、OS 分流、Brewfile / packages.txt 套件清單管理</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/08-sync-bootstrap/sync-strategy-secret/" data-link-title="跨機器同步、Secret 管理與環境重建流程" data-link-desc="多台機器的 dotfile 怎麼同步、哪些東西不該進 repo 時回來讀">跨機器同步、Secret 管理與環境重建流程</a></td>
          <td>Git push/pull vs 自動同步、secret 三層級管理、從空白機器到完整工作環境的 end-to-end（範例用 Arch + Hyprland，macOS 同樣適用）</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構</a>：stow / chezmoi 選型與跨平台三層模型</li>
<li>→ <a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置</a>：環境重建流程裡 monitor 設定的硬體相關調整</li>
<li>→ <a href="/blog/linux/dotfile/09-team-environment/" data-link-title="模組九：從個人到團隊" data-link-desc="個人 dotfile 管理的思想要延伸到團隊開發環境標準化時回來讀 — devcontainer、nix、商業環境配置管理">模組九：從個人到團隊</a>：個人 bootstrap 的思想怎麼延伸到團隊 onboarding</li>
</ul>
]]></content:encoded></item><item><title>模組九：從個人到團隊</title><link>https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/</guid><description>&lt;p>個人 dotfile 管理解決的是「一個人的環境可重現性」。當同樣的需求擴展到團隊——新人 onboarding 要多久能開始寫 code、團隊成員的開發環境差異造成「在我電腦上能跑」的問題、CI 環境跟本機環境不一致——就進入了「團隊開發環境標準化」的範疇。這個模組教的是個人 dotfile 的思想怎麼往上延伸，以及在商業環境中有哪些成熟的做法。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/devcontainer-nix/" data-link-title="Devcontainer 與 Nix：容器化和宣告式的開發環境" data-link-desc="團隊開發環境要標準化、或評估 devcontainer 和 nix 跟個人 dotfile 怎麼共存時回來讀">Devcontainer 與 Nix&lt;/a>&lt;/td>
 &lt;td>容器化和宣告式的開發環境、devcontainer 跟個人 dotfile 的互動、Home Manager&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/commercial-environment/" data-link-title="商業環境的開發環境配置管理" data-link-desc="企業的開發環境標準化要走到什麼程度、什麼訊號該從個人 dotfile 往團隊層級推進">商業環境的開發環境配置管理&lt;/a>&lt;/td>
 &lt;td>四個層級的做法（README → 腳本化 → Devcontainer → MDM）、跟 Infra 的銜接、推進判讀&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">模組零：Dotfile 心智模型&lt;/a>：個人環境 as code 跟組織 IaC 的平行&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建&lt;/a>：bootstrap script 是團隊腳本化層級的基礎&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/" data-link-title="Infra 基礎設施建置指南" data-link-desc="從零循序漸進把雲端基礎設施做起來 — IaC、身分憑證、網路地基、環境分離、核心服務、可觀測性、自動化 review 與治理習慣，含怎麼在組織內推動">Infra 基礎設施建置指南&lt;/a>：Infra IaC 是組織層的環境 as code&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/" data-link-title="斷網環境的 infra：沒有網路時怎麼做" data-link-desc="實體隔離或無法連網的環境裡，IaC、套件管理、容器映像、監控、CI/CD 怎麼運作 — 原則不變、工具路徑全部要換">Infra 斷網模組&lt;/a>：離線環境的 devcontainer 限制&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>個人 dotfile 管理解決的是「一個人的環境可重現性」。當同樣的需求擴展到團隊——新人 onboarding 要多久能開始寫 code、團隊成員的開發環境差異造成「在我電腦上能跑」的問題、CI 環境跟本機環境不一致——就進入了「團隊開發環境標準化」的範疇。這個模組教的是個人 dotfile 的思想怎麼往上延伸，以及在商業環境中有哪些成熟的做法。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/09-team-environment/devcontainer-nix/" data-link-title="Devcontainer 與 Nix：容器化和宣告式的開發環境" data-link-desc="團隊開發環境要標準化、或評估 devcontainer 和 nix 跟個人 dotfile 怎麼共存時回來讀">Devcontainer 與 Nix</a></td>
          <td>容器化和宣告式的開發環境、devcontainer 跟個人 dotfile 的互動、Home Manager</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/09-team-environment/commercial-environment/" data-link-title="商業環境的開發環境配置管理" data-link-desc="企業的開發環境標準化要走到什麼程度、什麼訊號該從個人 dotfile 往團隊層級推進">商業環境的開發環境配置管理</a></td>
          <td>四個層級的做法（README → 腳本化 → Devcontainer → MDM）、跟 Infra 的銜接、推進判讀</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">模組零：Dotfile 心智模型</a>：個人環境 as code 跟組織 IaC 的平行</li>
<li>→ <a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建</a>：bootstrap script 是團隊腳本化層級的基礎</li>
<li>→ <a href="/blog/infra/" data-link-title="Infra 基礎設施建置指南" data-link-desc="從零循序漸進把雲端基礎設施做起來 — IaC、身分憑證、網路地基、環境分離、核心服務、可觀測性、自動化 review 與治理習慣，含怎麼在組織內推動">Infra 基礎設施建置指南</a>：Infra IaC 是組織層的環境 as code</li>
<li>→ <a href="/blog/infra/air-gapped/" data-link-title="斷網環境的 infra：沒有網路時怎麼做" data-link-desc="實體隔離或無法連網的環境裡，IaC、套件管理、容器映像、監控、CI/CD 怎麼運作 — 原則不變、工具路徑全部要換">Infra 斷網模組</a>：離線環境的 devcontainer 限制</li>
</ul>
]]></content:encoded></item><item><title>Compositor（合成器）</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/compositor/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/compositor/</guid><description>&lt;p>Compositor（合成器）是 Wayland 下負責把各個應用視窗的畫面合成到螢幕、同時管理視窗位置與輸入的核心程式。它一個角色承擔了舊 X11 世界裡分給多個程式的責任——畫面合成、視窗管理、輸入處理，在 Wayland 架構裡合在同一個程式。&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">Hyprland&lt;/a> 就是一個 Wayland compositor。&lt;/p>
&lt;p>跟 X11 的分工對照能看清它的定位。X11 時代，X server 負責畫面、一個獨立的 window manager 負責視窗排列，兩者透過協定溝通；Wayland 取消這個分家，compositor 直接兼任兩者。所以在 Wayland 語境裡，「compositor」和「window manager」常指同一個東西——Hyprland 既是 compositor 也是 tiling window manager。&lt;/p>
&lt;p>它跟桌面環境（desktop environment）是不同層次。桌面環境（GNOME、KDE）是一整套元件（面板、設定、通知、檔案管理），其中內含一個 compositor；而 Hyprland 這類「只有 compositor」的方案不含那圈桌面服務，面板、啟動器、鎖屏都要另外自己接。這條「整合度 vs 自己組裝」的軸線，是&lt;a href="https://tarrragon.github.io/blog/linux/tools/gui/desktop-environment-selection/" data-link-title="桌面環境選型：整合度與組裝自由度的取捨" data-link-desc="從 Windows/macOS 轉來或要挑第一個 Linux 桌面、在 GNOME / KDE / Hyprland / XFCE / Cinnamon 之間拿不定、想知道各自的定位與代價（資源、客製自由、穩定性、Wayland 支援）時回來讀">桌面環境選型&lt;/a>的主題。&lt;/p>
&lt;p>compositor 在故障排除中是一個關鍵的責任邊界，因為多個系統狀態由它持有、而非別的層：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>它握著 DRM master&lt;/strong>：直接畫到顯示裝置的獨佔權由 compositor 持有，這是為什麼 compositor 必須從實體圖形 VT 起、不能從 SSH pty 起（SSH 連線裡沒有那個 seat 與 DRM 資源），也是為什麼它跟同樣要畫到 DRM 的 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/tty/" data-link-title="TTY" data-link-desc="恢復操作提到切 TTY 但不知道 TTY 是什麼時讀 — Linux 核心直接提供的純文字終端機介面">kmscon 之類 userspace console&lt;/a> 會相衝。&lt;/li>
&lt;li>&lt;strong>它持有 session lock 狀態&lt;/strong>：Wayland 的 &lt;code>ext-session-lock&lt;/code> 鎖是 compositor 層的狀態、跟 logind 獨立，殺掉鎖屏程式 compositor 仍保持鎖定——詳見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock&lt;/a>。&lt;/li>
&lt;li>&lt;strong>它掛了、桌面才真的黑&lt;/strong>：反過來說，只要 kernel 還活著、compositor 以外的東西壞了，&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/tty/" data-link-title="TTY" data-link-desc="恢復操作提到切 TTY 但不知道 TTY 是什麼時讀 — Linux 核心直接提供的純文字終端機介面">TTY&lt;/a> 仍能登入操作。判斷「畫面黑」是 compositor 掛了、還是沒 getty、還是顯示輸出沒接，是桌面故障排除的第一個分岔。&lt;/li>
&lt;/ul>
&lt;p>相關概念：&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/tty/" data-link-title="TTY" data-link-desc="恢復操作提到切 TTY 但不知道 TTY 是什麼時讀 — Linux 核心直接提供的純文字終端機介面">TTY&lt;/a>（不依賴 compositor 的救生通道）、&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock&lt;/a>（compositor 持有的鎖定狀態）、&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/rice/" data-link-title="Rice（桌面視覺客製化）" data-link-desc="Linux 桌面文章裡看到 rice / ricing / ricer 不確定意思時回來讀">Rice&lt;/a>（在 compositor 上做視覺客製）。&lt;/p></description><content:encoded><![CDATA[<p>Compositor（合成器）是 Wayland 下負責把各個應用視窗的畫面合成到螢幕、同時管理視窗位置與輸入的核心程式。它一個角色承擔了舊 X11 世界裡分給多個程式的責任——畫面合成、視窗管理、輸入處理，在 Wayland 架構裡合在同一個程式。<a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">Hyprland</a> 就是一個 Wayland compositor。</p>
<p>跟 X11 的分工對照能看清它的定位。X11 時代，X server 負責畫面、一個獨立的 window manager 負責視窗排列，兩者透過協定溝通；Wayland 取消這個分家，compositor 直接兼任兩者。所以在 Wayland 語境裡，「compositor」和「window manager」常指同一個東西——Hyprland 既是 compositor 也是 tiling window manager。</p>
<p>它跟桌面環境（desktop environment）是不同層次。桌面環境（GNOME、KDE）是一整套元件（面板、設定、通知、檔案管理），其中內含一個 compositor；而 Hyprland 這類「只有 compositor」的方案不含那圈桌面服務，面板、啟動器、鎖屏都要另外自己接。這條「整合度 vs 自己組裝」的軸線，是<a href="/blog/linux/tools/gui/desktop-environment-selection/" data-link-title="桌面環境選型：整合度與組裝自由度的取捨" data-link-desc="從 Windows/macOS 轉來或要挑第一個 Linux 桌面、在 GNOME / KDE / Hyprland / XFCE / Cinnamon 之間拿不定、想知道各自的定位與代價（資源、客製自由、穩定性、Wayland 支援）時回來讀">桌面環境選型</a>的主題。</p>
<p>compositor 在故障排除中是一個關鍵的責任邊界，因為多個系統狀態由它持有、而非別的層：</p>
<ul>
<li><strong>它握著 DRM master</strong>：直接畫到顯示裝置的獨佔權由 compositor 持有，這是為什麼 compositor 必須從實體圖形 VT 起、不能從 SSH pty 起（SSH 連線裡沒有那個 seat 與 DRM 資源），也是為什麼它跟同樣要畫到 DRM 的 <a href="/blog/linux/dotfile/knowledge-cards/tty/" data-link-title="TTY" data-link-desc="恢復操作提到切 TTY 但不知道 TTY 是什麼時讀 — Linux 核心直接提供的純文字終端機介面">kmscon 之類 userspace console</a> 會相衝。</li>
<li><strong>它持有 session lock 狀態</strong>：Wayland 的 <code>ext-session-lock</code> 鎖是 compositor 層的狀態、跟 logind 獨立，殺掉鎖屏程式 compositor 仍保持鎖定——詳見 <a href="/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock</a>。</li>
<li><strong>它掛了、桌面才真的黑</strong>：反過來說，只要 kernel 還活著、compositor 以外的東西壞了，<a href="/blog/linux/dotfile/knowledge-cards/tty/" data-link-title="TTY" data-link-desc="恢復操作提到切 TTY 但不知道 TTY 是什麼時讀 — Linux 核心直接提供的純文字終端機介面">TTY</a> 仍能登入操作。判斷「畫面黑」是 compositor 掛了、還是沒 getty、還是顯示輸出沒接，是桌面故障排除的第一個分岔。</li>
</ul>
<p>相關概念：<a href="/blog/linux/dotfile/knowledge-cards/tty/" data-link-title="TTY" data-link-desc="恢復操作提到切 TTY 但不知道 TTY 是什麼時讀 — Linux 核心直接提供的純文字終端機介面">TTY</a>（不依賴 compositor 的救生通道）、<a href="/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock</a>（compositor 持有的鎖定狀態）、<a href="/blog/linux/dotfile/knowledge-cards/rice/" data-link-title="Rice（桌面視覺客製化）" data-link-desc="Linux 桌面文章裡看到 rice / ricing / ricer 不確定意思時回來讀">Rice</a>（在 compositor 上做視覺客製）。</p>
]]></content:encoded></item><item><title>TTY</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/tty/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/tty/</guid><description>&lt;p>TTY（TeleTYpewriter）是 Linux 核心直接提供的純文字終端機介面，獨立於任何桌面環境或圖形介面。&lt;/p>
&lt;p>名稱來自早期電腦透過電報打字機（teletypewriter）做輸入輸出的歷史。現代 Linux 的 TTY 是 virtual console——核心在記憶體中模擬的文字終端機，不需要實體硬體。&lt;/p>
&lt;p>systemd 預設配置下有 6 個 virtual console（TTY1-TTY6）。Wayland compositor（如 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">Hyprland&lt;/a>）通常佔用 TTY1 顯示圖形桌面，TTY2-TTY6 保持為純文字介面可用。&lt;/p>
&lt;p>切換方式：&lt;code>Ctrl+Alt+F2&lt;/code>（切到 TTY2）到 &lt;code>Ctrl+Alt+F6&lt;/code>（切到 TTY6）。&lt;code>Ctrl+Alt+F1&lt;/code> 切回圖形桌面（TTY1）。&lt;/p>
&lt;p>TTY 在桌面故障排除中的價值在於它&lt;strong>不依賴 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/compositor/" data-link-title="Compositor（合成器）" data-link-desc="教材反覆出現 compositor / 合成器、想確認它到底負責什麼、跟 window manager 和桌面環境差在哪時讀 — Wayland 下把畫面合成與視窗管理合一的核心程式">compositor&lt;/a>&lt;/strong>（Wayland 下負責畫面與視窗的核心程式）。Compositor 掛了、GPU driver 出問題導致畫面凍結——只要 kernel 還活著，TTY 就能登入操作。這是 Linux 桌面環境「掛了不等於崩潰」的關鍵機制，詳見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">桌面環境維護與故障排除&lt;/a>。&lt;/p>
&lt;p>相關概念：&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/rice/" data-link-title="Rice（桌面視覺客製化）" data-link-desc="Linux 桌面文章裡看到 rice / ricing / ricer 不確定意思時回來讀">Rice&lt;/a>（桌面客製化）、&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/gnu-stow/" data-link-title="GNU Stow" data-link-desc="dotfile 管理文章裡提到 stow、symlink、package 看不懂時回來讀 — stow 的核心概念和常用指令">GNU Stow&lt;/a>（dotfile 管理）。&lt;/p></description><content:encoded><![CDATA[<p>TTY（TeleTYpewriter）是 Linux 核心直接提供的純文字終端機介面，獨立於任何桌面環境或圖形介面。</p>
<p>名稱來自早期電腦透過電報打字機（teletypewriter）做輸入輸出的歷史。現代 Linux 的 TTY 是 virtual console——核心在記憶體中模擬的文字終端機，不需要實體硬體。</p>
<p>systemd 預設配置下有 6 個 virtual console（TTY1-TTY6）。Wayland compositor（如 <a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">Hyprland</a>）通常佔用 TTY1 顯示圖形桌面，TTY2-TTY6 保持為純文字介面可用。</p>
<p>切換方式：<code>Ctrl+Alt+F2</code>（切到 TTY2）到 <code>Ctrl+Alt+F6</code>（切到 TTY6）。<code>Ctrl+Alt+F1</code> 切回圖形桌面（TTY1）。</p>
<p>TTY 在桌面故障排除中的價值在於它<strong>不依賴 <a href="/blog/linux/dotfile/knowledge-cards/compositor/" data-link-title="Compositor（合成器）" data-link-desc="教材反覆出現 compositor / 合成器、想確認它到底負責什麼、跟 window manager 和桌面環境差在哪時讀 — Wayland 下把畫面合成與視窗管理合一的核心程式">compositor</a></strong>（Wayland 下負責畫面與視窗的核心程式）。Compositor 掛了、GPU driver 出問題導致畫面凍結——只要 kernel 還活著，TTY 就能登入操作。這是 Linux 桌面環境「掛了不等於崩潰」的關鍵機制，詳見<a href="/blog/linux/dotfile/07-desktop-maintenance/" data-link-title="模組七：桌面環境維護與故障排除" data-link-desc="桌面凍結、compositor 掛了、或某個工具不回應時回來讀 — Linux 桌面的故障隔離模型、常見故障場景的恢復操作、日誌判讀與診斷工具">桌面環境維護與故障排除</a>。</p>
<p>相關概念：<a href="/blog/linux/dotfile/knowledge-cards/rice/" data-link-title="Rice（桌面視覺客製化）" data-link-desc="Linux 桌面文章裡看到 rice / ricing / ricer 不確定意思時回來讀">Rice</a>（桌面客製化）、<a href="/blog/linux/dotfile/knowledge-cards/gnu-stow/" data-link-title="GNU Stow" data-link-desc="dotfile 管理文章裡提到 stow、symlink、package 看不懂時回來讀 — stow 的核心概念和常用指令">GNU Stow</a>（dotfile 管理）。</p>
]]></content:encoded></item><item><title>initramfs</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/initramfs/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/initramfs/</guid><description>&lt;p>initramfs（initial RAM filesystem）是 kernel 開機初期、在真正的 root 檔案系統被掛起來之前，載入記憶體的一個小型臨時根檔系統。&lt;/p>
&lt;p>它的責任是「把掛載真 root 所需的東西先備齊」。kernel 本身不內建所有硬體與檔案系統的驅動，當 root 位在一個需要額外驅動才讀得到的裝置上——LVM 邏輯卷、LUKS 加密卷、特殊磁碟控制器——kernel 沒辦法直接掛它。initramfs 提供一個臨時環境，把這些驅動與工具載進來、把真 root 掛起來，然後把控制權交給真 root 上的 init。&lt;/p>
&lt;p>因為它要跟 kernel 一起被 bootloader 載入，所以它跟 kernel 放在同一個地方——&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/" data-link-title="UEFI 開機鏈" data-link-desc="在 bootloader 選型（GRUB / EFISTUB / systemd-boot）卡住、或機器重開後找不到 kernel、需要理解韌體怎麼找到並載入系統時讀 — 韌體到 kernel 的交棒過程">UEFI 開機鏈&lt;/a>裡的 ESP，或獨立的 &lt;code>/boot&lt;/code>。這也是為什麼估 ESP 大小時要把它算進去：一個 kernel 加上它的 initramfs（含 fallback 版本）大約一兩百 MB。&lt;/p>
&lt;p>生成工具依發行版而異：Arch 用 &lt;code>mkinitcpio&lt;/code>、Fedora 用 &lt;code>dracut&lt;/code>。換了 kernel 或改了開機需要的驅動，要重新生成 initramfs，否則新 kernel 可能掛不起 root。&lt;/p>
&lt;p>相關概念：&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/" data-link-title="UEFI 開機鏈" data-link-desc="在 bootloader 選型（GRUB / EFISTUB / systemd-boot）卡住、或機器重開後找不到 kernel、需要理解韌體怎麼找到並載入系統時讀 — 韌體到 kernel 的交棒過程">UEFI 開機鏈&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/partition-identification/" data-link-title="分區識別（PARTUUID / FSUUID）" data-link-desc="在 fstab 或 bootloader 設定要指定一個分區、不確定該用 PARTUUID、UUID 還是 /dev/sda1、或重格式化後系統開不了機時讀 — 分區的穩定識別方式">分區識別&lt;/a>。安裝時 ESP 大小怎麼估，見 &lt;a href="https://tarrragon.github.io/blog/linux/install/install-option-decisions/" data-link-title="Linux 安裝選項判讀" data-link-desc="在 Linux 安裝程式面對 locale、網路、磁碟分割、檔案系統、bootloader 等選項、需要判斷依據而非靠預設值硬選時回來讀">Linux 安裝選項判讀&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>initramfs（initial RAM filesystem）是 kernel 開機初期、在真正的 root 檔案系統被掛起來之前，載入記憶體的一個小型臨時根檔系統。</p>
<p>它的責任是「把掛載真 root 所需的東西先備齊」。kernel 本身不內建所有硬體與檔案系統的驅動，當 root 位在一個需要額外驅動才讀得到的裝置上——LVM 邏輯卷、LUKS 加密卷、特殊磁碟控制器——kernel 沒辦法直接掛它。initramfs 提供一個臨時環境，把這些驅動與工具載進來、把真 root 掛起來，然後把控制權交給真 root 上的 init。</p>
<p>因為它要跟 kernel 一起被 bootloader 載入，所以它跟 kernel 放在同一個地方——<a href="/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/" data-link-title="UEFI 開機鏈" data-link-desc="在 bootloader 選型（GRUB / EFISTUB / systemd-boot）卡住、或機器重開後找不到 kernel、需要理解韌體怎麼找到並載入系統時讀 — 韌體到 kernel 的交棒過程">UEFI 開機鏈</a>裡的 ESP，或獨立的 <code>/boot</code>。這也是為什麼估 ESP 大小時要把它算進去：一個 kernel 加上它的 initramfs（含 fallback 版本）大約一兩百 MB。</p>
<p>生成工具依發行版而異：Arch 用 <code>mkinitcpio</code>、Fedora 用 <code>dracut</code>。換了 kernel 或改了開機需要的驅動，要重新生成 initramfs，否則新 kernel 可能掛不起 root。</p>
<p>相關概念：<a href="/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/" data-link-title="UEFI 開機鏈" data-link-desc="在 bootloader 選型（GRUB / EFISTUB / systemd-boot）卡住、或機器重開後找不到 kernel、需要理解韌體怎麼找到並載入系統時讀 — 韌體到 kernel 的交棒過程">UEFI 開機鏈</a>、<a href="/blog/linux/dotfile/knowledge-cards/partition-identification/" data-link-title="分區識別（PARTUUID / FSUUID）" data-link-desc="在 fstab 或 bootloader 設定要指定一個分區、不確定該用 PARTUUID、UUID 還是 /dev/sda1、或重格式化後系統開不了機時讀 — 分區的穩定識別方式">分區識別</a>。安裝時 ESP 大小怎麼估，見 <a href="/blog/linux/install/install-option-decisions/" data-link-title="Linux 安裝選項判讀" data-link-desc="在 Linux 安裝程式面對 locale、網路、磁碟分割、檔案系統、bootloader 等選項、需要判斷依據而非靠預設值硬選時回來讀">Linux 安裝選項判讀</a>。</p>
]]></content:encoded></item><item><title>UEFI 開機鏈</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/</guid><description>&lt;p>UEFI 開機鏈是現代機器從通電到 kernel 跑起來的一段交棒過程：韌體 → 開機項 → bootloader → kernel。理解這條鏈，bootloader 選型與「重開機找不到系統」的故障才有判讀依據。&lt;/p>
&lt;p>UEFI 韌體開機時，要找到一個 EFI 執行檔來載入。它從兩個來源找：NVRAM（韌體用來存開機項的非揮發記憶體）裡登記的開機項，或 ESP（EFI System Partition，一個 FAT32 格式的分區）裡的標準路徑。找到的 EFI 執行檔可能是一個獨立的 bootloader，也可能直接是 kernel。&lt;/p>
&lt;p>這對應兩種風格。EFISTUB 讓韌體直接載入 kernel、不經過獨立 bootloader，最精簡，但典型上依賴 NVRAM 裡的開機項。獨立 bootloader（GRUB、systemd-boot）則多一層：它有開機選單、能救援、還能裝到 ESP 的 fallback 路徑（&lt;code>\EFI\BOOT\BOOT&amp;lt;ARCH&amp;gt;.EFI&lt;/code>，aarch64 是 &lt;code>BOOTAA64.EFI&lt;/code>、x86_64 是 &lt;code>BOOTX64.EFI&lt;/code>）。&lt;/p>
&lt;p>fallback 路徑是這條鏈的保命機制。NVRAM 的開機項可能丟失——QEMU 系的虛擬機尤其容易——這時靠 NVRAM 開機項的 EFISTUB 會開不了機，而 fallback 路徑上有 bootloader 的機器，韌體仍找得到。這就是「VM 上偏好獨立 bootloader」的根據。&lt;/p>
&lt;p>這條鏈預設 Secure Boot 關閉。Secure Boot 開啟時，韌體會拒載沒簽章的 EFI 執行檔（kernel 或 bootloader），這也是「重開後找不到 kernel」的一類成因——最小 VM 安裝通常把它關掉，但實體機若開著就要處理簽章。&lt;/p>
&lt;p>相關概念：&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/initramfs/" data-link-title="initramfs" data-link-desc="看到 ESP 大小要算進 initramfs、或開機卡在掛載 root 之前、不知道 initramfs 是什麼時讀 — 開機初期掛真 root 之前的臨時根檔系統">initramfs&lt;/a>（bootloader 載入的對象之一）、&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/partition-identification/" data-link-title="分區識別（PARTUUID / FSUUID）" data-link-desc="在 fstab 或 bootloader 設定要指定一個分區、不確定該用 PARTUUID、UUID 還是 /dev/sda1、或重格式化後系統開不了機時讀 — 分區的穩定識別方式">分區識別&lt;/a>。bootloader 選型的判讀，見 &lt;a href="https://tarrragon.github.io/blog/linux/install/install-option-decisions/" data-link-title="Linux 安裝選項判讀" data-link-desc="在 Linux 安裝程式面對 locale、網路、磁碟分割、檔案系統、bootloader 等選項、需要判斷依據而非靠預設值硬選時回來讀">Linux 安裝選項判讀&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>UEFI 開機鏈是現代機器從通電到 kernel 跑起來的一段交棒過程：韌體 → 開機項 → bootloader → kernel。理解這條鏈，bootloader 選型與「重開機找不到系統」的故障才有判讀依據。</p>
<p>UEFI 韌體開機時，要找到一個 EFI 執行檔來載入。它從兩個來源找：NVRAM（韌體用來存開機項的非揮發記憶體）裡登記的開機項，或 ESP（EFI System Partition，一個 FAT32 格式的分區）裡的標準路徑。找到的 EFI 執行檔可能是一個獨立的 bootloader，也可能直接是 kernel。</p>
<p>這對應兩種風格。EFISTUB 讓韌體直接載入 kernel、不經過獨立 bootloader，最精簡，但典型上依賴 NVRAM 裡的開機項。獨立 bootloader（GRUB、systemd-boot）則多一層：它有開機選單、能救援、還能裝到 ESP 的 fallback 路徑（<code>\EFI\BOOT\BOOT&lt;ARCH&gt;.EFI</code>，aarch64 是 <code>BOOTAA64.EFI</code>、x86_64 是 <code>BOOTX64.EFI</code>）。</p>
<p>fallback 路徑是這條鏈的保命機制。NVRAM 的開機項可能丟失——QEMU 系的虛擬機尤其容易——這時靠 NVRAM 開機項的 EFISTUB 會開不了機，而 fallback 路徑上有 bootloader 的機器，韌體仍找得到。這就是「VM 上偏好獨立 bootloader」的根據。</p>
<p>這條鏈預設 Secure Boot 關閉。Secure Boot 開啟時，韌體會拒載沒簽章的 EFI 執行檔（kernel 或 bootloader），這也是「重開後找不到 kernel」的一類成因——最小 VM 安裝通常把它關掉，但實體機若開著就要處理簽章。</p>
<p>相關概念：<a href="/blog/linux/dotfile/knowledge-cards/initramfs/" data-link-title="initramfs" data-link-desc="看到 ESP 大小要算進 initramfs、或開機卡在掛載 root 之前、不知道 initramfs 是什麼時讀 — 開機初期掛真 root 之前的臨時根檔系統">initramfs</a>（bootloader 載入的對象之一）、<a href="/blog/linux/dotfile/knowledge-cards/partition-identification/" data-link-title="分區識別（PARTUUID / FSUUID）" data-link-desc="在 fstab 或 bootloader 設定要指定一個分區、不確定該用 PARTUUID、UUID 還是 /dev/sda1、或重格式化後系統開不了機時讀 — 分區的穩定識別方式">分區識別</a>。bootloader 選型的判讀，見 <a href="/blog/linux/install/install-option-decisions/" data-link-title="Linux 安裝選項判讀" data-link-desc="在 Linux 安裝程式面對 locale、網路、磁碟分割、檔案系統、bootloader 等選項、需要判斷依據而非靠預設值硬選時回來讀">Linux 安裝選項判讀</a>。</p>
]]></content:encoded></item><item><title>分區識別（PARTUUID / FSUUID）</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/partition-identification/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/partition-identification/</guid><description>&lt;p>分區識別是 &lt;code>fstab&lt;/code>（開機時決定哪個分區掛到哪的設定檔）與 bootloader 指涉某個分區時用的名字，它的選擇決定一件事：重開機或重格式化後，系統還找不找得到自己的分區。&lt;/p>
&lt;p>有三種識別方式，穩定性不同。PARTUUID 是寫在 GPT 分區表裡的 ID，綁在分區本身、跨重開機穩定，而且重新格式化檔案系統也不會變。FSUUID 是檔案系統 superblock（檔案系統開頭記錄自身中繼資料的區塊）裡的 UUID，綁在檔案系統上，所以一重新格式化就變，會讓引用它的 &lt;code>fstab&lt;/code> 失效。kernel 名稱（&lt;code>/dev/sda1&lt;/code>、&lt;code>/dev/vda1&lt;/code>）則隨偵測順序浮動，多接一顆磁碟就可能對調，最不穩。&lt;code>fstab&lt;/code> 還吃 &lt;code>LABEL=&lt;/code> / &lt;code>PARTLABEL=&lt;/code>（你自己給的標籤），穩定性看你維不維護那個標籤，跟前三種「系統生成」的識別不同層級，這裡不展開。&lt;/p>
&lt;p>穩定性排序是 PARTUUID 優於 FSUUID 優於 kernel 名稱。在 GPT 磁碟上用 PARTUUID，得到「綁分區、重格不變」的最穩識別。這也是為什麼安裝程式問「device name scheme」時，GPT 磁碟選 PARTUUID。&lt;/p>
&lt;p>理解這個差異，能解釋一類典型故障：重新格式化某個分區後機器開不了機，往往是因為 &lt;code>fstab&lt;/code> 或 bootloader 用了 FSUUID，而格式化讓那個 UUID 變了。&lt;/p>
&lt;p>相關概念：&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/" data-link-title="UEFI 開機鏈" data-link-desc="在 bootloader 選型（GRUB / EFISTUB / systemd-boot）卡住、或機器重開後找不到 kernel、需要理解韌體怎麼找到並載入系統時讀 — 韌體到 kernel 的交棒過程">UEFI 開機鏈&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/initramfs/" data-link-title="initramfs" data-link-desc="看到 ESP 大小要算進 initramfs、或開機卡在掛載 root 之前、不知道 initramfs 是什麼時讀 — 開機初期掛真 root 之前的臨時根檔系統">initramfs&lt;/a>。安裝時的識別方式選擇，見 &lt;a href="https://tarrragon.github.io/blog/linux/install/install-option-decisions/" data-link-title="Linux 安裝選項判讀" data-link-desc="在 Linux 安裝程式面對 locale、網路、磁碟分割、檔案系統、bootloader 等選項、需要判斷依據而非靠預設值硬選時回來讀">Linux 安裝選項判讀&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>分區識別是 <code>fstab</code>（開機時決定哪個分區掛到哪的設定檔）與 bootloader 指涉某個分區時用的名字，它的選擇決定一件事：重開機或重格式化後，系統還找不找得到自己的分區。</p>
<p>有三種識別方式，穩定性不同。PARTUUID 是寫在 GPT 分區表裡的 ID，綁在分區本身、跨重開機穩定，而且重新格式化檔案系統也不會變。FSUUID 是檔案系統 superblock（檔案系統開頭記錄自身中繼資料的區塊）裡的 UUID，綁在檔案系統上，所以一重新格式化就變，會讓引用它的 <code>fstab</code> 失效。kernel 名稱（<code>/dev/sda1</code>、<code>/dev/vda1</code>）則隨偵測順序浮動，多接一顆磁碟就可能對調，最不穩。<code>fstab</code> 還吃 <code>LABEL=</code> / <code>PARTLABEL=</code>（你自己給的標籤），穩定性看你維不維護那個標籤，跟前三種「系統生成」的識別不同層級，這裡不展開。</p>
<p>穩定性排序是 PARTUUID 優於 FSUUID 優於 kernel 名稱。在 GPT 磁碟上用 PARTUUID，得到「綁分區、重格不變」的最穩識別。這也是為什麼安裝程式問「device name scheme」時，GPT 磁碟選 PARTUUID。</p>
<p>理解這個差異，能解釋一類典型故障：重新格式化某個分區後機器開不了機，往往是因為 <code>fstab</code> 或 bootloader 用了 FSUUID，而格式化讓那個 UUID 變了。</p>
<p>相關概念：<a href="/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/" data-link-title="UEFI 開機鏈" data-link-desc="在 bootloader 選型（GRUB / EFISTUB / systemd-boot）卡住、或機器重開後找不到 kernel、需要理解韌體怎麼找到並載入系統時讀 — 韌體到 kernel 的交棒過程">UEFI 開機鏈</a>、<a href="/blog/linux/dotfile/knowledge-cards/initramfs/" data-link-title="initramfs" data-link-desc="看到 ESP 大小要算進 initramfs、或開機卡在掛載 root 之前、不知道 initramfs 是什麼時讀 — 開機初期掛真 root 之前的臨時根檔系統">initramfs</a>。安裝時的識別方式選擇，見 <a href="/blog/linux/install/install-option-decisions/" data-link-title="Linux 安裝選項判讀" data-link-desc="在 Linux 安裝程式面對 locale、網路、磁碟分割、檔案系統、bootloader 等選項、需要判斷依據而非靠預設值硬選時回來讀">Linux 安裝選項判讀</a>。</p>
]]></content:encoded></item><item><title>字型的可用集合在 process 啟動時決定</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/</guid><description>&lt;p>一個 process 能用哪些字型，是在它&lt;strong>啟動的當下&lt;/strong>由 fontconfig（Linux 上統一管理字型搜尋與匹配的底層服務）決定並載入記憶體的。之後往系統裝新字型，不會回頭改變已經在跑的 process——它手上那份字型清單是啟動時的快照。「裝了字型卻還是豆腐」多數時候指向的是這個時序問題，而非安裝本身失敗。&lt;/p>
&lt;p>這個機制發生在 fontconfig + process 記憶體層，跟顯示協議無關——Wayland 和 X11 下的行為相同。&lt;/p>
&lt;h2 id="同一時刻兩種查詢結果">同一時刻、兩種查詢結果&lt;/h2>
&lt;p>裝完新字型後，在終端機用 fontconfig 的查詢工具 &lt;code>fc-match&lt;/code>（每次執行都是新 process）去查：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">fc-match &lt;span class="s2">&amp;#34;:lang=zh-tw&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># Noto Sans CJK → 系統快取已有這支字&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>查得到。但同時間，一直開著的狀態列或通知 daemon 仍顯示豆腐。矛盾的根源是兩者的啟動時間不同：&lt;code>fc-match&lt;/code> 剛啟動、讀到的是最新系統快取；那個豆腐的 daemon 是在裝字型&lt;strong>之前&lt;/strong>啟動的，記憶體裡的字型清單沒有這支字。&lt;/p>
&lt;p>套件管理器安裝字型時，post-install hook 通常已更新 fontconfig 的系統快取（pacman 會印 &lt;code>Updating fontconfig cache&lt;/code>）。手動把字型檔放進 &lt;code>~/.local/share/fonts/&lt;/code> 的情況下，需要自己跑 fontconfig 的快取重建工具 &lt;code>fc-cache&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">fc-cache -fv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># -f 忽略時間戳、強制全部重建&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># -v 印出處理了哪些目錄&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>fc-cache&lt;/code> 只更新系統快取層——磁碟上的索引檔。它不會觸及任何已啟動 process 的記憶體，跑再多次也改變不了舊 process 的字型清單。&lt;/p>
&lt;h2 id="判讀與操作">判讀與操作&lt;/h2>
&lt;p>&lt;strong>判讀訊號&lt;/strong>：&lt;code>fc-match&lt;/code> 在命令列回得出正確字型，但某個一直開著的程式仍顯示豆腐，幾乎可確定是「那個程式啟動早於裝字型」。&lt;/p>
&lt;p>&lt;strong>修法是重啟該程式，不是 reload&lt;/strong>。&lt;code>reload&lt;/code> 類指令（如 &lt;code>makoctl reload&lt;/code>、送 SIGHUP）重讀的是&lt;strong>設定檔&lt;/strong>——能換到 daemon 啟動時已可見的字型（例如從 A 字族改成 B 字族），但看不到啟動後才新裝的字型檔。根源是 reload 不重建記憶體裡的字型清單，只有重啟 process 才會從系統快取重新載入。&lt;/p>
&lt;p>&lt;strong>重啟的範圍&lt;/strong>取決於受影響的程式數量。單一 daemon（通知、狀態列）重啟那一個即可；由 compositor &lt;code>exec-once&lt;/code> 拉起的一批元件要同時吃到新字型，最乾淨的做法是重新登入，讓它們全部重新啟動。&lt;/p>
&lt;p>&lt;strong>正常開機不會踩到這個坑&lt;/strong>——字型在開機早期就裝好，&lt;code>exec-once&lt;/code> 啟動的元件從一開始就看得到完整字型集合。這個時序問題集中在「系統已經在跑、中途才補裝字型」的除錯情境。&lt;/p>
&lt;p>&lt;strong>延伸閱讀&lt;/strong>：Nerd Font 不含 CJK、需另裝 fallback 字型的具體案例見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/desktop-shell-components/" data-link-title="桌面 Shell 元件：狀態列、啟動器與通知" data-link-desc="Hyprland 桌面要拼哪些元件、各元件的配置檔怎麼寫時回來讀">桌面 Shell 元件：狀態列、啟動器與通知&lt;/a>；字型安裝方式見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/terminal-emulator-config/" data-link-title="Terminal Emulator 配置" data-link-desc="選 terminal emulator 時需要比對配置格式和跨平台能力、或想把配色和字型統一管理時回來讀">終端機與編輯器配置&lt;/a>的字型管理段。&lt;/p>
&lt;h2 id="邊界與例外">邊界與例外&lt;/h2>
&lt;p>&lt;strong>fc-match 也查不到&lt;/strong>：連新 process 都找不到剛裝的字型，問題在系統快取層（fontconfig 索引未更新），跑 &lt;code>fc-cache -fv&lt;/code> 解決。兩層的修法不同，&lt;code>fc-match&lt;/code> 是分辨在哪一層的第一步。&lt;/p>
&lt;p>&lt;strong>部分應用程式支援熱載入&lt;/strong>：瀏覽器等有獨立字型服務的程式可能在開新分頁時重新掃描字型，不需要重啟整個 process。長駐 daemon（mako、waybar）與狀態列預設是啟動時載入一次。&lt;/p>
&lt;p>&lt;strong>Flatpak / Snap 的字型隔離是不同問題&lt;/strong>：沙箱化應用程式看不到 host 的字型目錄，重啟 process 也無法解決——原因不是時序，而是沙箱的檔案系統隔離。需要透過 Flatpak 的 filesystem override 或把字型放進沙箱可存取的路徑。&lt;/p></description><content:encoded><![CDATA[<p>一個 process 能用哪些字型，是在它<strong>啟動的當下</strong>由 fontconfig（Linux 上統一管理字型搜尋與匹配的底層服務）決定並載入記憶體的。之後往系統裝新字型，不會回頭改變已經在跑的 process——它手上那份字型清單是啟動時的快照。「裝了字型卻還是豆腐」多數時候指向的是這個時序問題，而非安裝本身失敗。</p>
<p>這個機制發生在 fontconfig + process 記憶體層，跟顯示協議無關——Wayland 和 X11 下的行為相同。</p>
<h2 id="同一時刻兩種查詢結果">同一時刻、兩種查詢結果</h2>
<p>裝完新字型後，在終端機用 fontconfig 的查詢工具 <code>fc-match</code>（每次執行都是新 process）去查：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">fc-match <span class="s2">&#34;:lang=zh-tw&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># Noto Sans CJK → 系統快取已有這支字</span></span></span></code></pre></div><p>查得到。但同時間，一直開著的狀態列或通知 daemon 仍顯示豆腐。矛盾的根源是兩者的啟動時間不同：<code>fc-match</code> 剛啟動、讀到的是最新系統快取；那個豆腐的 daemon 是在裝字型<strong>之前</strong>啟動的，記憶體裡的字型清單沒有這支字。</p>
<p>套件管理器安裝字型時，post-install hook 通常已更新 fontconfig 的系統快取（pacman 會印 <code>Updating fontconfig cache</code>）。手動把字型檔放進 <code>~/.local/share/fonts/</code> 的情況下，需要自己跑 fontconfig 的快取重建工具 <code>fc-cache</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">fc-cache -fv
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># -f 忽略時間戳、強制全部重建</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># -v 印出處理了哪些目錄</span></span></span></code></pre></div><p><code>fc-cache</code> 只更新系統快取層——磁碟上的索引檔。它不會觸及任何已啟動 process 的記憶體，跑再多次也改變不了舊 process 的字型清單。</p>
<h2 id="判讀與操作">判讀與操作</h2>
<p><strong>判讀訊號</strong>：<code>fc-match</code> 在命令列回得出正確字型，但某個一直開著的程式仍顯示豆腐，幾乎可確定是「那個程式啟動早於裝字型」。</p>
<p><strong>修法是重啟該程式，不是 reload</strong>。<code>reload</code> 類指令（如 <code>makoctl reload</code>、送 SIGHUP）重讀的是<strong>設定檔</strong>——能換到 daemon 啟動時已可見的字型（例如從 A 字族改成 B 字族），但看不到啟動後才新裝的字型檔。根源是 reload 不重建記憶體裡的字型清單，只有重啟 process 才會從系統快取重新載入。</p>
<p><strong>重啟的範圍</strong>取決於受影響的程式數量。單一 daemon（通知、狀態列）重啟那一個即可；由 compositor <code>exec-once</code> 拉起的一批元件要同時吃到新字型，最乾淨的做法是重新登入，讓它們全部重新啟動。</p>
<p><strong>正常開機不會踩到這個坑</strong>——字型在開機早期就裝好，<code>exec-once</code> 啟動的元件從一開始就看得到完整字型集合。這個時序問題集中在「系統已經在跑、中途才補裝字型」的除錯情境。</p>
<p><strong>延伸閱讀</strong>：Nerd Font 不含 CJK、需另裝 fallback 字型的具體案例見<a href="/blog/linux/dotfile/06-rice-design/desktop-shell-components/" data-link-title="桌面 Shell 元件：狀態列、啟動器與通知" data-link-desc="Hyprland 桌面要拼哪些元件、各元件的配置檔怎麼寫時回來讀">桌面 Shell 元件：狀態列、啟動器與通知</a>；字型安裝方式見<a href="/blog/linux/dotfile/03-terminal-ecosystem/terminal-emulator-config/" data-link-title="Terminal Emulator 配置" data-link-desc="選 terminal emulator 時需要比對配置格式和跨平台能力、或想把配色和字型統一管理時回來讀">終端機與編輯器配置</a>的字型管理段。</p>
<h2 id="邊界與例外">邊界與例外</h2>
<p><strong>fc-match 也查不到</strong>：連新 process 都找不到剛裝的字型，問題在系統快取層（fontconfig 索引未更新），跑 <code>fc-cache -fv</code> 解決。兩層的修法不同，<code>fc-match</code> 是分辨在哪一層的第一步。</p>
<p><strong>部分應用程式支援熱載入</strong>：瀏覽器等有獨立字型服務的程式可能在開新分頁時重新掃描字型，不需要重啟整個 process。長駐 daemon（mako、waybar）與狀態列預設是啟動時載入一次。</p>
<p><strong>Flatpak / Snap 的字型隔離是不同問題</strong>：沙箱化應用程式看不到 host 的字型目錄，重啟 process 也無法解決——原因不是時序，而是沙箱的檔案系統隔離。需要透過 Flatpak 的 filesystem override 或把字型放進沙箱可存取的路徑。</p>
]]></content:encoded></item><item><title>Wayland Session Lock（鎖屏安全狀態）</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/session-lock/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/session-lock/</guid><description>&lt;h2 id="鎖屏是-compositor-持有的安全狀態">鎖屏是 compositor 持有的安全狀態&lt;/h2>
&lt;p>Wayland 下的 compositor（如 Hyprland、Sway）同時管理視窗排列與畫面輸出。鎖屏工具（Hyprlock、Swaylock）一旦啟動，桌面的「鎖定」狀態就由 compositor 透過 ext-session-lock-v1（Wayland 生態系的跨 compositor 鎖屏協議）持有。解鎖的正常動作是鎖屏 client 通過認證後呼叫 &lt;code>unlock_and_destroy&lt;/code>（協議定義的 request），compositor 收到這個信號才釋放鎖定。&lt;/p>
&lt;p>這個責任邊界在自動化測試、VM 演練、遠端操作時最容易出事，因為這些情境常用「殺 process」當「關掉一個東西」的通用手段。殺掉鎖屏 client 跳過了認證，compositor 不會釋放鎖——畫面會卡在失效保護狀態而非回到桌面。&lt;/p>
&lt;h2 id="logind-提示與-compositor-鎖的值可以不一致">logind 提示與 compositor 鎖的值可以不一致&lt;/h2>
&lt;p>鎖屏狀態牽涉兩個獨立的層，觸發方向和持有者不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層&lt;/th>
 &lt;th>持有者&lt;/th>
 &lt;th>查看方式&lt;/th>
 &lt;th>語意&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>logind 會話鎖&lt;/td>
 &lt;td>systemd-logind&lt;/td>
 &lt;td>&lt;code>loginctl show-session &amp;lt;id&amp;gt; -p LockedHint&lt;/code>&lt;/td>
 &lt;td>會話的鎖定提示，給登入管理器 / 螢幕保護程式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>compositor 鎖&lt;/td>
 &lt;td>Wayland compositor&lt;/td>
 &lt;td>畫面是否進得去、鎖屏 surface 是否在最上層&lt;/td>
 &lt;td>實際擋住畫面的那層&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>loginctl lock-session&lt;/code> 走 logind 層觸發鎖屏，鎖屏 client 收到信號後啟動、再向 compositor 取得 session lock。觸發方向是 logind → client → compositor；持有與強制執行方向是 compositor → 畫面。兩者方向相反，正好印證兩層是獨立的。&lt;/p>
&lt;p>實測會遇到 &lt;code>LockedHint=no&lt;/code>（logind 層說沒鎖）但畫面仍進不去——因為擋住畫面的是 compositor 的 ext-session-lock，跟 logind 提示是兩回事。判斷畫面進不進得去，看 compositor 層，不看 logind 層。&lt;/p>
&lt;h2 id="鎖屏-client-非正常結束時的失效保護">鎖屏 client 非正常結束時的失效保護&lt;/h2>
&lt;p>鎖屏 client 在持有鎖的狀態下死掉（被 &lt;code>kill&lt;/code>、crash），compositor 沒有收到認證通過的信號，只能維持鎖定並顯示失效保護畫面。Hyprland 的失效保護畫面會直接給恢復指令：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">hyprctl --instance 0 &amp;#39;keyword misc:allow_session_lock_restore 1&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">hyprctl --instance 0 &amp;#39;dispatch exec hyprlock&amp;#39;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>allow_session_lock_restore&lt;/code> 允許新的鎖屏 client 接管既有的鎖（否則新 client 會因「已經鎖了」被拒）。接管後是乾淨的鎖屏 prompt，用密碼正常解鎖。&lt;/p>
&lt;p>備好 restore 路徑時，殺掉無回應的鎖屏 client 是合理操作——問題不在「殺」、在「以為殺完就回桌面」。restore 的前提是有另一個可操作的 session：另一個 TTY 或 SSH 連線。ext-session-lock 的安全語意允許 compositor 攔截 VT 切換快捷鍵（&lt;code>Ctrl+Alt+Fn&lt;/code>），遇到 TTY 切不過去的情況，SSH 是替代救援通道（事先配好 SSH server，見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作&lt;/a>的 GPU hang 段）。&lt;/p>
&lt;h2 id="判讀與操作">判讀與操作&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>判讀鎖定狀態&lt;/strong>：&lt;code>loginctl show-session $(loginctl show-user $USER -p Display --value) -p LockedHint&lt;/code> 查 logind 層；compositor 層看畫面能否操作。兩層不一致時以 compositor 層為準。&lt;/li>
&lt;li>&lt;strong>正常解鎖&lt;/strong>：通過鎖屏 client 的認證（密碼 / 指紋），client 呼叫 &lt;code>unlock_and_destroy&lt;/code>，compositor 釋放鎖。&lt;/li>
&lt;li>&lt;strong>失效保護恢復&lt;/strong>：從另一個 TTY 或 SSH 執行 &lt;code>hyprctl --instance 0 'keyword misc:allow_session_lock_restore 1'&lt;/code> + &lt;code>hyprctl --instance 0 'dispatch exec hyprlock'&lt;/code>，重新拉起鎖屏 prompt 後認證解鎖。&lt;/li>
&lt;li>&lt;strong>自動化流程的代價&lt;/strong>：啟動鎖屏後，畫面會留在鎖定狀態直到有人通過認證。自動化測試若會觸發鎖屏，要把「需人工解鎖」算進代價。&lt;/li>
&lt;li>&lt;strong>診斷路由&lt;/strong>：「畫面卡住 / 螢幕鎖了沒」當成一般 Linux 狀態判讀問題（跟判程式活著、判服務歸屬同類）時，見&lt;a href="https://tarrragon.github.io/blog/linux/debug/process-service-state-diagnosis/" data-link-title="程序、服務與狀態怎麼判" data-link-desc="要判斷一個程式活著沒、某個系統服務現在由誰提供、桌面 session 有沒有被鎖、或終端機多工器的 session 還在不在時，用對的權威來源而不是靠畫面或猜的名字">程序、服務與狀態怎麼判&lt;/a>——它把「判 session 有沒有被鎖」放進「讀權威狀態、別看畫面猜」的通用診斷紀律裡。&lt;/li>
&lt;li>&lt;strong>延伸閱讀&lt;/strong>：鎖屏的視覺配置（背景、輸入框、時鐘 label）見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/color-system-theming/" data-link-title="配色系統、鎖屏與 GTK 主題" data-link-desc="桌面配色散亂看起來雜、或要換主題不知道該改哪些檔案時回來讀">配色系統、鎖屏與 GTK 主題&lt;/a>的 Hyprlock 段；桌面故障恢復流程見&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作&lt;/a>。持鎖的那個 compositor 到底是什麼、還握著哪些系統狀態，見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/compositor/" data-link-title="Compositor（合成器）" data-link-desc="教材反覆出現 compositor / 合成器、想確認它到底負責什麼、跟 window manager 和桌面環境差在哪時讀 — Wayland 下把畫面合成與視窗管理合一的核心程式">Compositor 術語卡&lt;/a>。&lt;/li>
&lt;/ul>
&lt;h2 id="邊界條件">邊界條件&lt;/h2>
&lt;p>正常認證解鎖（走 &lt;code>unlock_and_destroy&lt;/code>）後鎖屏 client 結束，compositor 已回到非鎖定狀態，不觸發失效保護。失效保護只在「持鎖中非正常結束」時出現。&lt;/p></description><content:encoded><![CDATA[<h2 id="鎖屏是-compositor-持有的安全狀態">鎖屏是 compositor 持有的安全狀態</h2>
<p>Wayland 下的 compositor（如 Hyprland、Sway）同時管理視窗排列與畫面輸出。鎖屏工具（Hyprlock、Swaylock）一旦啟動，桌面的「鎖定」狀態就由 compositor 透過 ext-session-lock-v1（Wayland 生態系的跨 compositor 鎖屏協議）持有。解鎖的正常動作是鎖屏 client 通過認證後呼叫 <code>unlock_and_destroy</code>（協議定義的 request），compositor 收到這個信號才釋放鎖定。</p>
<p>這個責任邊界在自動化測試、VM 演練、遠端操作時最容易出事，因為這些情境常用「殺 process」當「關掉一個東西」的通用手段。殺掉鎖屏 client 跳過了認證，compositor 不會釋放鎖——畫面會卡在失效保護狀態而非回到桌面。</p>
<h2 id="logind-提示與-compositor-鎖的值可以不一致">logind 提示與 compositor 鎖的值可以不一致</h2>
<p>鎖屏狀態牽涉兩個獨立的層，觸發方向和持有者不同：</p>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>持有者</th>
          <th>查看方式</th>
          <th>語意</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>logind 會話鎖</td>
          <td>systemd-logind</td>
          <td><code>loginctl show-session &lt;id&gt; -p LockedHint</code></td>
          <td>會話的鎖定提示，給登入管理器 / 螢幕保護程式</td>
      </tr>
      <tr>
          <td>compositor 鎖</td>
          <td>Wayland compositor</td>
          <td>畫面是否進得去、鎖屏 surface 是否在最上層</td>
          <td>實際擋住畫面的那層</td>
      </tr>
  </tbody>
</table>
<p><code>loginctl lock-session</code> 走 logind 層觸發鎖屏，鎖屏 client 收到信號後啟動、再向 compositor 取得 session lock。觸發方向是 logind → client → compositor；持有與強制執行方向是 compositor → 畫面。兩者方向相反，正好印證兩層是獨立的。</p>
<p>實測會遇到 <code>LockedHint=no</code>（logind 層說沒鎖）但畫面仍進不去——因為擋住畫面的是 compositor 的 ext-session-lock，跟 logind 提示是兩回事。判斷畫面進不進得去，看 compositor 層，不看 logind 層。</p>
<h2 id="鎖屏-client-非正常結束時的失效保護">鎖屏 client 非正常結束時的失效保護</h2>
<p>鎖屏 client 在持有鎖的狀態下死掉（被 <code>kill</code>、crash），compositor 沒有收到認證通過的信號，只能維持鎖定並顯示失效保護畫面。Hyprland 的失效保護畫面會直接給恢復指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">hyprctl --instance 0 &#39;keyword misc:allow_session_lock_restore 1&#39;
</span></span><span class="line"><span class="ln">2</span><span class="cl">hyprctl --instance 0 &#39;dispatch exec hyprlock&#39;</span></span></code></pre></div><p><code>allow_session_lock_restore</code> 允許新的鎖屏 client 接管既有的鎖（否則新 client 會因「已經鎖了」被拒）。接管後是乾淨的鎖屏 prompt，用密碼正常解鎖。</p>
<p>備好 restore 路徑時，殺掉無回應的鎖屏 client 是合理操作——問題不在「殺」、在「以為殺完就回桌面」。restore 的前提是有另一個可操作的 session：另一個 TTY 或 SSH 連線。ext-session-lock 的安全語意允許 compositor 攔截 VT 切換快捷鍵（<code>Ctrl+Alt+Fn</code>），遇到 TTY 切不過去的情況，SSH 是替代救援通道（事先配好 SSH server，見<a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作</a>的 GPU hang 段）。</p>
<h2 id="判讀與操作">判讀與操作</h2>
<ul>
<li><strong>判讀鎖定狀態</strong>：<code>loginctl show-session $(loginctl show-user $USER -p Display --value) -p LockedHint</code> 查 logind 層；compositor 層看畫面能否操作。兩層不一致時以 compositor 層為準。</li>
<li><strong>正常解鎖</strong>：通過鎖屏 client 的認證（密碼 / 指紋），client 呼叫 <code>unlock_and_destroy</code>，compositor 釋放鎖。</li>
<li><strong>失效保護恢復</strong>：從另一個 TTY 或 SSH 執行 <code>hyprctl --instance 0 'keyword misc:allow_session_lock_restore 1'</code> + <code>hyprctl --instance 0 'dispatch exec hyprlock'</code>，重新拉起鎖屏 prompt 後認證解鎖。</li>
<li><strong>自動化流程的代價</strong>：啟動鎖屏後，畫面會留在鎖定狀態直到有人通過認證。自動化測試若會觸發鎖屏，要把「需人工解鎖」算進代價。</li>
<li><strong>診斷路由</strong>：「畫面卡住 / 螢幕鎖了沒」當成一般 Linux 狀態判讀問題（跟判程式活著、判服務歸屬同類）時，見<a href="/blog/linux/debug/process-service-state-diagnosis/" data-link-title="程序、服務與狀態怎麼判" data-link-desc="要判斷一個程式活著沒、某個系統服務現在由誰提供、桌面 session 有沒有被鎖、或終端機多工器的 session 還在不在時，用對的權威來源而不是靠畫面或猜的名字">程序、服務與狀態怎麼判</a>——它把「判 session 有沒有被鎖」放進「讀權威狀態、別看畫面猜」的通用診斷紀律裡。</li>
<li><strong>延伸閱讀</strong>：鎖屏的視覺配置（背景、輸入框、時鐘 label）見<a href="/blog/linux/dotfile/06-rice-design/color-system-theming/" data-link-title="配色系統、鎖屏與 GTK 主題" data-link-desc="桌面配色散亂看起來雜、或要換主題不知道該改哪些檔案時回來讀">配色系統、鎖屏與 GTK 主題</a>的 Hyprlock 段；桌面故障恢復流程見<a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作</a>。持鎖的那個 compositor 到底是什麼、還握著哪些系統狀態，見 <a href="/blog/linux/dotfile/knowledge-cards/compositor/" data-link-title="Compositor（合成器）" data-link-desc="教材反覆出現 compositor / 合成器、想確認它到底負責什麼、跟 window manager 和桌面環境差在哪時讀 — Wayland 下把畫面合成與視窗管理合一的核心程式">Compositor 術語卡</a>。</li>
</ul>
<h2 id="邊界條件">邊界條件</h2>
<p>正常認證解鎖（走 <code>unlock_and_destroy</code>）後鎖屏 client 結束，compositor 已回到非鎖定狀態，不觸發失效保護。失效保護只在「持鎖中非正常結束」時出現。</p>
<p>Sway/swaylock 在 client 死掉時沒有畫面上的恢復提示（不像 Hyprland 會印指令），得預先知道走 TTY 或 SSH 執行 restore。「鎖是 compositor 持有、解鎖要認證」是 ext-session-lock 協議層的共通約束；失效保護的具體呈現方式因 compositor 而異。</p>
]]></content:encoded></item><item><title>fontconfig — 字型搜尋、匹配與 fallback 服務</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/fontconfig/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/fontconfig/</guid><description>&lt;p>fontconfig 是 Linux 上統一管理字型搜尋、匹配與 fallback 的底層服務。應用程式透過 fontconfig 的 API 查詢可用字型，而非自行掃描字型目錄——無論是終端機、狀態列、通知 daemon 還是瀏覽器，底層都走同一套查詢介面。&lt;/p>
&lt;h2 id="fc--工具分工">fc-* 工具分工&lt;/h2>
&lt;p>fontconfig 附帶一組命令列工具，各自負責一件事：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;th>常用情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>fc-list&lt;/code>&lt;/td>
 &lt;td>列出系統已知的所有字型（字族名、檔案路徑）&lt;/td>
 &lt;td>確認某支字型有沒有裝、查實際字族名&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>fc-match&lt;/code>&lt;/td>
 &lt;td>查詢指定條件的最佳匹配結果&lt;/td>
 &lt;td>確認 config 裡寫的字族名會匹配到哪支字&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>fc-cache&lt;/code>&lt;/td>
 &lt;td>重建 fontconfig 的系統快取&lt;/td>
 &lt;td>手動放字型檔後更新快取（套件安裝通常自動跑）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>fc-pattern&lt;/code>&lt;/td>
 &lt;td>印出字型的完整屬性（除錯用）&lt;/td>
 &lt;td>查字型支援的語言、字重、字形變體&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>fc-list&lt;/code> 和 &lt;code>fc-match&lt;/code> 每次執行都是新 process，讀到的是當下最新的系統快取。這跟已啟動的長駐程式不同——長駐程式的字型清單是啟動時的快照，詳見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">font-availability-at-startup&lt;/a>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">fc-list &lt;span class="p">|&lt;/span> grep -i meslo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># 確認 MesloLGS Nerd Font 有沒有裝、實際字族名是什麼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">fc-match &lt;span class="s2">&amp;#34;MesloLGS Nerd Font&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 查 config 裡寫的名字會匹配到哪支字型檔&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">fc-match &lt;span class="s2">&amp;#34;:lang=zh-tw&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 查系統有沒有可用的繁體中文字型&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="fallback-chain">Fallback chain&lt;/h2>
&lt;p>應用程式在 config 裡指定字族名（如 &lt;code>MesloLGS Nerd Font&lt;/code>），fontconfig 依以下順序處理：&lt;/p>
&lt;ol>
&lt;li>在已知字型中找&lt;strong>完全匹配&lt;/strong>的字族&lt;/li>
&lt;li>找不到就沿 fallback chain 往下找候選——fontconfig 的預設 fallback 規則定義在 &lt;code>/etc/fonts/conf.d/&lt;/code> 的 XML 設定檔中&lt;/li>
&lt;li>CJK fallback 依語言優先序決定——&lt;code>fc-match &amp;quot;:lang=zh-tw&amp;quot;&lt;/code> 回的是 fontconfig 認為最適合該語言的字型&lt;/li>
&lt;/ol>
&lt;p>Nerd Font（MesloLGS、JetBrainsMono 等）只含 Latin 字元與圖示 glyph，CJK 字元靠 fallback 到另一支字型（如 &lt;code>noto-fonts-cjk&lt;/code>）補齊。fontconfig 的 fallback 對應用程式透明——應用程式只指定主字型，缺字時 fontconfig 自動補。&lt;/p>
&lt;h2 id="系統快取">系統快取&lt;/h2>
&lt;p>fontconfig 把字型目錄的掃描結果存成快取檔，避免每次查詢都重新掃描整個檔案系統：&lt;/p>
&lt;ul>
&lt;li>系統層快取：&lt;code>/var/cache/fontconfig/&lt;/code>&lt;/li>
&lt;li>使用者層快取：&lt;code>~/.cache/fontconfig/&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>套件管理器安裝字型時，post-install hook 會自動執行 &lt;code>fc-cache&lt;/code> 更新系統快取（pacman 安裝完會印 &lt;code>Updating fontconfig cache&lt;/code>）。手動把字型檔放進 &lt;code>~/.local/share/fonts/&lt;/code> 時需要自己跑 &lt;code>fc-cache&lt;/code>——不跑的話 fontconfig 看不到新字型。&lt;/p>
&lt;p>&lt;code>fc-cache -f&lt;/code> 的 &lt;code>-f&lt;/code> 是 force，忽略時間戳全部重建；不加 &lt;code>-f&lt;/code> 只更新有變動的目錄。兩者都只動系統快取層——已啟動的 process 記憶體中的字型清單不受影響，那是另一個層級的問題（見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">font-availability-at-startup&lt;/a>）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>字型安裝方式：&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">終端機與編輯器&lt;/a>的字型管理段&lt;/li>
&lt;li>裝了字型但應用程式還是看不到：&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">font-availability-at-startup&lt;/a>（process 啟動時快照的時序問題）&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>fontconfig 是 Linux 上統一管理字型搜尋、匹配與 fallback 的底層服務。應用程式透過 fontconfig 的 API 查詢可用字型，而非自行掃描字型目錄——無論是終端機、狀態列、通知 daemon 還是瀏覽器，底層都走同一套查詢介面。</p>
<h2 id="fc--工具分工">fc-* 工具分工</h2>
<p>fontconfig 附帶一組命令列工具，各自負責一件事：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>用途</th>
          <th>常用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>fc-list</code></td>
          <td>列出系統已知的所有字型（字族名、檔案路徑）</td>
          <td>確認某支字型有沒有裝、查實際字族名</td>
      </tr>
      <tr>
          <td><code>fc-match</code></td>
          <td>查詢指定條件的最佳匹配結果</td>
          <td>確認 config 裡寫的字族名會匹配到哪支字</td>
      </tr>
      <tr>
          <td><code>fc-cache</code></td>
          <td>重建 fontconfig 的系統快取</td>
          <td>手動放字型檔後更新快取（套件安裝通常自動跑）</td>
      </tr>
      <tr>
          <td><code>fc-pattern</code></td>
          <td>印出字型的完整屬性（除錯用）</td>
          <td>查字型支援的語言、字重、字形變體</td>
      </tr>
  </tbody>
</table>
<p><code>fc-list</code> 和 <code>fc-match</code> 每次執行都是新 process，讀到的是當下最新的系統快取。這跟已啟動的長駐程式不同——長駐程式的字型清單是啟動時的快照，詳見 <a href="/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">font-availability-at-startup</a>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">fc-list <span class="p">|</span> grep -i meslo
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 確認 MesloLGS Nerd Font 有沒有裝、實際字族名是什麼</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">fc-match <span class="s2">&#34;MesloLGS Nerd Font&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 查 config 裡寫的名字會匹配到哪支字型檔</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl">fc-match <span class="s2">&#34;:lang=zh-tw&#34;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># 查系統有沒有可用的繁體中文字型</span></span></span></code></pre></div><h2 id="fallback-chain">Fallback chain</h2>
<p>應用程式在 config 裡指定字族名（如 <code>MesloLGS Nerd Font</code>），fontconfig 依以下順序處理：</p>
<ol>
<li>在已知字型中找<strong>完全匹配</strong>的字族</li>
<li>找不到就沿 fallback chain 往下找候選——fontconfig 的預設 fallback 規則定義在 <code>/etc/fonts/conf.d/</code> 的 XML 設定檔中</li>
<li>CJK fallback 依語言優先序決定——<code>fc-match &quot;:lang=zh-tw&quot;</code> 回的是 fontconfig 認為最適合該語言的字型</li>
</ol>
<p>Nerd Font（MesloLGS、JetBrainsMono 等）只含 Latin 字元與圖示 glyph，CJK 字元靠 fallback 到另一支字型（如 <code>noto-fonts-cjk</code>）補齊。fontconfig 的 fallback 對應用程式透明——應用程式只指定主字型，缺字時 fontconfig 自動補。</p>
<h2 id="系統快取">系統快取</h2>
<p>fontconfig 把字型目錄的掃描結果存成快取檔，避免每次查詢都重新掃描整個檔案系統：</p>
<ul>
<li>系統層快取：<code>/var/cache/fontconfig/</code></li>
<li>使用者層快取：<code>~/.cache/fontconfig/</code></li>
</ul>
<p>套件管理器安裝字型時，post-install hook 會自動執行 <code>fc-cache</code> 更新系統快取（pacman 安裝完會印 <code>Updating fontconfig cache</code>）。手動把字型檔放進 <code>~/.local/share/fonts/</code> 時需要自己跑 <code>fc-cache</code>——不跑的話 fontconfig 看不到新字型。</p>
<p><code>fc-cache -f</code> 的 <code>-f</code> 是 force，忽略時間戳全部重建；不加 <code>-f</code> 只更新有變動的目錄。兩者都只動系統快取層——已啟動的 process 記憶體中的字型清單不受影響，那是另一個層級的問題（見 <a href="/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">font-availability-at-startup</a>）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>字型安裝方式：<a href="/blog/linux/dotfile/03-terminal-ecosystem/" data-link-title="模組三：終端機與編輯器" data-link-desc="終端機相關工具的配置檔散落在不同位置、不確定哪些該進 dotfile repo 時回來讀">終端機與編輯器</a>的字型管理段</li>
<li>裝了字型但應用程式還是看不到：<a href="/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">font-availability-at-startup</a>（process 啟動時快照的時序問題）</li>
</ul>
]]></content:encoded></item><item><title>Dotfile 術語卡</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/</guid><description>&lt;p>本系列使用的關鍵術語。各卡片會在對應章節深入說明、這裡提供快速查閱入口。&lt;/p>
&lt;p>術語卡會隨教材擴展逐步補充。&lt;/p>
&lt;h2 id="語言與工具">語言與工具&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>卡片&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/lua-scripting-language/" data-link-title="Lua 腳本語言" data-link-desc="在 Hyprland 或 Neovim 配置檔遇到 Lua 語法看不懂時回來讀 — 配置檔需要的最小 Lua 知識">Lua 腳本語言&lt;/a>&lt;/td>
 &lt;td>Hyprland / Neovim 配置檔使用的腳本語言，配置檔需要的最小知識&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/gnu-stow/" data-link-title="GNU Stow" data-link-desc="dotfile 管理文章裡提到 stow、symlink、package 看不懂時回來讀 — stow 的核心概念和常用指令">GNU Stow&lt;/a>&lt;/td>
 &lt;td>symlink farm manager，dotfile 管理的核心工具之一&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="系統概念">系統概念&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>卡片&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/tty/" data-link-title="TTY" data-link-desc="恢復操作提到切 TTY 但不知道 TTY 是什麼時讀 — Linux 核心直接提供的純文字終端機介面">TTY&lt;/a>&lt;/td>
 &lt;td>Linux 核心的純文字終端機介面，桌面故障時的救生通道&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/initramfs/" data-link-title="initramfs" data-link-desc="看到 ESP 大小要算進 initramfs、或開機卡在掛載 root 之前、不知道 initramfs 是什麼時讀 — 開機初期掛真 root 之前的臨時根檔系統">initramfs&lt;/a>&lt;/td>
 &lt;td>開機初期掛真 root 之前的臨時根檔系統，ESP 大小要算進它&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/" data-link-title="UEFI 開機鏈" data-link-desc="在 bootloader 選型（GRUB / EFISTUB / systemd-boot）卡住、或機器重開後找不到 kernel、需要理解韌體怎麼找到並載入系統時讀 — 韌體到 kernel 的交棒過程">UEFI 開機鏈&lt;/a>&lt;/td>
 &lt;td>韌體到 kernel 的交棒過程，bootloader 選型與開機故障的依據&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/partition-identification/" data-link-title="分區識別（PARTUUID / FSUUID）" data-link-desc="在 fstab 或 bootloader 設定要指定一個分區、不確定該用 PARTUUID、UUID 還是 /dev/sda1、或重格式化後系統開不了機時讀 — 分區的穩定識別方式">分區識別（PARTUUID / FSUUID）&lt;/a>&lt;/td>
 &lt;td>分區的穩定識別方式，fstab / bootloader 怎麼指涉分區&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">字型的可用集合在 process 啟動時決定&lt;/a>&lt;/td>
 &lt;td>裝了字型但畫面還是豆腐時的判讀依據&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock&lt;/a>&lt;/td>
 &lt;td>鎖屏是 compositor 持有的安全狀態，殺 process 不等於解鎖&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/compositor/" data-link-title="Compositor（合成器）" data-link-desc="教材反覆出現 compositor / 合成器、想確認它到底負責什麼、跟 window manager 和桌面環境差在哪時讀 — Wayland 下把畫面合成與視窗管理合一的核心程式">Compositor（合成器）&lt;/a>&lt;/td>
 &lt;td>Wayland 下把畫面合成與視窗管理合一的核心程式，多個系統狀態的持有者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/fontconfig/" data-link-title="fontconfig — 字型搜尋、匹配與 fallback 服務" data-link-desc="不確定 fc-list / fc-match / fc-cache 各做什麼、或 fontconfig fallback 機制怎麼運作時回來讀">fontconfig&lt;/a>&lt;/td>
 &lt;td>字型搜尋、匹配與 fallback 的底層服務，fc-* 工具分工&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="文化與術語">文化與術語&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>卡片&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/rice/" data-link-title="Rice（桌面視覺客製化）" data-link-desc="Linux 桌面文章裡看到 rice / ricing / ricer 不確定意思時回來讀">Rice（桌面視覺客製化）&lt;/a>&lt;/td>
 &lt;td>Linux 桌面社群的視覺客製化文化，詞源和涵蓋範圍&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<p>本系列使用的關鍵術語。各卡片會在對應章節深入說明、這裡提供快速查閱入口。</p>
<p>術語卡會隨教材擴展逐步補充。</p>
<h2 id="語言與工具">語言與工具</h2>
<table>
  <thead>
      <tr>
          <th>卡片</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/knowledge-cards/lua-scripting-language/" data-link-title="Lua 腳本語言" data-link-desc="在 Hyprland 或 Neovim 配置檔遇到 Lua 語法看不懂時回來讀 — 配置檔需要的最小 Lua 知識">Lua 腳本語言</a></td>
          <td>Hyprland / Neovim 配置檔使用的腳本語言，配置檔需要的最小知識</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/knowledge-cards/gnu-stow/" data-link-title="GNU Stow" data-link-desc="dotfile 管理文章裡提到 stow、symlink、package 看不懂時回來讀 — stow 的核心概念和常用指令">GNU Stow</a></td>
          <td>symlink farm manager，dotfile 管理的核心工具之一</td>
      </tr>
  </tbody>
</table>
<h2 id="系統概念">系統概念</h2>
<table>
  <thead>
      <tr>
          <th>卡片</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/knowledge-cards/tty/" data-link-title="TTY" data-link-desc="恢復操作提到切 TTY 但不知道 TTY 是什麼時讀 — Linux 核心直接提供的純文字終端機介面">TTY</a></td>
          <td>Linux 核心的純文字終端機介面，桌面故障時的救生通道</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/knowledge-cards/initramfs/" data-link-title="initramfs" data-link-desc="看到 ESP 大小要算進 initramfs、或開機卡在掛載 root 之前、不知道 initramfs 是什麼時讀 — 開機初期掛真 root 之前的臨時根檔系統">initramfs</a></td>
          <td>開機初期掛真 root 之前的臨時根檔系統，ESP 大小要算進它</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/knowledge-cards/uefi-boot-chain/" data-link-title="UEFI 開機鏈" data-link-desc="在 bootloader 選型（GRUB / EFISTUB / systemd-boot）卡住、或機器重開後找不到 kernel、需要理解韌體怎麼找到並載入系統時讀 — 韌體到 kernel 的交棒過程">UEFI 開機鏈</a></td>
          <td>韌體到 kernel 的交棒過程，bootloader 選型與開機故障的依據</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/knowledge-cards/partition-identification/" data-link-title="分區識別（PARTUUID / FSUUID）" data-link-desc="在 fstab 或 bootloader 設定要指定一個分區、不確定該用 PARTUUID、UUID 還是 /dev/sda1、或重格式化後系統開不了機時讀 — 分區的穩定識別方式">分區識別（PARTUUID / FSUUID）</a></td>
          <td>分區的穩定識別方式，fstab / bootloader 怎麼指涉分區</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/knowledge-cards/font-availability-at-startup/" data-link-title="字型的可用集合在 process 啟動時決定" data-link-desc="裝了字型但應用程式 / 狀態列 / 通知還是看不到、還是豆腐時回來讀">字型的可用集合在 process 啟動時決定</a></td>
          <td>裝了字型但畫面還是豆腐時的判讀依據</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock</a></td>
          <td>鎖屏是 compositor 持有的安全狀態，殺 process 不等於解鎖</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/knowledge-cards/compositor/" data-link-title="Compositor（合成器）" data-link-desc="教材反覆出現 compositor / 合成器、想確認它到底負責什麼、跟 window manager 和桌面環境差在哪時讀 — Wayland 下把畫面合成與視窗管理合一的核心程式">Compositor（合成器）</a></td>
          <td>Wayland 下把畫面合成與視窗管理合一的核心程式，多個系統狀態的持有者</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/knowledge-cards/fontconfig/" data-link-title="fontconfig — 字型搜尋、匹配與 fallback 服務" data-link-desc="不確定 fc-list / fc-match / fc-cache 各做什麼、或 fontconfig fallback 機制怎麼運作時回來讀">fontconfig</a></td>
          <td>字型搜尋、匹配與 fallback 的底層服務，fc-* 工具分工</td>
      </tr>
  </tbody>
</table>
<h2 id="文化與術語">文化與術語</h2>
<table>
  <thead>
      <tr>
          <th>卡片</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/knowledge-cards/rice/" data-link-title="Rice（桌面視覺客製化）" data-link-desc="Linux 桌面文章裡看到 rice / ricing / ricer 不確定意思時回來讀">Rice（桌面視覺客製化）</a></td>
          <td>Linux 桌面社群的視覺客製化文化，詞源和涵蓋範圍</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>模組零：Dotfile 心智模型</title><link>https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/</guid><description>&lt;p>Dotfile 管理的核心能力是&lt;strong>環境可重現性&lt;/strong>：把個人開發環境的配置狀態變成版控下的代碼，讓任何一台空白機器都能用一份 Git repo 還原成你熟悉的工作桌面。&lt;/p>
&lt;p>Unix 系統用檔名開頭的 &lt;code>.&lt;/code> 標記隱藏檔。shell 配置（&lt;code>.bashrc&lt;/code>、&lt;code>.zshrc&lt;/code>）、Git 設定（&lt;code>.gitconfig&lt;/code>）、SSH 設定（&lt;code>.ssh/config&lt;/code>）、以及 &lt;code>~/.config/&lt;/code> 底下各種工具的配置目錄，都屬於這個範疇。「dotfile 管理」指的是把這些散落在家目錄各處的配置檔集中到一個 Git repo，建立版本歷史、可以跨機器同步、可以在新環境一鍵部署。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/environment-reproducibility/" data-link-title="環境可重現性與配置分類" data-link-desc="想釐清哪些配置該進 dotfile repo、哪些不該進時回來讀">環境可重現性與配置分類&lt;/a>&lt;/td>
 &lt;td>為什麼要管理 dotfile、哪些東西該進 repo、核心層 / 工具層 / 桌面層的分類判讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/dotfile-iac-parallel/" data-link-title="Dotfile 跟 Infra IaC 的平行關係" data-link-desc="想理解 dotfile 管理在工程實踐裡的定位、或釐清「重建指令」跟「備份」的差異時回來讀">Dotfile 跟 Infra IaC 的平行關係&lt;/a>&lt;/td>
 &lt;td>兩者共用的原則與差異、「重建指令不是備份」的心智模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">環境建置的操作順序&lt;/a>&lt;/td>
 &lt;td>第一次建環境時先做什麼後做什麼——依賴順序路線圖，每步附對應模組連結&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構&lt;/a>：怎麼把散落的配置檔收進 Git repo&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建&lt;/a>：環境重建的完整流程&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/09-team-environment/" data-link-title="模組九：從個人到團隊" data-link-desc="個人 dotfile 管理的思想要延伸到團隊開發環境標準化時回來讀 — devcontainer、nix、商業環境配置管理">模組九：從個人到團隊&lt;/a>：dotfile 思想往團隊環境延伸&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/" data-link-title="Infra 基礎設施建置指南" data-link-desc="從零循序漸進把雲端基礎設施做起來 — IaC、身分憑證、網路地基、環境分離、核心服務、可觀測性、自動化 review 與治理習慣，含怎麼在組織內推動">Infra 基礎設施建置指南&lt;/a>：組織層級的環境 as code&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Dotfile 管理的核心能力是<strong>環境可重現性</strong>：把個人開發環境的配置狀態變成版控下的代碼，讓任何一台空白機器都能用一份 Git repo 還原成你熟悉的工作桌面。</p>
<p>Unix 系統用檔名開頭的 <code>.</code> 標記隱藏檔。shell 配置（<code>.bashrc</code>、<code>.zshrc</code>）、Git 設定（<code>.gitconfig</code>）、SSH 設定（<code>.ssh/config</code>）、以及 <code>~/.config/</code> 底下各種工具的配置目錄，都屬於這個範疇。「dotfile 管理」指的是把這些散落在家目錄各處的配置檔集中到一個 Git repo，建立版本歷史、可以跨機器同步、可以在新環境一鍵部署。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/00-dotfile-mindset/environment-reproducibility/" data-link-title="環境可重現性與配置分類" data-link-desc="想釐清哪些配置該進 dotfile repo、哪些不該進時回來讀">環境可重現性與配置分類</a></td>
          <td>為什麼要管理 dotfile、哪些東西該進 repo、核心層 / 工具層 / 桌面層的分類判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/00-dotfile-mindset/dotfile-iac-parallel/" data-link-title="Dotfile 跟 Infra IaC 的平行關係" data-link-desc="想理解 dotfile 管理在工程實踐裡的定位、或釐清「重建指令」跟「備份」的差異時回來讀">Dotfile 跟 Infra IaC 的平行關係</a></td>
          <td>兩者共用的原則與差異、「重建指令不是備份」的心智模型</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/00-dotfile-mindset/setup-order-guide/" data-link-title="環境建置的操作順序" data-link-desc="第一次從零建立 Linux 或 macOS 開發環境、不確定先做什麼後做什麼時讀 — 依賴順序路線圖，每一步附對應模組連結">環境建置的操作順序</a></td>
          <td>第一次建環境時先做什麼後做什麼——依賴順序路線圖，每步附對應模組連結</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一：管理工具與目錄結構</a>：怎麼把散落的配置檔收進 Git repo</li>
<li>→ <a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建</a>：環境重建的完整流程</li>
<li>→ <a href="/blog/linux/dotfile/09-team-environment/" data-link-title="模組九：從個人到團隊" data-link-desc="個人 dotfile 管理的思想要延伸到團隊開發環境標準化時回來讀 — devcontainer、nix、商業環境配置管理">模組九：從個人到團隊</a>：dotfile 思想往團隊環境延伸</li>
<li>→ <a href="/blog/infra/" data-link-title="Infra 基礎設施建置指南" data-link-desc="從零循序漸進把雲端基礎設施做起來 — IaC、身分憑證、網路地基、環境分離、核心服務、可觀測性、自動化 review 與治理習慣，含怎麼在組織內推動">Infra 基礎設施建置指南</a>：組織層級的環境 as code</li>
</ul>
]]></content:encoded></item></channel></rss>