📝

思考のスピードでマインドマップを書けるエディタを Tauri で作った

に公開
2

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
  • 検索、ズーム、パン
  • ローカル永続化

https://github.com/KASAHARA-Kyohei/vikokoro

なぜ 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 キーでした。

このアプリでは、

  • TabEnter でノードを増やす
  • 編集が終わったら 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 ci
  • npm run tauri build
  • 生成された bundle を Artifacts にアップロード

公式の tauri-action を使っているので、
自前で複雑なことはしていません。

生成される成果物

ビルドが終わると、Artifacts から次のようなファイルを取得できます。

  • Windows
    • .exe(NSIS)または .msi
  • macOS
    • .app(設定次第で .dmg

https://github.com/KASAHARA-Kyohei/vikokoro/actions/runs/21734983651

署名や notarize はまだ対応していないため、
初回起動時に警告は出ますが、
自分用や配布テストとしては十分です。

なぜ入れたか

理由は単純で、

  • ローカルツールとして普通に使いたい
  • 毎回 npm run tauri dev はやりたくない
  • 「ちゃんとアプリになっている」状態で触りたかった

からです。

GitHub Actions で自動化しておくと、
workflow を実行するだけで
配布可能な実行ファイルが手に入るのはかなり便利でした。

ちなみに即使用できるようにRaycastでoption + vで即起動できるようにしました!

macOS でビルドしたアプリを実行する方法(補足)

GitHub Actions でビルドした macOS 向けの実行ファイルは、
署名や notarize をしていない場合、そのままだと起動できないことがあります。

今回は次の手順で問題なく実行できました。

手順

  1. Artifactsからダウンロードしたファイルを解凍

  2. .dmg から今回作成したものをApplications にコピー
    dmg の場合はマウントして Applications にドラッグ)

  3. quarantine 属性を外すために以下のコマンド

xattr -dr com.apple.quarantine "/Applications/vikokoro.app"
  1. Finder で /Applications/vikokoro.app を右クリック →「開く」

何をしているか(軽く)

macOS では、
インターネット経由で取得したアプリに quarantine(隔離)属性 が付きます。

xattr コマンドは、その属性を削除して
「これは自己責任で実行するアプリですよ」と macOS に伝えるためのものです。

一度この手順を踏めば、
以降は通常通りアプリを起動できます。

署名や notarize を行えば不要になる手順ですが、
個人開発や配布テスト用途ならこれで十分でした。

おわりに

vikokoro は

  • 思考を止めずに書く
  • マウスに手を伸ばさない
  • ファイル管理に悩まない

ことを目的に作りました。

時間がある時に

  • レイアウト改善
  • 折りたたみ
  • データ連携

などを、余裕があれば試していきたいです。

https://github.com/KASAHARA-Kyohei/vikokoro

Discussion