Main.rs
多くのRATATUIアプリケーションの main
ファイルは、スタートアップループを保存する場所であり、場合によってはイベント処理です。 Event Handling でイベントを処理する方法を見る
このアプリケーションでは、 main
関数を使用してスタートアップ手順を実行し、メインループを開始します。また、このファイルにメインループロジックとイベント処理を配置します。
Main
メイン関数では、ターミナルをセットアップし、アプリケーション状態を作成してアプリケーションを実行し、最終的にターミナルを見つけた状態にリセットします。
アプリケーション動作前の手順
ratatui
アプリケーションは画面全体を取り、すべてのキーボード入力をキャプチャするため、 main
関数の開始時にボイラープレートが必要です。
use ratatui::crossterm::event::EnableMouseCapture;use ratatui::crossterm::execute;use ratatui::crossterm::terminal::{enable_raw_mode, EnterAlternateScreen};use std::io;
fn main() -> Result<(), Box<dyn Error>> { // setup terminal enable_raw_mode()?; let mut stderr = io::stderr(); // This is a special case. Normally using stdout is fine execute!(stderr, EnterAlternateScreen, EnableMouseCapture)?; // --snip--
出力に stderr
を使用していることにお気づきかもしれません。これは、ユーザーが完成した json を ratatui-tutorial > output.json
などの他のプログラムにパイプできるようにするためです。これを行うには、stderr
が stdout
とは異なる方法でパイプされるという事実を利用し、プロジェクトを stderr
にレンダリングし、完成した json を stdout
に出力します。
詳細については、 crossterm documentation をお読みください
状態の作成、およびループの開始
アプリケーションを実行するためのターミナルの準備ができたので、実際に実行してみましょう。
まず、プログラムのすべての状態を保持する App
のインスタンスを作成し、次に、イベントと描画ループを処理する関数を呼び出します。
// --snip-- let backend = CrosstermBackend::new(stderr); let mut terminal = Terminal::new(backend)?;
// create app and run it let mut app = App::new(); let res = run_app(&mut terminal, &mut app); // --snip--
アプリケーション動作後の手順
ratatui
アプリケーションは 実行前定型文 によってユーザーのターミナルの状態を変更したため、実行した操作を元に戻し、ターミナルを元の状態に戻す必要があります。
これらの機能のほとんどは、単に上記のことをしたことの逆になります。
use ratatui::crossterm::event::DisableMouseCapture;use ratatui::crossterm::terminal::{disable_raw_mode, LeaveAlternateScreen};
// --snip-- // restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; // --snip--
アプリケーションがこの終了ボイラープレートを実行せずに終了すると、ターミナルの動作が非常に異常になり、ユーザーは通常、ターミナル セッションを終了して新しいセッションを開始する必要があります。したがって、この最後の部分を呼び出すことができるようにエラーを処理することが重要です。
// --snip-- if let Ok(do_print) = res { if do_print { app.print_json()?; } } else if let Err(err) = res { println!("{err:?}"); }
Ok(())}
ボイラープレートの最後の if ステートメントは、run_app
関数にエラーが発生したか、または Ok
状態が返されたかどうかを確認します。Ok
状態が返された場合は、json を印刷するかどうかをチェックする必要があります。
execute!(LeaveAlternateScreen)
を呼び出す前に印刷関数を呼び出さない場合、印刷は古い画面にレンダリングされ、代替画面を終了すると失われます。(この仕組みの詳細については、Crossterm ドキュメント を参照してください)
つまり、完成した関数は次のようになります。
fn main() -> Result<(), Box<dyn Error>> { // setup terminal enable_raw_mode()?; let mut stderr = io::stderr(); // This is a special case. Normally using stdout is fine execute!(stderr, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stderr); let mut terminal = Terminal::new(backend)?;
// create app and run it let mut app = App::new(); let res = run_app(&mut terminal, &mut app);
// restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?;
if let Ok(do_print) = res { if do_print { app.print_json()?; } } else if let Err(err) = res { println!("{err:?}"); }
Ok(())}
run_app
この関数では、実際のロジックを実行し始めます。
メソッド署名
メソッドの署名から始めましょう:
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<bool> { // --snip--
この関数は ratatui::backend::Backend
全体で汎用的に作成されていることにお気づきでしょう。前のセクションでは、CrosstermBackend
をハードコードしました。このtrait アプローチにより、コードをバックエンドに依存しないようにすることができます。
このメソッドは、ratatui::backend::Backend
trait を実装する Terminal
型のオブジェクトを受け入れます。このtrait には、ratatui
に含まれる 3 つの (TestBackend
を含めると 4 つ) 公式にサポートされているバックエンドが含まれます。これにより、サードパーティのバックエンドを実装できます。
run_app
には、このプロジェクトで定義されているように、アプリケーション状態オブジェクトへの可変借用も必要です。
最後に、run_app
は、Err
状態の IO エラーがあったかどうかを示す io::Result<bool>
と、プログラムが完成した json を出力するかどうかを示す Ok(true)
または Ok(false)
を返します。
UIループ
ratatui
は独自のイベント/UIループを実装する必要があるため、次のコードを使用してメインループを更新するだけです。
// --snip-- loop { terminal.draw(|f| ui(f, app))?; // --snip--
draw
を迅速に呼び出してみましょう。
terminal
は引数として受け取るTerminal<Backend>
です。draw
はターミナルにFrame
を描画するratatui
コマンドです[^note]。|f| ui(f, &app)
はdraw
にf: <Frame>
を受け取って関数ui
に渡すように指示し、ui
はそのFrame
に描画します。
アプリケーションの状態の不変の借用も ui
関数に渡していることに注意してください。これは後で重要になります。
イベント処理
アプリを開始し、UIレンダリングをセットアップしたので、イベント処理を実装します。
ポーリング
crossterm
を使用しているため、キーボードイベントを単純に投票できます
if let Event::Key(key) = event::read()? { dbg!(key.code)}
そして、結果を一致させます。
あるいは、バックグラウンドで実行して Event
をポーリングして送信するスレッドを設定することもできますが、ここでは説明のために単純にしておきます。
イベントをポーリングするプロセスは、使用しているバックエンドによって異なるため、詳細についてはそのバックエンドのドキュメントを参照する必要があることに注意してください。
メイン画面
CurrentScreen::Main
のKeybindsとイベント処理から始めます。
// --snip-- if let Event::Key(key) = event::read()? { if key.kind == event::KeyEventKind::Release { // Skip events that are not KeyEventKind::Press continue; } match app.current_screen { CurrentScreen::Main => match key.code { KeyCode::Char('e') => { app.current_screen = CurrentScreen::Editing; app.currently_editing = Some(CurrentlyEditing::Key); } KeyCode::Char('q') => { app.current_screen = CurrentScreen::Exiting; } _ => {} }, // --snip--
Main
列挙型に一致させた後、イベントに一致させます。ユーザーがメイン画面にいる場合、キーバインドは 2 つだけであり、残りは無視されます。
この場合、KeyCode::Char('e')
は現在の画面を CurrentScreen::Editing
に変更し、CurrentlyEditing
を Some
に設定し、ユーザーが Value
フィールドではなく Key
値フィールドを編集する必要があることを通知します。
KeyCode::Char('q')
は単純で、アプリケーションを Exiting
画面に切り替え、UI と今後のイベント処理の実行で残りの処理を実行できるようにします。
終了
次に準備するハンドラーは、アプリケーションが CurrentScreen::Exiting
にある間にイベントを処理します。この画面の役割は、ユーザーが json を出力せずに終了するかどうかを尋ねることです。これは単に y/n
の質問なので、それだけをリッスンします。また、q
で代替終了キーを追加します。ユーザーが json を出力することを選択した場合は、Ok(true)
を返します。これは、ターミナルを通常の状態にリセットした後、main
関数が app.print_json()
を呼び出してシリアル化と印刷を実行する必要があることを示します。
// --snip-- CurrentScreen::Exiting => match key.code { KeyCode::Char('y') => { return Ok(true); } KeyCode::Char('n') | KeyCode::Char('q') => { return Ok(false); } _ => {} }, // --snip--
編集
最後のハンドラーは、内部変数の状態を変更するため、もう少し複雑になります。
Enter
キーには 2 つの目的を持たせます。ユーザーが Key
を編集しているときに、Enter キーで Value
の編集にフォーカスを切り替えます。ただし、Value
が現在編集中の場合、Enter
はキーと値のペアを保存し、Main
画面に戻ります。
// --snip-- CurrentScreen::Editing if key.kind == KeyEventKind::Press => { match key.code { KeyCode::Enter => { if let Some(editing) = &app.currently_editing { match editing { CurrentlyEditing::Key => { app.currently_editing = Some(CurrentlyEditing::Value); } CurrentlyEditing::Value => { app.save_key_value(); app.current_screen = CurrentScreen::Main; } } } } // --snip--
Backspace
が押されたら、最初にユーザーが Key
または Value
を編集しているかどうかを判断する必要があります。
// --snip-- KeyCode::Backspace => { if let Some(editing) = &app.currently_editing { match editing { CurrentlyEditing::Key => { app.key_input.pop(); } CurrentlyEditing::Value => { app.value_input.pop(); } } } } // --snip--
Escape
が押されたら、編集を終了します。
// --snip-- KeyCode::Esc => { app.current_screen = CurrentScreen::Main; app.currently_editing = None; } // --snip--
Tab
が押されたら、現在編集している選択を切り替えたいと思います。
// --snip-- KeyCode::Tab => { app.toggle_editing(); } // --snip--
そして最後に、ユーザーが有効な文字を入力した場合、それをキャプチャし、最終的なキーまたは値である文字列に追加します。
// --snip-- KeyCode::Char(value) => { if let Some(editing) = &app.currently_editing { match editing { CurrentlyEditing::Key => { app.key_input.push(value); } CurrentlyEditing::Value => { app.value_input.push(value); } } } } // --snip--
全体として、イベントループは次のようになります。
// --snip-- if let Event::Key(key) = event::read()? { if key.kind == event::KeyEventKind::Release { // Skip events that are not KeyEventKind::Press continue; } match app.current_screen { CurrentScreen::Main => match key.code { KeyCode::Char('e') => { app.current_screen = CurrentScreen::Editing; app.currently_editing = Some(CurrentlyEditing::Key); } KeyCode::Char('q') => { app.current_screen = CurrentScreen::Exiting; } _ => {} }, CurrentScreen::Exiting => match key.code { KeyCode::Char('y') => { return Ok(true); } KeyCode::Char('n') | KeyCode::Char('q') => { return Ok(false); } _ => {} }, CurrentScreen::Editing if key.kind == KeyEventKind::Press => { match key.code { KeyCode::Enter => { if let Some(editing) = &app.currently_editing { match editing { CurrentlyEditing::Key => { app.currently_editing = Some(CurrentlyEditing::Value); } CurrentlyEditing::Value => { app.save_key_value(); app.current_screen = CurrentScreen::Main; } } } } KeyCode::Backspace => { if let Some(editing) = &app.currently_editing { match editing { CurrentlyEditing::Key => { app.key_input.pop(); } CurrentlyEditing::Value => { app.value_input.pop(); } } } } KeyCode::Esc => { app.current_screen = CurrentScreen::Main; app.currently_editing = None; } KeyCode::Tab => { app.toggle_editing(); } KeyCode::Char(value) => { if let Some(editing) = &app.currently_editing { match editing { CurrentlyEditing::Key => { app.key_input.push(value); } CurrentlyEditing::Value => { app.value_input.push(value); } } } } _ => {} } } _ => {} } } // --snip--