Przeglądaj źródła

normal mode (#28)

* normal mode

* 实现了功能的自定义(除了delete_some)

* 纠正default.yaml: 'G' -> 'shift-G'

* 删除单词,移动到下一单词

* 移动undo, redo等到buffer模块

移动到前一单词,下一单词末尾

* CMD_COUNTER作为Application的成员变量
Z YS 5 miesięcy temu
rodzic
commit
bea2f3edf8

+ 1 - 0
held_core/src/interface/render.rs

@@ -0,0 +1 @@
+

+ 5 - 0
src/application/handler/app.rs

@@ -21,3 +21,8 @@ pub fn to_workspace_mode(app: &mut Application) -> Result<()> {
     app.switch_mode(ModeKey::Workspace);
     Ok(())
 }
+
+pub fn to_delete_mode(app: &mut Application) -> Result<()> {
+    app.switch_mode(ModeKey::Delete);
+    Ok(())
+}

+ 21 - 0
src/application/handler/buffer.rs

@@ -44,3 +44,24 @@ pub fn insert_tab(app: &mut Application) -> Result<()> {
     }
     Ok(())
 }
+
+pub fn redo(app: &mut Application) -> Result<()> {
+    if let Some(ref mut buffer) = app.workspace.current_buffer {
+        buffer.redo();
+    }
+    Ok(())
+}
+
+pub fn save_file(app: &mut Application) -> Result<()> {
+    if let Some(ref mut buffer) = app.workspace.current_buffer {
+        buffer.save()?;
+    }
+    Ok(())
+}
+
+pub fn undo(app: &mut Application) -> Result<()> {
+    if let Some(ref mut buffer) = app.workspace.current_buffer {
+        buffer.undo();
+    }
+    Ok(())
+}

+ 7 - 0
src/application/handler/cursor.rs

@@ -39,3 +39,10 @@ pub fn move_to_start_of_line(app: &mut Application) -> Result<()> {
     }
     Ok(())
 }
+
+pub fn move_to_end_of_line(app: &mut Application) -> Result<()> {
+    if let Some(buffer) = &mut app.workspace.current_buffer {
+        buffer.cursor.move_to_end_of_line();
+    }
+    Ok(())
+}

+ 37 - 0
src/application/handler/delete.rs

@@ -0,0 +1,37 @@
+use held_core::utils::position::Position;
+use held_core::utils::range::Range;
+
+use crate::application::mode::motion::locate_next_words_begin;
+use crate::application::Application;
+use crate::errors::*;
+
+use super::normal::{self};
+
+pub fn delete_words(app: &mut Application) -> Result<()> {
+    let count = app.cmd_counter.max(1);
+    let buf = app.workspace.current_buffer.as_mut().unwrap();
+    let current_pos = &buf.cursor.position;
+    let search_range = if let Some(str) = buf.read_rest(&current_pos) {
+        str
+    } else {
+        return Ok(());
+    };
+    let next_words_pos = locate_next_words_begin(count, &search_range, current_pos);
+    let del_range = Range::new(*current_pos, next_words_pos);
+    buf.delete_range(del_range);
+    normal::reset(app)?;
+    Ok(())
+}
+
+pub fn delete_lines(app: &mut Application) -> Result<()> {
+    let count = app.cmd_counter;
+    let buf = app.workspace.current_buffer.as_mut().unwrap();
+    let start_pos = Position::new(buf.cursor.line, 0);
+    let end_pos = Position::new(
+        start_pos.line + count.min(buf.line_count() - start_pos.line).max(1),
+        0,
+    );
+    buf.delete_range(Range::new(start_pos, end_pos));
+    normal::reset(app)?;
+    Ok(())
+}

+ 2 - 0
src/application/handler/mod.rs

@@ -5,8 +5,10 @@ use crate::errors::*;
 mod app;
 mod buffer;
 mod cursor;
+mod delete;
 mod insert;
 mod monitor;
+mod normal;
 mod workspace;
 
 pub fn handle_map() -> HashMap<&'static str, fn(&mut Application) -> Result<()>> {

+ 16 - 0
src/application/handler/monitor.rs

@@ -14,3 +14,19 @@ pub fn scroll_to_center(app: &mut Application) -> Result<()> {
     }
     Ok(())
 }
+
+pub fn scroll_to_first_line(app: &mut Application) -> Result<()> {
+    if let Some(ref mut buffer) = app.workspace.current_buffer {
+        buffer.cursor.move_to_first_line();
+        app.monitor.scroll_to_cursor(buffer)?;
+    }
+    Ok(())
+}
+
+pub fn scroll_to_last_line(app: &mut Application) -> Result<()> {
+    if let Some(ref mut buffer) = app.workspace.current_buffer {
+        buffer.cursor.move_to_last_line();
+        app.monitor.scroll_to_cursor(buffer)?;
+    }
+    Ok(())
+}

+ 178 - 0
src/application/handler/normal.rs

@@ -0,0 +1,178 @@
+use crossterm::event::KeyCode;
+use held_core::utils::distance::Distance;
+use held_core::utils::position::Position;
+use held_core::utils::range::Range;
+use unicode_segmentation::UnicodeSegmentation;
+
+use crate::application::mode::motion;
+use crate::application::Application;
+use crate::errors::*;
+
+pub fn count_cmd(app: &mut Application) -> Result<()> {
+    if let Some(key) = app.monitor.last_key {
+        if let KeyCode::Char(ch) = key.code {
+            if let Some(digit) = ch.to_digit(10) {
+                let count = &mut app.cmd_counter;
+                *count *= 10;
+                *count += digit as usize;
+            }
+        }
+    }
+    Ok(())
+}
+
+// 可以指定执行次数的命令,数字必须先于命令字符;而命令字符可以在配置文件指定
+
+pub fn move_down_n(app: &mut Application) -> Result<()> {
+    let count = app.cmd_counter.max(1);
+    if let Some(buffer) = &mut app.workspace.current_buffer {
+        for _ in 0..count.min(buffer.line_count() - buffer.cursor.line) {
+            buffer.cursor.move_down();
+        }
+        app.monitor.scroll_to_cursor(buffer).unwrap();
+        reset(app)?;
+    }
+    Ok(())
+}
+
+pub fn move_up_n(app: &mut Application) -> Result<()> {
+    let count = app.cmd_counter.max(1);
+    if let Some(buffer) = &mut app.workspace.current_buffer {
+        for _ in 0..count.min(buffer.cursor.line) {
+            buffer.cursor.move_up();
+        }
+        app.monitor.scroll_to_cursor(buffer).unwrap();
+        reset(app)?;
+    }
+    Ok(())
+}
+
+pub fn move_to_target_line(app: &mut Application) -> Result<()> {
+    if let Some(buffer) = &mut app.workspace.current_buffer {
+        let count = app.cmd_counter;
+        if count > 0 {
+            let target_line = count.min(buffer.line_count());
+            let offset = buffer.cursor.offset;
+            if !buffer
+                .cursor
+                .move_to(Position::new(target_line - 1, offset))
+            {
+                let target_offset = buffer
+                    .data()
+                    .lines()
+                    .nth(target_line - 1)
+                    .unwrap()
+                    .graphemes(true)
+                    .count();
+                buffer
+                    .cursor
+                    .move_to(Position::new(target_line - 1, target_offset));
+            }
+        }
+        app.monitor.scroll_to_cursor(buffer).unwrap();
+        reset(app)?;
+    }
+    Ok(())
+}
+
+pub fn move_left_n(app: &mut Application) -> Result<()> {
+    let mut count = app.cmd_counter.max(1);
+    if let Some(buffer) = &mut app.workspace.current_buffer {
+        let offset = buffer.cursor.offset;
+        count = count.min(offset);
+        for _ in 0..count {
+            buffer.cursor.move_left();
+        }
+        app.monitor.scroll_to_cursor(buffer)?;
+        reset(app)?;
+    }
+    Ok(())
+}
+
+pub fn move_right_n(app: &mut Application) -> Result<()> {
+    let mut count = app.cmd_counter.max(1);
+    if let Some(buffer) = &mut app.workspace.current_buffer {
+        let max_offset = buffer
+            .data()
+            .lines()
+            .nth(buffer.cursor.line)
+            .unwrap()
+            .graphemes(true)
+            .count();
+        let offset = buffer.cursor.offset;
+        count = count.min(max_offset - offset);
+        for _ in 0..count {
+            buffer.cursor.move_right();
+        }
+        app.monitor.scroll_to_cursor(buffer)?;
+        reset(app)?;
+    }
+    Ok(())
+}
+
+pub fn reset(app: &mut Application) -> Result<()> {
+    app.cmd_counter = 0;
+    Ok(())
+}
+
+pub fn move_to_next_words(app: &mut Application) -> Result<()> {
+    if let Some(buffer) = &mut app.workspace.current_buffer {
+        let current_pos = buffer.cursor.position;
+        // 从当前位置向后搜索
+        let search_range = if let Some(str) = buffer.read_rest(&current_pos) {
+            str
+        } else {
+            return Ok(());
+        };
+        let count = app.cmd_counter;
+        let next_words_pos =
+            motion::locate_next_words_begin(count.max(1), &search_range, &current_pos);
+        buffer.cursor.move_to(next_words_pos);
+        app.monitor.scroll_to_cursor(buffer)?;
+        reset(app)?;
+    }
+    Ok(())
+}
+
+pub fn move_to_prev_words(app: &mut Application) -> Result<()> {
+    if let Some(buffer) = &mut app.workspace.current_buffer {
+        let current_pos = buffer.cursor.position
+            + Distance {
+                lines: 0,
+                offset: 1,
+            }; // 由于是左闭右开区间,所以需要向后移动一个字符
+               // 从当前位置向前搜索
+        let search_range =
+            if let Some(str) = buffer.read(&Range::new(Position::new(0, 0), current_pos)) {
+                str
+            } else {
+                return Ok(());
+            };
+        let count = app.cmd_counter;
+        let prev_words_pos =
+            motion::locate_previous_words(count.max(1), &search_range, &current_pos);
+        buffer.cursor.move_to(prev_words_pos);
+        app.monitor.scroll_to_cursor(buffer)?;
+        reset(app)?;
+    }
+    Ok(())
+}
+
+pub fn move_to_next_words_end(app: &mut Application) -> Result<()> {
+    if let Some(buffer) = &mut app.workspace.current_buffer {
+        let current_pos = buffer.cursor.position;
+        // 从当前位置向后搜索
+        let search_range = if let Some(str) = buffer.read_rest(&current_pos) {
+            str
+        } else {
+            return Ok(());
+        };
+        let count = app.cmd_counter;
+        let next_words_pos =
+            motion::locate_next_words_end(count.max(1), &search_range, &current_pos);
+        buffer.cursor.move_to(next_words_pos);
+        app.monitor.scroll_to_cursor(buffer)?;
+        reset(app)?;
+    }
+    Ok(())
+}

+ 0 - 14
src/application/handler/workspace.rs

@@ -2,20 +2,6 @@ use crate::application::mode::{ModeData, ModeKey};
 use crate::application::Application;
 use crate::errors::*;
 
-pub fn save_file(app: &mut Application) -> Result<()> {
-    if let Some(ref mut buffer) = app.workspace.current_buffer {
-        buffer.save()?;
-    }
-    Ok(())
-}
-
-pub fn undo(app: &mut Application) -> Result<()> {
-    if let Some(ref mut buffer) = app.workspace.current_buffer {
-        buffer.undo();
-    }
-    Ok(())
-}
-
 pub fn to_normal_mode(app: &mut Application) -> Result<()> {
     if let ModeData::Workspace(ref mode) = app.mode {
         app.workspace.select_buffer(mode.prev_buffer_id);

+ 4 - 0
src/application/mod.rs

@@ -45,6 +45,7 @@ pub struct Application {
     >,
     plugin_system: Rc<RefCell<PluginSystem>>,
     pub state_data: ApplicationStateData,
+    pub cmd_counter: usize,
 }
 
 impl Application {
@@ -83,6 +84,7 @@ impl Application {
             input_map,
             plugin_system,
             state_data: ApplicationStateData::default(),
+            cmd_counter: 0,
         })
     }
 
@@ -112,6 +114,7 @@ impl Application {
                 &mut self.monitor,
             )?),
         );
+        self.mode_history.insert(ModeKey::Delete, ModeData::Delete);
 
         Ok(())
     }
@@ -124,6 +127,7 @@ impl Application {
             self.listen_event()?;
 
             if let ModeKey::Exit = &self.mode_key {
+                disable_raw_mode()?;
                 return Ok(());
             }
         }

+ 39 - 0
src/application/mode/delete.rs

@@ -0,0 +1,39 @@
+use held_core::view::{colors::Colors, style::CharStyle};
+
+use super::ModeRenderer;
+use crate::{
+    errors::*,
+    view::status_data::{buffer_status_data, StatusLineData},
+};
+pub(super) struct DeleteRenderer;
+
+impl ModeRenderer for DeleteRenderer {
+    fn render(
+        workspace: &mut crate::workspace::Workspace,
+        monitor: &mut crate::view::monitor::Monitor,
+        _mode: &mut super::ModeData,
+    ) -> Result<()> {
+        let mut presenter = monitor.build_presenter()?;
+
+        if let Some(buffer) = &workspace.current_buffer {
+            warn!("Delete buffer id: {}", buffer.id.unwrap());
+            let data = buffer.data();
+            presenter.print_buffer(buffer, &data, &workspace.syntax_set, None, None)?;
+
+            let mode_name_data = StatusLineData {
+                content: " DELETE ".to_string(),
+                color: Colors::Inverted,
+                style: CharStyle::Bold,
+            };
+            presenter.print_status_line(&[
+                mode_name_data,
+                buffer_status_data(&workspace.current_buffer),
+            ])?;
+
+            presenter.present()?;
+        } else {
+        }
+
+        Ok(())
+    }
+}

+ 2 - 2
src/application/mode/error.rs

@@ -4,8 +4,8 @@ pub struct ErrorRenderer;
 
 impl ModeRenderer for ErrorRenderer {
     fn render(
-        workspace: &mut crate::workspace::Workspace,
-        monitor: &mut crate::view::monitor::Monitor,
+        _workspace: &mut crate::workspace::Workspace,
+        _monitor: &mut crate::view::monitor::Monitor,
         mode: &mut super::ModeData,
     ) -> Result<()> {
         if let ModeData::Error(e) = mode {

+ 9 - 2
src/application/mode/mod.rs

@@ -2,6 +2,7 @@ use std::collections::HashMap;
 
 use crate::errors::*;
 use crate::{view::monitor::Monitor, workspace::Workspace};
+use delete::DeleteRenderer;
 use error::ErrorRenderer;
 use error_chain::bail;
 use insert::InsertRenderer;
@@ -15,9 +16,12 @@ use yaml_rust::Yaml;
 use super::handler::handle_map;
 use super::Application;
 
+pub mod motion;
+
+pub mod delete;
 pub mod error;
 mod insert;
-mod normal;
+pub mod normal;
 pub mod workspace;
 
 pub enum ModeData {
@@ -26,7 +30,7 @@ pub enum ModeData {
     Exit,
     Insert,
     Workspace(WorkspaceModeData),
-    // Other(OtherData)
+    Delete, // Other(OtherData)
 }
 
 #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, EnumIter)]
@@ -36,6 +40,7 @@ pub enum ModeKey {
     Exit,
     Insert,
     Workspace,
+    Delete,
 }
 
 impl ModeKey {
@@ -44,6 +49,7 @@ impl ModeKey {
             ModeKey::Normal => Some("normal".into()),
             ModeKey::Insert => Some("insert".into()),
             ModeKey::Workspace => Some("workspace".into()),
+            ModeKey::Delete => Some("delete".into()),
             _ => None,
         }
     }
@@ -141,6 +147,7 @@ impl ModeRenderer for ModeRouter {
             ModeData::Insert => InsertRenderer::render(workspace, monitor, mode),
             ModeData::Workspace(_) => WorkspaceRender::render(workspace, monitor, mode),
             ModeData::Exit => todo!(),
+            ModeData::Delete => DeleteRenderer::render(workspace, monitor, mode),
         }
     }
 }

+ 158 - 0
src/application/mode/motion.rs

@@ -0,0 +1,158 @@
+use held_core::utils::position::Position;
+
+pub fn locate_next_words_begin(count: usize, str: &str, current_pos: &Position) -> Position {
+    let s = str.as_bytes();
+    let mut left = 0;
+    let mut right = left;
+    for _ in 0..count {
+        while left <= right && right < s.len() {
+            let lchar = s[left] as char;
+            let rchar = s[right] as char;
+            if lchar.is_alphanumeric() {
+                left += 1;
+                right += 1;
+                continue;
+            }
+            if rchar.is_alphanumeric() {
+                left = right;
+                break;
+            }
+            right += 1;
+        }
+    }
+    right = right.min(s.len() - 1);
+    // 向下移动的行数
+    let down_line = str[..right].matches('\n').count();
+
+    let new_offset = if let Some(idx) = str[..right].rfind('\n') {
+        // 即目标位置到行首的距离
+        right - idx - 1
+    } else {
+        // 新旧行之间没有换行符
+        current_pos.offset + right
+    };
+    let pos = Position::new(current_pos.line + down_line, new_offset);
+    return pos;
+}
+
+pub fn locate_previous_words(count: usize, str: &str, current_pos: &Position) -> Position {
+    let s = str.as_bytes();
+    let mut left = s.len() - 1;
+    let mut right = left;
+    for _ in 0..count {
+        while left <= right && left > 0 {
+            let lchar = s[left] as char;
+            let rchar = s[right] as char;
+            if !rchar.is_alphanumeric() {
+                left -= 1;
+                right -= 1;
+                continue;
+            }
+            if !lchar.is_alphanumeric() {
+                right = left;
+                break;
+            }
+            left -= 1;
+        }
+    }
+    let up_line = str[left..].matches('\n').count();
+    let new_line = current_pos.line - up_line;
+    let new_line_len = str.lines().nth(new_line).unwrap().len();
+    let new_offset = if let Some(back_offset) = str[left..].find('\n') {
+        // back_offset为目标位置到行尾的距离
+        // new_line_len - back_offset为目标位置到行首的距离
+        new_line_len - back_offset
+    } else {
+        // 新旧行之间没有换行符
+        current_pos.offset - (s.len() - left)
+    };
+    return Position::new(new_line, new_offset);
+}
+
+pub fn locate_next_words_end(count: usize, str: &str, current_pos: &Position) -> Position {
+    let s = str.as_bytes();
+    let mut left = 0;
+    let mut right = left;
+    let mut tmp_pos = right;
+    for _ in 0..count {
+        while left <= right && right < s.len() {
+            let lchar = s[left] as char;
+            let rchar = s[right] as char;
+            if !lchar.is_alphanumeric() {
+                left += 1;
+                right += 1;
+                continue;
+            }
+            if !rchar.is_alphanumeric() {
+                if right == tmp_pos + 1 {
+                    left = right;
+                    continue;
+                }
+                right -= 1;
+                left = right;
+                tmp_pos = right;
+                break;
+            }
+            right += 1;
+        }
+    }
+    right = right.min(s.len() - 1);
+    // 向下移动的行数
+    let down_line = str[..right].matches('\n').count();
+
+    let new_offset = if let Some(idx) = str[..right].rfind('\n') {
+        // 即目标位置到行首的距离
+        right - idx - 1
+    } else {
+        // 新旧行之间没有换行符
+        current_pos.offset + right
+    };
+    let pos = Position::new(current_pos.line + down_line, new_offset);
+    return pos;
+}
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn next_word_test() {
+        let mut v = Vec::new();
+        v.push("pub fn locate_next_words(count: usize, str\n: &str) -> Position {");
+        v.push(stringify!(let new_offset = if let Some(idx) = str[..mt
+            ch.start()].rfind('\n')));
+        v.push(";\nmod utils;\nmod view;\nmod workspace;\n");
+
+        assert_eq!(next_word_search(1, v[0], 0), 4);
+        assert_eq!(next_word_search(1, v[0], 4), 7);
+
+        assert_eq!(next_word_search(1, v[1], 0), 4);
+        assert_eq!(next_word_search(1, v[1], 3), 4);
+
+        assert_eq!(next_word_search(1, v[2], 0), 2);
+        assert_eq!(next_word_search(2, v[2], 0), 6);
+    }
+
+    fn next_word_search(count: usize, str: &str, at: usize) -> usize {
+        let s = str.as_bytes();
+        let mut left = at;
+        let mut right = left;
+        for _ in 0..count {
+            while left <= right && right < s.len() {
+                let lchar = s[left] as char;
+                let rchar = s[right] as char;
+                if rchar.is_ascii_punctuation() && right != at {
+                    break;
+                }
+                if lchar.is_alphanumeric() {
+                    left += 1;
+                    right += 1;
+                    continue;
+                }
+                if rchar.is_alphanumeric() {
+                    left = right;
+                    break;
+                }
+                right += 1;
+            }
+        }
+        return right;
+    }
+}

+ 19 - 0
src/buffer/gap_buffer.rs

@@ -86,6 +86,25 @@ impl GapBuffer {
         Some(data)
     }
 
+    pub fn read_rest(&self, position: &Position) -> Option<String> {
+        let offset = match self.find_offset(position) {
+            Some(offset) => offset,
+            None => return None,
+        };
+
+        let data = if offset < self.gap_start {
+            let mut data = String::from_utf8_lossy(&self.data[offset..self.gap_start]).into_owned();
+            data.push_str(
+                String::from_utf8_lossy(&self.data[self.gap_start + self.gap_length..]).borrow(),
+            );
+            data
+        } else {
+            String::from_utf8_lossy(&self.data[offset..]).into_owned()
+        };
+
+        Some(data)
+    }
+
     // | data | gap |   data    |
     pub fn delete(&mut self, range: &Range) {
         let start_offset = match self.find_offset(&range.start()) {

+ 4 - 0
src/buffer/mod.rs

@@ -136,6 +136,10 @@ impl Buffer {
         self.data.borrow().read(range)
     }
 
+    pub fn read_rest(&self, position: &Position) -> Option<String> {
+        self.data.borrow().read_rest(position)
+    }
+
     pub fn search(&self, needle: &str) -> Vec<Position> {
         let mut results = Vec::new();
 

+ 46 - 3
src/modules/input/default.yaml

@@ -5,8 +5,41 @@ normal:
   down: cursor::move_down
   ctrl-c: app::exit
   i: app::to_insert_mode
+  a: 
+    - app::to_insert_mode
+    - cursor::move_right
+  shift-A: 
+    - app::to_insert_mode
+    - cursor::move_to_end_of_line
+  shift-I: 
+    - app::to_insert_mode
+    - cursor::move_to_start_of_line
   backspace: cursor::move_left
+  escape: normal::reset
+  shift-L: cursor::move_to_end_of_line
+  shift-H: cursor::move_to_start_of_line
+  shift-T: monitor::scroll_to_first_line
+  shift-B: monitor::scroll_to_last_line
+  shift-G: normal::move_to_target_line
+  shift-O: 
+    - cursor::move_to_start_of_line
+    - buffer::new_line
+  o: 
+    - cursor::move_to_end_of_line
+    - buffer::new_line
+    - cursor::move_down
+  j: normal::move_down_n
+  k: normal::move_up_n
+  h: normal::move_left_n
+  l: normal::move_right_n
+  d: app::to_delete_mode
   w: app::to_workspace_mode
+  n: normal::move_to_next_words
+  b: normal::move_to_prev_words
+  e: normal::move_to_next_words_end
+  u: buffer::undo
+  ctrl-r: buffer::redo
+  num: normal::count_cmd
 insert:
   escape: app::to_normal_mode
   left: cursor::move_left
@@ -14,8 +47,8 @@ insert:
   up: cursor::move_up
   down: cursor::move_down
   ctrl-c: app::exit
-  ctrl-s: workspace::save_file
-  ctrl-z: workspace::undo
+  ctrl-s: buffer::save_file
+  ctrl-z: buffer::undo
   enter: 
     - buffer::new_line
     - cursor::move_down
@@ -29,4 +62,14 @@ workspace:
   down: workspace::move_down
   enter: workspace::enter
   escape: workspace::to_normal_mode
-  ctrl-c: app::exit
+  ctrl-c: app::exit
+delete:
+  ctrl-c: app::exit
+  escape: app::to_normal_mode
+  w: 
+    - delete::delete_words
+    - app::to_normal_mode
+  d: 
+    - delete::delete_lines
+    - cursor::move_to_start_of_line
+    - app::to_normal_mode

+ 7 - 1
src/modules/input/mod.rs

@@ -128,7 +128,13 @@ impl InputMapper {
                 crossterm::event::KeyCode::Delete => "delete".into(),
                 crossterm::event::KeyCode::Insert => "insert".into(),
                 crossterm::event::KeyCode::F(f) => format!("f{f}"),
-                crossterm::event::KeyCode::Char(c) => c.into(),
+                crossterm::event::KeyCode::Char(c) => {
+                    if c.is_digit(10) {
+                        "num".to_string()
+                    } else {
+                        c.into()
+                    }
+                }
                 crossterm::event::KeyCode::Null => "".into(),
                 crossterm::event::KeyCode::Esc => "escape".into(),
                 crossterm::event::KeyCode::CapsLock => "caps_lock".into(),