mui 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +163 -0
  3. data/CHANGELOG.md +448 -0
  4. data/README.md +309 -6
  5. data/docs/_config.yml +56 -0
  6. data/docs/configuration.md +301 -0
  7. data/docs/getting-started.md +140 -0
  8. data/docs/index.md +55 -0
  9. data/docs/jobs.md +297 -0
  10. data/docs/keybindings.md +229 -0
  11. data/docs/plugins.md +285 -0
  12. data/docs/syntax-highlighting.md +149 -0
  13. data/exe/mui +1 -2
  14. data/lib/mui/autocmd.rb +66 -0
  15. data/lib/mui/buffer.rb +275 -0
  16. data/lib/mui/buffer_word_cache.rb +131 -0
  17. data/lib/mui/buffer_word_completer.rb +77 -0
  18. data/lib/mui/color_manager.rb +136 -0
  19. data/lib/mui/color_scheme.rb +63 -0
  20. data/lib/mui/command_completer.rb +30 -0
  21. data/lib/mui/command_context.rb +90 -0
  22. data/lib/mui/command_history.rb +89 -0
  23. data/lib/mui/command_line.rb +167 -0
  24. data/lib/mui/command_registry.rb +44 -0
  25. data/lib/mui/completion_renderer.rb +84 -0
  26. data/lib/mui/completion_state.rb +58 -0
  27. data/lib/mui/config.rb +58 -0
  28. data/lib/mui/editor.rb +395 -0
  29. data/lib/mui/error.rb +29 -0
  30. data/lib/mui/file_completer.rb +51 -0
  31. data/lib/mui/floating_window.rb +161 -0
  32. data/lib/mui/handler_result.rb +107 -0
  33. data/lib/mui/highlight.rb +22 -0
  34. data/lib/mui/highlighters/base.rb +23 -0
  35. data/lib/mui/highlighters/search_highlighter.rb +27 -0
  36. data/lib/mui/highlighters/selection_highlighter.rb +48 -0
  37. data/lib/mui/highlighters/syntax_highlighter.rb +107 -0
  38. data/lib/mui/input.rb +17 -0
  39. data/lib/mui/insert_completion_renderer.rb +92 -0
  40. data/lib/mui/insert_completion_state.rb +77 -0
  41. data/lib/mui/job.rb +81 -0
  42. data/lib/mui/job_manager.rb +113 -0
  43. data/lib/mui/key_code.rb +30 -0
  44. data/lib/mui/key_handler/base.rb +187 -0
  45. data/lib/mui/key_handler/command_mode.rb +511 -0
  46. data/lib/mui/key_handler/insert_mode.rb +323 -0
  47. data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
  48. data/lib/mui/key_handler/normal_mode.rb +552 -0
  49. data/lib/mui/key_handler/operators/base_operator.rb +134 -0
  50. data/lib/mui/key_handler/operators/change_operator.rb +179 -0
  51. data/lib/mui/key_handler/operators/delete_operator.rb +176 -0
  52. data/lib/mui/key_handler/operators/paste_operator.rb +119 -0
  53. data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
  54. data/lib/mui/key_handler/search_mode.rb +191 -0
  55. data/lib/mui/key_handler/visual_line_mode.rb +20 -0
  56. data/lib/mui/key_handler/visual_mode.rb +402 -0
  57. data/lib/mui/key_handler/window_command.rb +112 -0
  58. data/lib/mui/key_handler.rb +16 -0
  59. data/lib/mui/key_notation_parser.rb +152 -0
  60. data/lib/mui/key_sequence.rb +67 -0
  61. data/lib/mui/key_sequence_buffer.rb +85 -0
  62. data/lib/mui/key_sequence_handler.rb +163 -0
  63. data/lib/mui/key_sequence_matcher.rb +79 -0
  64. data/lib/mui/layout/calculator.rb +15 -0
  65. data/lib/mui/layout/leaf_node.rb +33 -0
  66. data/lib/mui/layout/node.rb +29 -0
  67. data/lib/mui/layout/split_node.rb +132 -0
  68. data/lib/mui/line_renderer.rb +173 -0
  69. data/lib/mui/mode.rb +13 -0
  70. data/lib/mui/mode_manager.rb +186 -0
  71. data/lib/mui/motion.rb +139 -0
  72. data/lib/mui/plugin.rb +35 -0
  73. data/lib/mui/plugin_manager.rb +106 -0
  74. data/lib/mui/register.rb +110 -0
  75. data/lib/mui/screen.rb +103 -0
  76. data/lib/mui/search_completer.rb +50 -0
  77. data/lib/mui/search_input.rb +40 -0
  78. data/lib/mui/search_state.rb +121 -0
  79. data/lib/mui/selection.rb +55 -0
  80. data/lib/mui/status_line_renderer.rb +40 -0
  81. data/lib/mui/syntax/language_detector.rb +106 -0
  82. data/lib/mui/syntax/lexer_base.rb +106 -0
  83. data/lib/mui/syntax/lexers/c_lexer.rb +127 -0
  84. data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
  85. data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
  86. data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
  87. data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
  88. data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
  89. data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
  90. data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
  91. data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
  92. data/lib/mui/syntax/token.rb +42 -0
  93. data/lib/mui/syntax/token_cache.rb +91 -0
  94. data/lib/mui/tab_bar_renderer.rb +87 -0
  95. data/lib/mui/tab_manager.rb +96 -0
  96. data/lib/mui/tab_page.rb +35 -0
  97. data/lib/mui/terminal_adapter/base.rb +92 -0
  98. data/lib/mui/terminal_adapter/curses.rb +164 -0
  99. data/lib/mui/terminal_adapter.rb +4 -0
  100. data/lib/mui/themes/default.rb +315 -0
  101. data/lib/mui/undo_manager.rb +83 -0
  102. data/lib/mui/undoable_action.rb +175 -0
  103. data/lib/mui/unicode_width.rb +100 -0
  104. data/lib/mui/version.rb +1 -1
  105. data/lib/mui/window.rb +201 -0
  106. data/lib/mui/window_manager.rb +256 -0
  107. data/lib/mui/wrap_cache.rb +40 -0
  108. data/lib/mui/wrap_helper.rb +84 -0
  109. data/lib/mui.rb +171 -2
  110. metadata +123 -5
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module KeyHandler
5
+ class WindowCommand
6
+ def initialize(window_manager)
7
+ @window_manager = window_manager
8
+ end
9
+
10
+ def handle(key)
11
+ if key.is_a?(Integer)
12
+ result = handle_control_key(key)
13
+ return result if result
14
+ end
15
+
16
+ char = key_to_char(key)
17
+ return :done unless char
18
+
19
+ case char
20
+ when "s"
21
+ handle_split_horizontal
22
+ when "v"
23
+ handle_split_vertical
24
+ when "h", "H"
25
+ handle_focus_direction(:left)
26
+ when "j", "J"
27
+ handle_focus_direction(:down)
28
+ when "k", "K"
29
+ handle_focus_direction(:up)
30
+ when "l", "L"
31
+ handle_focus_direction(:right)
32
+ when "w"
33
+ handle_focus_next
34
+ when "W"
35
+ handle_focus_previous
36
+ when "c", "q"
37
+ handle_close_window
38
+ when "o"
39
+ handle_close_all_except_current
40
+ else
41
+ :done
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def handle_control_key(key)
48
+ case key
49
+ when KeyCode::CTRL_S
50
+ handle_split_horizontal
51
+ when KeyCode::CTRL_V
52
+ handle_split_vertical
53
+ when KeyCode::CTRL_H
54
+ handle_focus_direction(:left)
55
+ when KeyCode::CTRL_J
56
+ handle_focus_direction(:down)
57
+ when KeyCode::CTRL_K
58
+ handle_focus_direction(:up)
59
+ when KeyCode::CTRL_L
60
+ handle_focus_direction(:right)
61
+ when KeyCode::CTRL_W
62
+ handle_focus_next
63
+ when KeyCode::CTRL_C
64
+ handle_close_window
65
+ when KeyCode::CTRL_O
66
+ handle_close_all_except_current
67
+ end
68
+ end
69
+
70
+ def key_to_char(key)
71
+ key.is_a?(String) ? key : key.chr
72
+ rescue RangeError
73
+ nil
74
+ end
75
+
76
+ def handle_split_horizontal
77
+ @window_manager.split_horizontal
78
+ :split_horizontal
79
+ end
80
+
81
+ def handle_split_vertical
82
+ @window_manager.split_vertical
83
+ :split_vertical
84
+ end
85
+
86
+ def handle_focus_direction(direction)
87
+ @window_manager.focus_direction(direction)
88
+ :"focus_#{direction}"
89
+ end
90
+
91
+ def handle_focus_next
92
+ @window_manager.focus_next
93
+ :focus_next
94
+ end
95
+
96
+ def handle_focus_previous
97
+ @window_manager.focus_previous
98
+ :focus_previous
99
+ end
100
+
101
+ def handle_close_window
102
+ @window_manager.close_current_window
103
+ :close_window
104
+ end
105
+
106
+ def handle_close_all_except_current
107
+ @window_manager.close_all_except_current
108
+ :close_all_except_current
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "key_handler/motions/motion_handler"
4
+ require_relative "key_handler/operators/base_operator"
5
+ require_relative "key_handler/operators/delete_operator"
6
+ require_relative "key_handler/operators/change_operator"
7
+ require_relative "key_handler/operators/yank_operator"
8
+ require_relative "key_handler/operators/paste_operator"
9
+ require_relative "key_handler/base"
10
+ require_relative "key_handler/normal_mode"
11
+ require_relative "key_handler/insert_mode"
12
+ require_relative "key_handler/command_mode"
13
+ require_relative "key_handler/visual_mode"
14
+ require_relative "key_handler/visual_line_mode"
15
+ require_relative "key_handler/search_mode"
16
+ require_relative "key_handler/window_command"
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Mui
6
+ # Parser for Vim-style key notation strings
7
+ # Converts notation like "<Leader>gd", "<C-x><C-s>", "<Space>w" to internal key arrays
8
+ module KeyNotationParser
9
+ # Special key mappings (case-insensitive)
10
+ SPECIAL_KEYS = {
11
+ "space" => " ",
12
+ "tab" => "\t",
13
+ "cr" => "\r",
14
+ "enter" => "\r",
15
+ "return" => "\r",
16
+ "esc" => "\e",
17
+ "escape" => "\e",
18
+ "bs" => "\x7f",
19
+ "backspace" => "\x7f",
20
+ "del" => "\x7f",
21
+ "delete" => "\x7f",
22
+ "lt" => "<",
23
+ "gt" => ">",
24
+ "bar" => "|",
25
+ "bslash" => "\\",
26
+ "leader" => :leader
27
+ }.freeze
28
+
29
+ # Ctrl key mappings (a-z and some special characters)
30
+ CTRL_CHARS = {
31
+ "@" => 0, # NUL
32
+ "a" => 1,
33
+ "b" => 2,
34
+ "c" => 3,
35
+ "d" => 4,
36
+ "e" => 5,
37
+ "f" => 6,
38
+ "g" => 7,
39
+ "h" => 8, # Also backspace
40
+ "i" => 9, # Also tab
41
+ "j" => 10, # Also newline
42
+ "k" => 11,
43
+ "l" => 12,
44
+ "m" => 13, # Also carriage return
45
+ "n" => 14,
46
+ "o" => 15,
47
+ "p" => 16,
48
+ "q" => 17,
49
+ "r" => 18,
50
+ "s" => 19,
51
+ "t" => 20,
52
+ "u" => 21,
53
+ "v" => 22,
54
+ "w" => 23,
55
+ "x" => 24,
56
+ "y" => 25,
57
+ "z" => 26,
58
+ "[" => 27, # Also escape
59
+ "\\" => 28,
60
+ "]" => 29,
61
+ "^" => 30,
62
+ "_" => 31
63
+ }.freeze
64
+
65
+ class << self
66
+ # Parse a key notation string into an array of keys
67
+ # @param notation [String] Key notation (e.g., "<Leader>gd", "<C-x><C-s>")
68
+ # @return [Array<String, Symbol>] Array of normalized keys
69
+ def parse(notation)
70
+ return [] if notation.nil? || notation.empty?
71
+
72
+ tokens = []
73
+ scanner = StringScanner.new(notation)
74
+
75
+ until scanner.eos?
76
+ if scanner.scan(/<([^>]+)>/)
77
+ # Special key notation <...>
78
+ tokens << parse_special(scanner[1])
79
+ else
80
+ # Regular character
81
+ char = scanner.getch
82
+ tokens << char if char
83
+ end
84
+ end
85
+
86
+ tokens
87
+ end
88
+
89
+ # Parse a special key notation (content inside < >)
90
+ # @param name [String] Special key name (e.g., "C-x", "Leader", "Space")
91
+ # @return [String, Symbol] Normalized key
92
+ def parse_special(name)
93
+ return :leader if name.casecmp?("leader")
94
+
95
+ # Handle Ctrl key: <C-x>, <Ctrl-x>, <C-X>
96
+ return parse_ctrl_key(::Regexp.last_match(2)) if name =~ /\A(c|ctrl)-(.+)\z/i
97
+
98
+ # Handle Shift key: <S-x>, <Shift-x>
99
+ return parse_shift_key(::Regexp.last_match(2)) if name =~ /\A(s|shift)-(.+)\z/i
100
+
101
+ # Handle other special keys
102
+ normalized_name = name.downcase
103
+ SPECIAL_KEYS[normalized_name] || name
104
+ end
105
+
106
+ # Normalize an input key (from terminal) to internal representation
107
+ # @param key [Integer, String] Raw key input
108
+ # @return [String, nil] Normalized key string, or nil if invalid
109
+ def normalize_input_key(key)
110
+ case key
111
+ when String
112
+ key
113
+ when Integer
114
+ normalize_integer_key(key)
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def parse_ctrl_key(char)
121
+ char_lower = char.downcase
122
+ code = CTRL_CHARS[char_lower]
123
+ code ? code.chr : char
124
+ end
125
+
126
+ def parse_shift_key(char)
127
+ # Shift typically produces uppercase for letters
128
+ char.length == 1 ? char.upcase : char
129
+ end
130
+
131
+ def normalize_integer_key(key)
132
+ case key
133
+ when KeyCode::ENTER_CR, KeyCode::ENTER_LF
134
+ "\r"
135
+ when KeyCode::ESCAPE
136
+ "\e"
137
+ when KeyCode::TAB
138
+ "\t"
139
+ when KeyCode::BACKSPACE
140
+ "\x7f"
141
+ when 0..31
142
+ # Control characters - convert to the character they represent
143
+ key.chr
144
+ when KeyCode::PRINTABLE_MIN..KeyCode::PRINTABLE_MAX
145
+ key.chr(Encoding::UTF_8)
146
+ end
147
+ rescue RangeError
148
+ nil
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Represents a sequence of keys for key mapping
5
+ # Parses Vim-style notation and provides normalization for matching
6
+ class KeySequence
7
+ attr_reader :keys, :notation
8
+
9
+ # @param notation [String] Key notation string (e.g., "<Leader>gd", "<C-x><C-s>")
10
+ def initialize(notation)
11
+ @notation = notation
12
+ @keys = KeyNotationParser.parse(notation)
13
+ end
14
+
15
+ # Normalize the key sequence by expanding :leader to actual leader key
16
+ # @param leader_key [String] The actual leader key (e.g., "\\", " ")
17
+ # @return [Array<String>] Array of normalized key strings
18
+ def normalize(leader_key)
19
+ @keys.map { |k| k == :leader ? leader_key : k }
20
+ end
21
+
22
+ # Get the length of the key sequence
23
+ # @return [Integer] Number of keys in the sequence
24
+ def length
25
+ @keys.length
26
+ end
27
+
28
+ # Check if this sequence contains a leader key
29
+ # @return [Boolean]
30
+ def leader?
31
+ @keys.include?(:leader)
32
+ end
33
+
34
+ # Check if this is a single key sequence
35
+ # @return [Boolean]
36
+ def single_key?
37
+ @keys.length == 1
38
+ end
39
+
40
+ # Check equality with another KeySequence
41
+ # @param other [KeySequence]
42
+ # @return [Boolean]
43
+ def ==(other)
44
+ return false unless other.is_a?(KeySequence)
45
+
46
+ @keys == other.keys
47
+ end
48
+
49
+ alias eql? ==
50
+
51
+ # Hash for use in Hash keys
52
+ def hash
53
+ @keys.hash
54
+ end
55
+
56
+ # Convert back to notation string for display
57
+ # @return [String]
58
+ def to_s
59
+ @notation
60
+ end
61
+
62
+ # Inspect for debugging
63
+ def inspect
64
+ "#<Mui::KeySequence #{@notation.inspect} => #{@keys.inspect}>"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Buffer for accumulating key inputs for multi-key sequence matching
5
+ # Tracks timing for timeout detection
6
+ class KeySequenceBuffer
7
+ attr_reader :last_input_time
8
+
9
+ def initialize
10
+ @buffer = []
11
+ @last_input_time = nil
12
+ end
13
+
14
+ # Push a key into the buffer
15
+ # @param key [Integer, String] Raw key input from terminal
16
+ # @return [Boolean] true if key was added, false if key was invalid
17
+ def push(key)
18
+ normalized = KeyNotationParser.normalize_input_key(key)
19
+ return false unless normalized
20
+
21
+ @buffer << normalized
22
+ @last_input_time = Time.now
23
+ true
24
+ end
25
+
26
+ # Clear the buffer
27
+ def clear
28
+ @buffer.clear
29
+ @last_input_time = nil
30
+ end
31
+
32
+ # Check if buffer is empty
33
+ # @return [Boolean]
34
+ def empty?
35
+ @buffer.empty?
36
+ end
37
+
38
+ # Get buffer length
39
+ # @return [Integer]
40
+ def length
41
+ @buffer.length
42
+ end
43
+
44
+ alias size length
45
+
46
+ # Get copy of buffer contents as array
47
+ # @return [Array<String>]
48
+ def to_a
49
+ @buffer.dup
50
+ end
51
+
52
+ # Get the first key in the buffer
53
+ # @return [String, nil]
54
+ def first
55
+ @buffer.first
56
+ end
57
+
58
+ # Remove and return the first key
59
+ # @return [String, nil]
60
+ def shift
61
+ key = @buffer.shift
62
+ @last_input_time = nil if @buffer.empty?
63
+ key
64
+ end
65
+
66
+ # Check if the buffer has timed out
67
+ # @param timeout_ms [Integer] Timeout in milliseconds
68
+ # @return [Boolean]
69
+ def timeout?(timeout_ms)
70
+ return false unless @last_input_time
71
+ return false if @buffer.empty?
72
+
73
+ elapsed_ms = (Time.now - @last_input_time) * 1000
74
+ elapsed_ms > timeout_ms
75
+ end
76
+
77
+ # Get elapsed time since last input in milliseconds
78
+ # @return [Float, nil] Elapsed time in ms, or nil if no input
79
+ def elapsed_ms
80
+ return nil unless @last_input_time
81
+
82
+ (Time.now - @last_input_time) * 1000
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Main handler for multi-key sequence processing
5
+ # Integrates buffer, matcher, and timeout handling
6
+ class KeySequenceHandler
7
+ DEFAULT_TIMEOUT_MS = 1000
8
+
9
+ # Process result types
10
+ RESULT_HANDLED = :handled # Handler executed
11
+ RESULT_PENDING = :pending # Waiting for more keys
12
+ RESULT_PASSTHROUGH = :passthrough # No match, pass key to built-in handler
13
+
14
+ attr_reader :buffer
15
+
16
+ # @param config [Config] Configuration object
17
+ def initialize(config)
18
+ @config = config
19
+ @buffer = KeySequenceBuffer.new
20
+ @keymaps = {} # { mode => { KeySequence => handler } }
21
+ @pending_handler = nil # Handler for exact match while waiting for longer
22
+ end
23
+
24
+ # Get the leader key from config
25
+ # @return [String]
26
+ def leader_key
27
+ @config.get(:leader) || "\\"
28
+ end
29
+
30
+ # Get the timeout in milliseconds
31
+ # @return [Integer]
32
+ def timeout_ms
33
+ @config.get(:timeoutlen) || DEFAULT_TIMEOUT_MS
34
+ end
35
+
36
+ # Register a key sequence mapping
37
+ # @param mode [Symbol] Mode (:normal, :insert, etc.)
38
+ # @param key_notation [String] Key notation (e.g., "<Leader>gd")
39
+ # @param handler [Proc] Handler to execute
40
+ def register(mode, key_notation, handler)
41
+ @keymaps[mode] ||= {}
42
+ sequence = KeySequence.new(key_notation)
43
+ @keymaps[mode][sequence] = handler
44
+ end
45
+
46
+ # Process an input key
47
+ # @param key [Integer, String] Raw key input
48
+ # @param mode [Symbol] Current mode
49
+ # @return [Array<Symbol, Object>] [result_type, data]
50
+ # - [:handled, handler] - Execute the handler
51
+ # - [:pending, nil] - Wait for more input
52
+ # - [:passthrough, key] - Pass key to built-in handler
53
+ def process(key, mode)
54
+ # Check timeout first - if timed out, handle before processing new key
55
+ if @buffer.timeout?(timeout_ms) && !@buffer.empty?
56
+ result = handle_timeout(mode)
57
+ # If we got a result, return it; the new key will be processed next time
58
+ return result if result[0] == RESULT_HANDLED
59
+
60
+ # If passthrough, clear buffer and continue with new key
61
+ @buffer.clear
62
+ @pending_handler = nil
63
+ end
64
+
65
+ # Add key to buffer
66
+ unless @buffer.push(key)
67
+ # Invalid key, pass through as-is
68
+ return [RESULT_PASSTHROUGH, key]
69
+ end
70
+
71
+ # Match against keymaps
72
+ matcher = KeySequenceMatcher.new(@keymaps, leader_key)
73
+ match_type, handler = matcher.match(mode, @buffer.to_a)
74
+
75
+ case match_type
76
+ when KeySequenceMatcher::MATCH_EXACT
77
+ has_longer = matcher.longer_sequences?(mode, @buffer.to_a)
78
+ if has_longer
79
+ # Exact match but longer sequences exist
80
+ # Store handler and wait for more input or timeout
81
+ @pending_handler = handler
82
+ [RESULT_PENDING, nil]
83
+ else
84
+ # Exact match, no longer sequences - execute immediately
85
+ @buffer.clear
86
+ @pending_handler = nil
87
+ [RESULT_HANDLED, handler]
88
+ end
89
+
90
+ when KeySequenceMatcher::MATCH_PARTIAL
91
+ # Could become a match, wait for more input
92
+ [RESULT_PENDING, nil]
93
+
94
+ else
95
+ # No match - pass through first key
96
+ handle_no_match
97
+ end
98
+ end
99
+
100
+ # Check for timeout and handle if needed (called from main loop)
101
+ # @param mode [Symbol] Current mode
102
+ # @return [Array<Symbol, Object>, nil] Result if timed out, nil otherwise
103
+ def check_timeout(mode)
104
+ return nil if @buffer.empty?
105
+ return nil unless @buffer.timeout?(timeout_ms)
106
+
107
+ handle_timeout(mode)
108
+ end
109
+
110
+ # Check if there are pending keys in the buffer
111
+ # @return [Boolean]
112
+ def pending?
113
+ !@buffer.empty?
114
+ end
115
+
116
+ # Clear the buffer and pending state
117
+ def clear
118
+ @buffer.clear
119
+ @pending_handler = nil
120
+ end
121
+
122
+ # Get pending key display for status line
123
+ # @return [String, nil]
124
+ def pending_keys_display
125
+ return nil if @buffer.empty?
126
+
127
+ @buffer.to_a.join
128
+ end
129
+
130
+ # Rebuild internal keymaps from config
131
+ # Called after config changes
132
+ def rebuild_keymaps
133
+ @keymaps = {}
134
+ @config.keymaps.each do |mode, mappings|
135
+ mappings.each do |key_notation, handler|
136
+ register(mode, key_notation, handler)
137
+ end
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def handle_timeout(_mode)
144
+ if @pending_handler
145
+ # We had an exact match, execute it
146
+ handler = @pending_handler
147
+ @buffer.clear
148
+ @pending_handler = nil
149
+ [RESULT_HANDLED, handler]
150
+ else
151
+ # No exact match, passthrough first key
152
+ handle_no_match
153
+ end
154
+ end
155
+
156
+ def handle_no_match
157
+ first_key = @buffer.shift
158
+ @buffer.clear
159
+ @pending_handler = nil
160
+ [RESULT_PASSTHROUGH, first_key]
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Matches input key sequences against registered key mappings
5
+ class KeySequenceMatcher
6
+ # Match result types
7
+ MATCH_EXACT = :exact # Complete match found
8
+ MATCH_PARTIAL = :partial # Input is prefix of one or more registered sequences
9
+ MATCH_NONE = :none # No match possible
10
+
11
+ # @param keymaps [Hash] Mode => { KeySequence => handler }
12
+ # @param leader_key [String] The leader key to expand :leader symbols
13
+ def initialize(keymaps, leader_key)
14
+ @keymaps = keymaps
15
+ @leader_key = leader_key
16
+ end
17
+
18
+ # Match input keys against registered keymaps for a mode
19
+ # @param mode [Symbol] The current mode (:normal, :insert, etc.)
20
+ # @param input_keys [Array<String>] Array of normalized input keys
21
+ # @return [Array<Symbol, Object>] [match_type, handler_or_nil]
22
+ def match(mode, input_keys)
23
+ mode_keymaps = @keymaps[mode]
24
+ return [MATCH_NONE, nil] unless mode_keymaps
25
+ return [MATCH_NONE, nil] if input_keys.empty?
26
+
27
+ exact_match = nil
28
+ has_longer_match = false
29
+
30
+ mode_keymaps.each do |sequence, handler|
31
+ seq_keys = sequence.normalize(@leader_key)
32
+
33
+ if seq_keys == input_keys
34
+ # Exact match found
35
+ exact_match = handler
36
+ elsif prefix_match?(input_keys, seq_keys)
37
+ # Input is a prefix of this sequence (longer sequence exists)
38
+ has_longer_match = true
39
+ end
40
+ end
41
+
42
+ if exact_match
43
+ # Exact match found - return it
44
+ # If there are also longer matches, caller may want to wait for timeout
45
+ [MATCH_EXACT, exact_match]
46
+ elsif has_longer_match
47
+ # No exact match, but input could lead to a match
48
+ [MATCH_PARTIAL, nil]
49
+ else
50
+ # No match possible
51
+ [MATCH_NONE, nil]
52
+ end
53
+ end
54
+
55
+ # Check if there are any longer sequences that could match
56
+ # Used to determine if we should wait for more input
57
+ # @param mode [Symbol]
58
+ # @param input_keys [Array<String>]
59
+ # @return [Boolean]
60
+ def longer_sequences?(mode, input_keys)
61
+ mode_keymaps = @keymaps[mode]
62
+ return false unless mode_keymaps
63
+
64
+ mode_keymaps.any? do |sequence, _handler|
65
+ seq_keys = sequence.normalize(@leader_key)
66
+ seq_keys.length > input_keys.length && prefix_match?(input_keys, seq_keys)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # Check if input_keys is a prefix of seq_keys
73
+ def prefix_match?(input_keys, seq_keys)
74
+ return false if input_keys.length >= seq_keys.length
75
+
76
+ seq_keys.take(input_keys.length) == input_keys
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Layout
5
+ class Calculator
6
+ def calculate(root, x, y, width, height)
7
+ root.x = x
8
+ root.y = y
9
+ root.width = width
10
+ root.height = height
11
+ root.apply_geometry
12
+ end
13
+ end
14
+ end
15
+ end