<?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>Terminal on Tarragon</title><link>https://tarrragon.github.io/blog/tags/terminal/</link><description>Recent content in Terminal 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/terminal/index.xml" rel="self" type="application/rss+xml"/><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>遠端連線與終端機問題</title><link>https://tarrragon.github.io/blog/linux/debug/ssh-and-terminal-troubleshooting/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/debug/ssh-and-terminal-troubleshooting/</guid><description>&lt;p>遠端操作 Linux 時，很多問題出在「你的終端機」與「遠端 session」之間那條連線的狀態，而不在遠端那台機器本身。終端機被上一個程式留在奇怪的模式、字元編碼與終端機能力沒對上、或你想從一條純文字的 SSH 連線去驅動一個需要實體螢幕的圖形桌面——這些問題的共同點是：現象發生在連線的某一層，判斷對是哪一層，修復就很直接。&lt;/p>
&lt;p>SSH「連不上」本身（&lt;code>Permission denied&lt;/code>、&lt;code>Host key verification failed&lt;/code>、&lt;code>Connection refused&lt;/code>）的判讀與修復，見 &lt;a href="../../install/ssh-keyless-bootstrap/">外部連入與無 key 的 bootstrap 路徑&lt;/a> 的重連段落。這篇處理的是「連上了、但終端機或 session 的狀態不對」的那些情況。&lt;/p>
&lt;h2 id="ssh-斷線後本機終端機噴亂碼狂跳字元">SSH 斷線後本機終端機噴亂碼、狂跳字元&lt;/h2>
&lt;p>一個嚇人但無害的情況：SSH 連線被中斷後，你本機的終端機開始瘋狂輸出像 &lt;code>&amp;lt;35;80;24M&lt;/code> 這樣的序列，尤其在你移動滑鼠時狂跳。這不是遠端機器在打字，是&lt;strong>你本機的終端機被卡在滑鼠回報模式&lt;/strong>。&lt;/p>
&lt;p>判讀關鍵在「什麼時候噴」：如果那串亂碼只在你移動滑鼠時出現、而且形如 &lt;code>數字;數字M&lt;/code>，那就是滑鼠座標回報。成因是遠端跑的某個全螢幕程式（TUI、編輯器、終端機多工器）啟動時對終端機開了滑鼠追蹤模式，SSH 被硬斷時它來不及送出「關閉滑鼠模式」的序列就死了，於是你本機終端機還停在回報模式，滑鼠一動就把游標座標當輸入送進來。&lt;/p>
&lt;p>修復是重置終端機的模式，跟遠端機器無關：&lt;/p>
&lt;ul>
&lt;li>最快：開一個新的終端機分頁 / 視窗。模式是「那個終端機 session」的狀態，新視窗是乾淨的。&lt;/li>
&lt;li>救現有視窗：先把滑鼠移開別動（洪流會停），盲打 &lt;code>reset&lt;/code> 再 Enter，送出終端機重置。&lt;/li>
&lt;li>若 &lt;code>reset&lt;/code> 沒清掉，補送關閉滑鼠回報的序列：&lt;code>printf '\033[?1000l\033[?1002l\033[?1003l\033[?1006l'&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>同一類的還有「alternate screen 沒還原」——遠端的全螢幕程式異常結束時，本機終端機可能卡在替代畫面緩衝區，看起來像畫面清空或凍結。&lt;code>reset&lt;/code> 同樣能救。歸納起來：&lt;strong>SSH 被硬斷後本機終端機行為異常，先懷疑「對端程式來不及還原終端機模式」，用 &lt;code>reset&lt;/code> 或開新視窗處理本機終端機狀態，不必急著重連遠端。&lt;/strong>&lt;/p>
&lt;h2 id="遠端打字變亂碼重複位置錯亂">遠端打字變亂碼、重複、位置錯亂&lt;/h2>
&lt;p>連上遠端後，如果互動式輸入變得不對——打一個字出現好幾個、游標位置錯亂、畫面重繪殘影——通常是兩層問題之一，判讀方式是分開排除。&lt;/p>
&lt;p>第一層是&lt;strong>字元編碼（locale）&lt;/strong>。從某些本機（例如 macOS）SSH 進 Linux 時，本機會把 &lt;code>LC_CTYPE&lt;/code> 之類的變數帶過去；如果遠端沒有對應的 locale、就會退回 POSIX/C locale，讓終端機的行編輯（ZLE、readline）對多位元組字元的寬度判斷出錯，表現為輸入重複或錯位。判斷方式是在遠端 &lt;code>locale&lt;/code> 看目前值、&lt;code>locale -a&lt;/code> 看有沒有裝對應的 UTF-8 locale。修法是在遠端明確設好 &lt;code>LANG&lt;/code> / &lt;code>LC_CTYPE&lt;/code> 到一個實際存在的 UTF-8 locale，而不是讓它繼承一個遠端不認得的值。&lt;/p>
&lt;p>第二層是&lt;strong>終端機能力資料庫（terminfo）&lt;/strong>。你本機終端機的 &lt;code>TERM&lt;/code> 值（例如某些新終端機用 &lt;code>xterm-ghostty&lt;/code> 之類的自訂值）如果在遠端沒有對應的 terminfo 條目，遠端程式就不知道怎麼正確地清行、移動游標、重繪，畫面就會亂。判斷方式是在遠端 &lt;code>echo $TERM&lt;/code> 看值、&lt;code>infocmp $TERM&lt;/code> 看遠端認不認得。修法是把本機的 terminfo 條目送過去讓遠端安裝：&lt;code>infocmp -x $TERM | ssh &amp;lt;遠端&amp;gt; 'tic -x -'&lt;/code>。&lt;/p>
&lt;p>先分清是 locale 還是 terminfo，兩者症狀相似但修法不同：locale 是編碼寬度、terminfo 是繪製指令。查 &lt;code>locale&lt;/code> 跟查 &lt;code>$TERM&lt;/code> + &lt;code>infocmp&lt;/code> 就能分開。&lt;/p>
&lt;h2 id="從-ssh-操控遠端的圖形桌面">從 SSH 操控遠端的圖形桌面&lt;/h2>
&lt;p>想從一條純文字的 SSH 連線去操作遠端的 Wayland 圖形桌面（例如啟動應用、截圖、送 IPC 指令）時，會撞到兩類界線，判斷對是哪一類就知道怎麼繞。&lt;/p>
&lt;p>第一類是&lt;strong>圖形程式需要知道連到哪個顯示&lt;/strong>。SSH 進來的 shell 預設沒有圖形環境的環境變數，直接跑圖形程式會找不到 display。要對著遠端那個已經在跑的 Wayland session 操作，得補上它的環境變數：&lt;code>XDG_RUNTIME_DIR&lt;/code>（通常 &lt;code>/run/user/&amp;lt;uid&amp;gt;&lt;/code>）、&lt;code>WAYLAND_DISPLAY&lt;/code>（socket 名，如 &lt;code>wayland-1&lt;/code>）、必要時還有該 compositor 的 instance 變數與 &lt;code>DBUS_SESSION_BUS_ADDRESS&lt;/code>。這些值怎麼撈：socket 名用 &lt;code>ls /run/user/$(id -u)/wayland-*&lt;/code> 看；其餘變數直接從那個圖形 session 既有行程的環境複製最準——&lt;code>cat /proc/&amp;lt;compositor-pid&amp;gt;/environ | tr '\0' '\n' | grep -E 'WAYLAND_DISPLAY|XDG_RUNTIME_DIR|DBUS_SESSION|_INSTANCE_'&lt;/code>（&lt;code>&amp;lt;compositor-pid&amp;gt;&lt;/code> 用 &lt;code>pgrep -x Hyprland&lt;/code> 之類找）。撈到後 &lt;code>export&lt;/code> 進當前 SSH shell，這條連線就能對遠端的圖形 session 下指令、&lt;code>grim&lt;/code> 截圖。&lt;/p>
&lt;p>第二類是&lt;strong>有些東西必須從實體圖形終端機（VT，即 &lt;code>Ctrl+Alt+F1&lt;/code>~&lt;code>F6&lt;/code> 切換的那些文字主控台）啟動，SSH 的 pty 起不來&lt;/strong>。Wayland 的合成器（compositor，畫桌面、把視窗合成到螢幕、管輸入輸出的核心程式，如 Hyprland）需要一個真正的圖形 VT 上的登入 session，拿到 DRM master（對顯示卡的獨佔繪圖控制權）與 logind seat（一組綁在一起的實體螢幕／鍵鼠裝置）才能啟動；從 SSH 的 pty 起它的&lt;strong>預設 backend&lt;/strong> 會直接失敗（例如報 backend 建立失敗），因為預設 backend 要的 DRM master 與 seat 在 SSH 這條連線上不存在。判讀訊號：合成器一啟動就報 seat / DRM / backend 相關的錯，而你是從 SSH 起的——那就是這個界線。（例外：合成器多半有 headless backend，例如設 &lt;code>WLR_BACKENDS=headless&lt;/code> 就不要 DRM master、不需 VT，專給 CI、雲端、自動化測試用；nested（跑在另一個 Wayland session 裡）也不需要。所以精確說是「預設 backend 需要圖形 VT」，不是「合成器一定起不來」。）&lt;/p></description><content:encoded><![CDATA[<p>遠端操作 Linux 時，很多問題出在「你的終端機」與「遠端 session」之間那條連線的狀態，而不在遠端那台機器本身。終端機被上一個程式留在奇怪的模式、字元編碼與終端機能力沒對上、或你想從一條純文字的 SSH 連線去驅動一個需要實體螢幕的圖形桌面——這些問題的共同點是：現象發生在連線的某一層，判斷對是哪一層，修復就很直接。</p>
<p>SSH「連不上」本身（<code>Permission denied</code>、<code>Host key verification failed</code>、<code>Connection refused</code>）的判讀與修復，見 <a href="../../install/ssh-keyless-bootstrap/">外部連入與無 key 的 bootstrap 路徑</a> 的重連段落。這篇處理的是「連上了、但終端機或 session 的狀態不對」的那些情況。</p>
<h2 id="ssh-斷線後本機終端機噴亂碼狂跳字元">SSH 斷線後本機終端機噴亂碼、狂跳字元</h2>
<p>一個嚇人但無害的情況：SSH 連線被中斷後，你本機的終端機開始瘋狂輸出像 <code>&lt;35;80;24M</code> 這樣的序列，尤其在你移動滑鼠時狂跳。這不是遠端機器在打字，是<strong>你本機的終端機被卡在滑鼠回報模式</strong>。</p>
<p>判讀關鍵在「什麼時候噴」：如果那串亂碼只在你移動滑鼠時出現、而且形如 <code>數字;數字M</code>，那就是滑鼠座標回報。成因是遠端跑的某個全螢幕程式（TUI、編輯器、終端機多工器）啟動時對終端機開了滑鼠追蹤模式，SSH 被硬斷時它來不及送出「關閉滑鼠模式」的序列就死了，於是你本機終端機還停在回報模式，滑鼠一動就把游標座標當輸入送進來。</p>
<p>修復是重置終端機的模式，跟遠端機器無關：</p>
<ul>
<li>最快：開一個新的終端機分頁 / 視窗。模式是「那個終端機 session」的狀態，新視窗是乾淨的。</li>
<li>救現有視窗：先把滑鼠移開別動（洪流會停），盲打 <code>reset</code> 再 Enter，送出終端機重置。</li>
<li>若 <code>reset</code> 沒清掉，補送關閉滑鼠回報的序列：<code>printf '\033[?1000l\033[?1002l\033[?1003l\033[?1006l'</code>。</li>
</ul>
<p>同一類的還有「alternate screen 沒還原」——遠端的全螢幕程式異常結束時，本機終端機可能卡在替代畫面緩衝區，看起來像畫面清空或凍結。<code>reset</code> 同樣能救。歸納起來：<strong>SSH 被硬斷後本機終端機行為異常，先懷疑「對端程式來不及還原終端機模式」，用 <code>reset</code> 或開新視窗處理本機終端機狀態，不必急著重連遠端。</strong></p>
<h2 id="遠端打字變亂碼重複位置錯亂">遠端打字變亂碼、重複、位置錯亂</h2>
<p>連上遠端後，如果互動式輸入變得不對——打一個字出現好幾個、游標位置錯亂、畫面重繪殘影——通常是兩層問題之一，判讀方式是分開排除。</p>
<p>第一層是<strong>字元編碼（locale）</strong>。從某些本機（例如 macOS）SSH 進 Linux 時，本機會把 <code>LC_CTYPE</code> 之類的變數帶過去；如果遠端沒有對應的 locale、就會退回 POSIX/C locale，讓終端機的行編輯（ZLE、readline）對多位元組字元的寬度判斷出錯，表現為輸入重複或錯位。判斷方式是在遠端 <code>locale</code> 看目前值、<code>locale -a</code> 看有沒有裝對應的 UTF-8 locale。修法是在遠端明確設好 <code>LANG</code> / <code>LC_CTYPE</code> 到一個實際存在的 UTF-8 locale，而不是讓它繼承一個遠端不認得的值。</p>
<p>第二層是<strong>終端機能力資料庫（terminfo）</strong>。你本機終端機的 <code>TERM</code> 值（例如某些新終端機用 <code>xterm-ghostty</code> 之類的自訂值）如果在遠端沒有對應的 terminfo 條目，遠端程式就不知道怎麼正確地清行、移動游標、重繪，畫面就會亂。判斷方式是在遠端 <code>echo $TERM</code> 看值、<code>infocmp $TERM</code> 看遠端認不認得。修法是把本機的 terminfo 條目送過去讓遠端安裝：<code>infocmp -x $TERM | ssh &lt;遠端&gt; 'tic -x -'</code>。</p>
<p>先分清是 locale 還是 terminfo，兩者症狀相似但修法不同：locale 是編碼寬度、terminfo 是繪製指令。查 <code>locale</code> 跟查 <code>$TERM</code> + <code>infocmp</code> 就能分開。</p>
<h2 id="從-ssh-操控遠端的圖形桌面">從 SSH 操控遠端的圖形桌面</h2>
<p>想從一條純文字的 SSH 連線去操作遠端的 Wayland 圖形桌面（例如啟動應用、截圖、送 IPC 指令）時，會撞到兩類界線，判斷對是哪一類就知道怎麼繞。</p>
<p>第一類是<strong>圖形程式需要知道連到哪個顯示</strong>。SSH 進來的 shell 預設沒有圖形環境的環境變數，直接跑圖形程式會找不到 display。要對著遠端那個已經在跑的 Wayland session 操作，得補上它的環境變數：<code>XDG_RUNTIME_DIR</code>（通常 <code>/run/user/&lt;uid&gt;</code>）、<code>WAYLAND_DISPLAY</code>（socket 名，如 <code>wayland-1</code>）、必要時還有該 compositor 的 instance 變數與 <code>DBUS_SESSION_BUS_ADDRESS</code>。這些值怎麼撈：socket 名用 <code>ls /run/user/$(id -u)/wayland-*</code> 看；其餘變數直接從那個圖形 session 既有行程的環境複製最準——<code>cat /proc/&lt;compositor-pid&gt;/environ | tr '\0' '\n' | grep -E 'WAYLAND_DISPLAY|XDG_RUNTIME_DIR|DBUS_SESSION|_INSTANCE_'</code>（<code>&lt;compositor-pid&gt;</code> 用 <code>pgrep -x Hyprland</code> 之類找）。撈到後 <code>export</code> 進當前 SSH shell，這條連線就能對遠端的圖形 session 下指令、<code>grim</code> 截圖。</p>
<p>第二類是<strong>有些東西必須從實體圖形終端機（VT，即 <code>Ctrl+Alt+F1</code>~<code>F6</code> 切換的那些文字主控台）啟動，SSH 的 pty 起不來</strong>。Wayland 的合成器（compositor，畫桌面、把視窗合成到螢幕、管輸入輸出的核心程式，如 Hyprland）需要一個真正的圖形 VT 上的登入 session，拿到 DRM master（對顯示卡的獨佔繪圖控制權）與 logind seat（一組綁在一起的實體螢幕／鍵鼠裝置）才能啟動；從 SSH 的 pty 起它的<strong>預設 backend</strong> 會直接失敗（例如報 backend 建立失敗），因為預設 backend 要的 DRM master 與 seat 在 SSH 這條連線上不存在。判讀訊號：合成器一啟動就報 seat / DRM / backend 相關的錯，而你是從 SSH 起的——那就是這個界線。（例外：合成器多半有 headless backend，例如設 <code>WLR_BACKENDS=headless</code> 就不要 DRM master、不需 VT，專給 CI、雲端、自動化測試用；nested（跑在另一個 Wayland session 裡）也不需要。所以精確說是「預設 backend 需要圖形 VT」，不是「合成器一定起不來」。）</p>
<p>繞法是回到那台機器的實體圖形終端機去啟動 compositor，但「回到 VT」這件事也可以從 SSH 遠端做：</p>
<ul>
<li><code>sudo chvt &lt;N&gt;</code> 從 SSH 切換那台機器目前顯示的虛擬終端機到第 N 個，比在虛擬機視窗裡跟宿主的 <code>Ctrl+Alt+Fn</code> 快捷鍵搏鬥穩定。</li>
<li>切過去卻是空白、沒有登入提示，通常是那個 VT 上沒有 getty 在跑：<code>sudo systemctl start getty@tty&lt;N&gt;</code>（開機時 <code>enabled</code> 但 <code>inactive</code> 是常見狀態，logind 的 autovt 沒觸發）。</li>
<li><code>sudo fgconsole</code> 確認目前是哪個 VT 在前景。</li>
</ul>
<p>還有一個容易混淆的點：一台虛擬機可能同時有「序列主控台」跟「圖形顯示」兩個獨立輸出。在 guest 內 <code>chvt</code> 只切圖形那側，序列主控台看到的畫面不會變。如果你在虛擬機軟體裡看的是序列主控台，圖形桌面得切到顯示輸出那個 view 才看得到。判讀：切了 VT 但畫面沒反應，先確認你正在看的是哪個輸出。</p>
<h2 id="判讀路由">判讀路由</h2>
<p>遠端 / 終端機問題的分流：</p>
<ul>
<li>本機終端機噴亂碼、只在動滑鼠時噴 → 滑鼠回報模式沒關（本機終端機狀態），<code>reset</code> 或開新視窗。</li>
<li>遠端打字重複 / 錯位 → 分 locale（查 <code>locale</code>）與 terminfo（查 <code>$TERM</code> + <code>infocmp</code>）。</li>
<li>圖形程式在 SSH 下找不到 display → 補 <code>WAYLAND_DISPLAY</code> / <code>XDG_RUNTIME_DIR</code> 等環境變數。</li>
<li>compositor 從 SSH 起不來、報 seat/DRM 錯 → 它需要實體 VT，用 <code>chvt</code> + <code>getty@tty&lt;N&gt;</code> 回到圖形 VT 啟動。</li>
<li>SSH 連不上（拒絕 / host key / refused）→ 見 <a href="../../install/ssh-keyless-bootstrap/">外部連入與無 key 的 bootstrap 路徑</a>。</li>
</ul>
<p>這幾種分流的共同底線是先讀權威狀態（<code>locale</code>、<code>$TERM</code>、runtime 目錄、<code>loginctl</code>、<code>fgconsole</code>）再下判斷；背後的方法論見 <a href="../diagnosis-read-authoritative-state/">診斷心法</a>。</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>Terminal app 輸入設計</title><link>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/terminal-input-design/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/terminal-input-design/</guid><description>&lt;p>Terminal app 在手機上的輸入需求和一般文字輸入有根本差異。CLI 指令是結構化語法，路徑分隔符、flag 縮寫、管線符號都有精確語意 — 手機鍵盤為自然語言設計的自動行為（校正、建議、學習）在 CLI 場景中全部變成干擾。&lt;/p>
&lt;h2 id="cli-輸入的特殊性">CLI 輸入的特殊性&lt;/h2>
&lt;p>桌面終端機的鍵盤直接傳送按鍵事件，沒有中間的輸入法處理層。使用者按 &lt;code>l&lt;/code> 就是 &lt;code>l&lt;/code>，按 Tab 就是 Tab，按 Ctrl+C 就是 interrupt signal。&lt;/p>
&lt;p>手機鍵盤在使用者和 app 之間插入了 IME 層。使用者按 &lt;code>l&lt;/code> 時，IME 可能等待後續按鍵組合成完整詞彙再傳送；使用者按的按鍵可能被自動校正替換；使用者的輸入被記錄到 IME 詞庫供跨 app 學習。&lt;/p>
&lt;p>Terminal app 需要繞過或控制 IME 層的這些行為。app_tunnel 的 TextField 用 &lt;code>TextInputType.visiblePassword&lt;/code> + &lt;code>autocorrect: false&lt;/code> + &lt;code>enableSuggestions: false&lt;/code> + &lt;code>enableIMEPersonalizedLearning: false&lt;/code> 四個參數關閉 IME 的自動行為（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>）。&lt;/p>
&lt;h2 id="整行送出-vs-逐字元protocol-層的影響">整行送出 vs 逐字元：protocol 層的影響&lt;/h2>
&lt;p>整行送出和逐字元送出在 UI 層看起來只是「按 Enter 送出整行」和「每個按鍵即時送出」的差別，但在 protocol 層是兩種不同的通訊模式。&lt;/p>
&lt;h3 id="整行送出">整行送出&lt;/h3>
&lt;p>Client 端累積使用者輸入，使用者按 Enter 時傳送完整指令字串加換行符（&lt;code>ls -la\n&lt;/code>）。Server 端收到完整行後處理。&lt;/p>
&lt;p>Protocol 設計簡單：每個 WebSocket frame 是一個完整指令。Server 不需要管理部分輸入的狀態，也不需要即時回應 Tab 或方向鍵。&lt;/p>
&lt;p>代價：使用者無法在手機上使用 Tab 補全（Tab 被 IME 攔截或不存在）、無法用方向鍵在指令中移動游標（移動的是 TextField 的游標，不是 server 端的 readline 游標）。&lt;/p>
&lt;h3 id="逐字元送出">逐字元送出&lt;/h3>
&lt;p>Client 端每個按鍵即時傳送一個 WebSocket frame。Server 端的 shell 即時處理每個字元，包括 Tab 補全（server 回傳補全結果）、Ctrl+C（server 中斷當前程序）、方向鍵（server 端 readline 移動游標）。&lt;/p>
&lt;p>Protocol 設計複雜：每個按鍵一個 frame，frame 內容是單一字元或控制序列。Server 端必須維護 readline 狀態。Client 端必須正確編碼控制字元（Ctrl+C = 0x03, Tab = 0x09）。&lt;/p>
&lt;p>代價：protocol 複雜度高，每個按鍵都有網路延遲。在高延遲網路上輸入體驗差（打字後要等 round-trip 才看到回顯）。&lt;/p>
&lt;h3 id="決策在-protocol-層做">決策在 protocol 層做&lt;/h3>
&lt;p>app_tunnel 選擇整行送出，犧牲 Tab 補全換取簡單的 protocol 設計。這個決策應該在 protocol spec 階段做 — 因為它影響 server 端（ttyd）的行為預期和 client 端的 frame 格式。在 UI 實作時才臨時決定，可能和 server 端的行為預期不一致。&lt;/p>
&lt;h2 id="特殊按鍵的-ui-方案">特殊按鍵的 UI 方案&lt;/h2>
&lt;p>手機沒有 Esc、Tab、Ctrl、方向鍵。Terminal app 需要額外的 UI 元件提供這些按鍵。&lt;/p>
&lt;h3 id="底部工具列">底部工具列&lt;/h3>
&lt;p>固定在鍵盤上方的一排按鈕，提供常用特殊鍵。app_tunnel 的工具列包含 Esc、Tab、Ctrl、四個方向鍵。&lt;/p>
&lt;p>工具列的設計考量：按鈕大小（手指能精確觸碰的最小尺寸約 44x44 pt）、排列順序（最常用的放中間）、長按行為（長按 Ctrl 是否支援 Ctrl 組合鍵）。&lt;/p>
&lt;h3 id="ctrl-組合鍵">Ctrl 組合鍵&lt;/h3>
&lt;p>Ctrl+C（中斷）、Ctrl+D（EOF）、Ctrl+Z（暫停）在 CLI 操作中頻繁使用。手機上的實作方式通常是：按下 Ctrl 按鈕後進入「Ctrl 模式」，下一個按鍵自動加 Ctrl 前綴。&lt;/p></description><content:encoded><![CDATA[<p>Terminal app 在手機上的輸入需求和一般文字輸入有根本差異。CLI 指令是結構化語法，路徑分隔符、flag 縮寫、管線符號都有精確語意 — 手機鍵盤為自然語言設計的自動行為（校正、建議、學習）在 CLI 場景中全部變成干擾。</p>
<h2 id="cli-輸入的特殊性">CLI 輸入的特殊性</h2>
<p>桌面終端機的鍵盤直接傳送按鍵事件，沒有中間的輸入法處理層。使用者按 <code>l</code> 就是 <code>l</code>，按 Tab 就是 Tab，按 Ctrl+C 就是 interrupt signal。</p>
<p>手機鍵盤在使用者和 app 之間插入了 IME 層。使用者按 <code>l</code> 時，IME 可能等待後續按鍵組合成完整詞彙再傳送；使用者按的按鍵可能被自動校正替換；使用者的輸入被記錄到 IME 詞庫供跨 app 學習。</p>
<p>Terminal app 需要繞過或控制 IME 層的這些行為。app_tunnel 的 TextField 用 <code>TextInputType.visiblePassword</code> + <code>autocorrect: false</code> + <code>enableSuggestions: false</code> + <code>enableIMEPersonalizedLearning: false</code> 四個參數關閉 IME 的自動行為（<a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a>）。</p>
<h2 id="整行送出-vs-逐字元protocol-層的影響">整行送出 vs 逐字元：protocol 層的影響</h2>
<p>整行送出和逐字元送出在 UI 層看起來只是「按 Enter 送出整行」和「每個按鍵即時送出」的差別，但在 protocol 層是兩種不同的通訊模式。</p>
<h3 id="整行送出">整行送出</h3>
<p>Client 端累積使用者輸入，使用者按 Enter 時傳送完整指令字串加換行符（<code>ls -la\n</code>）。Server 端收到完整行後處理。</p>
<p>Protocol 設計簡單：每個 WebSocket frame 是一個完整指令。Server 不需要管理部分輸入的狀態，也不需要即時回應 Tab 或方向鍵。</p>
<p>代價：使用者無法在手機上使用 Tab 補全（Tab 被 IME 攔截或不存在）、無法用方向鍵在指令中移動游標（移動的是 TextField 的游標，不是 server 端的 readline 游標）。</p>
<h3 id="逐字元送出">逐字元送出</h3>
<p>Client 端每個按鍵即時傳送一個 WebSocket frame。Server 端的 shell 即時處理每個字元，包括 Tab 補全（server 回傳補全結果）、Ctrl+C（server 中斷當前程序）、方向鍵（server 端 readline 移動游標）。</p>
<p>Protocol 設計複雜：每個按鍵一個 frame，frame 內容是單一字元或控制序列。Server 端必須維護 readline 狀態。Client 端必須正確編碼控制字元（Ctrl+C = 0x03, Tab = 0x09）。</p>
<p>代價：protocol 複雜度高，每個按鍵都有網路延遲。在高延遲網路上輸入體驗差（打字後要等 round-trip 才看到回顯）。</p>
<h3 id="決策在-protocol-層做">決策在 protocol 層做</h3>
<p>app_tunnel 選擇整行送出，犧牲 Tab 補全換取簡單的 protocol 設計。這個決策應該在 protocol spec 階段做 — 因為它影響 server 端（ttyd）的行為預期和 client 端的 frame 格式。在 UI 實作時才臨時決定，可能和 server 端的行為預期不一致。</p>
<h2 id="特殊按鍵的-ui-方案">特殊按鍵的 UI 方案</h2>
<p>手機沒有 Esc、Tab、Ctrl、方向鍵。Terminal app 需要額外的 UI 元件提供這些按鍵。</p>
<h3 id="底部工具列">底部工具列</h3>
<p>固定在鍵盤上方的一排按鈕，提供常用特殊鍵。app_tunnel 的工具列包含 Esc、Tab、Ctrl、四個方向鍵。</p>
<p>工具列的設計考量：按鈕大小（手指能精確觸碰的最小尺寸約 44x44 pt）、排列順序（最常用的放中間）、長按行為（長按 Ctrl 是否支援 Ctrl 組合鍵）。</p>
<h3 id="ctrl-組合鍵">Ctrl 組合鍵</h3>
<p>Ctrl+C（中斷）、Ctrl+D（EOF）、Ctrl+Z（暫停）在 CLI 操作中頻繁使用。手機上的實作方式通常是：按下 Ctrl 按鈕後進入「Ctrl 模式」，下一個按鍵自動加 Ctrl 前綴。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>四維度決策表 → <a href="/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表</a></li>
<li>安全敏感輸入框的 IME 控制 → <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">IME 安全 checklist</a></li>
<li>表單場景的輸入設計 → <a href="/blog/ux-design/03-input-mechanism/form-ux-pattern/" data-link-title="表單 UX 模式" data-link-desc="表單輸入的驗證時機、auto-fill 支援、錯誤回饋設計 — 和 terminal 輸入的決策維度相同但選項不同">表單 UX 模式</a></li>
</ul>
]]></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>T.C3 ANSI parser 測試資料不覆蓋真實 shell output</title><link>https://tarrragon.github.io/blog/testing/cases/ansi-parser-test-data-blindspot/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/cases/ansi-parser-test-data-blindspot/</guid><description>&lt;p>這個案例的核心責任是說明 unit test 的輸入資料品質如何決定測試的有效性。Parser 邏輯正確、斷言正確、覆蓋率高 — 但測試資料是人工挑選的乾淨子集，跟真實環境的輸入分佈不同。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 的 &lt;code>AnsiParser&lt;/code> 負責解析終端機輸出的 ANSI escape 序列，轉換為帶色彩的文字 token。unit test 用手寫字串驗證：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 測試資料範例 — 乾淨的 SGR 色彩碼
&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="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;紅色文字&amp;#39;&lt;/span>&lt;span class="p">,&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="kd">final&lt;/span> &lt;span class="n">tokens&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">parser&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="se">\x1B&lt;/span>&lt;span class="s1">[31mhello&lt;/span>&lt;span class="se">\x1B&lt;/span>&lt;span class="s1">[0m&amp;#39;&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">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tokens&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">first&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">isA&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">TextToken&lt;/span>&lt;span class="o">&amp;gt;&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>真實 zsh prompt 啟動後送出的控制序列（擷取自實機 log）：&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">\x1B]0;user@host: ~\x07 ← OSC：設定終端機視窗標題
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">\x1B[?2004h ← CSI private mode：啟用括號貼上模式
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">\x1B[?1h ← CSI private mode：啟用應用程式游標鍵
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">\x1B(B ← 字元集指定：選擇 ASCII
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">\x1B[?25l ← CSI private mode：隱藏游標&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Parser 只認識 &lt;code>\x1B[{數字;數字}{字母}&lt;/code> 格式的標準 CSI，其他全部殘留在輸出中。&lt;/p>
&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;/td>
 &lt;td>18 個 AnsiParser test，全過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>測試覆蓋的序列類型&lt;/td>
 &lt;td>SGR 色彩碼（&lt;code>\x1B[31m&lt;/code> 等）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>真實環境的序列類型&lt;/td>
 &lt;td>SGR + OSC + CSI private mode + 字元集指定 + 其他&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>實機表現&lt;/td>
 &lt;td>終端機畫面散佈 &lt;code>]0;user@host&lt;/code> 等亂碼片段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復&lt;/td>
 &lt;td>新增 3 個 RegExp 過濾 OSC / CSI private / 其他 escape&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>測試資料的代表性是隱性假設&lt;/strong>。18 個 test 的斷言都正確 — &lt;code>\x1B[31m&lt;/code> 確實應該產生紅色 token。但「測試輸入能代表真實輸入」是一個未經驗證的假設。真實 zsh 的輸出包含 5+ 種 escape 序列類型，測試只覆蓋了 1 種。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Parser 的行為對未知序列是「透傳」而非「報錯」&lt;/strong>。這是合理的設計 — 不認識的序列不應該讓 parser 崩潰。但透傳的後果是亂碼靜默出現在畫面上，不觸發任何錯誤或 log，開發者在 unit test 環境完全不會察覺。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>手寫測試資料 vs 錄製真實資料&lt;/strong>。如果測試資料是從真實 shell session 錄製的（capture 一次真實 zsh 啟動輸出），OSC 和 CSI private mode 會自然出現在測試輸入中，parser 的透傳行為會在 test 階段就被看到。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>從真實環境錄製測試資料&lt;/strong>：用 &lt;code>script&lt;/code> 命令或 WebSocket log 錄一次真實 shell session 的完整輸出，作為 integration test 的輸入。即使不改 parser 邏輯，至少能看到「哪些序列被透傳了」。&lt;/li>
&lt;li>&lt;strong>Parser 對未知序列記 warning log&lt;/strong>：透傳是合理的 fallback，但加一行 &lt;code>developer.log('Unknown escape: ${escape.codeUnits}')&lt;/code> 讓開發者知道有未處理的序列。&lt;/li>
&lt;li>&lt;strong>測試分兩類&lt;/strong>：「功能正確性」用手寫乾淨字串；「環境相容性」用錄製的真實輸出。兩類測試回答不同問題。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想理解測試資料代表性 → &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/test-data-representativeness/" data-link-title="Test data 代表性" data-link-desc="手寫 vs 錄製 vs 生成三種測試資料來源 — 測試資料的代表性是一個隱性假設，決定了 test 能發現什麼問題">Test data 代表性&lt;/a>&lt;/li>
&lt;li>想建 protocol integration test 用真實 ttyd 輸出 → &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">模組三：協議整合測試&lt;/a>&lt;/li>
&lt;li>類似案例（mock 遮蔽） → &lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type mock 遮蔽&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 unit test 的輸入資料品質如何決定測試的有效性。Parser 邏輯正確、斷言正確、覆蓋率高 — 但測試資料是人工挑選的乾淨子集，跟真實環境的輸入分佈不同。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 的 <code>AnsiParser</code> 負責解析終端機輸出的 ANSI escape 序列，轉換為帶色彩的文字 token。unit test 用手寫字串驗證：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 測試資料範例 — 乾淨的 SGR 色彩碼
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;紅色文字&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kd">final</span> <span class="n">tokens</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">parse</span><span class="p">(</span><span class="s1">&#39;</span><span class="se">\x1B</span><span class="s1">[31mhello</span><span class="se">\x1B</span><span class="s1">[0m&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">tokens</span><span class="p">.</span><span class="n">first</span><span class="p">,</span> <span class="n">isA</span><span class="o">&lt;</span><span class="n">TextToken</span><span class="o">&gt;</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>真實 zsh prompt 啟動後送出的控制序列（擷取自實機 log）：</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">\x1B]0;user@host: ~\x07          ← OSC：設定終端機視窗標題
</span></span><span class="line"><span class="ln">2</span><span class="cl">\x1B[?2004h                      ← CSI private mode：啟用括號貼上模式
</span></span><span class="line"><span class="ln">3</span><span class="cl">\x1B[?1h                         ← CSI private mode：啟用應用程式游標鍵
</span></span><span class="line"><span class="ln">4</span><span class="cl">\x1B(B                           ← 字元集指定：選擇 ASCII
</span></span><span class="line"><span class="ln">5</span><span class="cl">\x1B[?25l                        ← CSI private mode：隱藏游標</span></span></code></pre></div><p>Parser 只認識 <code>\x1B[{數字;數字}{字母}</code> 格式的標準 CSI，其他全部殘留在輸出中。</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>測試案例數</td>
          <td>18 個 AnsiParser test，全過</td>
      </tr>
      <tr>
          <td>測試覆蓋的序列類型</td>
          <td>SGR 色彩碼（<code>\x1B[31m</code> 等）</td>
      </tr>
      <tr>
          <td>真實環境的序列類型</td>
          <td>SGR + OSC + CSI private mode + 字元集指定 + 其他</td>
      </tr>
      <tr>
          <td>實機表現</td>
          <td>終端機畫面散佈 <code>]0;user@host</code> 等亂碼片段</td>
      </tr>
      <tr>
          <td>修復</td>
          <td>新增 3 個 RegExp 過濾 OSC / CSI private / 其他 escape</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>測試資料的代表性是隱性假設</strong>。18 個 test 的斷言都正確 — <code>\x1B[31m</code> 確實應該產生紅色 token。但「測試輸入能代表真實輸入」是一個未經驗證的假設。真實 zsh 的輸出包含 5+ 種 escape 序列類型，測試只覆蓋了 1 種。</p>
</li>
<li>
<p><strong>Parser 的行為對未知序列是「透傳」而非「報錯」</strong>。這是合理的設計 — 不認識的序列不應該讓 parser 崩潰。但透傳的後果是亂碼靜默出現在畫面上，不觸發任何錯誤或 log，開發者在 unit test 環境完全不會察覺。</p>
</li>
<li>
<p><strong>手寫測試資料 vs 錄製真實資料</strong>。如果測試資料是從真實 shell session 錄製的（capture 一次真實 zsh 啟動輸出），OSC 和 CSI private mode 會自然出現在測試輸入中，parser 的透傳行為會在 test 階段就被看到。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li><strong>從真實環境錄製測試資料</strong>：用 <code>script</code> 命令或 WebSocket log 錄一次真實 shell session 的完整輸出，作為 integration test 的輸入。即使不改 parser 邏輯，至少能看到「哪些序列被透傳了」。</li>
<li><strong>Parser 對未知序列記 warning log</strong>：透傳是合理的 fallback，但加一行 <code>developer.log('Unknown escape: ${escape.codeUnits}')</code> 讓開發者知道有未處理的序列。</li>
<li><strong>測試分兩類</strong>：「功能正確性」用手寫乾淨字串；「環境相容性」用錄製的真實輸出。兩類測試回答不同問題。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想理解測試資料代表性 → <a href="/blog/testing/05-test-design-judgment/test-data-representativeness/" data-link-title="Test data 代表性" data-link-desc="手寫 vs 錄製 vs 生成三種測試資料來源 — 測試資料的代表性是一個隱性假設，決定了 test 能發現什麼問題">Test data 代表性</a></li>
<li>想建 protocol integration test 用真實 ttyd 輸出 → <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">模組三：協議整合測試</a></li>
<li>類似案例（mock 遮蔽） → <a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type mock 遮蔽</a></li>
</ul>
]]></content:encoded></item><item><title>U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField</title><link>https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/</guid><description>&lt;p>這個案例的核心責任是說明輸入機制是設計產物（在企劃階段決定），不是實作細節（在寫 code 時順便加）。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 的 Terminal 畫面在 W2 修復前沒有任何文字輸入元件。使用者只能透過底部工具列的特殊鍵（Esc/Tab/Ctrl/方向鍵）操作終端機，無法打字。&lt;/p>
&lt;p>W2-001 修復時加入的 &lt;code>TextField&lt;/code> 及其參數：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">TextField&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="nl">keyboardType:&lt;/span> &lt;span class="n">TextInputType&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">visiblePassword&lt;/span>&lt;span class="p">,&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="c1">&lt;/span> &lt;span class="nl">enableSuggestions:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&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="c1">&lt;/span> &lt;span class="nl">autocorrect:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &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="c1">&lt;/span> &lt;span class="nl">enableIMEPersonalizedLearning:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 關閉 IME 個人化學習
&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 class="nl">onSubmitted:&lt;/span> &lt;span class="n">_submitInput&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Enter 送出整行
&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">&lt;/span> &lt;span class="nl">textInputAction:&lt;/span> &lt;span class="n">TextInputAction&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">send&lt;/span>&lt;span class="p">,&lt;/span> &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="c1">&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個參數都是一個設計決策，但沒有一個是事前規劃的 — 全部是寫 code 時臨時判斷。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>設計決策&lt;/th>
 &lt;th>事前規劃&lt;/th>
 &lt;th>事後 hotfix 的風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>visiblePassword&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果用預設 &lt;code>text&lt;/code>，iOS 會自動校正 &lt;code>ls -la&lt;/code> 成其他東西&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>enableSuggestions: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>建議列遮擋終端機畫面下方&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>autocorrect: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>路徑 &lt;code>/usr/bin/&lt;/code> 可能被校正&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>enableIMEPersonalizedLearning: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>CLI 輸入含密碼和路徑，IME 學習是安全風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>onSubmitted&lt;/code>（整行送出）&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果逐字元送出，Tab 補全和命令編輯需要完全不同的 protocol 設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>TextInputAction.send&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果用 &lt;code>newline&lt;/code>，使用者按 Enter 會換行不送出&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>輸入設計影響 UI layout 和 protocol&lt;/strong>。&lt;code>onSubmitted&lt;/code>（整行送出）vs 逐字元即時送出不只是 UI 問題 — 整行送出代表 protocol 層送的是 &lt;code>command\n&lt;/code>，逐字元送出代表每個按鍵都是一個 WS frame。這個決策應該在 protocol spec 階段就做，因為它影響 server 端的行為預期。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>IME 控制有安全意涵&lt;/strong>。&lt;code>enableIMEPersonalizedLearning: false&lt;/code> 不只是 UX 偏好 — CLI 輸入可能包含資料庫密碼、API key、伺服器路徑。IME 學習這些內容等於把 secret 存到了 IME 的詞庫裡，跨 app 可用。這是安全問題，不是 UX 問題。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>事後 hotfix 的六個參數每個都有 gotcha&lt;/strong>。如果這些決策在企劃階段做，可以寫成決策表並在 code review 時對照。事後 hotfix 時開發者可能漏掉其中一兩個（例如只加 &lt;code>autocorrect: false&lt;/code> 但忘了 &lt;code>enableIMEPersonalizedLearning: false&lt;/code>），漏掉的那個就成為安全漏洞。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>功能規格新增「輸入機制決策表」&lt;/strong>：keyboard type / submit model / IME policy / special keys 四個維度，每個列出選項和取捨理由。&lt;/li>
&lt;li>&lt;strong>輸入機制跟 protocol 一起設計&lt;/strong>：「整行送出」還是「逐字元」決定了 WS 訊框的設計，必須在 protocol spec 階段決定。&lt;/li>
&lt;li>&lt;strong>安全敏感參數強制列入 review checklist&lt;/strong>：&lt;code>enableIMEPersonalizedLearning&lt;/code>、&lt;code>autocorrect&lt;/code> 在處理 secret 的輸入框中是安全要求，不是可選項。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想設計 mobile 輸入機制 → &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表&lt;/a>&lt;/li>
&lt;li>想看 protocol 跟輸入的關聯 → &lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type&lt;/a>（sendData 的型別決策）&lt;/li>
&lt;li>想做安全審查 → 待補：CLI 輸入安全 checklist&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明輸入機制是設計產物（在企劃階段決定），不是實作細節（在寫 code 時順便加）。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 的 Terminal 畫面在 W2 修復前沒有任何文字輸入元件。使用者只能透過底部工具列的特殊鍵（Esc/Tab/Ctrl/方向鍵）操作終端機，無法打字。</p>
<p>W2-001 修復時加入的 <code>TextField</code> 及其參數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">TextField</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nl">keyboardType:</span> <span class="n">TextInputType</span><span class="p">.</span><span class="n">visiblePassword</span><span class="p">,</span>   <span class="c1">// 避免自動校正
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nl">enableSuggestions:</span> <span class="kc">false</span><span class="p">,</span>                       <span class="c1">// 關閉建議列
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nl">autocorrect:</span> <span class="kc">false</span><span class="p">,</span>                             <span class="c1">// 關閉自動校正
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nl">enableIMEPersonalizedLearning:</span> <span class="kc">false</span><span class="p">,</span>           <span class="c1">// 關閉 IME 個人化學習
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nl">onSubmitted:</span> <span class="n">_submitInput</span><span class="p">,</span>                      <span class="c1">// Enter 送出整行
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="nl">textInputAction:</span> <span class="n">TextInputAction</span><span class="p">.</span><span class="n">send</span><span class="p">,</span>          <span class="c1">// 鍵盤顯示「傳送」
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="p">)</span></span></span></code></pre></div><p>每個參數都是一個設計決策，但沒有一個是事前規劃的 — 全部是寫 code 時臨時判斷。</p>
<table>
  <thead>
      <tr>
          <th>設計決策</th>
          <th>事前規劃</th>
          <th>事後 hotfix 的風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>visiblePassword</code></td>
          <td>沒有</td>
          <td>如果用預設 <code>text</code>，iOS 會自動校正 <code>ls -la</code> 成其他東西</td>
      </tr>
      <tr>
          <td><code>enableSuggestions: false</code></td>
          <td>沒有</td>
          <td>建議列遮擋終端機畫面下方</td>
      </tr>
      <tr>
          <td><code>autocorrect: false</code></td>
          <td>沒有</td>
          <td>路徑 <code>/usr/bin/</code> 可能被校正</td>
      </tr>
      <tr>
          <td><code>enableIMEPersonalizedLearning: false</code></td>
          <td>沒有</td>
          <td>CLI 輸入含密碼和路徑，IME 學習是安全風險</td>
      </tr>
      <tr>
          <td><code>onSubmitted</code>（整行送出）</td>
          <td>沒有</td>
          <td>如果逐字元送出，Tab 補全和命令編輯需要完全不同的 protocol 設計</td>
      </tr>
      <tr>
          <td><code>TextInputAction.send</code></td>
          <td>沒有</td>
          <td>如果用 <code>newline</code>，使用者按 Enter 會換行不送出</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>輸入設計影響 UI layout 和 protocol</strong>。<code>onSubmitted</code>（整行送出）vs 逐字元即時送出不只是 UI 問題 — 整行送出代表 protocol 層送的是 <code>command\n</code>，逐字元送出代表每個按鍵都是一個 WS frame。這個決策應該在 protocol spec 階段就做，因為它影響 server 端的行為預期。</p>
</li>
<li>
<p><strong>IME 控制有安全意涵</strong>。<code>enableIMEPersonalizedLearning: false</code> 不只是 UX 偏好 — CLI 輸入可能包含資料庫密碼、API key、伺服器路徑。IME 學習這些內容等於把 secret 存到了 IME 的詞庫裡，跨 app 可用。這是安全問題，不是 UX 問題。</p>
</li>
<li>
<p><strong>事後 hotfix 的六個參數每個都有 gotcha</strong>。如果這些決策在企劃階段做，可以寫成決策表並在 code review 時對照。事後 hotfix 時開發者可能漏掉其中一兩個（例如只加 <code>autocorrect: false</code> 但忘了 <code>enableIMEPersonalizedLearning: false</code>），漏掉的那個就成為安全漏洞。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li><strong>功能規格新增「輸入機制決策表」</strong>：keyboard type / submit model / IME policy / special keys 四個維度，每個列出選項和取捨理由。</li>
<li><strong>輸入機制跟 protocol 一起設計</strong>：「整行送出」還是「逐字元」決定了 WS 訊框的設計，必須在 protocol spec 階段決定。</li>
<li><strong>安全敏感參數強制列入 review checklist</strong>：<code>enableIMEPersonalizedLearning</code>、<code>autocorrect</code> 在處理 secret 的輸入框中是安全要求，不是可選項。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計 mobile 輸入機制 → <a href="/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表</a></li>
<li>想看 protocol 跟輸入的關聯 → <a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type</a>（sendData 的型別決策）</li>
<li>想做安全審查 → 待補：CLI 輸入安全 checklist</li>
</ul>
]]></content:encoded></item><item><title>每個畫面都需要出口：畫面狀態機設計與 UX 導航的系統性方法</title><link>https://tarrragon.github.io/blog/work-log/%E6%AF%8F%E5%80%8B%E7%95%AB%E9%9D%A2%E9%83%BD%E9%9C%80%E8%A6%81%E5%87%BA%E5%8F%A3%E7%95%AB%E9%9D%A2%E7%8B%80%E6%85%8B%E6%A9%9F%E8%A8%AD%E8%A8%88%E8%88%87-ux-%E5%B0%8E%E8%88%AA%E7%9A%84%E7%B3%BB%E7%B5%B1%E6%80%A7%E6%96%B9%E6%B3%95/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E6%AF%8F%E5%80%8B%E7%95%AB%E9%9D%A2%E9%83%BD%E9%9C%80%E8%A6%81%E5%87%BA%E5%8F%A3%E7%95%AB%E9%9D%A2%E7%8B%80%E6%85%8B%E6%A9%9F%E8%A8%AD%E8%A8%88%E8%88%87-ux-%E5%B0%8E%E8%88%AA%E7%9A%84%E7%B3%BB%E7%B5%B1%E6%80%A7%E6%96%B9%E6%B3%95/</guid><description>&lt;h2 id="這篇要解決什麼">這篇要解決什麼&lt;/h2>
&lt;blockquote>
&lt;p>使用者連上遠端終端機後、無法返回首頁。&lt;/p>&lt;/blockquote>
&lt;p>這是設計遺漏。Terminal 畫面的 &lt;code>connected&lt;/code> 狀態沒有 disconnect 按鈕也沒有 back 按鈕。&lt;code>error&lt;/code> 和 &lt;code>disconnected&lt;/code> 狀態也沒有。使用者被困在畫面裡，唯一的出路是殺掉 app。&lt;/p>
&lt;p>這不是「忘記加按鈕」的問題。回頭看企劃文件，操作盤點段確實列了「連線失敗顯示無法連線」這個失敗情境，但沒有系統性地問：&lt;strong>這個畫面有幾個狀態？每個狀態能做什麼操作？怎麼離開？&lt;/strong>&lt;/p>
&lt;p>本文整理畫面狀態機設計的方法、示範用狀態矩陣捕捉導航缺口、歸納 mobile app UX 的三個設計原則。&lt;/p>
&lt;hr>
&lt;h2 id="實際案例terminal-畫面的五個狀態">實際案例：Terminal 畫面的五個狀態&lt;/h2>
&lt;p>Terminal 畫面有一個 &lt;code>TerminalScreenUiState&lt;/code> enum 定義了五個狀態：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">enum&lt;/span> &lt;span class="n">TerminalScreenUiState&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">idle&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">connecting&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">connected&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">disconnected&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實機測試前、這五個狀態各自的 UI 長這樣：&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>idle&lt;/td>
 &lt;td>空白（自動開始連線）&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connecting&lt;/td>
 &lt;td>「連線中&amp;hellip;」進度指示&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connected&lt;/td>
 &lt;td>終端機畫面 + 工具列&lt;/td>
 &lt;td>打字、Esc/Tab/Ctrl/方向鍵&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>error&lt;/td>
 &lt;td>錯誤訊息 + 重連按鈕&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>disconnected&lt;/td>
 &lt;td>「連線中斷」+ 重連按鈕&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>五個狀態、零個退出路徑。使用者一旦進入 Terminal 畫面就出不去。&lt;/p>
&lt;hr>
&lt;h2 id="問題不在按鈕在設計方法">問題不在按鈕、在設計方法&lt;/h2>
&lt;p>加 back 按鈕是 5 分鐘的事。真正的問題是：&lt;strong>企劃階段沒有工具強制你為每個狀態想退出路徑。&lt;/strong>&lt;/p>
&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>日常連線&lt;/td>
 &lt;td>Face ID → 讀憑證 → WS 連線 → 雙向 I/O&lt;/td>
 &lt;td>辨識失敗；Tailscale 離線；ttyd 認證失敗&lt;/td>
 &lt;td>辨識失敗不讀憑證；連線失敗顯示「無法連線」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「前端引導」只有一句話。它沒有被展開成畫面狀態。「連線失敗顯示無法連線」這句話覆蓋了 &lt;code>error&lt;/code> 狀態的&lt;strong>顯示&lt;/strong>，但沒有回答&lt;strong>操作&lt;/strong>（重連？返回？）和&lt;strong>退出&lt;/strong>（怎麼離開這個畫面？）。&lt;/p>
&lt;hr>
&lt;h2 id="畫面狀態矩陣">畫面狀態矩陣&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;th>退出路徑&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Terminal.idle&lt;/td>
 &lt;td>空白&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>從首頁導航進入&lt;/td>
 &lt;td>back → 首頁&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Terminal.connecting&lt;/td>
 &lt;td>進度指示&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>自動觸發連線&lt;/td>
 &lt;td>back → 首頁（取消連線）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Terminal.connected&lt;/td>
 &lt;td>終端機 + 工具列&lt;/td>
 &lt;td>打字、特殊鍵&lt;/td>
 &lt;td>WS 連線成功&lt;/td>
 &lt;td>disconnect → idle；back → 首頁&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Terminal.error&lt;/td>
 &lt;td>錯誤訊息&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>連線失敗&lt;/td>
 &lt;td>back → 首頁；retry → connecting&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Terminal.disconnected&lt;/td>
 &lt;td>「連線中斷」&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>WS 斷線&lt;/td>
 &lt;td>back → 首頁；retry → connecting&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>表格的威力在「退出路徑」欄位：&lt;strong>如果這格是空的，這就是一個 UX 死胡同。&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="三個-mobile-app-ux-設計原則">三個 Mobile App UX 設計原則&lt;/h2>
&lt;p>從這個案例提煉出的三個原則，適用於所有 mobile app：&lt;/p>
&lt;h3 id="原則-1每個畫面的每個狀態都需要退出路徑">原則 1：每個畫面的每個狀態都需要退出路徑&lt;/h3>
&lt;p>沒有例外。即使是「connecting」這種過渡狀態，使用者也可能想取消。iOS 的 HIG 和 Material Design 都要求 modal 畫面提供 dismiss 機制 — 如果使用者進不了某個狀態的下一步（連線失敗、timeout、服務無回應），他至少得能退出。&lt;/p>
&lt;p>&lt;strong>反模式&lt;/strong>：假設使用者只走 happy path。「connected 之後使用者不會想回首頁」是開發者的假設，不是使用者的需求。&lt;/p>
&lt;h3 id="原則-2gate-必須有-fallback">原則 2：Gate 必須有 fallback&lt;/h3>
&lt;p>Gate = 使用者必須通過的關卡（biometric、network、auth）。每個 gate 的設計不只是「成功時怎麼做」，還包含「失敗時的替代路徑」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Gate&lt;/th>
 &lt;th>成功&lt;/th>
 &lt;th>失敗 fallback&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Biometric（Face ID / 指紋）&lt;/td>
 &lt;td>讀取憑證、繼續連線&lt;/td>
 &lt;td>密碼 fallback（&lt;code>biometricOnly: false&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network（Tailscale VPN）&lt;/td>
 &lt;td>WS 連線&lt;/td>
 &lt;td>顯示「網路不可用」+ 重試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Auth（ttyd basic auth）&lt;/td>
 &lt;td>進入終端機&lt;/td>
 &lt;td>顯示「認證失敗」+ 建議重新配對&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>biometricOnly: true&lt;/code> 就是缺少 fallback 的典型案例 — Face ID 不可用（戴口罩、光線差、指紋模糊）時使用者直接被擋住，沒有替代方案。改為 &lt;code>biometricOnly: false&lt;/code> 讓系統提供密碼 fallback。&lt;/p></description><content:encoded><![CDATA[<h2 id="這篇要解決什麼">這篇要解決什麼</h2>
<blockquote>
<p>使用者連上遠端終端機後、無法返回首頁。</p></blockquote>
<p>這是設計遺漏。Terminal 畫面的 <code>connected</code> 狀態沒有 disconnect 按鈕也沒有 back 按鈕。<code>error</code> 和 <code>disconnected</code> 狀態也沒有。使用者被困在畫面裡，唯一的出路是殺掉 app。</p>
<p>這不是「忘記加按鈕」的問題。回頭看企劃文件，操作盤點段確實列了「連線失敗顯示無法連線」這個失敗情境，但沒有系統性地問：<strong>這個畫面有幾個狀態？每個狀態能做什麼操作？怎麼離開？</strong></p>
<p>本文整理畫面狀態機設計的方法、示範用狀態矩陣捕捉導航缺口、歸納 mobile app UX 的三個設計原則。</p>
<hr>
<h2 id="實際案例terminal-畫面的五個狀態">實際案例：Terminal 畫面的五個狀態</h2>
<p>Terminal 畫面有一個 <code>TerminalScreenUiState</code> enum 定義了五個狀態：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">enum</span> <span class="n">TerminalScreenUiState</span> <span class="p">{</span> <span class="n">idle</span><span class="p">,</span> <span class="n">connecting</span><span class="p">,</span> <span class="n">connected</span><span class="p">,</span> <span class="n">error</span><span class="p">,</span> <span class="n">disconnected</span> <span class="p">}</span></span></span></code></pre></div><p>實機測試前、這五個狀態各自的 UI 長這樣：</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>顯示</th>
          <th>可用操作</th>
          <th>退出路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>idle</td>
          <td>空白（自動開始連線）</td>
          <td>無</td>
          <td><strong>無</strong></td>
      </tr>
      <tr>
          <td>connecting</td>
          <td>「連線中&hellip;」進度指示</td>
          <td>無</td>
          <td><strong>無</strong></td>
      </tr>
      <tr>
          <td>connected</td>
          <td>終端機畫面 + 工具列</td>
          <td>打字、Esc/Tab/Ctrl/方向鍵</td>
          <td><strong>無</strong></td>
      </tr>
      <tr>
          <td>error</td>
          <td>錯誤訊息 + 重連按鈕</td>
          <td>重新連線</td>
          <td><strong>無</strong></td>
      </tr>
      <tr>
          <td>disconnected</td>
          <td>「連線中斷」+ 重連按鈕</td>
          <td>重新連線</td>
          <td><strong>無</strong></td>
      </tr>
  </tbody>
</table>
<p>五個狀態、零個退出路徑。使用者一旦進入 Terminal 畫面就出不去。</p>
<hr>
<h2 id="問題不在按鈕在設計方法">問題不在按鈕、在設計方法</h2>
<p>加 back 按鈕是 5 分鐘的事。真正的問題是：<strong>企劃階段沒有工具強制你為每個狀態想退出路徑。</strong></p>
<p>操作盤點表長這樣：</p>
<table>
  <thead>
      <tr>
          <th>操作</th>
          <th>主情境</th>
          <th>失敗情境</th>
          <th>前端引導</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>日常連線</td>
          <td>Face ID → 讀憑證 → WS 連線 → 雙向 I/O</td>
          <td>辨識失敗；Tailscale 離線；ttyd 認證失敗</td>
          <td>辨識失敗不讀憑證；連線失敗顯示「無法連線」</td>
      </tr>
  </tbody>
</table>
<p>「前端引導」只有一句話。它沒有被展開成畫面狀態。「連線失敗顯示無法連線」這句話覆蓋了 <code>error</code> 狀態的<strong>顯示</strong>，但沒有回答<strong>操作</strong>（重連？返回？）和<strong>退出</strong>（怎麼離開這個畫面？）。</p>
<hr>
<h2 id="畫面狀態矩陣">畫面狀態矩陣</h2>
<p>把狀態機設計變成一張表，強制回答每個狀態的四個面向：</p>
<table>
  <thead>
      <tr>
          <th>畫面.狀態</th>
          <th>顯示</th>
          <th>可用操作</th>
          <th>進入條件</th>
          <th>退出路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Terminal.idle</td>
          <td>空白</td>
          <td>—</td>
          <td>從首頁導航進入</td>
          <td>back → 首頁</td>
      </tr>
      <tr>
          <td>Terminal.connecting</td>
          <td>進度指示</td>
          <td>—</td>
          <td>自動觸發連線</td>
          <td>back → 首頁（取消連線）</td>
      </tr>
      <tr>
          <td>Terminal.connected</td>
          <td>終端機 + 工具列</td>
          <td>打字、特殊鍵</td>
          <td>WS 連線成功</td>
          <td>disconnect → idle；back → 首頁</td>
      </tr>
      <tr>
          <td>Terminal.error</td>
          <td>錯誤訊息</td>
          <td>重新連線</td>
          <td>連線失敗</td>
          <td>back → 首頁；retry → connecting</td>
      </tr>
      <tr>
          <td>Terminal.disconnected</td>
          <td>「連線中斷」</td>
          <td>重新連線</td>
          <td>WS 斷線</td>
          <td>back → 首頁；retry → connecting</td>
      </tr>
  </tbody>
</table>
<p>表格的威力在「退出路徑」欄位：<strong>如果這格是空的，這就是一個 UX 死胡同。</strong></p>
<hr>
<h2 id="三個-mobile-app-ux-設計原則">三個 Mobile App UX 設計原則</h2>
<p>從這個案例提煉出的三個原則，適用於所有 mobile app：</p>
<h3 id="原則-1每個畫面的每個狀態都需要退出路徑">原則 1：每個畫面的每個狀態都需要退出路徑</h3>
<p>沒有例外。即使是「connecting」這種過渡狀態，使用者也可能想取消。iOS 的 HIG 和 Material Design 都要求 modal 畫面提供 dismiss 機制 — 如果使用者進不了某個狀態的下一步（連線失敗、timeout、服務無回應），他至少得能退出。</p>
<p><strong>反模式</strong>：假設使用者只走 happy path。「connected 之後使用者不會想回首頁」是開發者的假設，不是使用者的需求。</p>
<h3 id="原則-2gate-必須有-fallback">原則 2：Gate 必須有 fallback</h3>
<p>Gate = 使用者必須通過的關卡（biometric、network、auth）。每個 gate 的設計不只是「成功時怎麼做」，還包含「失敗時的替代路徑」。</p>
<table>
  <thead>
      <tr>
          <th>Gate</th>
          <th>成功</th>
          <th>失敗 fallback</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Biometric（Face ID / 指紋）</td>
          <td>讀取憑證、繼續連線</td>
          <td>密碼 fallback（<code>biometricOnly: false</code>）</td>
      </tr>
      <tr>
          <td>Network（Tailscale VPN）</td>
          <td>WS 連線</td>
          <td>顯示「網路不可用」+ 重試</td>
      </tr>
      <tr>
          <td>Auth（ttyd basic auth）</td>
          <td>進入終端機</td>
          <td>顯示「認證失敗」+ 建議重新配對</td>
      </tr>
  </tbody>
</table>
<p><code>biometricOnly: true</code> 就是缺少 fallback 的典型案例 — Face ID 不可用（戴口罩、光線差、指紋模糊）時使用者直接被擋住，沒有替代方案。改為 <code>biometricOnly: false</code> 讓系統提供密碼 fallback。</p>
<h3 id="原則-3輸入機制是設計產物不是實作細節">原則 3：輸入機制是設計產物，不是實作細節</h3>
<p>「手機打字操作 CLI」的輸入設計決策比想像的多：</p>
<table>
  <thead>
      <tr>
          <th>設計決策</th>
          <th>選項</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Keyboard type</td>
          <td><code>visiblePassword</code>（無自動校正）vs <code>text</code>（有校正）</td>
          <td>CLI 命令不需要自動校正，<code>visiblePassword</code> 避免系統「幫忙」修改輸入</td>
      </tr>
      <tr>
          <td>Submit model</td>
          <td>Enter 送出整行 vs 逐字元即時送出</td>
          <td>整行送出減少網路來回，但沒有即時 tab 補全回饋</td>
      </tr>
      <tr>
          <td>IME policy</td>
          <td>關閉建議、關閉自動校正、關閉個人化學習</td>
          <td>CLI 輸入內容可能包含密碼和路徑，IME 學習是安全風險</td>
      </tr>
      <tr>
          <td>Special keys</td>
          <td>Esc / Tab / Ctrl 組合鍵</td>
          <td>手機鍵盤沒有這些鍵，需要自訂工具列</td>
      </tr>
  </tbody>
</table>
<p>這些決策在企劃階段就應該做，因為它們影響 UI layout（是否需要輸入框？工具列放什麼鍵？）和 protocol 設計（逐字元還是整行？）。事後補的 <code>TextField</code> 參數列表（<code>enableSuggestions: false, autocorrect: false, enableIMEPersonalizedLearning: false</code>）全是散落的 hotfix，不是設計產物。</p>
<hr>
<h2 id="系統性方法從操作盤點到畫面狀態矩陣">系統性方法：從操作盤點到畫面狀態矩陣</h2>
<p>操作盤點是 BDD 的起點（使用者做什麼、成功時發生什麼、失敗時發生什麼）。但盤點到「前端引導」就停了 — 它回答了「顯示什麼」但沒回答「能做什麼」「怎麼離開」。</p>
<p>補上的步驟：</p>
<ol>
<li><strong>從操作盤點列出所有畫面</strong>：每個操作涉及哪些畫面？（首頁 → 配對畫面 → QR 掃描 → 終端機畫面）</li>
<li><strong>每個畫面列出所有狀態</strong>：這個畫面有哪些 enum 值或邏輯分支？</li>
<li><strong>填畫面狀態矩陣</strong>：顯示 / 可用操作 / 進入條件 / 退出路徑。退出路徑欄位為空 = UX 死胡同</li>
<li><strong>每個 gate 標注 fallback</strong>：biometric / network / auth 各有什麼替代方案？</li>
<li><strong>輸入機制列決策表</strong>：keyboard type / submit model / IME policy / special keys</li>
</ol>
<p>這是操作盤點本來就該產出的下一層。一張表能在 10 分鐘內暴露所有 UX 死胡同，省掉實機測試才發現的成本。</p>
<h2 id="延伸閱讀">延伸閱讀</h2>
<p>本文的觀察和判讀在 <a href="/blog/ux-design/" data-link-title="UX 設計實務指南" data-link-desc="整理畫面狀態機、導航設計、Gate fallback、輸入機制與使用者行為驗證 — 從「使用者被困在畫面裡出不去」的結構性遺漏出發，建立系統性的 UX 設計方法">UX Design 畫面設計</a> 教學系列中展開為系統性的教學模組：<a href="/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法</a>、<a href="/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法</a>、<a href="/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表</a>。</p>
]]></content:encoded></item><item><title>tmux 基礎：遠端 session 持久化與基本操作</title><link>https://tarrragon.github.io/blog/linux/tools/cli/tmux-persistence-and-basics/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/tools/cli/tmux-persistence-and-basics/</guid><description>&lt;p>tmux 是終端機多工器，核心責任是把終端機 session 的生命週期與連線本身脫鉤，並在單一連線裡分割出多個工作區。在遠端 SSH 開發下，它解決最痛的一個問題：連線斷了，伺服器上跑的東西不會跟著消失。把工作放進 tmux，連線中斷後 session 仍在伺服器上運作，重連 attach 回去就接續原狀。&lt;/p>
&lt;p>遠端伺服器優先選 tmux 的理由是可用性。它幾乎是事實標準，多數 Linux 發行版的套件庫都有、很多伺服器甚至預裝。&lt;code>zellij&lt;/code> 功能新、畫面提示友善，但通常要自行安裝；在不能隨意裝套件的機器上，tmux 處處可用就是決定性優勢。兩者的取捨在最後一節展開。&lt;/p>
&lt;p>本文承接 &lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽&lt;/a> 的多工器分類，聚焦 tmux 在遠端情境的實際操作。&lt;/p>
&lt;h2 id="持久化工作流detach-與-reattach">持久化工作流：detach 與 reattach&lt;/h2>
&lt;p>tmux 對遠端最重要的能力是 session 持久化：session 跑在伺服器上，跟當前這條 SSH 連線無關，所以主動離開或被動斷線後它都還在。這條工作流由四個指令構成。&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>開新具名 session&lt;/td>
 &lt;td>&lt;code>tmux new -s work&lt;/code>&lt;/td>
 &lt;td>用名字開，之後好辨識&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主動離開&lt;/td>
 &lt;td>&lt;code>prefix&lt;/code> 後按 &lt;code>d&lt;/code>&lt;/td>
 &lt;td>detach，session 留在背景繼續跑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>列出現有 session&lt;/td>
 &lt;td>&lt;code>tmux ls&lt;/code>&lt;/td>
 &lt;td>看伺服器上有哪些 session&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>接回&lt;/td>
 &lt;td>&lt;code>tmux attach -t work&lt;/code>&lt;/td>
 &lt;td>reattach，回到離開時的狀態（可簡寫 &lt;code>tmux a -t work&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵在於被動斷線與主動 detach 的結果相同：手機從 Wi-Fi 切到行動網路、SSH 連線逾時、筆電闔上，這些情況下 tmux session 都留在伺服器上，重連後 &lt;code>tmux a&lt;/code> 就接回去。判讀訊號很單純：任何超過幾秒、不想因斷線重來的工作（build、資料遷移、&lt;code>tail -f&lt;/code> 追 log、跑測試），開始前先進 tmux。&lt;/p>
&lt;h2 id="prefix-keytmux-操作的入口">prefix key：tmux 操作的入口&lt;/h2>
&lt;p>tmux 的所有指令都以 prefix key 起手，預設是 &lt;code>Ctrl-b&lt;/code>。操作方式是按下 &lt;code>Ctrl-b&lt;/code> 放開、再按功能鍵，而不是同時按住。理解這個「兩段式」是上手 tmux 的第一道門檻；若按住不放或間隔太久而沒反應，多半是兩段式沒按對，重來一次即可。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>操作&lt;/th>
 &lt;th>按鍵&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>開新 window&lt;/td>
 &lt;td>&lt;code>prefix&lt;/code> 後按 &lt;code>c&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>切換上一個 / 下一個 window&lt;/td>
 &lt;td>&lt;code>prefix&lt;/code> 後按 &lt;code>p&lt;/code> / &lt;code>n&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跳到第 N 個 window&lt;/td>
 &lt;td>&lt;code>prefix&lt;/code> 後按數字&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>垂直分割（左右兩個 pane）&lt;/td>
 &lt;td>&lt;code>prefix&lt;/code> 後按 &lt;code>%&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>水平分割（上下兩個 pane）&lt;/td>
 &lt;td>&lt;code>prefix&lt;/code> 後按 &lt;code>&amp;quot;&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在 pane 間移動&lt;/td>
 &lt;td>&lt;code>prefix&lt;/code> 後按方向鍵&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>關閉當前 pane&lt;/td>
 &lt;td>&lt;code>prefix&lt;/code> 後按 &lt;code>x&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>單一 pane 全螢幕放大 / 還原&lt;/td>
 &lt;td>&lt;code>prefix&lt;/code> 後按 &lt;code>z&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>進 copy mode（往回捲歷史）&lt;/td>
 &lt;td>&lt;code>prefix&lt;/code> 後按 &lt;code>[&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>window 與 pane 是兩個層級：window 是整頁工作區（類似分頁），pane 是一個 window 內切出的子區塊。遠端開發常見的佈局是一個 window 切成數個 pane，一個跑編輯器、一個跑 &lt;code>tail -f&lt;/code>、一個留著敲指令。捲動歷史要先進 copy mode（&lt;code>prefix&lt;/code> 後按 &lt;code>[&lt;/code>），用方向鍵或 &lt;code>PageUp&lt;/code> 往回看，按 &lt;code>q&lt;/code> 離開 — 這是初學最容易卡住的點，因為進了 tmux 後終端機原本的捲動行為改由 tmux 接管。&lt;/p>
&lt;h2 id="遠端與手機的調校">遠端與手機的調校&lt;/h2>
&lt;p>tmux 預設設定對手機與慢速連線不夠順，幾項調整能明顯改善體感，全部寫在 &lt;code>~/.tmux.conf&lt;/code>。&lt;/p>
&lt;p>prefix key &lt;code>Ctrl-b&lt;/code> 在手機虛擬鍵盤上難按，常見的調整是改綁成 &lt;code>Ctrl-a&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"># ~/.tmux.conf&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;/code>&lt;/pre>&lt;/div>&lt;p>滑鼠支援讓觸控裝置能直接點選 pane 與捲動，在手機與平板特別有用：&lt;/p></description><content:encoded><![CDATA[<p>tmux 是終端機多工器，核心責任是把終端機 session 的生命週期與連線本身脫鉤，並在單一連線裡分割出多個工作區。在遠端 SSH 開發下，它解決最痛的一個問題：連線斷了，伺服器上跑的東西不會跟著消失。把工作放進 tmux，連線中斷後 session 仍在伺服器上運作，重連 attach 回去就接續原狀。</p>
<p>遠端伺服器優先選 tmux 的理由是可用性。它幾乎是事實標準，多數 Linux 發行版的套件庫都有、很多伺服器甚至預裝。<code>zellij</code> 功能新、畫面提示友善，但通常要自行安裝；在不能隨意裝套件的機器上，tmux 處處可用就是決定性優勢。兩者的取捨在最後一節展開。</p>
<p>本文承接 <a href="/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽</a> 的多工器分類，聚焦 tmux 在遠端情境的實際操作。</p>
<h2 id="持久化工作流detach-與-reattach">持久化工作流：detach 與 reattach</h2>
<p>tmux 對遠端最重要的能力是 session 持久化：session 跑在伺服器上，跟當前這條 SSH 連線無關，所以主動離開或被動斷線後它都還在。這條工作流由四個指令構成。</p>
<table>
  <thead>
      <tr>
          <th>動作</th>
          <th>指令</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>開新具名 session</td>
          <td><code>tmux new -s work</code></td>
          <td>用名字開，之後好辨識</td>
      </tr>
      <tr>
          <td>主動離開</td>
          <td><code>prefix</code> 後按 <code>d</code></td>
          <td>detach，session 留在背景繼續跑</td>
      </tr>
      <tr>
          <td>列出現有 session</td>
          <td><code>tmux ls</code></td>
          <td>看伺服器上有哪些 session</td>
      </tr>
      <tr>
          <td>接回</td>
          <td><code>tmux attach -t work</code></td>
          <td>reattach，回到離開時的狀態（可簡寫 <code>tmux a -t work</code>）</td>
      </tr>
  </tbody>
</table>
<p>關鍵在於被動斷線與主動 detach 的結果相同：手機從 Wi-Fi 切到行動網路、SSH 連線逾時、筆電闔上，這些情況下 tmux session 都留在伺服器上，重連後 <code>tmux a</code> 就接回去。判讀訊號很單純：任何超過幾秒、不想因斷線重來的工作（build、資料遷移、<code>tail -f</code> 追 log、跑測試），開始前先進 tmux。</p>
<h2 id="prefix-keytmux-操作的入口">prefix key：tmux 操作的入口</h2>
<p>tmux 的所有指令都以 prefix key 起手，預設是 <code>Ctrl-b</code>。操作方式是按下 <code>Ctrl-b</code> 放開、再按功能鍵，而不是同時按住。理解這個「兩段式」是上手 tmux 的第一道門檻；若按住不放或間隔太久而沒反應，多半是兩段式沒按對，重來一次即可。</p>
<table>
  <thead>
      <tr>
          <th>操作</th>
          <th>按鍵</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>開新 window</td>
          <td><code>prefix</code> 後按 <code>c</code></td>
      </tr>
      <tr>
          <td>切換上一個 / 下一個 window</td>
          <td><code>prefix</code> 後按 <code>p</code> / <code>n</code></td>
      </tr>
      <tr>
          <td>跳到第 N 個 window</td>
          <td><code>prefix</code> 後按數字</td>
      </tr>
      <tr>
          <td>垂直分割（左右兩個 pane）</td>
          <td><code>prefix</code> 後按 <code>%</code></td>
      </tr>
      <tr>
          <td>水平分割（上下兩個 pane）</td>
          <td><code>prefix</code> 後按 <code>&quot;</code></td>
      </tr>
      <tr>
          <td>在 pane 間移動</td>
          <td><code>prefix</code> 後按方向鍵</td>
      </tr>
      <tr>
          <td>關閉當前 pane</td>
          <td><code>prefix</code> 後按 <code>x</code></td>
      </tr>
      <tr>
          <td>單一 pane 全螢幕放大 / 還原</td>
          <td><code>prefix</code> 後按 <code>z</code></td>
      </tr>
      <tr>
          <td>進 copy mode（往回捲歷史）</td>
          <td><code>prefix</code> 後按 <code>[</code></td>
      </tr>
  </tbody>
</table>
<p>window 與 pane 是兩個層級：window 是整頁工作區（類似分頁），pane 是一個 window 內切出的子區塊。遠端開發常見的佈局是一個 window 切成數個 pane，一個跑編輯器、一個跑 <code>tail -f</code>、一個留著敲指令。捲動歷史要先進 copy mode（<code>prefix</code> 後按 <code>[</code>），用方向鍵或 <code>PageUp</code> 往回看，按 <code>q</code> 離開 — 這是初學最容易卡住的點，因為進了 tmux 後終端機原本的捲動行為改由 tmux 接管。</p>
<h2 id="遠端與手機的調校">遠端與手機的調校</h2>
<p>tmux 預設設定對手機與慢速連線不夠順，幾項調整能明顯改善體感，全部寫在 <code>~/.tmux.conf</code>。</p>
<p>prefix key <code>Ctrl-b</code> 在手機虛擬鍵盤上難按，常見的調整是改綁成 <code>Ctrl-a</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"># ~/.tmux.conf</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></code></pre></div><p>滑鼠支援讓觸控裝置能直接點選 pane 與捲動，在手機與平板特別有用：</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"># ~/.tmux.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">set</span> -g mouse on</span></span></code></pre></div><p>頻寬層面，tmux 本身傳輸的是純文字、量很低，斷線重連的成本也小。真正吃頻寬的是跑在 tmux 裡的全螢幕 TUI（例如 <code>btop</code>）的高頻重畫 — 這要調的是那個工具自己的刷新率，而非 tmux。改完設定檔後，在既有 session 內用 <code>prefix</code> 後按 <code>:</code> 輸入 <code>source-file ~/.tmux.conf</code> 重新載入。</p>
<h2 id="tmux-與-zellij-的選型對照">tmux 與 zellij 的選型對照</h2>
<p>tmux 與 zellij 解決同一類問題，session 持久化是兩者共有的基本能力（zellij 甚至內建 resurrection），真正的選擇依據是可用性與上手成本。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>tmux</th>
          <th>zellij</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>預設可用性</td>
          <td>多數伺服器預裝或套件庫直接有</td>
          <td>通常需要自行安裝</td>
      </tr>
      <tr>
          <td>上手成本</td>
          <td>需記快捷鍵</td>
          <td>畫面有提示列，操作邊看邊學</td>
      </tr>
      <tr>
          <td>session 持久化</td>
          <td>有（detach / reattach）</td>
          <td>有，另內建 resurrection（結束後重建）</td>
      </tr>
      <tr>
          <td>設定生態</td>
          <td>成熟、範例與設定檔分享多</td>
          <td>內建 layout、設定較直覺</td>
      </tr>
      <tr>
          <td>資源佔用</td>
          <td>低</td>
          <td>略高但仍輕量（差在閒置記憶體、與傳輸頻寬無關）</td>
      </tr>
  </tbody>
</table>
<p>選型分界很清楚：受限或陌生的伺服器、要求處處可用，選 tmux；自己掌控的機器、想要友善的上手體驗與內建 layout，選 zellij。對 prefix 快捷鍵還不熟的人，這條分界仍成立：在別人的伺服器上工作優先學 tmux，因為無法保證對方裝了 zellij，可用性約束高於上手體驗；zellij 的友善體驗留給自己能掌控安裝的機器。兩者的指令心智模型相近（都靠一個 prefix/modifier 起手），學會一個再換另一個成本不高。zellij 路線的實際操作在本資料夾另有兩篇：pane 的 CLI 操作見 <a href="/blog/linux/tools/cli/zellij-pane/" data-link-title="Zellij 多終端機操作指南" data-link-desc="Zellij pane 的佈局查看、內容讀取、大小調整等 CLI 操作方式，適合搭配 AI 工具使用。">Zellij 多終端機操作指南</a>、瀏覽器遠端連線見 <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 Client 外網連線教學</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>在多工器的 pane 裡擺即時監控：見 <a href="/blog/linux/tools/cli/tui-monitoring-tools/" data-link-title="TUI 監控工具：btop、htop、k9s 的遠端使用與刷新率調校" data-link-desc="全螢幕 TUI 監控工具在遠端 SSH 情境的使用：htop 進程操作、btop 多資源儀表板、k9s 管 Kubernetes，以及慢速連線下刷新率與頻寬的取捨。">TUI 監控工具</a>。</li>
<li>zellij 的進階用法：<a href="/blog/linux/tools/cli/zellij-pane/" data-link-title="Zellij 多終端機操作指南" data-link-desc="Zellij pane 的佈局查看、內容讀取、大小調整等 CLI 操作方式，適合搭配 AI 工具使用。">Zellij 多終端機操作指南</a> 與 <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 Client 外網連線教學</a>。</li>
<li>多工器在三類遠端工具中的定位：<a href="/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽</a>。</li>
</ul>
]]></content:encoded></item><item><title>Zellij Web Client 外網連線教學</title><link>https://tarrragon.github.io/blog/linux/tools/cli/zellij-remote-web-client/</link><pubDate>Mon, 09 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/tools/cli/zellij-remote-web-client/</guid><description>&lt;p>Zellij Web Client 讓他人透過瀏覽器連線到指定的 Zellij session，承擔的責任是把終端機多工環境分享給沒有 SSH 連線的協作者。本文承接 &lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽&lt;/a> 的多工器分類；zellij 的本機 pane 操作見 &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 多終端機操作指南&lt;/a>、tmux 的持久化基礎見 &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>。&lt;/p>
&lt;hr>
&lt;h2 id="安裝-zellij">安裝 Zellij&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="c1"># macOS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">brew install zellij
&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（使用安裝腳本）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">bash &amp;lt;&lt;span class="o">(&lt;/span>curl -L zellij.dev/launch&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>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># Windows（需要支援原生 Windows 的版本，詳見 GitHub Releases）&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"># 從 https://github.com/zellij-org/zellij/releases 下載 Windows 版 .zip&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"># 解壓後將 zellij.exe 加入 PATH&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>確認版本（需 v0.43.0 以上）：&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">zellij --version&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="事前準備">事前準備&lt;/h2>
&lt;ul>
&lt;li>一個網域名稱（或固定 IP）&lt;/li>
&lt;li>SSL 憑證（對外連線強制要求）&lt;/li>
&lt;li>SSH 連線能力（如需遠端操作主機）→ 參考 &lt;a href="https://tarrragon.github.io/blog/work-log/ssh-key-%E8%A8%AD%E5%AE%9A%E7%AD%86%E8%A8%98macos-/-linux-/-windows/">SSH Key 設定筆記&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="步驟一取得-ssl-憑證">步驟一：取得 SSL 憑證&lt;/h2>
&lt;p>外網連線強制使用 HTTPS，必須提供 SSL 憑證。&lt;/p>
&lt;blockquote>
&lt;p>取得 Let&amp;rsquo;s Encrypt 憑證的 &lt;code>certbot&lt;/code> 指令需真實網域、本機未實機驗證；自簽憑證的 &lt;code>openssl&lt;/code> 指令、以及 zellij web server 啟停與 token 管理已在 localhost 實機驗證。&lt;/p>&lt;/blockquote>
&lt;h3 id="使用-lets-encrypt免費推薦">使用 Let&amp;rsquo;s Encrypt（免費，推薦）&lt;/h3>
&lt;p>需要先安裝 &lt;code>certbot&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"># macOS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">brew install certbot
&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"># Ubuntu / Debian&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">sudo apt install certbot
&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"># Windows（使用 Chocolatey）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">choco install certbot
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 或使用 win-acme（Windows 原生替代方案）：https://www.win-acme.com/&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>申請憑證（將 &lt;code>your-domain.com&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">sudo certbot certonly --standalone -d your-domain.com&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>Windows 上若未使用 WSL，建議改用 &lt;a href="https://www.win-acme.com/">win-acme&lt;/a>，操作更直覺。&lt;/p>&lt;/blockquote>
&lt;p>憑證預設存放在：&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"># macOS / Linux
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">/etc/letsencrypt/live/your-domain.com/fullchain.pem # 憑證
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">/etc/letsencrypt/live/your-domain.com/privkey.pem # 私鑰
&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"># Windows（certbot）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">C:\Certbot\live\your-domain.com\fullchain.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">C:\Certbot\live\your-domain.com\privkey.pem&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">openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days &lt;span class="m">365&lt;/span> -nodes&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>注意：自簽憑證會讓瀏覽器在連線時顯示安全警告，於測試環境手動選擇繼續即可；正式對外服務改用上面的 Let&amp;rsquo;s Encrypt 憑證。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="步驟二開放防火牆-port">步驟二：開放防火牆 Port&lt;/h2>
&lt;p>Zellij web server 預設只綁本機 &lt;code>127.0.0.1:8082&lt;/code>，要讓外網連入必須顯式綁到對外位址（見步驟四的 &lt;code>--ip 0.0.0.0&lt;/code>）並開放對應 port。本教學以 port &lt;code>3000&lt;/code> 為例（port 可自選），需對外開放這個 port：&lt;/p></description><content:encoded><![CDATA[<p>Zellij Web Client 讓他人透過瀏覽器連線到指定的 Zellij session，承擔的責任是把終端機多工環境分享給沒有 SSH 連線的協作者。本文承接 <a href="/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽</a> 的多工器分類；zellij 的本機 pane 操作見 <a href="/blog/linux/tools/cli/zellij-pane/" data-link-title="Zellij 多終端機操作指南" data-link-desc="Zellij pane 的佈局查看、內容讀取、大小調整等 CLI 操作方式，適合搭配 AI 工具使用。">Zellij 多終端機操作指南</a>、tmux 的持久化基礎見 <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>。</p>
<hr>
<h2 id="安裝-zellij">安裝 Zellij</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="c1"># macOS</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew install zellij
</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（使用安裝腳本）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">bash &lt;<span class="o">(</span>curl -L zellij.dev/launch<span class="o">)</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"># Windows（需要支援原生 Windows 的版本，詳見 GitHub Releases）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># 從 https://github.com/zellij-org/zellij/releases 下載 Windows 版 .zip</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># 解壓後將 zellij.exe 加入 PATH</span></span></span></code></pre></div><p>確認版本（需 v0.43.0 以上）：</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">zellij --version</span></span></code></pre></div><hr>
<h2 id="事前準備">事前準備</h2>
<ul>
<li>一個網域名稱（或固定 IP）</li>
<li>SSL 憑證（對外連線強制要求）</li>
<li>SSH 連線能力（如需遠端操作主機）→ 參考 <a href="https://tarrragon.github.io/blog/work-log/ssh-key-%E8%A8%AD%E5%AE%9A%E7%AD%86%E8%A8%98macos-/-linux-/-windows/">SSH Key 設定筆記</a></li>
</ul>
<hr>
<h2 id="步驟一取得-ssl-憑證">步驟一：取得 SSL 憑證</h2>
<p>外網連線強制使用 HTTPS，必須提供 SSL 憑證。</p>
<blockquote>
<p>取得 Let&rsquo;s Encrypt 憑證的 <code>certbot</code> 指令需真實網域、本機未實機驗證；自簽憑證的 <code>openssl</code> 指令、以及 zellij web server 啟停與 token 管理已在 localhost 實機驗證。</p></blockquote>
<h3 id="使用-lets-encrypt免費推薦">使用 Let&rsquo;s Encrypt（免費，推薦）</h3>
<p>需要先安裝 <code>certbot</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"># macOS</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew install certbot
</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"># Ubuntu / Debian</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">sudo apt install certbot
</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"># Windows（使用 Chocolatey）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">choco install certbot
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># 或使用 win-acme（Windows 原生替代方案）：https://www.win-acme.com/</span></span></span></code></pre></div><p>申請憑證（將 <code>your-domain.com</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 certbot certonly --standalone -d your-domain.com</span></span></code></pre></div><blockquote>
<p>Windows 上若未使用 WSL，建議改用 <a href="https://www.win-acme.com/">win-acme</a>，操作更直覺。</p></blockquote>
<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"># macOS / Linux
</span></span><span class="line"><span class="ln">2</span><span class="cl">/etc/letsencrypt/live/your-domain.com/fullchain.pem   # 憑證
</span></span><span class="line"><span class="ln">3</span><span class="cl">/etc/letsencrypt/live/your-domain.com/privkey.pem      # 私鑰
</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"># Windows（certbot）
</span></span><span class="line"><span class="ln">6</span><span class="cl">C:\Certbot\live\your-domain.com\fullchain.pem
</span></span><span class="line"><span class="ln">7</span><span class="cl">C:\Certbot\live\your-domain.com\privkey.pem</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">openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days <span class="m">365</span> -nodes</span></span></code></pre></div><blockquote>
<p>注意：自簽憑證會讓瀏覽器在連線時顯示安全警告，於測試環境手動選擇繼續即可；正式對外服務改用上面的 Let&rsquo;s Encrypt 憑證。</p></blockquote>
<hr>
<h2 id="步驟二開放防火牆-port">步驟二：開放防火牆 Port</h2>
<p>Zellij web server 預設只綁本機 <code>127.0.0.1:8082</code>，要讓外網連入必須顯式綁到對外位址（見步驟四的 <code>--ip 0.0.0.0</code>）並開放對應 port。本教學以 port <code>3000</code> 為例（port 可自選），需對外開放這個 port：</p>
<blockquote>
<p>以下防火牆指令（<code>ufw</code> / <code>pf</code> / Windows Defender）依各平台官方用法、環境特定、本機未實機驗證。</p></blockquote>
<h3 id="linuxufw">Linux（ufw）</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 ufw allow 3000/tcp
</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"># 或指定來源 IP（更安全）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">sudo ufw allow from 1.2.3.4 to any port <span class="m">3000</span></span></span></code></pre></div><h3 id="macos">macOS</h3>
<p>macOS 內建的防火牆是應用程式層級的，無法直接開放特定 port。通常有兩種做法：</p>
<ol>
<li><strong>系統偏好設定</strong> → 網路 → 防火牆 → 確認沒有擋住 Zellij</li>
<li><strong>使用 <code>pf</code></strong>（進階，通常不需要）：</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"># 新增規則到 /etc/pf.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;pass in proto tcp from any to any port 3000&#34;</span> <span class="p">|</span> sudo tee -a /etc/pf.conf
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo pfctl -f /etc/pf.conf</span></span></code></pre></div><blockquote>
<p>macOS 預設防火牆通常不會擋住主動開啟的服務，多數情況下不需要額外設定。如果是在家用網路，記得在路由器設定 port forwarding。</p></blockquote>
<h3 id="windows">Windows</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># 使用 Windows Defender Firewall（以系統管理員執行 PowerShell）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">New-NetFirewallRule</span> <span class="n">-DisplayName</span> <span class="s2">&#34;Zellij Web&#34;</span> <span class="n">-Direction</span> <span class="n">Inbound</span> <span class="n">-Protocol</span> <span class="n">TCP</span> <span class="n">-LocalPort</span> <span class="mf">3000</span> <span class="n">-Action</span> <span class="n">Allow</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="c"># 或限制來源 IP（更安全）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">New-NetFirewallRule</span> <span class="n">-DisplayName</span> <span class="s2">&#34;Zellij Web&#34;</span> <span class="n">-Direction</span> <span class="n">Inbound</span> <span class="n">-Protocol</span> <span class="n">TCP</span> <span class="n">-LocalPort</span> <span class="mf">3000</span> <span class="n">-RemoteAddress</span> <span class="mf">1.2</span><span class="p">.</span><span class="py">3</span><span class="p">.</span><span class="py">4</span> <span class="n">-Action</span> <span class="n">Allow</span></span></span></code></pre></div><blockquote>
<p>Zellij 已支援原生 Windows，直接在 PowerShell 或 Windows Terminal 中執行即可。</p></blockquote>
<p>如果是雲端主機（AWS、GCP、Azure 等），記得同步在後台的安全群組開放 port 3000。</p>
<hr>
<h2 id="步驟三啟動-zellij">步驟三：啟動 Zellij</h2>
<p>先啟動一個 Zellij session：</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">zellij</span></span></code></pre></div><hr>
<h2 id="步驟四啟動-web-server">步驟四：啟動 Web Server</h2>
<p>在 Zellij 內，按 <code>Ctrl+o</code> 然後按 <code>s</code> 開啟 share plugin，從 UI 啟動 web server。</p>
<p>或直接用 CLI 啟動並指定憑證：</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">zellij web <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --ip 0.0.0.0 --port <span class="m">3000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --cert /etc/letsencrypt/live/your-domain.com/fullchain.pem <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --key /etc/letsencrypt/live/your-domain.com/privkey.pem</span></span></code></pre></div><p>背景執行（daemon 模式）：</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">zellij web -d <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --ip 0.0.0.0 --port <span class="m">3000</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --cert /path/to/cert.pem <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --key /path/to/key.pem</span></span></code></pre></div><p>停止 web server：</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">zellij web --stop</span></span></code></pre></div><p>確認 web server 執行狀態：</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">zellij web --status</span></span></code></pre></div><p>Zellij web 預設綁 <code>127.0.0.1:8082</code>、只接受本機連線；對外服務必須用 <code>--ip 0.0.0.0</code> 顯式綁到對外位址、並用 <code>--port</code> 指定埠（本教學用 <code>3000</code>）。改用其他 port 時把 <code>--port</code> 一併調整（例如 <code>--port 8443</code>），防火牆規則也要同步改成該 port。</p>
<hr>
<h2 id="步驟五產生登入-token">步驟五：產生登入 Token</h2>
<p>為了安全，別人連線前需要用 token 登入，<strong>token 只會顯示一次</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">zellij web --create-token</span></span></code></pre></div><p>或在 share plugin（<code>Ctrl+o</code> + <code>s</code>）裡產生。</p>
<p>將 token 分享給要連線的人。</p>
<hr>
<h2 id="步驟六連線">步驟六：連線</h2>
<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">https://your-domain.com:3000/實際-session-名稱</span></span></code></pre></div><p>首次連線會要求輸入 token，驗證後即可進入 session。若連線後畫面沒有回應，多半是 port 未對外開放，確認防火牆與雲端主機安全群組是否放行該 port。</p>
<hr>
<h2 id="連線後的行為">連線後的行為</h2>
<table>
  <thead>
      <tr>
          <th>情況</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Session 正在執行</td>
          <td>直接 attach 進去</td>
      </tr>
      <tr>
          <td>Session 曾存在但已結束</td>
          <td>Zellij 自動重建（resurrection）</td>
      </tr>
      <tr>
          <td>全新 session 名稱</td>
          <td>建立新的 session</td>
      </tr>
  </tbody>
</table>
<p>多人連線時，每個人都有自己的游標，可以同時操作。</p>
<hr>
<h2 id="安全建議">安全建議</h2>
<ul>
<li>Token 用完後記得撤銷：從 share plugin 或 CLI 管理</li>
<li>盡量限制開放的來源 IP，避免對全網開放</li>
<li>不建議長期開啟 web server，用完就關</li>
<li>撤銷 token 時，所有對應的 session token 也會一併失效</li>
</ul>
<hr>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>zellij 的本機 pane 操作（查看佈局、讀取其他 pane、調整大小）：<a href="/blog/linux/tools/cli/zellij-pane/" data-link-title="Zellij 多終端機操作指南" data-link-desc="Zellij pane 的佈局查看、內容讀取、大小調整等 CLI 操作方式，適合搭配 AI 工具使用。">Zellij 多終端機操作指南</a>。</li>
<li>不需要瀏覽器、純 SSH 的多工器持久化：<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>。</li>
<li>多工器在整個遠端工具選型中的定位：<a href="/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽</a>。</li>
</ul>
]]></content:encoded></item><item><title>Zellij 多終端機操作指南</title><link>https://tarrragon.github.io/blog/linux/tools/cli/zellij-pane/</link><pubDate>Mon, 09 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/tools/cli/zellij-pane/</guid><description>&lt;p>Zellij 是終端機多工器，能在單一畫面分割多個 pane。本文整理透過 zellij CLI 查看佈局、讀取其他 pane 內容、調整 pane 大小的操作方式 — CLI 介面既適合遠端腳本化操作，也適合搭配看不到螢幕的 AI 工具（例如 Claude）在終端機協作。本文承接 &lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽&lt;/a> 的多工器分類；瀏覽器遠端連線見 &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 Client 外網連線教學&lt;/a>、tmux 的持久化基礎見 &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>。&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">zellij action dump-layout&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>會輸出完整的 KDL 格式佈局，包含所有 pane 的大小、位置、指令等資訊。&lt;/p>
&lt;h2 id="讀取其他終端機-pane-的內容">讀取其他終端機 pane 的內容&lt;/h2>
&lt;p>Claude 無法直接看到螢幕，但可以透過以下步驟讀取其他 pane 的輸出：&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"># 1. 切換 focus 到目標 pane（focus-next-pane 會依序切換）&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"># 2. dump 該 pane 的螢幕內容到檔案&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"># 3. 切回原本的 pane&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"># 4. 讀取 dump 的檔案&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">zellij action focus-next-pane &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span>zellij action focus-next-pane &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span>zellij action dump-screen /tmp/zellij-pane-output.txt &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span>zellij action focus-previous-pane &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span>zellij action focus-previous-pane&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>dump-screen&lt;/code> 只 dump 當前可見的內容&lt;/li>
&lt;li>&lt;code>dump-screen -f&lt;/code> 會包含完整的 scrollback 歷史&lt;/li>
&lt;li>切換次數取決於目標 pane 的位置，需根據 &lt;code>dump-layout&lt;/code> 的結果判斷&lt;/li>
&lt;/ul>
&lt;h2 id="調整-pane-大小">調整 pane 大小&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="c1"># 縮小當前 pane（向左縮）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">zellij action resize decrease right
&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"># 放大當前 pane（向右擴）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">zellij action resize increase right
&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"># 每次約改變 ~4-5% 寬度，可用迴圈批次調整&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">for&lt;/span> i in &lt;span class="k">$(&lt;/span>seq &lt;span class="m">1&lt;/span> 3&lt;span class="k">)&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span> zellij action resize decrease right&lt;span class="p">;&lt;/span> &lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每次的步長是經驗值、不是固定比例 — zellij 的 resize 幅度依版本與 pane 當前尺寸而定，迴圈次數需視 &lt;code>dump-layout&lt;/code> 的結果微調。&lt;/p>
&lt;h2 id="使用者的-resize-快捷鍵">使用者的 Resize 快捷鍵&lt;/h2>
&lt;ol>
&lt;li>&lt;code>Ctrl + n&lt;/code> 進入 Resize 模式&lt;/li>
&lt;li>&lt;code>h&lt;/code>/&lt;code>l&lt;/code> 或方向鍵調整大小&lt;/li>
&lt;li>&lt;code>Esc&lt;/code> 退出&lt;/li>
&lt;/ol>
&lt;p>注意：在 Claude 互動式程式內，快捷鍵可能被吃掉，建議讓 Claude 用指令操作。&lt;/p>
&lt;h2 id="注意事項">注意事項&lt;/h2>
&lt;ul>
&lt;li>&lt;code>Ctrl + p&lt;/code> 進入 Pane 模式，其中 &lt;code>r&lt;/code> 用於在右邊新開 pane（調整大小是 &lt;code>Ctrl + n&lt;/code> 的 Resize 模式）&lt;/li>
&lt;li>使用者的典型佈局：左側 Claude（~35%），右側上下兩個終端機&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>把 session 分享給沒有 SSH 連線的協作者（瀏覽器連入）：&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 Client 外網連線教學&lt;/a>。&lt;/li>
&lt;li>純 SSH 的多工器持久化與 tmux 對照：&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>。&lt;/li>
&lt;li>多工器在遠端工具選型中的定位：&lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Zellij 是終端機多工器，能在單一畫面分割多個 pane。本文整理透過 zellij CLI 查看佈局、讀取其他 pane 內容、調整 pane 大小的操作方式 — CLI 介面既適合遠端腳本化操作，也適合搭配看不到螢幕的 AI 工具（例如 Claude）在終端機協作。本文承接 <a href="/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽</a> 的多工器分類；瀏覽器遠端連線見 <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 Client 外網連線教學</a>、tmux 的持久化基礎見 <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>。</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">zellij action dump-layout</span></span></code></pre></div><p>會輸出完整的 KDL 格式佈局，包含所有 pane 的大小、位置、指令等資訊。</p>
<h2 id="讀取其他終端機-pane-的內容">讀取其他終端機 pane 的內容</h2>
<p>Claude 無法直接看到螢幕，但可以透過以下步驟讀取其他 pane 的輸出：</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. 切換 focus 到目標 pane（focus-next-pane 會依序切換）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 2. dump 該 pane 的螢幕內容到檔案</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 3. 切回原本的 pane</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 4. 讀取 dump 的檔案</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">zellij action focus-next-pane <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>zellij action focus-next-pane <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>zellij action dump-screen /tmp/zellij-pane-output.txt <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>zellij action focus-previous-pane <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>zellij action focus-previous-pane</span></span></code></pre></div><ul>
<li><code>dump-screen</code> 只 dump 當前可見的內容</li>
<li><code>dump-screen -f</code> 會包含完整的 scrollback 歷史</li>
<li>切換次數取決於目標 pane 的位置，需根據 <code>dump-layout</code> 的結果判斷</li>
</ul>
<h2 id="調整-pane-大小">調整 pane 大小</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="c1"># 縮小當前 pane（向左縮）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">zellij action resize decrease right
</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"># 放大當前 pane（向右擴）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">zellij action resize increase right
</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"># 每次約改變 ~4-5% 寬度，可用迴圈批次調整</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="k">for</span> i in <span class="k">$(</span>seq <span class="m">1</span> 3<span class="k">)</span><span class="p">;</span> <span class="k">do</span> zellij action resize decrease right<span class="p">;</span> <span class="k">done</span></span></span></code></pre></div><p>每次的步長是經驗值、不是固定比例 — zellij 的 resize 幅度依版本與 pane 當前尺寸而定，迴圈次數需視 <code>dump-layout</code> 的結果微調。</p>
<h2 id="使用者的-resize-快捷鍵">使用者的 Resize 快捷鍵</h2>
<ol>
<li><code>Ctrl + n</code> 進入 Resize 模式</li>
<li><code>h</code>/<code>l</code> 或方向鍵調整大小</li>
<li><code>Esc</code> 退出</li>
</ol>
<p>注意：在 Claude 互動式程式內，快捷鍵可能被吃掉，建議讓 Claude 用指令操作。</p>
<h2 id="注意事項">注意事項</h2>
<ul>
<li><code>Ctrl + p</code> 進入 Pane 模式，其中 <code>r</code> 用於在右邊新開 pane（調整大小是 <code>Ctrl + n</code> 的 Resize 模式）</li>
<li>使用者的典型佈局：左側 Claude（~35%），右側上下兩個終端機</li>
</ul>
<hr>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>把 session 分享給沒有 SSH 連線的協作者（瀏覽器連入）：<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 Client 外網連線教學</a>。</li>
<li>純 SSH 的多工器持久化與 tmux 對照：<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>。</li>
<li>多工器在遠端工具選型中的定位：<a href="/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽</a>。</li>
</ul>
]]></content:encoded></item></channel></rss>