Skip to content

Action.rs

Tui および EventHandler を作成したので、 Command パターンも紹介します。

これらは通常、 Action または Message とも呼ばれます。

pub enum Action {
Quit,
Tick,
Increment,
Decrement,
Noop,
}

EventHandler からのすべての Event がenumから Action にマッピングされるように、単純な impl App を定義しましょう。

#[derive(Default)]
struct App {
counter: i64,
should_quit: bool,
}
impl App {
pub fn new() -> Self {
Self::default()
}
pub async fn run(&mut self) -> Result<()> {
let t = Tui::new();
t.enter();
let mut events = EventHandler::new(tick_rate);
loop {
let event = events.next().await;
let action = self.handle_events(event);
self.update(action);
t.terminal.draw(|f| self.draw(f))?;
if self.should_quit {
break
}
};
t.exit();
Ok(())
}
fn handle_events(&mut self, event: Option<Event>) -> Action {
match event {
Some(Event::Quit) => Action::Quit,
Some(Event::AppTick) => Action::Tick,
Some(Event::Key(key_event)) => {
if let Some(key) = event {
match key.code {
KeyCode::Char('q') => Action::Quit,
KeyCode::Char('j') => Action::Increment,
KeyCode::Char('k') => Action::Decrement
_ => {}
}
}
},
Some(_) => Action::Noop,
None => Action::Noop,
}
}
fn update(&mut self, action: Action) {
match action {
Action::Quit => self.should_quit = true,
Action::Tick => self.tick(),
Action::Increment => self.increment(),
Action::Decrement => self.decrement(),
}
fn increment(&mut self) {
self.counter += 1;
}
fn decrement(&mut self) {
self.counter -= 1;
}
fn draw(&mut self, f: &mut Frame<'_>) {
f.render_widget(
Paragraph::new(format!(
"Press j or k to increment or decrement.
Counter: {}",
self.counter
))
)
}
}

handle_events(event) -> Action を使用して Event を取得し、それを Action にマッピングします。update(action) を使用して Action を取得し、アプリの状態を変更します。

このアプローチの利点の 1 つは、必要に応じて handle_key_events() を変更してキー構成を使用できるため、ユーザーがキーからアクションへの独自のマッピングを定義できることです。

この方法のもう 1 つの利点は、Tui または EventHandler のインスタンスを作成しなくても、App 構造体のビジネス ロジックをテストできることです。例:

mod tests {
#[test]
fn test_app() {
let mut app = App::new();
let old_counter = app.counter;
app.update(Action::Increment);
assert!(app.counter == old_counter + 1);
}
}

上記のテストでは、Tui または EventHandler のインスタンスを作成しておらず、run 関数も呼び出していませんが、アプリケーションのビジネス ロジックをテストすることはできます。 Action でアプリの状態を更新することで、アプリケーションを「状態マシン」にすることに一歩近づき、理解とテスト可能性が向上します。

純粋にしたい場合は、AppState を不変にして、次のような update 関数を使用します。

fn update(app_state::AppState, action::Action) -> new_app_state::State {
let mut state = app_state.clone();
state.counter += 1;
// ...
state
}

まれに、 update で将来のアクションを選択することもできます。

fn update(app_state::AppState, action::Action) -> (new_app_state::State, Option<action::Action>) {
let mut state = app_state.clone();
state.counter += 1;
// ...
(state, Action::Tick)
}

Rust でこのアーキテクチャに従うコードを書くには (私の意見では)、より事前の設計が必要です。 主な理由は、AppState 構造体を Clone 対応にする必要があるためです。 TUI の調査段階またはプロトタイプ段階であれば、そのようなことはしたくないでしょう。 設計を把握してから、このようにリファクタリングすることだけに興味があります。

これに対する私の回避策は (前に説明したように)、update&mut self を受け取るメソッドにすることです。

impl App {
fn update(&mut self, action: Action) -> Option<Action> {
self.counter += 1
None
}
}

フィット感のように、コードを自由に再編成できます!

必要に応じてさらにアクションを追加することもできます。たとえば、テンプレート内のすべてのアクションがあります。

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Display, Deserialize)]
pub enum Action {
Tick,
Render,
Resize(u16, u16),
Suspend,
Resume,
Quit,
Refresh,
Error(String),
Help,
ToggleShowHelp,
ScheduleIncrement,
ScheduleDecrement,
Increment(usize),
Decrement(usize),
CompleteInput(String),
EnterNormal,
EnterInsert,
EnterProcessing,
ExitProcessing,
Update,
}