思考のスピードでマインドマップを書けるエディタを Tauri で作った
Zed の Code at the speed of thought(思考のスピードでコードを書く) みたいなタイトルですみません。
でも、本当にそれを意識したアプリを作ってみました。
Tauri v2 と React + TypeScript を使って、
キーボード操作中心のマインドマップ(ツリー)エディタを作りました。
名前は vikokoro です。
(vim と 心で vikokoro です。最初はvimindにしようと思っていましたが、すでにありました…)
いわゆるマインドマップですが、思想としては
Vim 操作で扱えるツリー構造エディタにかなり近いです。
綺麗に整理するというより、
とにかく早く整理することを目的にしています。
何を作ったか
- 左が親、右に行くほど詳細になる右方向ツリー
- Normal / Insert のモードを持つ Vim ライクな操作感
-
Tab/Enterでノードを追加して即編集 -
hjkl+j/kによるノード移動 - Undo / Redo
- 検索、ズーム、パン
- ローカル永続化

なぜ Tauri を選んだか
理由はかなりシンプルです。
- おしゃれな見た目にしたい
- サクサク動かしたい
- 実行ファイルとして配布したい
Tauri なら、
- フロントは普通の Web 技術
- 永続化やファイル I/O は Rust 側
- GitHub Actions でそのままビルドして配布
という構成が取りやすく、今回の用途にちょうど合っていました。
操作体系(Vimライク)
操作は完全にキーボード中心です。
クリックも一応できますが、ほとんど使いませんでした。
Normal モードでやること
-
Tab
子ノードを右に追加して、そのまま Insert -
Enter
兄弟ノードを下に追加して、そのまま Insert -
h / j / k / l
親 / 次 / 前 / 子 へ移動 -
J / K
兄弟ノードの順序を入れ替え -
dd
ノード削除(ルートは不可) -
u / Ctrl+r
Undo / Redo -
Ctrl+T
新規タブ -
Ctrl+W
タブを閉じる -
Ctrl+F
検索 -
?
ヘルプ表示
Insert モードでやること
-
i
Insert に入る -
Esc
編集を確定して Normal に戻る -
Enter
編集確定(IME の挙動は後述)
UIを reducer と状態機械で組み立てる
このアプリの中核は reducer ベースの状態管理です。
- モード(Normal / Insert)
- カーソル位置
- Undo / Redo
- タブ
- 検索状態
これらをすべて 1 つの state と action で管理しています。
その結果、
- 今どの状態かが分かりやすい
- キー入力の分岐が整理される
- Vim っぽい体験を作りやすい
という構成にできました。
データモデルは「ツリーをそのまま持たない」
このアプリは見た目こそマインドマップですが、
内部では ツリー構造をそのままネストして持つことはしていません。
理由は単純で、
- ノードを頻繁に移動したい
- Undo / Redo を安定させたい
- JSON としてそのまま保存したい
この3つを同時に満たすには、
ネスト構造のまま扱うと辛くなるからです。
そこで、ツリーを一度バラして
id 参照で管理する形にしました。
実際のデータ構造
-
Workspace
- tabs
- activeDocId
- documents
-
Document
- rootId
- cursorId
- nodes
- undoStack / redoStack
-
Node
- id
- text
- parentId
- childrenIds
各ノードは、
- 自分の親を
parentIdで知っている - 子ノードは
childrenIdsの配列で持っている
という、かなりフラットな形になっています。
この構造にしたことで、
- ノード移動や削除が楽
- Undo / Redo をスナップショットで扱いやすい
- 永続化がシンプル
というメリットがありました。
レイアウトは「まず動く最小実装」
レイアウトは最初から凝りませんでした。
- 深さで x 座標を決定
- DFS で y 座標を順に割り当て
- 親ノードは子ノード群の中央に配置
かなり素朴なアルゴリズムですが、
思考用ツールとしては十分だと感じています。
改善余地としては、
- ノードの衝突回避
- 折り返し
- サブツリーの圧縮
などがあります。
Undo / Redo は「編集確定時に積む」
Undo / Redo は snapshot 方式です。
一番のポイントは、
キー入力ごとに Undo を積まないことです。
- Insert 開始時に origin を保持
- 編集確定時に差分があれば undoStack に積む
これにより、
- Undo の粒度が自然
- 操作が軽い
- Vim 的な「編集単位」に近い体験
を作ることができました。
自動保存は debounce + 直列化
永続化は Tauri 側で workspace.json に保存しています。
工夫した点は次の通りです。
- フロント側で 250ms の debounce
- 保存処理は必ず直列化
- Rust 側で tmp → rename の atomic write
- JSON が壊れていたら退避して起動継続
「保存に失敗して起動できない」を避けるための設計です。
IME と Enter キーの扱いが一番難しかった
一番悩んだのは 日本語入力と Enter キーでした。
このアプリでは、
-
TabやEnterでノードを増やす - 編集が終わったら Normal モードに戻る
という操作感を前提にしています。
問題は Enter キーの意味が入力方式で変わることでした。
- 英数入力
Enter = 編集確定 - 日本語入力
Enter = まず変換確定
理想の挙動
- 日本語入力時
1回目の Enter は変換確定
2回目の Enter で編集確定して Normal へ - 英数入力時
Enter 1 回で編集確定して Normal へ
挙動の違いを図で表すと
実装方針
-
compositionstart/compositionendを基準にする -
nativeEvent.isComposingは信用しすぎない - 一度でも IME を使ったかどうかをフラグで保持
この方針により、
- 日本語入力でも違和感が少ない
- 英数入力で Enter の二度押しを強制しない
挙動に落ち着きました。
GitHub Actions(tauri-build)で実行ファイルを作る
せっかく作ったので
macOS / Windows 向けの実行ファイルも生成できるようにしました。
Tauri はここが本当に楽です。
.github/workflows/tauri-build.ymlの例
name: tauri-build
on:
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22.12.0
cache: npm
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install npm dependencies
run: npm ci
- name: Build (Tauri)
run: npm run tauri build
- name: Upload bundle artifacts
uses: actions/upload-artifact@v4
with:
name: vikokoro-${{ matrix.os }}
if-no-files-found: error
path: |
src-tauri/target/release/bundle/**
やっていること
GitHub Actions に tauri-build 用の workflow を1つ置いています。
中身はだいたい次の流れだけです。
- Node / Rust のセットアップ
npm cinpm run tauri build- 生成された bundle を Artifacts にアップロード
公式の tauri-action を使っているので、
自前で複雑なことはしていません。
生成される成果物
ビルドが終わると、Artifacts から次のようなファイルを取得できます。
- Windows
-
.exe(NSIS)または.msi
-
- macOS
-
.app(設定次第で.dmg)
-
署名や notarize はまだ対応していないため、
初回起動時に警告は出ますが、
自分用や配布テストとしては十分です。
なぜ入れたか
理由は単純で、
- ローカルツールとして普通に使いたい
- 毎回
npm run tauri devはやりたくない - 「ちゃんとアプリになっている」状態で触りたかった
からです。
GitHub Actions で自動化しておくと、
workflow を実行するだけで
配布可能な実行ファイルが手に入るのはかなり便利でした。
ちなみに即使用できるようにRaycastでoption + vで即起動できるようにしました!
macOS でビルドしたアプリを実行する方法(補足)
GitHub Actions でビルドした macOS 向けの実行ファイルは、
署名や notarize をしていない場合、そのままだと起動できないことがあります。
今回は次の手順で問題なく実行できました。
手順
-
Artifactsからダウンロードしたファイルを解凍
-
.dmgから今回作成したものをApplicationsにコピー
(dmgの場合はマウントして Applications にドラッグ) -
quarantine 属性を外すために以下のコマンド
xattr -dr com.apple.quarantine "/Applications/vikokoro.app"
- Finder で
/Applications/vikokoro.appを右クリック →「開く」
何をしているか(軽く)
macOS では、
インターネット経由で取得したアプリに quarantine(隔離)属性 が付きます。
xattr コマンドは、その属性を削除して
「これは自己責任で実行するアプリですよ」と macOS に伝えるためのものです。
一度この手順を踏めば、
以降は通常通りアプリを起動できます。
署名や notarize を行えば不要になる手順ですが、
個人開発や配布テスト用途ならこれで十分でした。
おわりに
vikokoro は
- 思考を止めずに書く
- マウスに手を伸ばさない
- ファイル管理に悩まない
ことを目的に作りました。
時間がある時に
- レイアウト改善
- 折りたたみ
- データ連携
などを、余裕があれば試していきたいです。
Discussion
最&高
感&謝