Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rust で TUI アプリケーションを作った話

ynqa
December 08, 2024
370

Rust で TUI アプリケーションを作った話

ynqa

December 08, 2024
Tweet

Transcript

  1. My Rust projects promkit • 対話型 TUI アプリケーションを構築するためのライブラリ promkit を利用した

    TUI アプリケーションについて • jnv ◦ インタラクティブに jq を実行できる • sig ◦ ストリーミングデータに対してインタラクティブに grep できる • logu ◦ 非構造化ログから Drain という仕組みを使ってパターン抽出する
  2. My Rust projects promkit • 対話型 TUI アプリケーションを構築するためのライブラリ promkit を利用した

    TUI アプリケーションについて • jnv ◦ インタラクティブに jq を実行できる • sig ◦ ストリーミングデータに対してインタラクティブに grep できる • logu ◦ 非構造化ログから Drain という仕組みを使ってパターン抽出する
  3. jnv

  4. sig

  5. 目次 描画 • 対話的に jq フィルタ を入力し、JSON を出力する ◦ TUI

    アプリケーションのためのライブラリを紹介 ◦ promkit crate の紹介 実行 • TUI アプリケーション実行中に jq を実行する ◦ j9-sys, j9 crate の紹介 リリース • cargo-dist を用いたリリース方法の紹介
  6. TUI

  7. TUI ライブラリ マウス、キー操作などのイベントを取得したりテキストカラーの変更やカーソルの移動を可 能にするライブラリ • crossterm • termion • termwiz

    (WezTerm のために開発されている) ncurses (new curses) をベースにしたライブラリ (※どちらもほとんどメンテされていなさそ う) • ncurses • pancurses
  8. crossterm examples/event-read.rs (crossterm) より • event::read を使って event::Event を取得する ◦

    イベントにはマウスも対応 • 受け取ったイベントを元に特定の処 理を実行するような使い方 ◦ e.g. Esc が押されたら処理を終 える
  9. TUI ライブラリ GNU Readline のような対話型アプリケーションを構築するためのライブラリ • rustyline (deno の REPL

    で利用されている) • reedline (Nushell のラインエディタとして利用されている ) 以下のライブラリはさらに派生してセレクトボックスなどのプロンプト (i.e. UI コンポーネント) も構築できる • inquire • dialoguer
  10. やりたいこと jq フィルタ を入力するエディタと JSON を出力する • 最悪 crossterm を使えばできる

    ◦ 実装量が多くなりそう... • もっとライブラリからサポート受けたい ◦ やりたいこと的に ratatui よりも対話型アプリケーションのライブラリ群の方が適 してそう • 対話型アプリケーションのためのライブラリ (rustyline, reedline, inquire, dialoguer) を使って実現できそう?
  11. rustyline, reedline, inquire, dialoguer どのライブラリを使っても jq フィルタ を入力させるエディタは作れそう • e.g.

    inquire の Text 一方で JSON を出力することは難しい...? • rustyline, reedline はエディタのみのサポートなので難しい • inquire, dialoguer では他の UI コンポーネントも提供している ◦ …が、JSON をレンダリングするコンポーネントは含まれていない したがって、JSON のための UI コンポーネントは実装する必要がある => inquire, dialoguer を使って実装できそうではあるが ...
  12. inquire, dialoguer UI コンポーネントごとに実装が独立しすぎている • 外部で定義した UI コンポーネントについて イベントループ の全てを実装する必要が

    ある ◦ i.e. イベントハンドラ (に伴う ステートの管理)、レンダリング のループ ◦ UI コンポーネント (今回だと JSON) を新規追加するときの実装コストが大きそう • また、UI コンポーネント同士を組み合わせられなさそう ◦ i.e. エディタとしての inquire の Text の仕組みの中に JSON も表示させる機能 を注入する...ということができない ◦ 別の UI を追加したい場合にシュッと追加できるようにしておきたい
  13. promkit 外部で定義した UI コンポーネントについて イベントループ の全てを実装する必要がある => Renderer Trait を定義した

    • Renderer を実装してさえすれば promkit の上で走らせることができる • Pane: 表示させたいデータ列
  14. promkit for jnv UI コンポーネント同士を組み合わせられなさそう => Pane を介した UI コンポーネント間の組み合わせ

    • 組み合わせで実現できた ◦ jq フィルタのエディタ (filter_editor) ◦ JSON の出力 (json) • さらに追加機能を 後から シュッと追加できた ◦ エラーメッセージを表示させるテキスト (hint_message) ◦ jq フィルタ補完候補を表示させるリスト (suggestions)
  15. TextEditor TextEditor • i.e StringBuffer ◦ 文字の追加/削除などを行うため の構造体 ◦ PieceTable,

    Gap, Rope などはい ずれ試したい Cursor • Vec などコレクションのインデックス (position) を移動させるための構造体
  16. StyledGraphme(s) String に以下の要素を追加した • width ◦ 絵文字 😄 や特殊文字の表示幅 •

    style ◦ crossterm::style::ContentStyle よりカラーや文字の属性 (e.g. Bold, Italic) などのスタイルを管 理
  17. まとめ Renderer Trait を作ったことで レンダリング と全体の イベントループ は共通化しライブラリ側で吸収した • 一方で

    イベントハンドラ (に伴う ステートの管理 ) とレンダリングのための Pane への変換は UI コ ンポーネント毎に対応する必要がある • したがって、新規作成の手間がちょっと減ったくらい... • ちなみに inquire も v0.7.0 からそんな感じの Trait を定義してある (e.g. impl Trait for Text) UI コンポーネントはそれぞれを組み合わせられるように作った • これは良かった(一方で組み合わせすぎると イベントハンドラ が複雑になる) フロントエンドの知識不足が否めない... • 例えば Bubble Tea (Go) が Elm Architecture を参考に実装されている
  18. jq を実行する TUI を実行中に jq を実行したい • promkit 的には Renderer

    Trait の fn evaluate の中で jq を実行したい シンプルに実現するには std::process::Command を使って jq コマンドを実行する • なんかヤダ • 実行環境の jq(の有無やバージョンなど)に依存してしまう bindgen を使って libjq の Foreign Function Interface (FFI) のコードを生成できるのでは?
  19. jq を実行する すでにあった • jq-sys, jq-rs 使える…? • 開発が止まっていそう •

    M2 Mac でビルドできない • jq 1.7 に対応しておきたい (v1.6 を対象にしていそう) • Recursive Descent を考慮すると jq_rs::run の返り値が Result<String> ではなく Result<Vec<String>> の方が好ましそう
  20. j9, j9-sys 作った。各 crate の役割は参考元と同じ (この xxx-sys と xxx という構成は一般的っぽい

    e.g. imgui-rs) • j9-sys ◦ libjq が提供しているそのままの API を Rust で扱えるように • j9 ◦ ユーザが利用しやすいような j9-sys のラッパー bindgen を使って libjq の FFI のコードを生成する過程を説明する
  21. j9-sys crate: src/lib.rs Include the Generated Bindings in src/lib.rs (The

    bindgen User Guide)より • bindgen で生成するコードを include! で展開するようにしておく ◦ bindgen で生成されるコードは Rust の規約に合っていない可 能性があるので許容しておく
  22. j9-sys crate: build.rs build.rs • cargo の機能だけで十分でない場合 に別途ビルド用のスクリプトを挟み込 める bindgen

    • C のヘッダファイルを読み込ませて出 力するコードを書くだけ • CLI もある • cargo install bindgen-cli
  23. j9-sys crate: build.rs libjq のビルドには autotools の crate を利 用

    • パラメータについては公式の手順に 則った (ref)
  24. j9-sys crate: build.rs libjq をリンクする cargo:rustc-link-search • rustc -L ◦

    ライブラリを探すためのパス cargo:rustc-link-lib • rustc -l ◦ リンクするライブラリ ▪ libxxx.a の xxx 部分 ◦ 動的リンクも指定可能 ▪ static を dylib に変える
  25. まとめ j9-sys, j9 crate のおかげでユーザの環境に jq コマンドは不要になった …が jnv に取り込んだ後に別の問題が出てきた

    • インストールの際のエラー報告が多発 ◦ ビルドするために必要なツール (e.g. autoconf) の用意が別途必要になってし まった • MUSL Linux への対応 結局 jnv の v0.3.0 以降では jaq (jq clone in Rust) へ移行した (jnv#24) 一方で jaq では v1.7 の一部のフィルタ (e.g. pick) は未対応だったりもする (jaq#112)
  26. (部分的な) jq フィルタの補完 • Trie (radix_trie crate) を使った prefix search

    なだけ ◦ Suggest は promkit 側で提供 ◦ したがって以下のフィルタのみし か補完できていない ▪ Identity (.) ▪ Object Identifier-Index (.foo) ▪ Array Index (.[0]) ◦ 実行時に構築
  27. クリップボード クエリや jq 実行後のJSON をクリップボードにコピーしたい • arboard crate を使って実現 •

    X11/Wayland 起因のエラーで実行できないケースがある ...? ◦ arboard#153 や jnv#62
  28. • cargo dist init を実行すると dist-workspace.toml が作成される (v0.23 以降) ◦

    公開先やターゲットプラットフォームなどの情報が書かれている ◦ v0.23 以前だと Cargo.toml に書かれる (ref) • さらに GitHub Action も有効化するとリリース用のワークフローファイルも作成される ◦ pull request 作成時にはそれぞれのプラットフォームでビルドを実行 ◦ リリースタグを切ると homebrew tap へのリリースとリリースページの作成を やってくれる cargo-dist
  29. About me @ynqa • Makoto Ito • 仕事では YAML を書いてます

    • Rust は 4, 5 年前に Kubernetes client 書いたあたりからちょくちょく 触っています (完全に個人プロジェクトのみ) • 最近は Aya 触ってます