ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

Mui(無為)—— Ruby製VimライクTUIエディタ

この記事はESM Advent Calendar 2025の1日目の記事です。

はじめに

こんにちは、S.H.です。

普段はVimを使ってお仕事をしていたのですが、使っていく中で「Vimの設定を書くこと」に課題を感じていました。 そうした中で「書きなれたRubyで設定を書けるエディタがあると良いのでは?」と考え、Mui(無為)という名前のVimライクな自作エディタを作り始めました。

この記事では、その自作エディタを作るまでの経緯や機能の紹介をします。

過去のエディタ変遷

元々はCを勉強していたため、Visual Studioを使っていました。 その後、UIなどが近いVSCodeをエディタとして使い始めましたが、使いにくさを感じる場面が出てきました。

僕はGUIよりもターミナル上でGitのコマンドを実行することが多く、その方がスムーズにブランチの切り替えなどができるため、VSCodeは僕の開発スタイルにはマッチしていませんでした。

また、VSCodeではCRubyのソースコードのインデントが崩れることも多く、その点でも扱いにくさを感じていました。

そうした中で、ターミナル内でのコマンド操作もしやすく、CRubyのインデントも崩れないVimを使うと良いのではと考え、Vimにコンバートしました。

Vimに移行してからはかなり快適になった一方で、使い続ける中でいくつか課題を感じるようになりました。

Vimを使い続けて感じた課題

基本的に、Vimでコードを書くこと自体には満足していましたが、設定ファイルを書くことには難しさを感じていました。

最初のころはvim scriptで設定を書いていましたが、あるタイミングで導入したプラグインがVim9 Scriptにしか対応しておらず、利用するにあたって設定ファイルをVim9 Scriptに置き換える必要がありました。 そのときは大きなトラブルなく移行できたものの、その後に追加しようとしたプラグインの中にはVim9 Scriptに対応していないものもあり、どうしたものかと悩む場面も出てきました。

また、プラグインを使ってリポジトリ単位でVimのキーマップを定義し、テストやリンターの実行をNormalモードからできるようにしていましたが、そのリポジトリごとの設定はvim scriptで書く必要がありました。

そのため、Vim9 Scriptとvim scriptの二つのスクリプトで設定を書く必要があり、この点も課題だと感じていました。

Ruby製エディタという前例:Textbringer

そういった課題を感じる中で、「Rubyで設定が書けるVimライクなエディタがあればいいのでは?」と考えるようになりました。 RubyでDSLとして設定を書く手法は、さまざまなgemでも使われていますし、設定を書く体験としても案外相性が良いのではないかと思いました。

そこで参考にしたのが、Textbringerです。TextbringerはRuby製のエディタとして長く開発されており、 実装にあたって参考になる情報があるのではないかと考え、コードを読み始めました。

github.com

実装を軽く読んでみたところ、依存関係としてcursesを使い、ターミナル上でのキー入力や画面描画を扱っていることがわかりました。 そのため、最低限cursesを経由してキー入力や描画を扱えれば、RubyでもTUIエディタが実装できそうだという感触を得ました。

VimライクTUIエディタ「Mui(無為)」

そうして今、僕が作っているのが Mui(無為)という名前の VimライクなTUIエディタです。 名前の由来は老荘思想における「無為自然」で、設定や操作を強く意識することなく、コードを書く人が自然体でコードに向き合えるエディタでありたいと考えています。

自作エディタの画面。画面中央を起点に左右にバッファがそれぞれ表示されており、そのバッファの中にRubyのコードがシンタックスハイライトされて表示されている

github.com

:e でファイルをバッファとして開くといった基本的なコマンドから、タブやシンタックスハイライトなど、コードを書くうえで最低限あると便利な機能を一通り実装しています。

実装前の仕様検討にはChatGPTを使い、実装はClaude Codeに任せつつ、テストが書きにくくなっている部分については僕が書き直す、といった形で開発を進めています。

また、設定ファイルはRuby DSLとして、以下のように自然な形で書けます。

# カラースキームの設定
Mui.set :colorscheme, "mui"

# シンタックスハイライトの有効化
Mui.set :syntax, true

# インデント設定
Mui.set :shiftwidth, 2
Mui.set :expandtab, true
Mui.set :tabstop, 8

# Gitプラグイン、バージョンを指定することもできる
Mui.use "mui-git", "0.2.0"

# fzfプラグイン
Mui.use "mui-fzf"

# LSPプラグイン
Mui.use "mui-lsp", "0.2.0"

Mui.lsp do
  # LSPプラグインでは Ruby LSP などのLSPをデフォルトでサポートしている
  # デフォルトでサポートしているものに関しては use :ruby_lsp で利用できる
  use :ruby_lsp
end

# ファイルの保存のキーマップ
Mui.keymap :normal, "<Leader>w" do |ctx|
  ctx.editor.execute_command("w")
end

# バッファを閉じるキーマップ
Mui.keymap :normal, "<Leader>q" do |ctx|
  ctx.editor.execute_command("q")
end

また、TUIエディタは内部状態が複雑になりやすいため、E2Eテストを追加し、実際の操作に近い形で動作を検証しています。 例えば、以下のようにファイルの編集から保存までが正しく行えるかをテストしています。

  def test_basic_file_editing
    # Scenario: Open existing file -> Edit -> Save
    Tempfile.create(["test", ".txt"]) do |f|
      f.write("Hello\nWorld")
      f.flush

      runner = ScriptRunner.new(f.path)

      runner
        .type("j")         # Move to line 2
        .assert_cursor(1, 0)
        .type("llll")      # Move toward end of line (4 times)
        .type("a")         # Append mode
        .type("!")         # Add "!"
        .type("<Esc>")     # Normal mode
        .assert_mode(Mui::Mode::NORMAL)
        .assert_line(1, "World!")
        .type(":w<Enter>") # Save
        .assert_message_contains("written")
        .assert_modified(false)

      # Verify file was actually saved
      assert_equal "Hello\nWorld!\n", File.read(f.path)
    end
  end

このE2Eテストのおかげで、新機能の追加や既存コードのリファクタリング時にも、リグレッションが起きていないことを確認しながら開発を進められています。

おわりに

現在は、お仕事やOSSのパッチを書く際には基本的にMui(無為)を使っており、たまにバグを踏むことはあるものの、大きな不満なく、楽しくコードを書けています。

今後は、見つけたバグの修正や、より便利に使えるプラグインの開発などを進めていければと思っています。

興味を持った方は、ぜひ使ってみてもらえると嬉しいです。


永和システムマネジメントでは、Rubyやアジャイルソフトウェア開発、そして自作エディタの開発を通じて、コミュニティとともに成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp