Config.rs
現時点では、私たちのキーはアプリにハードコード化されています。
impl Component for Home {
fn handle_key_events(&mut self, key: KeyEvent) -> Action { match self.mode { Mode::Normal | Mode::Processing => { match key.code { KeyCode::Char('q') => Action::Quit, KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::Quit, KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::Quit, KeyCode::Char('z') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::Suspend, KeyCode::Char('?') => Action::ToggleShowHelp, KeyCode::Char('j') => Action::ScheduleIncrement, KeyCode::Char('k') => Action::ScheduleDecrement, KeyCode::Char('/') => Action::EnterInsert, _ => Action::Tick, } }, Mode::Insert => { match key.code { KeyCode::Esc => Action::EnterNormal, KeyCode::Enter => Action::EnterNormal, _ => { self.input.handle_event(&crossterm::event::Event::Key(key)); Action::Update }, } }, } }
ユーザーが Up
および Down
矢印キーを押したい場合は、 ScheduleIncrement
および ScheduleDecrement
のキーを行う場合、ソースコードを変更してアプリを再コンパイルする唯一の方法です。ユーザーがキーをマップしてアクションをマップする構成ファイルを設定する方法を提供することをお勧めします。
たとえば、以下のように、 config.toml
ファイルで keyevents-to-actions マッピングをセットアップできるようにしたいと仮定します。
[keymap]"q" = "Quit""j" = "ScheduleIncrement""k" = "ScheduleDecrement""l" = "ToggleShowHelp""/" = "EnterInsert""ESC" = "EnterNormal""Enter" = "EnterNormal""Ctrl-d" = "Quit""Ctrl-c" = "Quit""Ctrl-z" = "Suspend"
the excellent config
crate を使用して Config
構造体を設定できます。
use std::collections::HashMap;
use color_eyre::eyre::Result;use ratatui::crossterm::event::KeyEvent;use serde_derive::Deserialize;
use crate::action::Action;
#[derive(Clone, Debug, Default, Deserialize)]pub struct Config { #[serde(default)] pub keymap: KeyMap,}
#[derive(Clone, Debug, Default, Deserialize)]pub struct KeyMap(pub HashMap<KeyEvent, Action>);
impl Config { pub fn new() -> Result<Self, config::ConfigError> { let mut builder = config::Config::builder(); builder = builder .add_source(config::File::from(config_dir.join("config.toml")).format(config::FileFormat::Toml).required(false)); builder.build()?.try_deserialize() }}
serde
を使用して、TOMLファイルからデシリアライズしています。
これで、デフォルトの KeyEvent
シリアル化形式はあまりユーザーフレンドリーではないので、独自のバージョンを実装しましょう。
#[derive(Clone, Debug, Default)]pub struct KeyMap(pub HashMap<KeyEvent, Action>);
impl<'de> Deserialize<'de> for KeyMap { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { struct KeyMapVisitor; impl<'de> Visitor<'de> for KeyMapVisitor { type Value = KeyMap; fn visit_map<M>(self, mut access: M) -> Result<KeyMap, M::Error> where M: MapAccess<'de>, { let mut keymap = HashMap::new(); while let Some((key_str, action)) = access.next_entry::<String, Action>()? { let key_event = parse_key_event(&key_str).map_err(de::Error::custom)?; keymap.insert(key_event, action); } Ok(KeyMap(keymap)) } } deserializer.deserialize_map(KeyMapVisitor) }}
あとは parse_key_event
関数を実装するだけです。 この実装の例についてはソースコードを確認してください。 。
実装が完了したら、Home
コンポーネントに KeyEvent
と Action
のマップを格納する HashMap
を追加できます。
#[derive(Default)]pub struct Home { ... pub keymap: HashMap<KeyEvent, Action>,}
これで、 Config
のインスタンスを作成し、keymapを Home
に渡す必要があります。
impl App { pub fn new(tick_rate: (u64, u64)) -> Result<Self> { let h = Home::new(); let config = Config::new()?; let h = h.keymap(config.keymap.0.clone()); let home = Arc::new(Mutex::new(h)); Ok(Self { tick_rate, home, should_quit: false, should_suspend: false, config }) }}
handle_key_events
では、 HashMap
から直接実行する必要がある Action
を取得します。
impl Component for Home { fn handle_key_events(&mut self, key: KeyEvent) -> Action { match self.mode { Mode::Normal | Mode::Processing => { if let Some(action) = self.keymap.get(&key) { *action } else { Action::Tick } }, Mode::Insert => { match key.code { KeyCode::Esc => Action::EnterNormal, KeyCode::Enter => Action::EnterNormal, _ => { self.input.handle_event(&crossterm::event::Event::Key(key)); Action::Update }, } }, } }}
テンプレートでは、Action
にマップされた Vec<KeyEvent>
を処理するように設定されています。これにより、たとえば次のようにマップできます。
<g><j>
からAction::GotoBottom
<g><k>
からAction::GotoTop
また、入力として複数のキーを使用しているため、それに応じて app.rs
メイン ループを更新して処理する必要があります。
// -- snip -- loop { if let Some(e) = tui.next().await { match e { // -- snip -- tui::Event::Key(key) => { if let Some(keymap) = self.config.keybindings.get(&self.mode) { // If the key is a single key action if let Some(action) = keymap.get(&vec![key.clone()]) { log::info!("Got action: {action:?}"); action_tx.send(action.clone())?; } else { // If the key was not handled as a single key action, // then consider it for multi-key combinations. self.last_tick_key_events.push(key);
// Check for multi-key combinations if let Some(action) = keymap.get(&self.last_tick_key_events) { log::info!("Got action: {action:?}"); action_tx.send(action.clone())?; } } }; }, _ => {}, } // -- snip -- } while let Ok(action) = action_rx.try_recv() { // -- snip -- for component in self.components.iter_mut() { if let Some(action) = component.update(action.clone())? { action_tx.send(action)? }; } } // -- snip -- } // -- snip --
これは、カウンターアプリケーションに使用するJSON構成です。
{ "keybindings": { "Home": { "<q>": "Quit", // Quit the application "<j>": "ScheduleIncrement", "<k>": "ScheduleDecrement", "<l>": "ToggleShowHelp", "</>": "EnterInsert", "<Ctrl-d>": "Quit", // Another way to quit "<Ctrl-c>": "Quit", // Yet another way to quit "<Ctrl-z>": "Suspend", // Suspend the application }, },}