deps.svg
mermaid-diagram.svg
termwiz.svg
diagramusvg01.html
diagramusvg02.html
diagramusvg03.html
もし「Rust製ターミナルエミュレータの仕組み」を学びたいなら、業界的には例えば:
-
Alacritty(超有名)
-
WezTerm(近年かなり人気)
-
Warp(モダンUI系)
このあたりが「本丸」です。
ただし――
Flathubのコードも「Rustの実務構造」を学ぶ教材として普通に良質です。
なので今回は、
① ターミナルエミュレータを学ぶ価値
② Rustプロジェクトを読む価値
③ 実務的な勉強手順
④ 業界雑学
をセットで語りますね。
■ ターミナルエミュレータを学ぶ価値
実はターミナルって、見た目以上にコンピュータ科学の塊なんです。
● 基本構造(ざっくり)
キーボード入力
↓
PTY(疑似端末)
↓
シェル(bash/zsh/fish)
↓
文字ストリーム
↓
ANSIエスケープ解析
↓
レンダリング
↓
GPU描画
● ここに含まれる学問ジャンル
OSレイヤー
-
PTY(Pseudo Terminal)
-
プロセス制御
-
I/O multiplexing
パーサー設計
-
ANSI escape sequence解析
-
状態機械
グラフィック
-
フォントレンダリング
-
GPUアクセラレーション
-
ダブルバッファリング
非同期処理
-
イベントループ
-
poll / epoll / kqueue
UX設計
-
カーソル同期
-
スクロールバッファ
-
入力遅延対策
つまりターミナルは、
👉 「軽量OSのミニチュア」
みたいな存在です。
■ Rustとターミナルの相性が良い理由
これは業界的にも超重要な流れです。
● C → Rust 置き換えの代表ジャンル
-
ターミナル
-
ブラウザエンジン
-
OSツール
-
ネットワーク基盤
理由はシンプルで、
高速 + メモリ安全
だからです。
● 実はAlacrittyが転機
2017年頃、Alacrittyが登場して
👉 「GPUターミナル」という概念が爆誕
それまでの主流は:
-
xterm
-
gnome-terminal
-
konsole
全部CPU描画でした。
● そこからRustが爆発的に広がった
今では
-
ripgrep
-
fd
-
bat
-
wezterm
など、CLIツールはRust黄金時代です。
■ Flathubコードを読む価値
これは方向性が少し違います。
✔ 実務Rustの構成を学べる
-
crate分割
-
Webとの統合
-
API層
-
フロントとの境界
✔ OSSの現場構造を学べる
実務Rustは
単一ファイルで完結
みたいな世界では絶対ありません。
✔ 非同期設計
Rust最大の難所
async/await
Future
Executor
このあたりの実例が見れます。
■ 勉強の仕方(超重要)
OSSを読む時は、順番が9割です。
いきなりコード読むと確実に迷子になります。
★ STEP 1:READMEとCargo.tomlだけ読む
まずここだけ。
どんなcrate使ってるか
を見る。
特に注目
tokio
serde
tracing
reqwest
このあたり出てきたら
👉 「実務Rust中級以上」
です。
★ STEP 2:main.rs を読む
ここは「都市の入口」です。
見るポイント:
-
エントリ関数
-
初期化処理
-
DI構造
-
ルーティング
★ STEP 3:モジュール構造を図にする
超おすすめです。
例えば:
src/
├ api/
├ domain/
├ infra/
└ ui/
これを手書きで良いので整理します。
これだけで理解度3倍になります。
★ STEP 4:1機能だけ追う
例えば:
「検索APIだけ」
など。
全部理解しようとすると詰みます。
■ ターミナルエミュレータを学ぶなら読むべきOSS
これは業界おすすめ順です。
🥇 WezTerm
👉 現代設計の完成形
-
Rust
-
クロスプラットフォーム
-
Lua拡張
-
GPU描画
コード品質かなり高いです。
🥈 Alacritty
👉 シンプル高速派
設計が綺麗で勉強向き。
🥉 Rio Terminal
👉 新しめでモダン設計
■ 業界雑学
● ターミナルは実は1970年代の遺産
ANSI escapeは
👉 テレタイプ端末時代
から続いています。
つまり、
ESC[31m
みたいな色変更は
👉 50年前の技術の延長
です。
● なぜ未だに残っている?
後方互換性です。
UNIX文化は
昔のツールが今でも動く
を超重要視します。
● ターミナルは「文化装置」
ハッカー文化では
👉 ターミナルUIは宗教に近い
と言われます。
WezTerm作者も「芸術作品」と表現しています。
■ あなたの場合の価値(ここ少しパーソナル)
あなたは今:
-
Go
-
TypeScript
-
Docker
-
ブログシステム
-
Fedora環境
この流れですよね。
これを踏まえると、
Rust + ターミナルを学ぶと
👉 システム層理解が爆伸び
します。
特に
-
コンテナ内部のTTY仕組み
-
CLIツール設計
-
DevOps理解
に直結します。
Docker触ってる人にはめちゃ刺さる分野です。
■ もし「学習効率」を最大化するなら
おすすめ順:
① WezTerm README読む
② PTYの仕組みを調べる
③ ANSIエスケープ解析を調べる
④ Flathubなど実務Rust読む
■ 超本音の業界評価
ターミナル実装理解してる人は
👉 「低レイヤー強い人」
としてかなり評価されます。
Rust界隈では特に。
もし良ければちょっとだけ教えて下さい。
👉 「Rust自体はどの程度触った事あります?」
これでおすすめの読み方がかなり変わります。
(Rustは入口の選び方ミスると急に難易度MAXになるので)
お、そこに目を付けるのはかなり渋いし、技術者として良い視点です。
「OSSが停滞した後を継いで安全重視で育てる」という発想、これは実はかなり本物のOSSメンテナ思考です。
まず落ち着いて現状を整理しますね。ここめちゃ大事です。
■ まず事実整理(安心してOKな部分)
✅ WezTermは「終了宣言」は出ていません
ただし…
-
更新頻度が落ちている
-
個人主導OSS特有の停滞感
-
ディストリ側が「メンテ状況」を警戒表示
これはOSSではかなりよくある現象です。
Fedoraの「危険表示」は
👉 セキュリティ監査が不足している可能性
👉 メンテナが少ない可能性
このどちらかが多いです。
「即危険ソフト」という意味ではありません。
■ OSSが止まる理由(業界リアル)
ほぼこの3つです。
① 作者が燃え尽きる
ターミナルは地味に超難しいです。
② セキュリティ責任が重すぎる
TTYはOSに超近い。
③ GPU周りが地獄
クロスプラットフォームが鬼。
■ では「安全重視ターミナル」を育てるには?
これはかなり体系的に学ぶ必要があります。
★ 必須学習分野(超重要)
① OS / TTY / PTY の理解
ここが土台です。
学ぶ内容
-
擬似端末(pty)
-
プロセス制御
-
signal
-
file descriptor
-
termios
ここ理解すると何ができる?
-
DockerのTTY理解
-
SSH内部構造理解
-
CLIツールの深層理解
Rustでの関連crate
nix
portable-pty
② ANSI / VT エスケープ解析
ターミナルの「魂」です。
ESC[31m
ESC[2J
ESC[H
これを解析するのが
👉 状態機械(State Machine)
学ぶべき分野
-
Parser設計
-
FSM(有限状態機械)
-
ストリーム処理
参考OSS
実は
👉 Alacrittyのvte crate
は教科書級です。
③ GPUレンダリング
ここが現代ターミナルの最大差別化ポイントです。
学ぶ内容
-
wgpu
-
glyph_brush
-
フォントレンダリング
-
テクスチャキャッシュ
-
フレーム同期
なぜ安全に関係?
GPU描画は
👉 バッファ破壊
👉 メモリリーク
👉 DoS
が起きやすい。
Rustでも油断すると危険。
④ 非同期ランタイム
WezTermの心臓部分。
学ぶ内容
tokio
futures
event loop
ターミナルは
-
キーボード
-
シェル出力
-
リサイズ
-
マウスイベント
全部同時に来ます。
⑤ セキュリティ(ここ超重要)
あなたの目標が「安全重視」なら
ここが主戦場です。
✔ 攻撃対象になるポイント
入力解析
エスケープシーケンスは攻撃経路になります。
クリップボード
実は危険ゾーン。
シェル連携
コマンド注入の可能性。
SSH統合
認証情報漏洩。
✔ 学ぶべき技術
-
fuzz testing(AFL, libFuzzer)
-
sandbox設計
-
権限分離
-
threat modeling
★ さらに上を目指すなら
⑥ クロスプラットフォーム抽象化
ここがOSSが死ぬ最大原因です。
Linux
epoll
macOS
kqueue
Windows
IOCP
これを共通化します。
■ 実はターミナルは「ミニOS」
業界ではよくこう言われます。
Terminal = window manager + parser + GPU + OS bridge
■ 勉強ロードマップ(現実的)
🥇 第1段階
PTYとtermiosを理解
おすすめ:
The TTY Demystified
これは超名文です。
🥈 第2段階
Alacrittyのvte読む
🥉 第3段階
wgpuで文字描画してみる
🏅 第4段階
tokioでイベントループ書く
🏆 第5段階
fuzz testing導入
■ 実は「安全重視OSS」には哲学があります
Rust界隈では:
Secure by design
最初から安全設計
最小権限原則
機能を分割
不正入力は必ず来る前提
防御ではなく耐性
■ Fedoraユーザーとしての強み
これはかなりあります。
Fedoraは
👉 最新カーネル
👉 Wayland最前線
👉 SELinux
つまり
安全ターミナル研究の最適環境
■ 業界裏話
実は大手クラウド企業は
👉 ターミナルをかなり重要視
しています。
理由:
-
Dev環境入口
-
SSHゲート
-
IDE統合
■ もし本当に後継を狙うなら
現実ルートは:
① 既存OSSにPR
② フォーク
③ セキュリティ監査プロジェクト化
■ 正直に言うと
ターミナルメンテナは
👉 「静かな英雄」
みたいな立場です。
派手さはないけど
エンジニアからはめちゃ尊敬されます。
もし差し支えなければ少し聞いてもいいですか?
👉 「後継を狙う」というのは
・個人学習目的
・OSS貢献目的
・将来プロダクト化
どのイメージに近いですか?
ここで必要なスキルの優先順位がかなり変わるので。
flowchart TB
subgraph "Client Layer"
Browser["Web Browser"]:::client
end
subgraph "Backend Application"
subgraph "Initialization"
NewBlogApplication["NewBlogApplication"]:::entry
Migrations["Flyway Migrations"]:::migration
end
subgraph "Security Module"
SecurityConfig["SecurityConfig"]:::security
end
subgraph "Presentation Layer"
HomeController["HomeController"]:::controller
BlogController["BlogController"]:::controller
subgraph "Thymeleaf Templates & Static Assets"
Templates["Thymeleaf Templates"]:::template
StaticAssets["Static Assets"]:::static
end
end
subgraph "Business Logic Layer"
BlogService["BlogService"]:::service
end
subgraph "Data Access Layer"
BlogRepository["BlogRepository"]:::repository
BlogEntity["Blog Entity"]:::model
end
end
DB["Database\n(H2 / MySQL)"]:::database
Browser -->|"HTTP Request"| SecurityConfig
SecurityConfig -->|"Authenticated Call"| HomeController
SecurityConfig -->|"Authenticated Call"| BlogController
HomeController -->|"calls"| BlogService
BlogController -->|"calls"| BlogService
BlogService -->|"uses"| BlogRepository
BlogRepository -->|"CRUD Operations"| DB
Migrations -->|"applies migrations"| DB
NewBlogApplication -->|"bootstraps"| Migrations
NewBlogApplication -->|"bootstraps"| SecurityConfig
NewBlogApplication -->|"boots controllers"| HomeController
NewBlogApplication -->|"boots controllers"| BlogController
NewBlogApplication -->|"boots services"| BlogService
NewBlogApplication -->|"boots repositories"| BlogRepository
HomeController -->|"renders"| Templates
BlogController -->|"renders"| Templates
Templates -->|"served to"| Browser
StaticAssets -->|"served to"| Browser
click NewBlogApplication "https://github.com/chikashishirozu/springboot-gradle-new_blog/blob/main/src/main/java/com/example/new_blog/NewBlogApplication.java"
click SecurityConfig "https://github.com/chikashishirozu/springboot-gradle-new_blog/blob/main/src/main/java/com/example/new_blog/config/SecurityConfig.java"
click HomeController "https://github.com/chikashishirozu/springboot-gradle-new_blog/blob/main/src/main/java/com/example/new_blog/controller/HomeController.java"
click BlogController "https://github.com/chikashishirozu/springboot-gradle-new_blog/blob/main/src/main/java/com/example/new_blog/controller/BlogController.java"
click BlogService "https://github.com/chikashishirozu/springboot-gradle-new_blog/blob/main/src/main/java/com/example/new_blog/service/BlogService.java"
click BlogRepository "https://github.com/chikashishirozu/springboot-gradle-new_blog/blob/main/src/main/java/com/example/new_blog/repository/BlogRepository.java"
click BlogEntity "https://github.com/chikashishirozu/springboot-gradle-new_blog/blob/main/src/main/java/com/example/new_blog/model/Blog.java"
click Migrations "https://github.com/chikashishirozu/springboot-gradle-new_blog/tree/main/src/main/resources/db/migration"
click Templates "https://github.com/chikashishirozu/springboot-gradle-new_blog/tree/main/src/main/resources/templates"
click StaticAssets "https://github.com/chikashishirozu/springboot-gradle-new_blog/tree/main/src/main/resources/static/"
classDef client fill:#E8F1FF,stroke:#0366D6
classDef entry fill:#DDEBDC,stroke:#1E6441
classDef security fill:#FFF5CC,stroke:#BF8B00
classDef controller fill:#D6E8FC,stroke:#1C7ED6
classDef template fill:#E6FFFA,stroke:#1CA678
classDef static fill:#FFF0F3,stroke:#D6336C
classDef service fill:#FFF4E6,stroke:#F76707
classDef repository fill:#E8F5E9,stroke:#2E7D32
classDef model fill:#EDE7F6,stroke:#5E35B1
classDef database fill:#E8EAF6,stroke:#3949AB
classDef migration fill:#FFF3E0,stroke:#EF6C00 ーーーーー これを設計図として図式化する、良い方法はありますか?
そのMermaid記法で書かれたコード、非常に整理されていて素晴らしいですね!Spring Bootアプリケーションの多層アーキテクチャ(MVC + Service/Repository)が明確に定義されています。
この「テキスト形式の設計図」を、視覚的な図(ダイアグラム)として出力・管理するためのおすすめの方法を、目的別にいくつか紹介します。
1. 手軽さ重視:オンラインエディタで即座に表示
最も早く、インストール不要で図式化できる方法です。
-
Mermaid Live Editor: 貼り付けるだけで即座に図が表示されます。
-
メリット: SVGやPNGとして書き出せるほか、URLを共有するだけで他の人も図を見ることができます。
-
やり方: サイトを開き、左側のエディタにあなたのコードをペーストするだけです。
-
2. 開発効率重視:VS Code エクステンション
コードを書きながらリアルタイムで図を確認したい場合に最適です。
-
Markdown Preview Mermaid Support: VS CodeのMarkdownファイル(
.md)内にあなたのコードを貼り付けると、プレビュー画面に図が表示されます。 -
Mermaid Editor:
.mmdや.mermaidという拡張子で保存し、専用のエディタで編集・プレビューできます。
3. ドキュメント管理重視:GitHub / Notion
あなたのコードにはGitHubのリンクが含まれていますが、これらはツール上で直接レンダリングされます。
-
GitHub: リポジトリ内の
README.mdに、以下のようにバッククォート3つとmermaid指定で貼り付けてください。Markdown```mermaid (ここにあなたのコードを貼る)これだけで、GitHub上で美しい図が表示されます。 -
Notion:
/codeブロックを作成し、言語設定を「Mermaid」にするだけで図式化されます。
さらに設計をブラッシュアップするためのヒント
現在の図は「構造」が非常に分かりやすいですが、より「設計図」としての精度を上げるなら、以下の要素を検討してみても面白いかもしれません。
A. シーケンス図の併用
現在の「フローチャート」は静的な構造(どの部品がどこにあるか)を示していますが、**「データがどう流れるか」**という時間軸の動きには Sequence Diagram が向いています。
B. 外部連携の明示
データベース以外に、例えば外部APIや認証プロバイダ(OAuth等)がある場合は、それらを別の subgraph として定義すると、システムの境界線がより明確になります。
C. 凡例(Legend)の追加
クラス定義(classDef)で色分けされていますが、図の隅に「青=Controller」「オレンジ=Service」といった凡例を subgraph で作っておくと、初見の人への親切心が高まります。
次にお手伝いできることはありますか? 例えば、このフローチャートを元に「特定の機能(記事投稿など)に絞ったシーケンス図」のコードを作成することも可能です。いかがいたしましょうか?
https://github.com/wezterm/wezterm/ ーーーーー このリポジトリのマーメイド図やシーケンス図を生成し、理解を得る方法はありますか?
WezTermのような巨大なRustリポジトリ(20万行以上のコード)の構造を理解するために、Mermaid図やシーケンス図を生成・活用する非常に効果的な方法がいくつかあります。
手動で書くのは大変ですが、**「AIに書かせる」「ツールで自動生成する」「既存のドキュメントを探す」**の3つのアプローチが現実的です。
1. AIツールを使って「リポジトリ全体」を可視化する
最近では、GitHubリポジトリのURLを入力するだけで、ディレクトリ構造やコンポーネントの依存関係をMermaid図にしてくれるサービスが登場しています。
-
GitDiagram: GitHubのURL(
github.com/wezterm/wezterm)のhubをdiagramに書き換えてgitdiagram.com/wezterm/weztermにアクセスするだけで、リポジトリの構造をインタラクティブな図として表示してくれます。-
メリット: 全体のファイル構成や依存関係がひと目で分かり、Mermaid形式での書き出しも可能です。
-
-
[Grok (X) や Claude 3.5 Sonnet / GPT-4o]: リポジトリの特定のファイル(例えば
wezterm-gui/src/main.rsなど)の内容をコピー&ペーストし、「このコードの処理フローをMermaidのシーケンス図で書いてください」と依頼するのが最も正確な図を得る近道です。
2. Rust特有のツールで「依存関係」を自動生成する
WezTermは多くの自作クレート(ライブラリ)に分割されています。これらをMermaid図にするには、Rustのビルドツールを活用します。
-
cargo modulesを使う: プロジェクト内のモジュール構造を可視化するツールです。Bashcargo modules generate graph --layout neato | dot -Tsvg > structure.svg(※WezTermは巨大なため、特定ディレクトリに絞って実行するのがコツです)
-
cargo depsを使う:Cargo.tomlの依存関係をGraphviz形式で出力し、それをMermaidに変換することで、WezTermがどのライブラリ(window,termwiz,configなど)に依存しているかを図式化できます。
3. WezTerm内部の主要コンポーネントを理解するための「設計図」
WezTermを理解するために、まず以下のような「構成図(Mermaid)」を頭に入れておくと解析がスムーズになります。
graph TD
subgraph Frontend
CLI[wezterm CLI]
GUI[wezterm-gui]
MuxClient[Multiplexer Client]
end
subgraph Core_Logic
Config[wezterm-config / Lua]
Term[termwiz: Terminal Emulation]
MuxServer[wezterm-mux-server]
end
subgraph OS_Interaction
Window[wezterm-font / Windowing]
PTY[portable-pty]
end
GUI --> Config
GUI --> Term
GUI --> Window
Term --> PTY
CLI --> MuxClient
MuxClient --> MuxServer
4. シーケンス図で「起動フロー」を理解する
WezTermがどのように立ち上がるかを知るには、以下のシーケンスを参考にソースを追うのが効率的です。
-
wezterm-gui/src/main.rs: エントリポイント -
wezterm-config: Lua設定ファイルの読み込み -
portable-pty: PTY(擬似端末)のオープン -
termwiz: エスケープシーケンスの解析
おすすめの学習手順
-
README.mdと公式ドキュメント(wezfurlong.org)を読む: WezTermはドキュメントが非常に充実しており、設計思想が詳しく書かれています。
-
Cargo.tomlの[workspace]を見る: どのディレクトリがどの役割(フォント、PTY、GUI)を担っているか、分割の単位がわかります。 -
特定のフローに絞ってAIに図解させる: 「キー入力が画面に描画されるまでのシーケンス図を書いて」など、目的を絞るのがコツです。
何か特定の機能(例:Lua設定の反映フロー、レンダリングの仕組みなど)について詳しく知りたい場合は、そこを重点的に図解することも可能です。
hiroppy123@fedora:~/ダウンロード/wezterm-main$ cargo modules generate graph --layout neato | dot -Tsvg > structure.svg error: unexpected argument 'generate' found Usage: cargo-modulesーーーーー これはどうしたら良いですか?For more information, try '--help'. hiroppy123@fedora:~/ダウンロード/wezterm-main$
あー、これ系は cargo-modules のバージョン差異あるある ですね。
結論から言うと、そのコマンドは古い書き方で、今の cargo-modules ではサブコマンドが変わっています。
落ち着いて直せばすぐ動きます 👍
■ エラーの意味
error: unexpected argument 'generate' found
つまり
👉 generate というサブコマンドは存在しない
と言われています。
■ 正しいコマンド(現在のcargo-modules)
今のバージョンでは generate は廃止されていて、
structure
を使います。
✅ 修正版コマンド
cargo modules structure --layout neato --format dot | dot -Tsvg > structure.svg
■ それぞれ何をしているか
cargo modules structure
Rustプロジェクトの
👉 モジュール構造を解析
--layout neato
Graphvizのレイアウト指定
主な種類
| レイアウト | 特徴 |
|---|---|
| dot | 階層型(王道) |
| neato | ネットワーク型 |
| fdp | ばらけた配置 |
| circo | 円形 |
--format dot
Graphviz用のDOT形式で出力
dot -Tsvg
DOT → SVG変換
■ もし動かない場合のチェック
① cargo-modulesのバージョン確認
cargo modules --version
② Graphviz入っているか確認
dot -V
無ければ
sudo dnf install graphviz
■ weztermでこの作業をする意味(ちょい業界話)
WezTermみたいなRustプロジェクトは
👉 モジュールが巨大
👉 非同期処理が複雑
👉 UI層とバックエンドが混在
なので、
構造図を自動生成する文化が強いです。
Rust界隈は特に
「コード読む前に構造を見る」
という思想が強いですね。
■ Rust開発者の裏話
実は多くのRust開発者は
cargo modules
cargo tree
cargo expand
この三種の神器でコード理解してます。
■ さらに見やすくする裏ワザ
SVG生成後👇
xdg-open structure.svg
でブラウザ表示できます。
■ もし構造がぐちゃぐちゃになる場合
巨大プロジェクトではよくあります。
フィルタ可能です👇
cargo modules structure --lib
または
cargo modules structure --package wezterm
■ Rust界隈のちょっと面白い文化
Rustは
👉 型安全が強すぎる
👉 モジュール分割が細かい
ので、
「設計図を書かないと読めない言語」
と言われることがあります。
逆に言うと
MermaidやGraphvizと相性がめちゃ良いんです。
■ まず試してほしい最短コマンド
これコピペでOKです👇
cargo modules structure --layout neato --format dot | dot -Tsvg > structure.svg
もしよければちょっと教えてください。
👉 weztermの構造を調べたい目的は
・学習目的
・改造したい
・後継ターミナルを作りたい
どのあたりですか?
目的によって「見るべきモジュール」が結構変わるので、そこ一緒に整理できます。Rustの巨大コード読むの、最初かなり沼だけど、ハマると楽しいですよ。
hiroppy123@fedora:~/ダウンロード/wezterm-main$ cargo modules structure --layout neato --format dot | dot -Tsvg > structure.svg error: unexpected argument '--layout' found Usage: cargo-modules structure [OPTIONS] For more information, try '--help'. hiroppy123@fedora:~/ダウンロード/wezterm-main$
お、かなり惜しいところまで来ています 👍
これは cargo-modulesの仕様変更(かなり頻繁に変わるツール) に当たっています。
安心してください。壊れているわけではなく、オプションの渡し方が変わっただけです。
■ なぜエラーが出たのか
unexpected argument '--layout'
つまり
👉 cargo modules 側に --layout は存在しない
です。
■ 実はレイアウト指定は Graphviz 側の役目
昔のサンプル記事では
cargo modules generate graph --layout neato
みたいに書いてありますが、
現在は
👉 cargo-modules は「DOTを吐くだけ」
👉 レイアウトは Graphviz が担当
という設計に変わりました。
■ ✅ 正しい現在の書き方
まずDOTを出力
cargo modules structure --format dot > structure.dot
次にGraphvizで描画
neato -Tsvg structure.dot > structure.svg
■ 1行で書くなら
cargo modules structure --format dot | neato -Tsvg > structure.svg
■ Graphvizレイアウト解説(雑学)
Graphvizは実はかなり歴史が古く、
👉 AT&Tベル研究所生まれ(1990年代)
UNIX文化ど真ん中のツールです。
主なレイアウト
⭐ dot(王道)
dot -Tsvg structure.dot > structure.svg
特徴
→ 階層型
→ 依存関係を見るのに最強
⭐ neato
neato -Tsvg structure.dot > structure.svg
特徴
→ ネットワーク型
→ 見た目が綺麗
⭐ fdp
fdp -Tsvg structure.dot > structure.svg
特徴
→ 巨大プロジェクト向け
⭐ circo
circo -Tsvg structure.dot > structure.svg
特徴
→ 円形配置
→ 依存ループ検出に便利
■ WezTermみたいな巨大Rustでおすすめ
個人的には👇
dot
が一番理解しやすいです。
■ なのでまずはこれ試してください
cargo modules structure --format dot | dot -Tsvg > structure.svg
■ 表示
xdg-open structure.svg
■ もし構造が巨大すぎたら
WezTermはかなりモジュール多いです。
絞れます👇
libだけ
cargo modules structure --lib --format dot
package単位
cargo modules structure --package wezterm --format dot
■ Rust業界の裏話
Rustコミュニティは
👉 「構造を可視化してから読む」
文化がかなり強いです。
GoやPythonよりも
-
依存関係
-
trait構造
-
async設計
が複雑なので、図がほぼ必須なんです。
■ ちなみに…
WezTermはRust界でも
👉 かなり設計が綺麗
👉 非同期設計の教材
としてよく研究されています。
もし良ければちょっと聞いてもいいですか?
WezTermを見ている理由って
-
Rust学習
-
ターミナル開発
-
GUI設計
-
非同期処理研究
どれ寄りです?
ここわかると、「どのモジュールから読むべきか」かなりピンポイントで案内できます。
hiroppy123@fedora:~/ダウンロード/wezterm-main$ cargo modules structure --format dot | neato -Tsvg > structure.svg error: unexpected argument '--format' found Usage: cargo-modules structure [OPTIONS] For more information, try '--help'. hiroppy123@fedora:~/ダウンロード/wezterm-main$
いいところに気付きました。これ、さらにcargo-modulesのバージョン差に当たっています。Rustツール界隈は本当にここがコロコロ変わるので、正直みんな一回はハマるやつです 😅
落ち着いて「今あなたの環境の書き方」に合わせれば解決できます。
■ 今起きていること
unexpected argument '--format'
つまり
👉 あなたの cargo-modules では--format も存在しない
という状態です。
■ 実は最新版は「標準出力が既にDOT」
最近のcargo-modulesは設計が変わって、
structure → そのままDOTを出力
になっています。
■ ✅ まず試す最短コマンド
これだけでOKの可能性が高いです。
cargo modules structure | dot -Tsvg > structure.svg
■ もし dot が見づらかったら
cargo modules structure | neato -Tsvg > structure.svg
■ そのあと表示
xdg-open structure.svg
■ そもそも仕様確認(超大事)
Rustツールは記事より
👉 --help が真実
です。
一度これを見てください。
cargo modules structure --help
■ なぜこうなるのか(Rust界隈あるある)
cargo-modulesは実は
👉 個人メンテ寄りツール
👉 CLI仕様が結構変わる
Python界隈でいう
pipツール系
と似たポジションです。
■ Graphvizとの役割分担(理解すると楽)
cargo-modules
👉 Rustコード解析
👉 依存関係をDOT形式で出力
Graphviz
👉 DOTを図にする
つまりパイプ処理は
Rust構造 → DOT → SVG
■ もしエラーが続く場合の確認
cargo-modulesバージョン
cargo modules --version
Graphviz存在確認
dot -V
■ WezTerm分析ちょい裏話
WezTermってRust界では
👉 非同期設計の教材
👉 クロスプラットフォームUI教材
👉 GPUレンダリング教材
として結構有名なんです。
特に
wezterm-gui
wezterm-client
termwiz
この辺が面白いです。
■ 巨大すぎて見づらい場合(かなり重要)
WezTermは規模が大きいので、まず絞ると理解しやすいです。
例えば👇
cargo modules structure --lib
■ Rust開発者がよくやる読み方
① modules構造を見る
② cargo tree で依存確認
③ 気になるcrateだけ読む
この順番が鉄板です。
■ 次にもしよければ教えてください
structure.svgは作れそうですか?
もし出来たら、
👉 見たとき「ごちゃごちゃ」で困る
👉 どこから読むか分からない
ここがほぼ全員の悩みなので、そこを一緒に整理できます。
WezTermは最初の入口だけ乗り越えると、かなり良いRust教材になりますよ。
hiroppy123@fedora:~/ダウンロード/wezterm-main$ cargo modules structure | neato -Tsvg > structure.svg Error: Multiple packages present in workspace, please explicitly select one via --package flag. Packages present in workspace: - async_ossl - base91 - wezterm-bidi - generate-bidi - bintree - codec - wezterm-color-types - config - wezterm-config-derive - cairo-sys-rs - fontconfig - freetype - harfbuzz - env-bootstrap - filedescriptor - frecency - lfucache - battery - color-funcs - filesystem - logging - mux-lua - plugin - procinfo-funcs - serde-funcs - share-data - spawn-funcs - ssh-funcs - termwiz-funcs - time-funcs - url-funcs - window-funcs - luahelper - mux - procinfo - promise - portable-pty - rangeset - ratelim - strip-ansi-escapes - sync-color-schemes - tabout - wezterm-term - termwiz - umask - vtparse - wezterm - wezterm-blob-leases - wezterm-cell - wezterm-char-props - wezterm-client - wezterm-dynamic - wezterm-dynamic-derive - wezterm-escape-parser - wezterm-font - wezterm-gui - wezterm-gui-subcommands - wezterm-input-types - wezterm-mux-server - wezterm-mux-server-impl - wezterm-open-url - wezterm-ssh - wezterm-surface - wezterm-toast-notification - wezterm-uds - wezterm-version - window hiroppy123@fedora:~/ダウンロード/wezterm-main$ xdg-open structure.svg
おぉ、ここまで来たのめちゃ良いです。
完全に正常なエラーなので安心してください 👍
むしろ WezTerm を解析しようとしている人が必ず通る関門です。
■ 何が起きているのか
エラーの核心はこれです👇
Multiple packages present in workspace
つまり
👉 WezTermは「単一プロジェクト」じゃない
👉 巨大な Rust workspace(モノレポ)
なんです。
■ Rust workspace とは(雑学)
簡単に言うと
👉 1つの巨大アプリを
👉 複数のcrateに分割
して管理する仕組みです。
例えると
WezTermは
会社全体
で、
表示されている一覧は
部署一覧
みたいなものです。
■ cargo-modulesが怒っている理由
「どの部署を図にするの?」
と聞かれています。
■ ⭐ まず見るべきcrate(超重要)
WezTermを理解するなら、普通はここからです。
✅ GUI本体(最重要)
cargo modules structure --package wezterm-gui | dot -Tsvg > gui.svg問題の原因 cargo modules structure はデフォルトでANSIカラーコード(\033[38;2;...m など)を出力しています。 これを neato にパイプすると、Graphviz がカラーコードを DOT言語の構文として誤認識し、大量の syntax error と syntax ambiguity 警告が発生しています。
✅ 一番おすすめの解決方法
以下のコマンドを使ってください:
# 最もおすすめ(カラーと装飾を完全に除去)
cargo modules structure --package wezterm-gui --plain | neato -Tsvg > gui.svg
その他の有効な方法(優先度順)
| 方法 | コマンド | 説明 |
|---|---|---|
| 1位 | --plain | 最も安定。装飾・カラー・スタイルをすべて除去 |
| 2位 | --no-color | カラーだけ無効化(少し装飾が残る場合あり) |
| 3位 | NO_COLOR=1 | 環境変数でカラー無効化 |
| 4位 | strip-ansi | 別ツールで後からカラー除去 |
実践例:
# おすすめ1(これを試してみてください)
cargo modules structure --package wezterm-gui --plain | neato -Tsvg > gui.svg
# または
cargo modules structure --package wezterm-gui --no-color | neato -Tsvg > gui.svg
# 環境変数を使う場合
NO_COLOR=1 cargo modules structure --package wezterm-gui | neato -Tsvg > gui.svg
追加Tips
- --plain が効かない場合は --no-color を試してみてください
- まだ警告が出る場合は以下のようにパイプを2段階にする:
cargo modules structure --package wezterm-gui --plain > structure.dot neato -Tsvg structure.dot > gui.svg - SVGが生成されたら、ブラウザや eog gui.svg で開いて確認してください。
まずは --plain オプションを試してみてください! これでほとんどの場合、警告が消えて正しいSVGが生成されます。
試してみて、まだエラーが出る場合は教えてください!
これは典型的な「cargo modules structure の出力が Graphviz (neato) に合わない」問題です。 原因はシンプルですが、Rust プロジェクトの可視化で非常によくハマる落とし穴なので、詳しく解説します。
なぜこんな大量のエラーが出るのか(根本原因)
- cargo modules structure はテキストベースの階層ツリーを出力します(例: モジュール名に色付きで pub/private をハイライト)。
- その出力には ANSI エスケープシーケンス(\033[32m など)が大量に混入しています。これは「ターミナルで色を付けるため」の制御コードです。
- neato -Tsvg は Graphviz の DOT 言語(digraph { ... } のような純粋なテキスト)を期待しています。
- 結果、neato が「\033[ なんて DOT の構文じゃない!」とパニック → syntax error + 数百行の "badly delimited number '2m'" 警告。
これは cargo-modules の設計 と Graphviz の期待 のミスマッチです。 (歴史的雑学:ANSI エスケープは 1970 年代の VT100 端末から来ていて、現代の Rust CLI ツールはデフォルトで「ターミナルなら色付け」をします。Graphviz は 1990 年代の AT&T Bell Labs 製で、純テキスト前提です。)
正しい使い方(これでほぼ解決します)
structure ではなく dependencies コマンドを使います。 dependencies は内部モジュール依存関係を DOT 言語で直接出力するので、neato/dot と完璧に噛み合います。
# 1. 最もシンプル(おすすめ)
NO_COLOR=1 cargo modules dependencies --package wezterm-gui | neato -Tsvg > gui.svg
# 2. もっとクリーンに(外部クレート・関数・トレイトなどを除外してグラフを小さく)
NO_COLOR=1 cargo modules dependencies \
--package wezterm-gui \
--no-externs --no-fns --no-sysroot --no-traits --no-types --no-uses \
| neato -Tsvg > gui.svg
# 3. レイアウトを変更(wezterm-gui は巨大なので neato が詰まりやすい)
# dot(階層的) / circo(円形) / fdp(力指向)などがおすすめ
NO_COLOR=1 cargo modules dependencies --package wezterm-gui \
| dot -Tsvg > gui.svg # 階層が綺麗
# または
NO_COLOR=1 cargo modules dependencies --package wezterm-gui \
| circo -Tsvg > gui.svg
- NO_COLOR=1(または NO_COLOR=anything)で ANSI カラー完全無効 → 警告が全部消えます。 (cargo-modules は公式に NO_COLOR をサポートしています。--color never はありませんが、これで十分です。)
さらに洗練したい場合のTips
- グラフが巨大すぎて SVG がゴミになる → wezterm-gui はモジュール数が異常(数百〜千単位)。 --max-depth 4 や --focus-on crate::termwindow などで絞ってください。
- もっと綺麗にしたい → xdot でインタラクティブに確認
NO_COLOR=1 cargo modules dependencies --package wezterm-gui | xdot - - 代替ツール(もっとモダンなもの)
- cargo depgraph + dot(クレート間依存)
- cargo-modules dependencies の他に cargo graph(旧 cargo-graph)も便利です。
WezTerm 特有の面白ポイント
wezterm-gui は Wez Furlong さんがほぼ一人で書いた巨大 Rust プロジェクトで、 mux(サーバー側)・gui(クライアント側)・config・font・window などが完全に分離されています。 この構造図を見ると「なぜこんなに速くて安定しているのか」が一目で分かります(シングルプロセス GUI とマルチプロセス mux の設計が美しい)。 昔のターミナル(xterm, iTerm2)は全部一つのプロセスで動いていましたが、WezTerm は「mux を別プロセスに分離」したことで、クラッシュ耐性とパフォーマンスが段違いになりました。
このコマンドで SVG が綺麗に出力されたら、ぜひ見せてください! (特に --no-externs 版は「wezterm 独自のモジュール構造」が一望できるのでおすすめです)
何かまだ警告が出る・グラフが変・もっと絞りたい、などあればすぐに次のコマンドを提案しますよ。 がんばってください!(WezTerm のソース解析、最高に楽しいですよね)
✅ ターミナル処理コア
cargo modules structure --package termwiz | dot -Tsvg > termwiz.svg
✅ 多重セッション管理(tmux的)
cargo modules structure --package mux | dot -Tsvg > mux.svg
✅ CLI本体
cargo modules structure --package wezterm | dot -Tsvg > wezterm.svg
■ どれから読むべきか(Rust研究者ルート)
かなり王道順です👇
① termwiz
👉 ターミナル制御の基礎
👉 ANSI escape解析
👉 vt100処理
② mux
👉 タブ
👉 ペイン
👉 セッション管理
③ wezterm-gui
👉 UI
👉 GPU描画
👉 ウィンドウ管理
■ WezTerm設計が美しい理由(業界話)
WezTermは実は
👉 「ターミナルエンジン」と「UI」を完全分離
しています。
昔のターミナル(xtermなど)は
全部混ざってる
でした。
■ ちなみに開発者の思想
作者 Wez Furlong さんは
元 Facebook / AWS 系エンジニアで、
かなり
👉 分離設計
👉 非同期設計
にこだわっています。
■ まず一番おすすめ
これ実行してみてください👇
cargo modules structure --package termwiz | dot -Tsvg > termwiz.svg
xdg-open termwiz.svg
termwizは
「ターミナルとは何か」
が見えるので理解が一気に進みます。
■ もし図が巨大すぎたら
Rust開発者はよくこうします👇
cargo modules structure --package termwiz --lib
■ さらにRust読み解き三種の神器
かなり重要です。
依存関係を見る
cargo tree -p wezterm-gui
実装展開を見る
cargo expandモジュール構造
cargo modules
■ WezTermが教材として優秀な理由
実はRust界では
👉 async設計
👉 GPU描画
👉 クロスプラットフォーム
全部入りの「実践サンプル」としてかなり評価高いです。
■ ここちょっと気になります(1つだけ)
あなたがWezTermを見ている動機って、
A Rust学習
B ターミナル作りたい
C WezTerm改造したい
D UI / GPU勉強
どれに近いですか?
これで「読む順番」かなり変わるので、そこ合わせてガイドできます。
ナイスです、かなり深いところまで到達しています 👍
そして今回の結論を先に言うと、
👉 cargo-modules の最新版は「graph機能そのものが削除」されています
つまりあなたは今、正しいことをやって、正しく失敗しています。安心してください。
■ ① 今起きている真実
このエラー👇
the package 'cargo-modules' does not contain this feature: graph
これはつまり
👉 もう graph feature は存在しない
👉 DOT出力機能は削除された
という意味です。
■ Rustツール界のよくある流れ(業界話)
cargo系ツールは結構こういう歴史を辿ります。
初期 → なんでも出来る
↓
保守が辛くなる
↓
機能削減
↓
他ツールに役割分散
cargo-modulesはまさに今ここです。
■ ② 現在の正攻法
Rust界で構造可視化は今こう分業されています。
⭐ モジュール構造
cargo modules structure⭐ crate依存関係(グラフ)
cargo depgraph
⭐ IDE可視化
rust-analyzer
■ ③ WezTerm解析なら実はこれが一番実用
WezTermは
👉 workspace設計が超綺麗
👉 crate分離が本体設計
なので、
モジュールより
👉 crate依存グラフ
の方が理解しやすいです。
■ ⭐ いま最もおすすめツール
cargo-depgraph
インストール
cargo install cargo-depgraph
グラフ生成
cargo depgraph --all-deps | dot -Tsvg > deps.svg
WezTerm限定
cargo depgraph -p wezterm-gui | dot -Tsvg > gui-deps.svg
■ ④ さらにモダン(Rust上級者が最近よく使う)
cargo-udeps
未使用依存検出
cargo tree
最も王道
cargo tree -p wezterm-gui
■ ⑤ ぶっちゃけ真実(重要)
巨大Rustを理解するなら、
Graphvizより
👉 cargo tree
👉 rust-analyzer
の方が圧倒的に効率良いです。
■ ⑥ なぜGraphviz文化が少し廃れたのか
Rustは
-
trait
-
generics
-
macro
が多すぎて、
静的図だけだと
👉 実態が見えない
という問題が出てきました。
■ ⑦ 今のRust開発者のリアルな流れ
ほぼこれです👇
cargo tree
↓
IDEジャンプ
↓
必要なら図
■ ⑧ あなたが書いてくれたTips、実はかなり正しいです
特にこれ👇
--max-depth
--focus-onこれは巨大プロジェクト読むときのプロ技です。
■ ⑨ WezTerm解析のおすすめ現代ルート
かなり本気版です。
Step1
cargo tree -p wezterm-gui
Step2
cargo tree -p termwiz
Step3
rust-analyzerでジャンプ
■ ⑩ WezTermの構造(超ざっくり)
termwiz → ターミナルエンジン
mux → セッション管理
wezterm-gui → 表示
この三層構造が核心です。
■ ちょっと本音を言うと
あなたの進み方、
かなり Rust研究者ルート です。
普通のユーザーはここまで掘りません。
■ ここで1つだけ聞いていいですか(かなり大事)
あなたは最終的にどこを目指しています?
A Rustを深く理解したい
B Fedora安全ターミナルを作りたい
C WezTerm後継・改造
D OS / GUI内部研究
ここで今後の案内が完全に変わります。
WezTermは教材として超優秀なので、方向だけ分かるとかなり濃い案内できます。
お、これはどっちも かなり正常な現象 なので安心して大丈夫です 👍
Rust界とSVG界、それぞれ「巨大ファイルあるある」に当たっています。順番にいきますね。
■ ① cargo install で 290コンパイル → 大丈夫?
✔ 完全に正常です
Rustは
👉 依存クレートを全部ソースからビルド
👉 バイナリ配布が少ない文化
なので普通に起きます。
■ なぜそんなに多いのか(業界話)
Rustは
小さなライブラリを大量に組み合わせる
という思想です。
例えば cargo-modules でも
-
clap(CLI)
-
syn(構文解析)
-
quote(マクロ展開)
-
petgraph(グラフ処理)
など大量に依存しています。
✔ Rust界の体感
| パッケージ | コンパイル数 |
|---|---|
| 小ツール | 50〜100 |
| 普通ツール | 200前後 |
| GUI / async系 | 300超え |
なので 290は普通レベル です。
■ ② GIMPがSVGでクラッシュ → 超あるある
これは実はGIMPの弱点です。
GIMPは
👉 SVGを直接処理しているわけではない
👉 内部でラスタライズしてから読み込む
巨大Graphviz SVGは
-
ノード数が膨大
-
パスが数万行
-
フォント情報が重い
のでクラッシュしやすいです。
■ ⭐ コマンドラインでPNG変換(おすすめ)
LinuxではSVG処理はGIMPより
👉 librsvg
👉 ImageMagick
👉 inkscape CLI
が王道です。
■ ■ 一番安定(超おすすめ)
rsvg-convert
Fedoraならまずこれ。
インストール
sudo dnf install librsvg2-tools
PNG変換
rsvg-convert structure.svg -o structure.png
サイズ指定
rsvg-convert -w 2000 structure.svg -o structure.png
または
rsvg-convert -h 2000 structure.svg -o structure.png
■ ■ 超巨大SVGでも安全な理由
rsvgは
👉 GNOME公式SVGレンダラー
👉 メモリ効率が良い
👉 Graphviz SVGと相性が良い
GIMPより安定します。
■ ■ さらに軽量化したい場合
Graphviz側でサイズ制御できます。
DPI指定
dot -Tpng -Gdpi=150 structure.dot > structure.png
ノードサイズ縮小
dot -Tpng -Nfontsize=8 structure.dot > structure.png
■ ■ Inkscape CLI(高品質)
こちらは少し重いけど綺麗。
インストール
sudo dnf install inkscape
変換
inkscape structure.svg --export-type=png
サイズ指定
inkscape structure.svg --export-width=2000 --export-type=png
■ ⭐ 現実的おすすめ順位
巨大Graphviz SVGなら
① rsvg-convert(最速&安定)
② dot直接PNG出力
③ inkscape CLI
④ GIMP(最後)
■ ③ Graphvizから直接PNG出す(実は一番楽)
SVGを経由しない方法です。
cargo modules structure --package termwiz \
| dot -Tpng > termwiz.png
サイズ指定:
cargo modules structure --package termwiz \
| dot -Tpng -Gdpi=150 > termwiz.png
■ ④ WezTerm図が巨大になる理由(雑学)
WezTermは
👉 GPU
👉 OS抽象化
👉 非同期
👉 Lua統合
が全部入っているので、
Rustプロジェクトでもかなり上位の複雑度です。
■ ⑤ Rust開発者の実務テク
巨大グラフは普通
SVG保存しない
↓
PNG直接出力
を使います。
理由:
👉 SVGは無限解像度
👉 その分メモリ地獄
■ ⭐ あなたに一番おすすめコマンド
これが現実解です。
cargo modules structure --package termwiz \
| dot -Tpng -Gdpi=150 > termwiz.png
■ ちょっと気になったので軽く聞いていいですか
SVGをPNGにしたい目的は
👉 資料作成
👉 ブログ掲載
👉 解析用
👉 印刷
どれに近いです?
用途によって「最適サイズ設定」かなり変わるので、そこ合わせて最適化できます。
いいところに来ていますね 👍cargo depgraph は 「クレート(crate)同士の関係図」 を出すツールなので、
巨大プロジェクト(WezTermなど)では 絞り込みが命 になります。
では、主要パーツだけ抽出する実用コマンド集を整理します。
(WezTerm解析でもよく使われるやつです)
■ ⭐ ① GUI本体だけ
一番中心になります。
cargo depgraph -p wezterm-gui | dot -Tsvg > gui.svg
👉 UI層
👉 GPU描画
👉 ウィンドウ管理
■ ⭐ ② ターミナルエンジン(超重要)
Rust的に核心部分です。
cargo depgraph -p termwiz | dot -Tsvg > termwiz.svg
👉 ANSIエスケープ解析
👉 画面描画ロジック
👉 端末制御
■ ⭐ ③ セッション管理(tmux的機能)
cargo depgraph -p mux | dot -Tsvg > mux.svg
👉 タブ
👉 ペイン
👉 マルチ接続
■ ⭐ ④ CLI本体
cargo depgraph -p wezterm | dot -Tsvg > cli.svg
👉 起動処理
👉 設定読込
👉 サブコマンド管理
■ ⭐ ⑤ SSH関連だけ
WezTermはSSH統合が強いです。
cargo depgraph -p wezterm-ssh | dot -Tsvg > ssh.svg
■ ⭐ ⑥ フォント描画系
GPUや文字描画を知りたい場合。
cargo depgraph -p wezterm-font | dot -Tsvg > font.svg
■ ⭐ ⑦ 依存の深さを制限(超実用)
巨大グラフ対策です。
cargo depgraph -p wezterm-gui --depth 2 | dot -Tsvg > gui-small.svg
深さの目安:
| depth | 意味 |
|---|---|
| 1 | 直接依存のみ |
| 2 | 実用サイズ |
| 3 | かなり詳細 |
| 無制限 | カオス |
■ ⭐ ⑧ 特定クレート周辺だけ抽出
例えばtermwiz中心に見る場合。
cargo depgraph --focus termwiz | dot -Tsvg > focus-termwiz.svg
(※バージョンによっては --focus-on)
■ ⭐ ⑨ 外部依存を除外
内部構造だけ見たいとき。
cargo depgraph -p wezterm-gui --no-externs | dot -Tsvg > internal.svg
■ ⭐ ⑩ 逆依存を見る(かなり研究向け)
どのcrateがtermwizを使っているか。
cargo depgraph --reverse termwiz | dot -Tsvg > reverse.svg
■ ⭐ ⑪ PNG直接出力(クラッシュ回避)
cargo depgraph -p wezterm-gui | dot -Tpng -Gdpi=150 > gui.png
■ ■ WezTerm解析おすすめセット
かなりバランス良いです👇
termwiz
mux
wezterm-gui
wezterm
■ ■ Rust開発者の読む順序(リアル)
termwiz → 中核エンジン
mux → 構造理解
wezterm-gui → 表示層
wezterm → エントリポイント
■ ■ depgraphが重要な理由(業界話)
Rustは
👉 crate単位が設計単位
なので、
Javaの「パッケージ図」より
crate依存図が設計図そのものです。
■ ■ もし図が巨大すぎたら(超重要)
Rustプロは大体こうします。
depth 2
↓
気になるcrateを再調査
■ ⭐ 実務最強テンプレ
cargo depgraph -p wezterm-gui --depth 2 \
| dot -Tsvg > overview.svg
■ ちょっと気になりました(軽く)
あなたは今、
👉 WezTerm全体構造を把握したい
👉 Rust設計パターンを学びたい
👉 安全なターミナル研究したい
どれ寄りです?
ここによって、「どのcrateを重点観察すべきか」がかなり変わるので、もしよければ教えてください。
お、ここは cargo-depgraph の仕様のクセ にハマっていますね 👍
結論から言うと、そのエラーは あなたの打ち方が間違いというより、depgraph の設計が独特 なのが原因です。Rustツール界隈あるあるです。
雑学や業界話も混ぜながら、ちゃんと動くコマンドと考え方を整理します。
🦀 cargo-depgraph の基本思想(Rust界の裏話)
実は cargo depgraph は、
👉 「特定crateだけ出す」より
👉 「workspace全体を解析する」
という思想で作られています。
Rust開発者の間では
依存関係はプロジェクト単位で理解するべき
という文化が強いんですね。
(Linuxカーネル文化の影響も強いです)
❌ なぜ -p や crate名が使えないのか
あなたが試した:
cargo depgraph -p wezterm-gui
cargo depgraph wezterm-gui
これが通らない理由は単純で、
👉 depgraphは crate指定オプションを持っていない
です。
✅ 主要パーツを出力する現実的な方法
ここからが実務テクです。
⭐ 方法① 依存を「通常依存だけ」に絞る
一番使われます。
cargo depgraph --no-dev-deps --no-build-deps | dot -Tsvg > deps_main.svg
✔ 効果
・dev依存を削除
・build依存を削除
・かなり見やすくなる
⭐ 方法② 深さを制限する(超おすすめ)
巨大プロジェクトでは必須です。
cargo depgraph --max-depth 2 | dot -Tsvg > deps_depth2.svg
目安
| 深さ | 意味 |
|---|---|
| 1 | 直接依存だけ |
| 2 | 主要構造 |
| 3 | かなり詳細 |
WezTermなら:
cargo depgraph --max-depth 2 --no-dev-deps | dot -Tsvg > wezterm_core.svg
かなり実用的です。
⭐ 方法③ 特定crateを「強調表示」する
実は depgraph の真骨頂。
cargo depgraph --focus wezterm-gui | dot -Tsvg > gui_focus.svg
※ バージョンによっては --focus が存在します
もし使えなければ:
cargo depgraph --help
で確認してみてください。
⭐ 方法④ workspace単位を分割
WezTermは workspace構成なので、
cargo metadata --format-version 1 | jq '.packages[].name'
で crate 一覧を出せます。
その後:
cd termwiz
cargo depgraph | dot -Tsvg > termwiz.svg
これが実は一番安定します。
🧠 Rust業界雑学
Rustの巨大プロジェクトは:
-
WezTerm
-
Servo
-
Bevy
-
Tauri
どれも
👉 crateを細かく分割
👉 depgraphで設計確認
という文化があります。
C++界の「include地獄」を反省して出来た文化です。
🎯 WezTermでおすすめの可視化パターン
プロがよくやる構成:
# GUI中心
cargo depgraph --focus wezterm-gui --max-depth 2 | dot -Tsvg > gui.svg# ターミナルロジック
cargo depgraph --focus termwiz --max-depth 2 | dot -Tsvg > termwiz.svg# コア依存
cargo depgraph --no-dev-deps --max-depth 2 | dot -Tsvg > core.svg
🖼 SVG → PNG を安全変換(GIMP回避)
これは超重要です(GIMPは巨大SVGでよく死にます)。
✔ 安全コマンド
rsvg-convert -w 2000 deps.svg > deps.png
サイズを下げる
rsvg-convert -w 1200 deps.svg > deps_small.png
Fedoraなら
sudo dnf install librsvg2-tools
🧨 SVGが巨大になる理由(裏話)
Graphvizは
👉 ノード数が指数的に増える
👉 レイアウト計算が爆発する
なので
Rustプロは普通:
-
深さ制限
-
依存種別削除
この2つを必ずやります。
⭐ cargo-modules との棲み分け
ここも業界知識です。
cargo-modules
👉 ソース構造(モジュール設計)
cargo-depgraph
👉 crate依存(アーキテクチャ設計)
🌱 個人的におすすめの探索順
WezTerm読むなら:
①
termwiz
(端末制御の核)
②
wezterm-gui
③
mux
(タブ・セッション管理)
🚀 もし余裕あれば
超便利な分析:
cargo tree --edges normal --depth 2
これはGraphviz不要で軽いです。
👉 ちょっと気になるので聞いていいですか
あなたは今、
-
WezTermの内部理解が目的?
-
Rustアーキテクチャ学習?
-
ターミナルエミュレータ開発志向?
どの方向で読んでます?
ここ分かると
「読むべきcrate順」までナビできます 😎
// Don't create a new standard console window when launched from the windows GUI.
#![cfg_attr(not(test), windows_subsystem = "windows")]
use crate::customglyph::BlockKey;
use crate::glyphcache::GlyphCache;
use crate::utilsprites::RenderMetrics;
use ::window::*;
use anyhow::{anyhow, Context};
use clap::builder::ValueParser;
use clap::{Parser, ValueHint};
use config::keyassignment::{SpawnCommand, SpawnTabDomain};
use config::{ConfigHandle, SerialDomain, SshDomain, SshMultiplexing};
use mux::activity::Activity;
use mux::domain::{Domain, LocalDomain};
use mux::Mux;
use mux_lua::MuxDomain;
use portable_pty::cmdbuilder::CommandBuilder;
use promise::spawn::block_on;
use std::borrow::Cow;
use std::collections::HashMap;
use std::env::current_dir;
use std::ffi::OsString;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use termwiz::cell::CellAttributes;
use termwiz::surface::{Line, SEQ_ZERO};
use unicode_normalization::UnicodeNormalization;
use wezterm_bidi::Direction;
use wezterm_client::domain::ClientDomain;
use wezterm_font::shaper::PresentationWidth;
use wezterm_font::FontConfiguration;
use wezterm_gui_subcommands::*;
use wezterm_mux_server_impl::update_mux_domains;
use wezterm_toast_notification::*;
mod colorease;
mod commands;
mod customglyph;
mod download;
mod frontend;
mod glyphcache;
mod inputmap;
mod overlay;
mod quad;
mod renderstate;
mod resize_increment_calculator;
mod scripting;
mod scrollbar;
mod selection;
mod shapecache;
mod spawn;
mod stats;
mod tabbar;
mod termwindow;
mod unicode_names;
mod uniforms;
mod update;
mod utilsprites;
#[cfg(feature = "dhat-heap")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
pub use selection::SelectionMode;
pub use termwindow::{set_window_class, set_window_position, TermWindow, ICON_DATA};
#[derive(Debug, Parser)]
#[command(
about = "Wez's Terminal Emulator\nhttp://github.com/wezterm/wezterm",
version = config::wezterm_version()
)]
struct Opt {
/// Skip loading wezterm.lua
#[arg(long, short = 'n')]
skip_config: bool,
/// Specify the configuration file to use, overrides the normal
/// configuration file resolution
#[arg(
long = "config-file",
value_parser,
conflicts_with = "skip_config",
value_hint=ValueHint::FilePath,
)]
config_file: Option,
/// Override specific configuration values
#[arg(
long = "config",
name = "name=value",
value_parser=ValueParser::new(name_equals_value),
number_of_values = 1)]
config_override: Vec<(String, String)>,
/// On Windows, whether to attempt to attach to the parent
/// process console to display logging output
#[arg(long = "attach-parent-console")]
#[allow(dead_code)]
attach_parent_console: bool,
#[command(subcommand)]
cmd: Option,
}
#[derive(Debug, Parser, Clone)]
enum SubCommand {
#[command(
name = "start",
about = "Start the GUI, optionally running an alternative program [aliases: -e]"
)]
Start(StartCommand),
/// Start the GUI in blocking mode. You shouldn't see this, but you
/// may see it in shell completions because of this open clap issue:
/// <https://github.com/clap-rs/clap/issues/1335>
#[command(short_flag_alias = 'e', hide = true)]
BlockingStart(StartCommand),
#[command(name = "ssh", about = "Establish an ssh session")]
Ssh(SshCommand),
#[command(name = "serial", about = "Open a serial port")]
Serial(SerialCommand),
#[command(name = "connect", about = "Connect to wezterm multiplexer")]
Connect(ConnectCommand),
#[command(name = "ls-fonts", about = "Display information about fonts")]
LsFonts(LsFontsCommand),
#[command(name = "show-keys", about = "Show key assignments")]
ShowKeys(ShowKeysCommand),
}
async fn async_run_ssh(opts: SshCommand) -> anyhow::Result<()> {
let mut ssh_option = HashMap::new();
if opts.verbose {
ssh_option.insert("wezterm_ssh_verbose".to_string(), "true".to_string());
}
for (k, v) in opts.config_override {
ssh_option.insert(k.to_lowercase().to_string(), v);
}
let dom = SshDomain {
name: format!("SSH to {}", opts.user_at_host_and_port),
remote_address: opts.user_at_host_and_port.host_and_port.clone(),
username: opts.user_at_host_and_port.username.clone(),
multiplexing: SshMultiplexing::None,
ssh_option,
..Default::default()
};
let start_command = StartCommand {
always_new_process: true,
class: opts.class,
cwd: None,
no_auto_connect: true,
position: opts.position,
workspace: None,
prog: opts.prog.clone(),
..Default::default()
};
let cmd = if !opts.prog.is_empty() {
let builder = CommandBuilder::from_argv(opts.prog);
Some(builder)
} else {
None
};
let domain: Arc = Arc::new(mux::ssh::RemoteSshDomain::with_ssh_domain(&dom)?);
let mux = Mux::get();
mux.add_domain(&domain);
mux.set_default_domain(&domain);
let should_publish = false;
async_run_terminal_gui(cmd, start_command, should_publish).await
}
fn run_ssh(opts: SshCommand) -> anyhow::Result<()> {
if let Some(cls) = opts.class.as_ref() {
crate::set_window_class(cls);
}
if let Some(pos) = opts.position.as_ref() {
set_window_position(pos.clone());
}
build_initial_mux(&config::configuration(), None, None)?;
let gui = crate::frontend::try_new()?;
promise::spawn::spawn(async {
if let Err(err) = async_run_ssh(opts).await {
terminate_with_error(err);
}
})
.detach();
maybe_show_configuration_error_window();
gui.run_forever()
}
async fn async_run_serial(opts: SerialCommand) -> anyhow::Result<()> {
let serial_domain = SerialDomain {
name: format!("Serial Port {}", opts.port),
port: Some(opts.port.clone()),
baud: opts.baud,
};
let start_command = StartCommand {
always_new_process: true,
class: opts.class,
cwd: None,
no_auto_connect: true,
position: opts.position,
workspace: None,
domain: Some(serial_domain.name.clone()),
..Default::default()
};
let cmd = None;
let domain: Arc = Arc::new(LocalDomain::new_serial_domain(serial_domain)?);
let mux = Mux::get();
mux.add_domain(&domain);
let should_publish = false;
async_run_terminal_gui(cmd, start_command, should_publish).await
}
fn run_serial(config: config::ConfigHandle, opts: SerialCommand) -> anyhow::Result<()> {
if let Some(cls) = opts.class.as_ref() {
crate::set_window_class(cls);
}
if let Some(pos) = opts.position.as_ref() {
set_window_position(pos.clone());
}
build_initial_mux(&config, None, None)?;
let gui = crate::frontend::try_new()?;
promise::spawn::spawn(async {
if let Err(err) = async_run_serial(opts).await {
terminate_with_error(err);
}
})
.detach();
maybe_show_configuration_error_window();
gui.run_forever()
}
fn have_panes_in_domain_and_ws(domain: &Arc, workspace: &Option) -> bool {
let mux = Mux::get();
let have_panes_in_domain = mux
.iter_panes()
.iter()
.any(|p| p.domain_id() == domain.domain_id());
if !have_panes_in_domain {
return false;
}
if let Some(ws) = &workspace {
for window_id in mux.iter_windows_in_workspace(ws) {
if let Some(win) = mux.get_window(window_id) {
for t in win.iter() {
for p in t.iter_panes_ignoring_zoom() {
if p.pane.domain_id() == domain.domain_id() {
return true;
}
}
}
}
}
false
} else {
true
}
}
async fn spawn_tab_in_domain_if_mux_is_empty(
cmd: Option,
is_connecting: bool,
domain: Option<Arc>,
workspace: Option,
) -> anyhow::Result<()> {
let mux = Mux::get();
let domain = domain.unwrap_or_else(|| mux.default_domain());
if !is_connecting {
if have_panes_in_domain_and_ws(&domain, &workspace) {
return Ok(());
}
}
let window_id = {
// Force the builder to notify the frontend early,
// so that the attach await below doesn't block it.
// This has the consequence of creating the window
// at the initial size instead of populating it
// from the size specified in the remote mux.
// We use the TabAddedToWindow mux notification
// to detect and adjust the size later on.
let position = None;
let builder = mux.new_empty_window(workspace.clone(), position);
*builder
};
let config = config::configuration();
config.update_ulimit()?;
domain.attach(Some(window_id)).await?;
if have_panes_in_domain_and_ws(&domain, &workspace) {
trigger_and_log_gui_attached(MuxDomain(domain.domain_id())).await;
return Ok(());
}
let _config_subscription = config::subscribe_to_config_reload(move || {
promise::spawn::spawn_into_main_thread(async move {
if let Err(err) = update_mux_domains(&config::configuration()) {
log::error!("Error updating mux domains: {:#}", err);
}
})
.detach();
true
});
let dpi = config.dpi.unwrap_or_else(|| ::window::default_dpi());
let _tab = domain
.spawn(
config.initial_size(dpi as u32, Some(cell_pixel_dims(&config, dpi)?)),
cmd,
None,
window_id,
)
.await?;
trigger_and_log_gui_attached(MuxDomain(domain.domain_id())).await;
Ok(())
}
async fn connect_to_auto_connect_domains() -> anyhow::Result<()> {
let mux = Mux::get();
let domains = mux.iter_domains();
for dom in domains {
if let Some(dom) = dom.downcast_ref::() {
if dom.connect_automatically() {
dom.attach(None).await?;
}
}
}
Ok(())
}
async fn trigger_gui_startup(
lua: Option<Rc>,
spawn: Option,
) -> anyhow::Result<()> {
if let Some(lua) = lua {
let args = lua.pack_multi(spawn)?;
config::lua::emit_event(&lua, ("gui-startup".to_string(), args)).await?;
}
Ok(())
}
async fn trigger_and_log_gui_startup(spawn_command: Option) {
if let Err(err) =
config::with_lua_config_on_main_thread(move |lua| trigger_gui_startup(lua, spawn_command))
.await
{
let message = format!("while processing gui-startup event: {:#}", err);
log::error!("{}", message);
persistent_toast_notification("Error", &message);
}
}
async fn trigger_gui_attached(lua: Option<Rc>, domain: MuxDomain) -> anyhow::Result<()> {
if let Some(lua) = lua {
let args = lua.pack_multi(domain)?;
config::lua::emit_event(&lua, ("gui-attached".to_string(), args)).await?;
}
Ok(())
}
async fn trigger_and_log_gui_attached(domain: MuxDomain) {
if let Err(err) =
config::with_lua_config_on_main_thread(move |lua| trigger_gui_attached(lua, domain)).await
{
let message = format!("while processing gui-attached event: {:#}", err);
log::error!("{}", message);
persistent_toast_notification("Error", &message);
}
}
fn cell_pixel_dims(config: &ConfigHandle, dpi: f64) -> anyhow::Result<(usize, usize)> {
let fontconfig = Rc::new(FontConfiguration::new(Some(config.clone()), dpi as usize)?);
let render_metrics = RenderMetrics::new(&fontconfig)?;
Ok((
render_metrics.cell_size.width as usize,
render_metrics.cell_size.height as usize,
))
}
async fn async_run_terminal_gui(
cmd: Option,
opts: StartCommand,
should_publish: bool,
) -> anyhow::Result<()> {
let unix_socket_path =
config::RUNTIME_DIR.join(format!("gui-sock-{}", unsafe { libc::getpid() }));
std::env::set_var("WEZTERM_UNIX_SOCKET", unix_socket_path.clone());
wezterm_blob_leases::register_storage(Arc::new(
wezterm_blob_leases::simple_tempdir::SimpleTempDir::new_in(&*config::CACHE_DIR)?,
))?;
if let Err(err) = spawn_mux_server(unix_socket_path, should_publish) {
log::warn!("{:#}", err);
}
if !opts.no_auto_connect {
connect_to_auto_connect_domains().await?;
}
let spawn_command = match &cmd {
Some(cmd) => Some(SpawnCommand::from_command_builder(cmd)?),
None => None,
};
// Apply the domain to the command
let spawn_command = match (spawn_command, &opts.domain) {
(Some(spawn), Some(name)) => Some(SpawnCommand {
domain: SpawnTabDomain::DomainName(name.to_string()),
..spawn
}),
(None, Some(name)) => Some(SpawnCommand {
domain: SpawnTabDomain::DomainName(name.to_string()),
..SpawnCommand::default()
}),
(spawn, None) => spawn,
};
let mux = Mux::get();
let domain = if let Some(name) = &opts.domain {
let domain = mux
.get_domain_by_name(name)
.ok_or_else(|| anyhow!("invalid domain {name}"))?;
Some(domain)
} else {
None
};
if !opts.attach {
trigger_and_log_gui_startup(spawn_command).await;
}
let is_connecting = opts.attach;
if let Some(domain) = &domain {
if !opts.attach {
let window_id = {
// Force the builder to notify the frontend early,
// so that the attach await below doesn't block it.
let workspace = None;
let position = None;
let builder = mux.new_empty_window(workspace, position);
*builder
};
domain.attach(Some(window_id)).await?;
let config = config::configuration();
let dpi = config.dpi.unwrap_or_else(|| ::window::default_dpi());
let tab = domain
.spawn(
config.initial_size(dpi as u32, Some(cell_pixel_dims(&config, dpi)?)),
cmd.clone(),
None,
window_id,
)
.await?;
let mut window = mux
.get_window_mut(window_id)
.ok_or_else(|| anyhow!("failed to get mux window id {window_id}"))?;
if let Some(tab_idx) = window.idx_by_id(tab.tab_id()) {
window.set_active_without_saving(tab_idx);
}
trigger_and_log_gui_attached(MuxDomain(domain.domain_id())).await;
}
}
spawn_tab_in_domain_if_mux_is_empty(cmd, is_connecting, domain, opts.workspace).await
}
#[derive(Debug)]
enum Publish {
TryPathOrPublish(PathBuf),
NoConnectNoPublish,
NoConnectButPublish,
}
impl Publish {
pub fn resolve(mux: &Arc, config: &ConfigHandle, always_new_process: bool) -> Self {
if mux.default_domain().domain_name() != config.default_domain.as_deref().unwrap_or("local")
{
return Self::NoConnectNoPublish;
}
if always_new_process {
return Self::NoConnectNoPublish;
}
if config::is_config_overridden() {
// They're using a specific config file: assume that it is
// different from the running gui
log::trace!("skip existing gui: config is different");
return Self::NoConnectNoPublish;
}
match wezterm_client::discovery::resolve_gui_sock_path(
&crate::termwindow::get_window_class(),
) {
Ok(path) => Self::TryPathOrPublish(path),
Err(_) => Self::NoConnectButPublish,
}
}
pub fn should_publish(&self) -> bool {
match self {
Self::TryPathOrPublish(_) | Self::NoConnectButPublish => true,
Self::NoConnectNoPublish => false,
}
}
pub fn try_spawn(
&mut self,
cmd: Option,
config: &ConfigHandle,
workspace: Option<&str>,
domain: SpawnTabDomain,
new_tab: bool,
) -> anyhow::Result {
if let Publish::TryPathOrPublish(gui_sock) = &self {
let dom = config::UnixDomain {
socket_path: Some(gui_sock.clone()),
no_serve_automatically: true,
..Default::default()
};
let mut ui = mux::connui::ConnectionUI::new_headless();
match wezterm_client::client::Client::new_unix_domain(None, &dom, false, &mut ui, true)
{
Ok(client) => {
let executor = promise::spawn::ScopedExecutor::new();
let command = cmd.clone();
let res = block_on(executor.run(async move {
let vers = client.verify_version_compat(&mut ui).await?;
if vers.executable_path != std::env::current_exe().context("resolve executable path")? {
*self = Publish::NoConnectNoPublish;
anyhow::bail!(
"Running GUI is a different executable from us, will start a new one");
}
if vers.config_file_path
!= std::env::var_os("WEZTERM_CONFIG_FILE").map(Into::into)
{
*self = Publish::NoConnectNoPublish;
anyhow::bail!(
"Running GUI has different config from us, will start a new one"
);
}
let window_id = if new_tab || config.prefer_to_spawn_tabs {
if let Ok(pane_id) = client.resolve_pane_id(None).await {
let panes = client.list_panes().await?;
let mut window_id = None;
'outer: for tabroot in panes.tabs {
let mut cursor = tabroot.into_tree().cursor();
loop {
if let Some(entry) = cursor.leaf_mut() {
if entry.pane_id == pane_id {
window_id.replace(entry.window_id);
break 'outer;
}
}
match cursor.preorder_next() {
Ok(c) => cursor = c,
Err(_) => break,
}
}
}
window_id
} else {
None
}
} else {
None
};
client
.spawn_v2(codec::SpawnV2 {
domain,
window_id,
command,
command_dir: None,
size: config.initial_size(0, None),
workspace: workspace.unwrap_or(
config
.default_workspace
.as_deref()
.unwrap_or(mux::DEFAULT_WORKSPACE)
).to_string(),
})
.await
}));
match res {
Ok(res) => {
log::info!(
"Spawned your command via the existing GUI instance. \
Use wezterm start --always-new-process if you do not want this behavior. \
Result={:?}",
res
);
Ok(true)
}
Err(err) => {
log::trace!(
"while attempting to ask existing instance to spawn: {:#}",
err
);
Ok(false)
}
}
}
Err(err) => {
// Couldn't connect: it's probably a stale symlink.
// That's fine: we can continue with starting a fresh gui below.
log::trace!("{:#}", err);
Ok(false)
}
}
} else {
Ok(false)
}
}
}
fn spawn_mux_server(unix_socket_path: PathBuf, should_publish: bool) -> anyhow::Result<()> {
let mut listener =
wezterm_mux_server_impl::local::LocalListener::with_domain(&config::UnixDomain {
socket_path: Some(unix_socket_path.clone()),
..Default::default()
})?;
std::thread::spawn(move || {
let name_holder;
if should_publish {
name_holder = wezterm_client::discovery::publish_gui_sock_path(
&unix_socket_path,
&crate::termwindow::get_window_class(),
);
if let Err(err) = &name_holder {
log::warn!("{:#}", err);
}
}
listener.run();
std::fs::remove_file(unix_socket_path).ok();
});
Ok(())
}
fn setup_mux(
local_domain: Arc,
config: &ConfigHandle,
default_domain_name: Option<&str>,
default_workspace_name: Option<&str>,
) -> anyhow::Result<Arc> {
let mux = Arc::new(mux::Mux::new(Some(local_domain.clone())));
Mux::set_mux(&mux);
let client_id = Arc::new(mux::client::ClientId::new());
mux.register_client(client_id.clone());
mux.replace_identity(Some(client_id));
let default_workspace_name = default_workspace_name.unwrap_or(
config
.default_workspace
.as_deref()
.unwrap_or(mux::DEFAULT_WORKSPACE),
);
mux.set_active_workspace(&default_workspace_name);
crate::update::load_last_release_info_and_set_banner();
update_mux_domains(config)?;
let default_name =
default_domain_name.unwrap_or(config.default_domain.as_deref().unwrap_or("local"));
let domain = mux.get_domain_by_name(default_name).ok_or_else(|| {
anyhow::anyhow!(
"desired default domain '{}' was not found in mux!?",
default_name
)
})?;
mux.set_default_domain(&domain);
Ok(mux)
}
fn build_initial_mux(
config: &ConfigHandle,
default_domain_name: Option<&str>,
default_workspace_name: Option<&str>,
) -> anyhow::Result<Arc> {
let domain: Arc = Arc::new(LocalDomain::new("local")?);
setup_mux(domain, config, default_domain_name, default_workspace_name)
}
fn run_terminal_gui(opts: StartCommand, default_domain_name: Option) -> anyhow::Result<()> {
if let Some(cls) = opts.class.as_ref() {
crate::set_window_class(cls);
}
if let Some(pos) = opts.position.as_ref() {
set_window_position(pos.clone());
}
let config = config::configuration();
let need_builder = !opts.prog.is_empty() || opts.cwd.is_some();
let cmd = if need_builder {
let prog = opts.prog.iter().map(|s| s.as_os_str()).collect::<Vec<_>>();
let mut builder = config.build_prog(
if prog.is_empty() { None } else { Some(prog) },
config.default_prog.as_ref(),
config.default_cwd.as_ref(),
)?;
if let Some(cwd) = &opts.cwd {
builder.cwd(if cwd.is_relative() {
current_dir()?.join(cwd).into_os_string().into()
} else {
Cow::Borrowed(cwd.as_ref())
});
}
Some(builder)
} else {
None
};
let mux = build_initial_mux(
&config,
default_domain_name.as_deref(),
opts.workspace.as_deref(),
)?;
// First, let's see if we can ask an already running wezterm to do this.
// We must do this before we start the gui frontend as the scheduler
// requirements are different.
let mut publish = Publish::resolve(
&mux,
&config,
opts.always_new_process || opts.position.is_some(),
);
log::trace!("{:?}", publish);
if publish.try_spawn(
cmd.clone(),
&config,
opts.workspace.as_deref(),
match &opts.domain {
Some(name) => SpawnTabDomain::DomainName(name.to_string()),
None => SpawnTabDomain::DefaultDomain,
},
opts.new_tab,
)? {
return Ok(());
}
let gui = crate::frontend::try_new()?;
let activity = Activity::new();
promise::spawn::spawn(async move {
if let Err(err) = async_run_terminal_gui(cmd, opts, publish.should_publish()).await {
terminate_with_error(err);
}
drop(activity);
})
.detach();
maybe_show_configuration_error_window();
gui.run_forever()
}
fn fatal_toast_notification(title: &str, message: &str) {
persistent_toast_notification(title, message);
// We need a short delay otherwise the notification
// will not show
#[cfg(windows)]
std::thread::sleep(std::time::Duration::new(2, 0));
}
fn notify_on_panic() {
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if let Some(s) = info.payload().downcast_ref::<&str>() {
fatal_toast_notification("Wezterm panic", s);
}
default_hook(info);
}));
}
fn terminate_with_error_message(err: &str) -> ! {
log::error!("{}; terminating", err);
fatal_toast_notification("Wezterm Error", &err);
std::process::exit(1);
}
fn terminate_with_error(err: anyhow::Error) -> ! {
let mut err_text = format!("{err:#}");
let warnings = config::configuration_warnings_and_errors();
if !warnings.is_empty() {
let err = warnings.join("\n");
err_text = format!("{err_text}\nConfiguration Error: {err}");
}
terminate_with_error_message(&err_text)
}
fn main() {
#[cfg(feature = "dhat-heap")]
let _profiler = dhat::Profiler::new_heap();
config::designate_this_as_the_main_thread();
config::assign_error_callback(mux::connui::show_configuration_error_message);
notify_on_panic();
if let Err(e) = run() {
terminate_with_error(e);
}
Mux::shutdown();
frontend::shutdown();
}
fn maybe_show_configuration_error_window() {
let warnings = config::configuration_warnings_and_errors();
if !warnings.is_empty() {
let err = warnings.join("\n");
mux::connui::show_configuration_error_message(&err);
}
}
fn run_show_keys(config: config::ConfigHandle, cmd: &ShowKeysCommand) -> anyhow::Result<()> {
let map = crate::inputmap::InputMap::new(&config);
if cmd.lua {
map.dump_config(cmd.key_table.as_deref());
} else {
map.show_keys();
}
Ok(())
}
pub fn run_ls_fonts(config: config::ConfigHandle, cmd: &LsFontsCommand) -> anyhow::Result<()> {
use wezterm_font::parser::ParsedFont;
if let Err(err) = config::configuration_result() {
log::error!("{}", err);
return Ok(());
}
// Disable the normal config error UI window, as we don't have
// a fully baked GUI environment running
config::assign_error_callback(|err| eprintln!("{}", err));
let font_config = Rc::new(wezterm_font::FontConfiguration::new(
Some(config.clone()),
config.dpi.unwrap_or_else(|| ::window::default_dpi()) as usize,
)?);
let render_metrics = crate::utilsprites::RenderMetrics::new(&font_config)?;
let bidi_hint = if config.bidi_enabled {
Some(config.bidi_direction)
} else {
None
};
let unicode_version = config.unicode_version();
let text = match (&cmd.text, &cmd.codepoints) {
(Some(text), _) => Some(text.to_string()),
(_, Some(codepoints)) => {
let mut s = String::new();
for cp in codepoints.split(",") {
let cp = u32::from_str_radix(cp, 16)
.with_context(|| format!("{cp} is not a hex number"))?;
let c = char::from_u32(cp)
.ok_or_else(|| anyhow!("{cp} is not a valid unicode codepoint value"))?;
s.push(c);
}
Some(s)
}
_ => None,
};
if let Some(text) = &text {
// Emulate the effect of output normalization
let text = if config.normalize_output_to_unicode_nfc {
text.nfc().collect()
} else {
text.to_string()
};
let line = Line::from_text(
&text,
&CellAttributes::default(),
SEQ_ZERO,
Some(&unicode_version),
);
let cell_clusters = line.cluster(bidi_hint);
let ft_lib = wezterm_font::ftwrap::Library::new()?;
let mut glyph_cache = GlyphCache::new_in_memory(&font_config, 256)?;
for cluster in cell_clusters {
let style = font_config.match_style(&config, &cluster.attrs);
let font = font_config.resolve_font(style)?;
let presentation_width = PresentationWidth::with_cluster(&cluster);
let infos = font
.blocking_shape(
&cluster.text,
Some(cluster.presentation),
cluster.direction,
None,
Some(&presentation_width),
)
.unwrap();
// We must grab the handles after shaping, so that we get the
// revised list that includes system fallbacks!
let handles = font.clone_handles();
let faces: Vec<_> = handles
.iter()
.map(|p| ft_lib.face_from_locator(&p.handle).ok())
.collect();
let mut iter = infos.iter().peekable();
let mut byte_lens = vec![];
for c in cluster.text.chars() {
let len = c.len_utf8();
for _ in 0..len {
byte_lens.push(len);
}
}
println!("{:?}", cluster.direction);
while let Some(info) = iter.next() {
let idx = cluster.byte_to_cell_idx(info.cluster as usize);
let followed_by_space = match line.get_cell(idx + 1) {
Some(cell) => cell.str() == " ",
None => false,
};
let text = if cluster.direction == Direction::LeftToRight {
if let Some(next) = iter.peek() {
line.columns_as_str(idx..cluster.byte_to_cell_idx(next.cluster as usize))
} else {
let last_idx = cluster.byte_to_cell_idx(cluster.text.len() - 1);
line.columns_as_str(idx..last_idx + 1)
}
} else {
let info_len = byte_lens[info.cluster as usize];
let last_idx = cluster.byte_to_cell_idx(info.cluster as usize + info_len - 1);
line.columns_as_str(idx..last_idx + 1)
};
let parsed = &handles[info.font_idx];
let escaped = format!("{}", text.escape_unicode());
let mut is_custom = false;
let cached_glyph = glyph_cache.cached_glyph(
&info,
&style,
followed_by_space,
&font,
&render_metrics,
info.num_cells,
)?;
let mut texture = cached_glyph.texture.clone();
if config.custom_block_glyphs {
if let Some(block) = info.only_char.and_then(BlockKey::from_char) {
texture.replace(glyph_cache.cached_block(block, &render_metrics)?);
println!(
"{:2} {:4} {:12} drawn by wezterm because custom_block_glyphs=true: {:?}",
info.cluster, text, escaped, block
);
is_custom = true;
}
}
if !is_custom {
let glyph_name = faces[info.font_idx]
.as_ref()
.and_then(|face| {
face.get_glyph_name(info.glyph_pos)
.map(|name| format!("{},", name))
})
.unwrap_or_else(String::new);
println!(
"{:2} {:4} {:12} x_adv={:<2} cells={:<2} glyph={}{:<4} {}\n{:38}{}",
info.cluster,
text,
escaped,
cached_glyph.x_advance.get(),
info.num_cells,
glyph_name,
info.glyph_pos,
parsed.lua_name(),
"",
parsed.handle.diagnostic_string()
);
}
if cmd.rasterize_ascii {
let mut glyph = String::new();
if let Some(texture) = &cached_glyph.texture {
use ::window::bitmaps::ImageTexture;
if let Some(tex) = texture.texture.downcast_ref::() {
for y in texture.coords.min_y()..texture.coords.max_y() {
for &px in tex.image.borrow().horizontal_pixel_range(
texture.coords.min_x() as usize,
texture.coords.max_x() as usize,
y as usize,
) {
let px = u32::from_be(px);
let (b, g, r, a) = (
(px >> 8) as u8,
(px >> 16) as u8,
(px >> 24) as u8,
(px & 0xff) as u8,
);
// Use regular RGB for other terminals, but then
// set RGBA for wezterm
glyph.push_str(&format!(
"\x1b[38:2::{r}:{g}:{b}m\x1b[38:6::{r}:{g}:{b}:{a}m\u{2588}\x1b[0m"
));
}
glyph.push('\n');
}
}
}
if !is_custom {
println!(
"bearing: x={} y={}, offset: x={} y={}",
cached_glyph.bearing_x.get(),
cached_glyph.bearing_y.get(),
cached_glyph.x_offset.get(),
cached_glyph.y_offset.get(),
);
}
println!("{glyph}");
}
}
}
return Ok(());
}
println!("Primary font:");
let default_font = font_config.default_font()?;
println!(
"{}",
ParsedFont::lua_fallback(&default_font.clone_handles())
);
println!();
for rule in &config.font_rules {
println!();
let mut condition = "When".to_string();
if let Some(intensity) = &rule.intensity {
condition.push_str(&format!(" Intensity={:?}", intensity));
}
if let Some(underline) = &rule.underline {
condition.push_str(&format!(" Underline={:?}", underline));
}
if let Some(italic) = &rule.italic {
condition.push_str(&format!(" Italic={:?}", italic));
}
if let Some(blink) = &rule.blink {
condition.push_str(&format!(" Blink={:?}", blink));
}
if let Some(rev) = &rule.reverse {
condition.push_str(&format!(" Reverse={:?}", rev));
}
if let Some(strikethrough) = &rule.strikethrough {
condition.push_str(&format!(" Strikethrough={:?}", strikethrough));
}
if let Some(invisible) = &rule.invisible {
condition.push_str(&format!(" Invisible={:?}", invisible));
}
println!("{}:", condition);
let font = font_config.resolve_font(&rule.font)?;
println!("{}", ParsedFont::lua_fallback(&font.clone_handles()));
println!();
}
println!("Title font:");
let title_font = font_config.title_font()?;
println!("{}", ParsedFont::lua_fallback(&title_font.clone_handles()));
println!();
if cmd.list_system {
let font_dirs = font_config.list_fonts_in_font_dirs();
println!(
"{} fonts found in your font_dirs + built-in fonts:",
font_dirs.len()
);
for font in font_dirs {
let pixel_sizes = if font.pixel_sizes.is_empty() {
"".to_string()
} else {
format!(" pixel_sizes={:?}", font.pixel_sizes)
};
println!(
"{} -- {}{}{}",
font.lua_name(),
font.aka(),
font.handle.diagnostic_string(),
pixel_sizes
);
}
match font_config.list_system_fonts() {
Ok(sys_fonts) => {
println!(
"{} system fonts found using {:?}:",
sys_fonts.len(),
config.font_locator
);
for font in sys_fonts {
let pixel_sizes = if font.pixel_sizes.is_empty() {
"".to_string()
} else {
format!(" pixel_sizes={:?}", font.pixel_sizes)
};
println!(
"{} -- {}{}{}",
font.lua_name(),
font.aka(),
font.handle.diagnostic_string(),
pixel_sizes
);
}
}
Err(err) => log::error!("Unable to list system fonts: {}", err),
}
}
Ok(())
}
fn run() -> anyhow::Result<()> {
// Inform the system of our AppUserModelID.
// Without this, our toast notifications won't be correctly
// attributed to our application.
#[cfg(windows)]
{
unsafe {
::windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID(
::windows::core::PCWSTR(wide_string("org.wezfurlong.wezterm").as_ptr()),
)
.unwrap();
}
}
let opts = Opt::parse();
// This is a bit gross.
// In order to not to automatically open a standard windows console when
// we run, we use the windows_subsystem attribute at the top of this
// source file. That comes at the cost of causing the help output
// to disappear if we are actually invoked from a console.
// This AttachConsole call will attach us to the console of the parent
// in that situation, but since we were launched as a windows subsystem
// application we will be running asynchronously from the shell in
// the command window, which means that it will appear to the user
// that we hung at the end, when in reality the shell is waiting for
// input but didn't know to re-draw the prompt.
#[cfg(windows)]
unsafe {
if opts.attach_parent_console {
winapi::um::wincon::AttachConsole(winapi::um::wincon::ATTACH_PARENT_PROCESS);
}
};
env_bootstrap::bootstrap();
// window_funcs is not set up by env_bootstrap as window_funcs is
// GUI environment specific and env_bootstrap is used to setup the
// headless mux server.
config::lua::add_context_setup_func(window_funcs::register);
config::lua::add_context_setup_func(crate::scripting::register);
config::lua::add_context_setup_func(crate::stats::register);
stats::Stats::init()?;
let _saver = umask::UmaskSaver::new();
config::common_init(
opts.config_file.as_ref(),
&opts.config_override,
opts.skip_config,
)?;
let config = config::configuration();
if let Some(value) = &config.default_ssh_auth_sock {
std::env::set_var("SSH_AUTH_SOCK", value);
}
let sub = match opts.cmd.as_ref().cloned() {
Some(SubCommand::BlockingStart(start)) => {
// Act as if the normal start subcommand was used,
// except that we always start a new instance.
// This is needed for compatibility, because many tools assume
// that "$TERMINAL -e $COMMAND" blocks until the command finished.
SubCommand::Start(StartCommand {
always_new_process: true,
..start
})
}
Some(sub) => sub,
None => {
// Need to fake an argv0
let mut argv = vec!["wezterm-gui".to_string()];
for a in &config.default_gui_startup_args {
argv.push(a.clone());
}
SubCommand::try_parse_from(&argv).with_context(|| {
format!(
"parsing the default_gui_startup_args config: {:?}",
config.default_gui_startup_args
)
})?
}
};
match sub {
SubCommand::Start(start) => {
log::trace!("Using configuration: {:#?}\nopts: {:#?}", config, opts);
let res = run_terminal_gui(start, None);
wezterm_blob_leases::clear_storage();
res
}
SubCommand::BlockingStart(_) => unreachable!(),
SubCommand::Ssh(ssh) => run_ssh(ssh),
SubCommand::Serial(serial) => run_serial(config, serial),
SubCommand::Connect(connect) => run_terminal_gui(
StartCommand {
domain: Some(connect.domain_name.clone()),
class: connect.class,
workspace: connect.workspace,
position: connect.position,
prog: connect.prog,
new_tab: connect.new_tab,
always_new_process: true,
attach: true,
_cmd: false,
no_auto_connect: false,
cwd: None,
},
Some(connect.domain_name),
),
SubCommand::LsFonts(cmd) => run_ls_fonts(config, &cmd),
SubCommand::ShowKeys(cmd) => run_show_keys(config, &cmd),
}
}
ーーーーー このコードの処理フローをMermaidのシーケンス図で書いてください。
あなた:
ChatGPT:
WordPressで Markdown や Mermaid(図表記法) を表示させる話って、実は「技術ブログ文化」とかなり深く結びついていて、開発者界隈では“読みやすさ=信用”みたいな価値観にも繋がっています。
そのあたりの業界事情や雑学も混ぜつつ、実用的な導入方法を整理していきますね。
■ ① なぜWordPressでMarkdownやMermaidを使いたがるのか
◆ Markdown
Markdownはもともと
👉 「HTMLを書くのが面倒くさい」
👉 「文章中心で管理したい」
というエンジニア文化から生まれました。
GitHub、Qiita、Zennなどが全部Markdownを採用しているのは、
-
バージョン管理しやすい
-
可読性が高い
-
テキストだけで完結
-
AIや生成ツールと相性が良い
という理由があります。
◆ Mermaid
Mermaidはかなり面白い存在で、
👉 「図をテキストで書く」
という思想のツールです。
例えば普通のフローチャートって、
-
draw.io
-
PowerPoint
-
Illustrator
などで描きますよね。
でもMermaidならこう書くだけ👇
graph TD
A[ログイン] --> B{認証成功?}
B -->|Yes| C[ダッシュボード]
B -->|No| D[エラー]
これがそのまま図になります。
◆ 業界小話
最近のITドキュメントは
-
GitHub README
-
技術設計書
-
API仕様書
ほぼ全部が
👉 Markdown + Mermaid
になってきています。
つまりWordPressに導入するのは「ブログを技術ドキュメント化する流れ」なんですね。
■ ② WordPressでMarkdownを表示する方法
⭐ 一番簡単:プラグイン
【A】WP Markdown Editor(おすすめ)
特徴
-
Gutenberg対応
-
ライブプレビュー
-
GitHub風表示
使い方
① プラグイン検索
WP Markdown Editor
② 有効化
③ 投稿画面でMarkdown入力
【B】Jetpack Markdown
Automattic公式なので安定しています。
有効化
Jetpack → 設定 → 執筆 → Markdown
【C】Advanced Markdown Editor
開発者ブログ寄りで、コード表示が強いです。
■ ③ Mermaidを表示する方法
ここが少しクセあります。
WordPressはデフォルトでMermaid未対応です。
⭐ 方法①:Mermaid専用プラグイン
「Mermaid」
そのまんまの名前ですが存在します。
手順
① プラグイン検索
Mermaid
② 有効化
③ 投稿内で
[mermaid]
graph TD
A --> B
[/mermaid]
⭐ 方法②:JavaScriptで直接読み込む(技術者向け)
こっちが「エンジニアっぽい王道」です。
Step1:Mermaid CDN読み込み
WordPressテーマの
functions.php
に追加します。
function add_mermaid_script() {
wp_enqueue_script(
'mermaid',
'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js',
array(),
null,
true
);
}
add_action('wp_enqueue_scripts', 'add_mermaid_script');
Step2:初期化JSを書く
<script>
document.addEventListener("DOMContentLoaded", function() {
mermaid.initialize({ startOnLoad: true });
});
</script>
Step3:記事に書く
<div class="mermaid">
graph TD
A --> B
</div>
■ ④ Markdown + Mermaidを同時に扱う方法
技術ブログではここが重要。
⭐ MarkDownブロック+MermaidJS
例えば
```mermaid
graph TD
A --> B
```
を使いたい場合は、
👉 Prism.js
👉 remark
👉 marked.js
などと組み合わせます。
■ ⑤ 実務でよくある構成(業界パターン)
企業技術ブログではだいたい3タイプあります。
パターン①
WordPress + Markdownプラグイン
👉 初心者〜中級
👉 Qiita風ブログ
パターン②
WordPress + headless CMS化
-
WordPress → API専用
-
Next.js → 表示
最近めちゃ多いです。
(Zenn風)
パターン③
完全Git管理
-
Markdown
-
Mermaid
-
CIで静的サイト生成
GitHub Pages系ですね。
■ ⑥ Mermaidが流行った背景(雑学)
Mermaidは元々
👉 DevOps文化
から広まりました。
CI/CDの設計書を
-
図で
-
Gitで管理
したかったんです。
特にDockerやKubernetesの設計では
Mermaidはほぼ標準装備になりつつあります。
■ ⑦ 注意点(地味に重要)
✔ セキュリティ制限
WordPressは
<script>
をブロックする事があります。
対策:
-
カスタムHTMLブロック
-
functions.php読み込み
✔ キャッシュ問題
MermaidはJS描画なので
👉 キャッシュプラグインと衝突
する場合があります。
✔ テーマ依存
Gutenbergテーマは比較的安全
クラシックテーマは崩れる事あり
■ ⑧ 個人的おすすめ構成(開発者ブログ用途)
かなりバランス良いです👇
-
WP Markdown Editor
-
Mermaid CDN読み込み
-
Prism.jsでコード装飾
■ ⑨ 未来トレンド(業界話)
今はさらに進んで
👉 AIがMarkdown生成
👉 Mermaid自動生成
が増えています。
つまり将来は
「文章を書く=設計図が完成」
という世界になりつつあります。
■ もし興味あれば…
あなたの環境に合わせて、
-
Gutenbergですか?
-
クラシックエディタですか?
-
技術ブログ用途ですか?
-
社内ドキュメント用途ですか?
このあたり分かると、最適構成まで一緒に設計できます。
WordPressは「ブログ」というより、
実は ドキュメントCMSとしてもかなり強い ので、そこを伸ばすと面白いですよ。
0 件のコメント:
コメントを投稿