Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

jnv

Slide 5

Slide 5 text

sig

Slide 6

Slide 6 text

目次 描画 ● 対話的に jq フィルタ を入力し、JSON を出力する ○ TUI アプリケーションのためのライブラリを紹介 ○ promkit crate の紹介 実行 ● TUI アプリケーション実行中に jq を実行する ○ j9-sys, j9 crate の紹介 リリース ● cargo-dist を用いたリリース方法の紹介

Slide 7

Slide 7 text

TUI

Slide 8

Slide 8 text

TUI ライブラリ マウス、キー操作などのイベントを取得したりテキストカラーの変更やカーソルの移動を可 能にするライブラリ ● crossterm ● termion ● termwiz (WezTerm のために開発されている) ncurses (new curses) をベースにしたライブラリ (※どちらもほとんどメンテされていなさそ う) ● ncurses ● pancurses

Slide 9

Slide 9 text

crossterm examples/event-read.rs (crossterm) より ● event::read を使って event::Event を取得する ○ イベントにはマウスも対応 ● 受け取ったイベントを元に特定の処 理を実行するような使い方 ○ e.g. Esc が押されたら処理を終 える

Slide 10

Slide 10 text

TUI ライブラリ 先述の crossterm などをバックエンドに持ちつつ、よりリッチなグラフィック (e.g. Sparkline) をターミナル上に描画するためのライブラリ ● ratatui (tui-rs からフォークして開発されている ) ● cursive

Slide 11

Slide 11 text

TUI ライブラリ GNU Readline のような対話型アプリケーションを構築するためのライブラリ ● rustyline (deno の REPL で利用されている) ● reedline (Nushell のラインエディタとして利用されている ) 以下のライブラリはさらに派生してセレクトボックスなどのプロンプト (i.e. UI コンポーネント) も構築できる ● inquire ● dialoguer

Slide 12

Slide 12 text

inquire: confirm

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

やりたいこと jq フィルタ を入力するエディタと JSON を出力する ● 最悪 crossterm を使えばできる ○ 実装量が多くなりそう... ● もっとライブラリからサポート受けたい ○ やりたいこと的に ratatui よりも対話型アプリケーションのライブラリ群の方が適 してそう ● 対話型アプリケーションのためのライブラリ (rustyline, reedline, inquire, dialoguer) を使って実現できそう?

Slide 15

Slide 15 text

rustyline, reedline, inquire, dialoguer どのライブラリを使っても jq フィルタ を入力させるエディタは作れそう ● e.g. inquire の Text 一方で JSON を出力することは難しい...? ● rustyline, reedline はエディタのみのサポートなので難しい ● inquire, dialoguer では他の UI コンポーネントも提供している ○ …が、JSON をレンダリングするコンポーネントは含まれていない したがって、JSON のための UI コンポーネントは実装する必要がある => inquire, dialoguer を使って実装できそうではあるが ...

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

promkit

Slide 18

Slide 18 text

Readline

Slide 19

Slide 19 text

JSON

Slide 20

Slide 20 text

promkit 外部で定義した UI コンポーネントについて イベントループ の全てを実装する必要がある => Renderer Trait を定義した ● Renderer を実装してさえすれば promkit の上で走らせることができる ● Pane: 表示させたいデータ列

Slide 21

Slide 21 text

event-loop

Slide 22

Slide 22 text

promkit for jnv UI コンポーネント同士を組み合わせられなさそう => Pane を介した UI コンポーネント間の組み合わせ ● 組み合わせで実現できた ○ jq フィルタのエディタ (filter_editor) ○ JSON の出力 (json) ● さらに追加機能を 後から シュッと追加できた ○ エラーメッセージを表示させるテキスト (hint_message) ○ jq フィルタ補完候補を表示させるリスト (suggestions)

Slide 23

Slide 23 text

text_editor::State (簡略)

Slide 24

Slide 24 text

TextEditor TextEditor ● i.e StringBuffer ○ 文字の追加/削除などを行うため の構造体 ○ PieceTable, Gap, Rope などはい ずれ試したい Cursor ● Vec などコレクションのインデックス (position) を移動させるための構造体

Slide 25

Slide 25 text

StyledGraphme(s) String に以下の要素を追加した ● width ○ 絵文字 😄 や特殊文字の表示幅 ● style ○ crossterm::style::ContentStyle よりカラーや文字の属性 (e.g. Bold, Italic) などのスタイルを管 理

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Future Works 開発は続いてます... ● 今のところ JSON とエディタを併用し て表示できるライブラリはない ● また procedural macro (promkit-derive) の開発中 ○ promkit#11, promkit#17

Slide 28

Slide 28 text

Run “jq”

Slide 29

Slide 29 text

jq を実行する TUI を実行中に jq を実行したい ● promkit 的には Renderer Trait の fn evaluate の中で jq を実行したい シンプルに実現するには std::process::Command を使って jq コマンドを実行する ● なんかヤダ ● 実行環境の jq(の有無やバージョンなど)に依存してしまう bindgen を使って libjq の Foreign Function Interface (FFI) のコードを生成できるのでは?

Slide 30

Slide 30 text

jq を実行する すでにあった ● jq-sys, jq-rs 使える…? ● 開発が止まっていそう ● M2 Mac でビルドできない ● jq 1.7 に対応しておきたい (v1.6 を対象にしていそう) ● Recursive Descent を考慮すると jq_rs::run の返り値が Result ではなく Result> の方が好ましそう

Slide 31

Slide 31 text

j9, j9-sys 作った。各 crate の役割は参考元と同じ (この xxx-sys と xxx という構成は一般的っぽい e.g. imgui-rs) ● j9-sys ○ libjq が提供しているそのままの API を Rust で扱えるように ● j9 ○ ユーザが利用しやすいような j9-sys のラッパー bindgen を使って libjq の FFI のコードを生成する過程を説明する

Slide 32

Slide 32 text

j9-sys crate: src/lib.rs Include the Generated Bindings in src/lib.rs (The bindgen User Guide)より ● bindgen で生成するコードを include! で展開するようにしておく ○ bindgen で生成されるコードは Rust の規約に合っていない可 能性があるので許容しておく

Slide 33

Slide 33 text

j9-sys crate: build.rs build.rs ● cargo の機能だけで十分でない場合 に別途ビルド用のスクリプトを挟み込 める bindgen ● C のヘッダファイルを読み込ませて出 力するコードを書くだけ ● CLI もある ● cargo install bindgen-cli

Slide 34

Slide 34 text

j9-sys crate: build.rs libjq のビルドには autotools の crate を利 用 ● パラメータについては公式の手順に 則った (ref)

Slide 35

Slide 35 text

j9-sys crate: build.rs libjq をリンクする cargo:rustc-link-search ● rustc -L ○ ライブラリを探すためのパス cargo:rustc-link-lib ● rustc -l ○ リンクするライブラリ ■ libxxx.a の xxx 部分 ○ 動的リンクも指定可能 ■ static を dylib に変える

Slide 36

Slide 36 text

target/ dir

Slide 37

Slide 37 text

j9 crate j9-sys だけだとユーザがシュッと jq を実行 することが難しいのでラッパーを用意してお く

Slide 38

Slide 38 text

まとめ 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)

Slide 39

Slide 39 text

jnv: その他

Slide 40

Slide 40 text

(部分的な) jq フィルタの補完 ● Trie (radix_trie crate) を使った prefix search なだけ ○ Suggest は promkit 側で提供 ○ したがって以下のフィルタのみし か補完できていない ■ Identity (.) ■ Object Identifier-Index (.foo) ■ Array Index (.[0]) ○ 実行時に構築

Slide 41

Slide 41 text

クリップボード クエリや jq 実行後のJSON をクリップボードにコピーしたい ● arboard crate を使って実現 ● X11/Wayland 起因のエラーで実行できないケースがある ...? ○ arboard#153 や jnv#62

Slide 42

Slide 42 text

cargo-dist を使ったリリース

Slide 43

Slide 43 text

モチベーション 作るもん作ったんでリリースするぞ! GoReleaser みたいに homebrew でインストールできる状態をシュッと作れるものはないだ ろうか...? => cargo-dist 見つけた

Slide 44

Slide 44 text

cargo-dist

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

dist-workspace.toml

Slide 47

Slide 47 text

release page and formula

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

About me @ynqa ● Makoto Ito ● 仕事では YAML を書いてます ● Rust は 4, 5 年前に Kubernetes client 書いたあたりからちょくちょく 触っています (完全に個人プロジェクトのみ) ● 最近は Aya 触ってます