Skip to content

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 などの他のプログラムにパイプできるようにするためです。これを行うには、stderrstdout とは異なる方法でパイプされるという事実を利用し、プロジェクトを 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)drawf: <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 に変更し、CurrentlyEditingSome に設定し、ユーザーが 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--