Basic Counter App
このページのコードの完全なコピーは、WebサイトのGitHubリポジトリで入手できます。
https://github.com/ratatui/ratatui-website/tree/main/code/tutorials/counter-app-basic。
新しいプロジェクトを作成する
新しいRustプロジェクトを作成し、エディタで開きます
cargo new ratatui-counter-appcd ratatui-counter-app$EDITOR .
Ratatui および Crossterm クレートを追加します (Crossterm を使用する理由の詳細については、[バックエンド] を参照してください)。
cargo add ratatui crossterm
Cargo.toml の依存関係セクションには次の内容が含まれるようになります。
[dependencies]ratatui = "0.29.0"crossterm = "0.28.1"
アプリケーションのセットアップ
主な Import
main.rs
で、Ratatui と crossterm に必要なインポートを追加します。これらは、このチュートリアルの後半で使用します。チュートリアルでは、コードを簡素化するためにワイルドカード インポートを使用するのが一般的ですが、明示的なインポートの方が好みのスタイルであれば、使用してもかまいません。
use std::io;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};use ratatui::{ buffer::Buffer, layout::Rect, style::Stylize, symbols::border, text::{Line, Text}, widgets::{Block, Paragraph, Widget}, DefaultTerminal, Frame,};
メイン関数
ほとんどのRatatuiアプリで見られる一般的なパターンは、次のことです。
- ターミナルを初期化する
- ユーザーがアプリを終了するまでアプリケーションをループで実行する
- ターミナルを元の状態に戻す
main
関数は、ratatui::init
メソッドと ratatui::restore
メソッドを呼び出してターミナルを設定し、その後、App (後で定義) を作成して実行します。ターミナルが復元されるまで、App::run()
の結果の戻り値の伝播を延期して、アプリケーションの終了後に Error
結果がユーザーに表示されるようにします。
メイン関数に記入してください:
fn main() -> io::Result<()> { let mut terminal = ratatui::init(); let app_result = App::default().run(&mut terminal); ratatui::restore(); app_result}
アプリケーション状態
カウンターアプリは、アプリケーションが終了することを示すために、いくつかの状態、カウンター、フラグを保存する必要があります。カウンターは8ビットの符号のないINTになり、出口フラグは簡単なブールになります。複数のメイン状態またはモードを持つアプリケーションは、代わりにこのフラグを表す列挙を使用する場合があります。
App
structを作成して、アプリケーションの状態を表します。
#[derive(Debug, Default)]pub struct App { counter: u8, exit: bool,}
App::default()
を呼び出すと、 counter
が0に設定された App
および exit
が false
に設定された App
が作成されます。
アプリケーションメインループ
ほとんどのアプリには、ユーザーが終了を選択するまで実行されるメイン ループがあります。ループの各反復では、Terminal::draw()
を呼び出して 1 つのフレームを描画し、アプリの状態を更新します。
アプリケーションのメイン ループとして機能する新しい run メソッドを使用して、App
の impl
ブロックを作成します。
impl App {
/// runs the application's main loop until the user quits pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { while !self.exit { terminal.draw(|frame| self.draw(frame))?; self.handle_events()?; } Ok(()) }
fn draw(&self, frame: &mut Frame) { todo!() }
fn handle_events(&mut self) -> io::Result<()> { todo!() }}
アプリケーションの表示
フレームをレンダリングする
UI をレンダリングするために、アプリケーションは Frame
を受け入れるクロージャを使用して Terminal::draw()
を呼び出します。
Frame
の最も重要なメソッドは render_widget()
です。これは、Paragraph
、List
などの Widget
トレイト を実装する任意の型をレンダリングします。レンダリングに関連するコードが 1 か所に整理されるように、App
構造体に Widget
trait を実装します。
これにより、Terminal::draw
に渡されるクロージャ内のアプリを使用して Frame::render_widget()
を呼び出すことができます。
まず、新しい impl Widget for &App
ブロックを追加します。レンダリング関数は状態を変更せず、呼び出し後にアプリを使用できるようにするために、これを App 型への参照に実装します。レンダリング関数は、タイトル、下部の説明テキスト、およびいくつかの境界線を含むブロックを作成します。ブロック内にアプリケーションの状態 (App
のカウンター フィールドの値) を含む Paragraph
ウィジェットをレンダリングします。ブロックと段落はウィジェットのサイズ全体を占めます。
impl Widget for &App { fn render(self, area: Rect, buf: &mut Buffer) { let title = Line::from(" Counter App Tutorial ".bold()); let instructions = Line::from(vec![ " Decrement ".into(), "<Left>".blue().bold(), " Increment ".into(), "<Right>".blue().bold(), " Quit ".into(), "<Q> ".blue().bold(), ]); let block = Block::bordered() .title(title.centered()) .title_bottom(instructions.centered()) .border_set(border::THICK);
let counter_text = Text::from(vec![Line::from(vec![ "Value: ".into(), self.counter.to_string().yellow(), ])]);
Paragraph::new(counter_text) .centered() .block(block) .render(area, buf); }}
次に、アプリをウィジェットとしてレンダリングします。
impl App { fn draw(&self, frame: &mut Frame) { frame.render_widget(self, frame.area()); }}
UI出力のテスト
render
が呼び出されたときに、ratatuiがウィジェットをどのように表示するかをテストするために、テストでバッファーにアプリをレンダリングできます。
次の tests
モジュールを main.rs
に追加します:
#[cfg(test)]mod tests { use super::*; use ratatui::style::Style;
#[test] fn render() { let app = App::default(); let mut buf = Buffer::empty(Rect::new(0, 0, 50, 4));
app.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![ "┏━━━━━━━━━━━━━ Counter App Tutorial ━━━━━━━━━━━━━┓", "┃ Value: 0 ┃", "┃ ┃", "┗━ Decrement <Left> Increment <Right> Quit <Q> ━━┛", ]); let title_style = Style::new().bold(); let counter_style = Style::new().yellow(); let key_style = Style::new().blue().bold(); expected.set_style(Rect::new(14, 0, 22, 1), title_style); expected.set_style(Rect::new(28, 1, 1, 1), counter_style); expected.set_style(Rect::new(13, 3, 6, 1), key_style); expected.set_style(Rect::new(30, 3, 7, 1), key_style); expected.set_style(Rect::new(43, 3, 4, 1), key_style);
assert_eq!(buf, expected); }}
このテストを実行するには、ターミナルで以下を実行します。
cargo test
次のように表示されます。
running 1 testtest tests::render ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
インタラクティブ性
アプリケーションは、標準入力を介してユーザーから来るイベントを受け入れる必要があります。このアプリケーションが心配する必要がある唯一のイベントは、重要なイベントです。他の利用可能なイベントの詳細については、Crossterm Events Module docsを参照してください。これらには、窓のサイズとフォーカス、貼り付け、マウスのイベントが含まれます。
より高度なアプリケーションでは、イベントはシステム、ネットワーク上、またはアプリケーションの他の部分からもたらされる可能性があります。
イベントを処理する
先ほど定義した handle_events
メソッドは、アプリが crossterm から提供されるイベントを待機して処理する場所です。
前に定義した handle_events
メソッドを更新します。
impl App {
// -- snip --
/// updates the application's state based on user input fn handle_events(&mut self) -> io::Result<()> { match event::read()? { // it's important to check that the event is a key press event as // crossterm also emits key release and repeat events on Windows. Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { self.handle_key_event(key_event) } _ => {} }; Ok(()) }}
キーボードイベントを処理する
カウンターアプリは、押されたキーに基づいて App
structのフィールドの状態を更新します。キーボードイベントには、このアプリに関心のある2つのフィールドがあります。
kind
: これがKeyEventKind::Press
と等しいことを確認することが重要です。そうでないと、アプリケーションで重複したイベント (キーダウン、キーリピート、キーアップ) が発生する可能性があります。code
: 押された特定のキーを表すKeyCode
。
キーイベントを処理するには、 handle_key_event
メソッドを App
に追加します。
impl App {
// -- snip --
fn handle_key_event(&mut self, key_event: KeyEvent) { match key_event.code { KeyCode::Char('q') => self.exit(), KeyCode::Left => self.decrement_counter(), KeyCode::Right => self.increment_counter(), _ => {} } }}
次に、アプリケーションの状態の更新を処理するためのメソッドをいくつか追加します。通常、これらのメソッドは、match ステートメントだけでなくアプリ上で定義することをお勧めします。これにより、イベントとは別にアプリケーションの動作を単体テストする簡単な方法が提供されます。
impl App {
// -- snip --
fn exit(&mut self) { self.exit = true; }
fn increment_counter(&mut self) { self.counter += 1; }
fn decrement_counter(&mut self) { self.counter -= 1; }}
キーボードイベントのテスト
このようにキーボード イベント処理を別の関数に分割すると、ターミナルをエミュレートしなくてもアプリケーションを簡単にテストできます。キーボード イベントを渡すテストを記述し、アプリケーションへの影響をテストできます。
tests
モジュールに handle_key_event
のテストを追加します。
#[cfg(test)]mod tests {
// -- snip --
#[test] fn handle_key_event() -> io::Result<()> { let mut app = App::default(); app.handle_key_event(KeyCode::Right.into()); assert_eq!(app.counter, 1);
app.handle_key_event(KeyCode::Left.into()); assert_eq!(app.counter, 0);
let mut app = App::default(); app.handle_key_event(KeyCode::Char('q').into()); assert!(app.exit);
Ok(()) }}
テストを実行します。
cargo test
次のように表示されます。
running 2 teststest tests::handle_key_event ... oktest tests::render ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
完成したアプリ
これを完全に言えば、これで次のファイルが必要です。
main.rs (click to expand)
use std::io;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};use ratatui::{ buffer::Buffer, layout::Rect, style::Stylize, symbols::border, text::{Line, Text}, widgets::{Block, Paragraph, Widget}, DefaultTerminal, Frame,};
fn main() -> io::Result<()> { let mut terminal = ratatui::init(); let app_result = App::default().run(&mut terminal); ratatui::restore(); app_result}
#[derive(Debug, Default)]pub struct App { counter: u8, exit: bool,}
impl App { /// runs the application's main loop until the user quits pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { while !self.exit { terminal.draw(|frame| self.draw(frame))?; self.handle_events()?; } Ok(()) }
fn draw(&self, frame: &mut Frame) { frame.render_widget(self, frame.area()); }
/// updates the application's state based on user input fn handle_events(&mut self) -> io::Result<()> { match event::read()? { // it's important to check that the event is a key press event as // crossterm also emits key release and repeat events on Windows. Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { self.handle_key_event(key_event) } _ => {} }; Ok(()) }
fn handle_key_event(&mut self, key_event: KeyEvent) { match key_event.code { KeyCode::Char('q') => self.exit(), KeyCode::Left => self.decrement_counter(), KeyCode::Right => self.increment_counter(), _ => {} } }
fn exit(&mut self) { self.exit = true; }
fn increment_counter(&mut self) { self.counter += 1; }
fn decrement_counter(&mut self) { self.counter -= 1; }}
impl Widget for &App { fn render(self, area: Rect, buf: &mut Buffer) { let title = Line::from(" Counter App Tutorial ".bold()); let instructions = Line::from(vec![ " Decrement ".into(), "<Left>".blue().bold(), " Increment ".into(), "<Right>".blue().bold(), " Quit ".into(), "<Q> ".blue().bold(), ]); let block = Block::bordered() .title(title.centered()) .title_bottom(instructions.centered()) .border_set(border::THICK);
let counter_text = Text::from(vec![Line::from(vec![ "Value: ".into(), self.counter.to_string().yellow(), ])]);
Paragraph::new(counter_text) .centered() .block(block) .render(area, buf); }}
#[cfg(test)]mod tests {
use super::*; use ratatui::style::Style;
#[test] fn render() { let app = App::default(); let mut buf = Buffer::empty(Rect::new(0, 0, 50, 4));
app.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![ "┏━━━━━━━━━━━━━ Counter App Tutorial ━━━━━━━━━━━━━┓", "┃ Value: 0 ┃", "┃ ┃", "┗━ Decrement <Left> Increment <Right> Quit <Q> ━━┛", ]); let title_style = Style::new().bold(); let counter_style = Style::new().yellow(); let key_style = Style::new().blue().bold(); expected.set_style(Rect::new(14, 0, 22, 1), title_style); expected.set_style(Rect::new(28, 1, 1, 1), counter_style); expected.set_style(Rect::new(13, 3, 6, 1), key_style); expected.set_style(Rect::new(30, 3, 7, 1), key_style); expected.set_style(Rect::new(43, 3, 4, 1), key_style);
assert_eq!(buf, expected); }
#[test] fn handle_key_event() -> io::Result<()> { let mut app = App::default(); app.handle_key_event(KeyCode::Right.into()); assert_eq!(app.counter, 1);
app.handle_key_event(KeyCode::Left.into()); assert_eq!(app.counter, 0);
let mut app = App::default(); app.handle_key_event(KeyCode::Char('q').into()); assert!(app.exit);
Ok(()) }}
アプリを実行する
すべてのファイルを保存し、上記の[インポート]がまだファイルの上部にあることを確認してください (一部のエディタは未使用のインポートを自動的に削除します) 。
ここでアプリを実行します:
cargo run
次のUIが表示されます。
左 と 右 矢印キーを押して、カウンターと対話します。q を押して終了します。
カウンターが0の場合、 左 を押すとどうなりますか?
Mac / Linuxコンソールでは、 reset
を実行してコンソールを修正できます。Windowsコンソールでは、問題をクリアするためにコンソールを再起動する必要がある場合があります。[エラー処理]に関するこのチュートリアルの次のセクションでこれを適切に処理します。
結論
このシンプルなカウンター アプリケーションで使用されている構造とコンポーネントを理解することで、ratatui
を使用してより複雑なターミナル ベースのインターフェイスを作成する準備が整います。