📁

Rust製・自作tuiファイルマネージャーの紹介

2021/11/19に公開

はじめまして。tuiファイルマネージャーを実装したので、その紹介記事です。

felix

https://github.com/kyoheiu/felix

linux(Arch Linux, ubuntu)およびmacOS上での動作を確認しています。Windowsは今のところ未対応です。
パッケージ名は、fileのアナグラムと、explorerの'x'の組み合わせです(好きなテニス選手のファーストネームでもあります)。これまで愛用していたVifmに大きな影響を受けつつ、

最低目標として

  • Vimの基本的な操作感をファイルマネージャーとして再現すること

ゴールとして

  • シンプルかつ高速に作ること
  • ファイルを開くためのアプリケーションを簡単に設定できること

を念頭に置いて開発を続けています。

インストール

crates.ioに登録済です。

cargo install felix

AURにも入れました。

yay -S felix-rs

NetBSDにも入れてもらえました。

pkgin install felix

もしくはGitHubから:

git clone https://github.com/kyoheiu/felix.git
cd felix
cargo install --path .

起動

fxでカレントディレクトリ内のアイテムを表示します。
fx <dir path>で開くディレクトリを指定することも可能。

キーマップ

キー 説明
j / k / ↑ / ↓ カーソル上下移動
h / ← (存在すれば)親ディレクトリへ移動
l / → ディレクトリの場合、ディレクトリ移動。ファイルの場合、設定ファイルに指定したアプリケーションで開く
gg 行頭へ移動
G 行末へ移動
dd アイテムを削除・yank
yy アイテムをyank
p yankしたアイテムをカレントディレクトリにputする。アイテム名が重複する場合は自動リネーム。
V セレクトモード
d (セレクトモード) 選択したアイテムを削除・yank
y (セレクトモード) 選択したアイテムをyank
Ctrl+c アイテム名をクリップボードへコピーする
backspace 隠しファイル・ディレクトリの表示/非表示切り替え(終了後も状態を保持します)
t リストの並び順をトグルする(アイテム名 <-> 更新時間)こちらも、終了後も状態を保持。
: シェルモード。:のあとに続けてコマンドを入力しEnterで実行。
c アイテム名をリネームする
/ フィルターモード。入力した文字列を含むカレントディレクトリ内のアイテムがリアルタイムで列挙される。
Esc ノーマルモードへ戻る
:e カレントディレクトリを再読込
:empty trashディレクトリを空にする
:h 簡易ヘルプの表示
:q / ZZ プログラムの終了

セレクトモードでの操作後のカーソル位置などが若干本家とは異なっていますが、このへんは使用しつつ違和感が強ければ修正したいです。

個人的に入れてよかったと思っているのは、シェルモードとアイテム名コピーです。
シェルモードはまだ実験的に入れているものですが、ちょっとしたコマンドを、ディレクトリの中身を確認しつつ実行できる使い勝手が気に入っています。
アイテム名コピーは、わざわざ実装しなくともカーソル選択後Ctrl+Shift+cなどでコピーできますが、そのためにマウスを触りたくないし、選択するときちょっと範囲を間違えちゃったりもしますよね。特に日本語ファイルをターミナル上でいじるときなんかに役立てています。
上記のアイテム名コピー機能は個人的に大変気に入っていたのですが、clipboardクレートがlinuxでのビルド時にX11まわりのライブラリに依存していることがわかり、そうした依存パッケージは極力減らしてシンプルにしたいため、v0.2.10からはこの機能自体を削除しています。

特徴(1) シンプルかつ高速に

cli/tuiはそもそもシンプルなものではありますが、画面にどの情報を出すか(情報量)のチョイスには作者の思想が色濃く反映されます。
たとえばrangerは非常に情報量の多い画面構成です。一方Vifmはある程度情報量を削ぎ落しつつ、理想の操作にマッチするようにマルチペインを採用しています。felixが目指したのは、Vifmよりもさらに情報量の少ない、lsとファイルマネージャーが融合したような見せ方です。
画面に出力されるのはアイテム名と更新時間のみ。思い切ってサイズ表示も削っています。ディレクトリ/ファイル/シンボリックリンクの区別は色で行います。あとはyank/put/delete時やエラー発生時のメッセージ表示と、フィルターモード・リネームモード・シェルモード中の入力表示だけ(すべて2行目に集約)。これにより、最も基本的なファイル操作に集中すること、またプログラムの軽快な動きを目指しています。
軽快さに関しては、たとえばyankした10000個の空のテキストファイルを別ディレクトリにputするという操作において、(計測が難しいので体感ですが)Vifmは自分の環境でput完了まで約4秒。対してfelixは、ほぼ一瞬で完了しています。

特徴(2) ファイルをどのアプリケーションで開くかを簡単に設定できる

たとえばVifmは、vifmrcという名前の設定ファイルでカスタマイズ可能です。これはVimscriptで記述されており、決して全体が複雑なわけではないのですが、ファイルオープンの設定のところはなかなかに難解です。

filextype {*.pdf},<application/pdf> zathura %c %i &, apvlv %c, xpdf %c
fileviewer {*.pdf},<application/pdf> pdftotext -nopgbrk %c -

" PostScript
filextype {*.ps,*.eps,*.ps.gz},<application/postscript>
        \ {View in zathura}
        \ zathura %f,
        \ {View in gv}
        \ gv %c %i &,
[...]

厳しい。僕も、正直この項目は何がなんだかわからないままにしています(一応希望通りのアプリケーションで開けてはいます)。
この拡張子 - アプリケーションの対応関係設定をもっと分かりやすく記述したい。たとえばこんな風に。

default = "nvim"

[exec]
feh = ["jpg", "jpeg", "png", "gif", "svg"]
zathura = ["pdf"]

これがfelixでの記述です。felixではtomlファイルを設定ファイルとし、<アプリケーション名> - <拡張子の配列>という形で対応関係を記述します。今のところオプションには対応していませんが、もし細かいオプション指定が必要であれば都度シェルモードから入力が可能です。
この場合、デフォルトのアプリケーションはNeovim。画像系はfeh、pdfはzathuraとなります。もちろんいくらでも追加可能です。

以降は記録も兼ねた製作まわりについてです。

ファイルマネージャーを自作したい!

ファイルマネージャーは今回挙げたもの以外にも代表選手が山のようにいる、枯れに枯れた歴史あるカテゴリであり、選択肢はたくさん用意されています。一方で、毎日のように起動するユーティリティでもあります。それだけに、今あるものではない、すみずみまで自分にフィットしたものを作りたい…という気持ちが次第に強くなってきていました。
また、Rust入門に何度か挑戦しては中断、を繰り返していて、そろそろちゃんとやりたい、という気持ちも高まっていたところでした。ですので今回は、Rustを学びつつ、兼ねてより興味があったファイルマネージャーの自作に挑んでみた、ということになります。

tuiライブラリについて

まずtuiを実装するライブラリを選ばなくてはいけません。curses系を使っていくというのがやはり第一候補ですが、今回はRustの学習意図も兼ねての作成だったので、termionを選択しました。

termionとは

Rust製OSであるRedoxOSプロジェクトの中で生まれた、ncursesの代替ライブラリ…という説明が一番分かりやすいかもしれません。薄いけれど最低限は揃っている、そんなライブラリです。

termionの正体は「stdoutに制御文字を直接書き込んでいくライブラリ」です。
たとえば、カーソルの位置を下げるtermion::cursor::Down(u16)という構造体のソースはこうなっています。

/// Move cursor down.
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Down(pub u16);

impl From<Down> for String {
    fn from(this: Down) -> String {
        let mut buf = [0u8; 20];
        ["\x1B[", this.0.numtoa_str(10, &mut buf), "B"].concat()
    }
}

impl fmt::Display for Down {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "\x1B[{}B", self.0)
    }
}

termionを使って記述していくというのは、このように、ひたすら制御文字をstdoutへ出力してカーソルや文字列、画面を制御していく、ということなのです。
こういうプリミティブ感のあるライブラリにはラッパーフレームワークがどんどん出来ていくものです。termionも例外ではなく、たとえばメジャーなRust製tuiライブラリであるtui-rsはバックエンドの1つとしてtermionを採用しています。これを使えば、おそらくかなり手軽に作れるだろうと思います。
ただ、個人的に、フレームワークから入ると内部の実装がよくわからないまま作れてしまう…というのが嫌でした。何が効率的で何が非効率なのか、実際この関数は何をするものなのか…そういった知識を少しでも得るには、フレームワーク任せにせずもっと下のところから自分でいじってみる、という作業が一度は必要です。また、実装が薄いライブラリを使うというのは、デバッグを楽にすることにもつながるように思います。

ただしtermionは実装だけでなくドキュメントも非常に薄いです。特にscreenまわりは思ったような挙動をしないことが多いので注意が必要。また、たとえば出力の際の色を指定するcolorがそれぞれ別の構造体で実装されているので、パターンマッチしたいときに非常に難儀です(現状マクロを使って処理。そもそもtermionがマクロで実装されている)。さらに、async_stdinを使うとカーソルの位置を取得できない、というバグ(? 仕様?)もあり、issueも立っていて僕も報告しましたが、特に動きはありません。大丈夫か…。
とにかくドキュメントのやる気がなさすぎるので、自分でどうにかできる/したい人向けではあります。

再帰的なディレクトリの処理について

Rustでは、さまざまなエッジケースへの対応が難しいという理由からか、ディレクトリを再帰的に操作するメソッドはstdには実装されていません。代わりにwalkdirというクレートがうまいことやってくれます。これを使うことで、ディレクトリ内のアイテムのイテレータを再帰的に取得できます。

ちなみにfs_extraというクレートに、やはり再帰的にディレクトリを操作する関数が実装されていますが、なぜかうまく動かなかったので、walkdirを使いつつ、主だったところは自分で実装しています。このクレートはダウンロード数が多く、メジャーなライブラリのようなのですが、ドキュメントが微妙に間違っていたりもするので、再帰的なコピーや削除程度の内容であれば、自力で実装するほうがよさそうです。

番外編 crates.ioについて

安定して動作するまで作ってからしばらくは自分でひっそり使っていたものの、次第にコミュニティにもちゃんと配布したいという気持ちが出てきて、crates.ioにも登録しました。
crates.ioはRustのライブラリやアプリケーションを登録・配布できる言語公式のパッケージレジストリで、基本的にはGitHubのアカウントがあれば中身を問わず誰でも登録できる、敷居の低い仕組みになっています。
この敷居の低さはコミュニティ拡大の後押しになる一方、難しい面もあるように思います。

名前衝突

たとえばGitHubのレポジトリでは、既存のものと同名のリポジトリを作成しても衝突することはありません。これはアカウント名で名前空間が区切られているからですが、crates.ioにはこの名前空間の区切りが存在しません。この区切りの不在が悪用されるケースが散見されます。特に短めのパッケージ名は、何年も前に中身が空のGitHubリポジトリにとられたっきり…といった場合もよくあります。
コミュニティでもこれは問題視されていました(います)が、名前空間を区切ることのデメリットと天秤にかけて結局今の形を選んだ、という経緯のようです。対策として、「内容に紐づいたオリジナルなパッケージ名を考えればよい」という提案も目にしました。一理ありますが、なかなか難しいです。felixも、重複を避けつつ(少なくとも自分が納得いくレベルの)オリジナル性のある名前にたどりつくまでがハードでした。

おわりに

最初から今に至るまで、Rustの良さをしみじみ味わいながら作っています。特にエラーハンドリングの快適さは思っていた以上でした。また、rust-analyserやclippyなどの開発支援ツールの出来の良さもすごくて、コミュニティの勢いがあるってこういうことなんだな、と日々実感しています。

以上、自作ファイルマネージャーのささやかな宣伝を兼ねた紹介でした。もしよかったら、ちょっと触ってみてください。

Discussion