Skip to content

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 コンポーネントに KeyEventAction のマップを格納する 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
},
},
}