mui 0.1.0 → 0.2.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +158 -0
  3. data/CHANGELOG.md +349 -0
  4. data/exe/mui +1 -2
  5. data/lib/mui/autocmd.rb +66 -0
  6. data/lib/mui/buffer.rb +275 -0
  7. data/lib/mui/buffer_word_cache.rb +131 -0
  8. data/lib/mui/buffer_word_completer.rb +77 -0
  9. data/lib/mui/color_manager.rb +136 -0
  10. data/lib/mui/color_scheme.rb +63 -0
  11. data/lib/mui/command_completer.rb +21 -0
  12. data/lib/mui/command_context.rb +90 -0
  13. data/lib/mui/command_line.rb +137 -0
  14. data/lib/mui/command_registry.rb +25 -0
  15. data/lib/mui/completion_renderer.rb +84 -0
  16. data/lib/mui/completion_state.rb +58 -0
  17. data/lib/mui/config.rb +56 -0
  18. data/lib/mui/editor.rb +319 -0
  19. data/lib/mui/error.rb +29 -0
  20. data/lib/mui/file_completer.rb +51 -0
  21. data/lib/mui/floating_window.rb +161 -0
  22. data/lib/mui/handler_result.rb +101 -0
  23. data/lib/mui/highlight.rb +22 -0
  24. data/lib/mui/highlighters/base.rb +23 -0
  25. data/lib/mui/highlighters/search_highlighter.rb +26 -0
  26. data/lib/mui/highlighters/selection_highlighter.rb +48 -0
  27. data/lib/mui/highlighters/syntax_highlighter.rb +105 -0
  28. data/lib/mui/input.rb +17 -0
  29. data/lib/mui/insert_completion_renderer.rb +92 -0
  30. data/lib/mui/insert_completion_state.rb +77 -0
  31. data/lib/mui/job.rb +81 -0
  32. data/lib/mui/job_manager.rb +113 -0
  33. data/lib/mui/key_code.rb +30 -0
  34. data/lib/mui/key_handler/base.rb +100 -0
  35. data/lib/mui/key_handler/command_mode.rb +443 -0
  36. data/lib/mui/key_handler/insert_mode.rb +354 -0
  37. data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
  38. data/lib/mui/key_handler/normal_mode.rb +579 -0
  39. data/lib/mui/key_handler/operators/base_operator.rb +134 -0
  40. data/lib/mui/key_handler/operators/change_operator.rb +179 -0
  41. data/lib/mui/key_handler/operators/delete_operator.rb +176 -0
  42. data/lib/mui/key_handler/operators/paste_operator.rb +113 -0
  43. data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
  44. data/lib/mui/key_handler/search_mode.rb +188 -0
  45. data/lib/mui/key_handler/visual_line_mode.rb +20 -0
  46. data/lib/mui/key_handler/visual_mode.rb +397 -0
  47. data/lib/mui/key_handler/window_command.rb +112 -0
  48. data/lib/mui/key_handler.rb +16 -0
  49. data/lib/mui/layout/calculator.rb +15 -0
  50. data/lib/mui/layout/leaf_node.rb +33 -0
  51. data/lib/mui/layout/node.rb +29 -0
  52. data/lib/mui/layout/split_node.rb +132 -0
  53. data/lib/mui/line_renderer.rb +122 -0
  54. data/lib/mui/mode.rb +13 -0
  55. data/lib/mui/mode_manager.rb +185 -0
  56. data/lib/mui/motion.rb +139 -0
  57. data/lib/mui/plugin.rb +35 -0
  58. data/lib/mui/plugin_manager.rb +106 -0
  59. data/lib/mui/register.rb +110 -0
  60. data/lib/mui/screen.rb +85 -0
  61. data/lib/mui/search_completer.rb +50 -0
  62. data/lib/mui/search_input.rb +40 -0
  63. data/lib/mui/search_state.rb +88 -0
  64. data/lib/mui/selection.rb +55 -0
  65. data/lib/mui/status_line_renderer.rb +40 -0
  66. data/lib/mui/syntax/language_detector.rb +74 -0
  67. data/lib/mui/syntax/lexer_base.rb +106 -0
  68. data/lib/mui/syntax/lexers/c_lexer.rb +127 -0
  69. data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
  70. data/lib/mui/syntax/token.rb +42 -0
  71. data/lib/mui/syntax/token_cache.rb +91 -0
  72. data/lib/mui/tab_bar_renderer.rb +87 -0
  73. data/lib/mui/tab_manager.rb +96 -0
  74. data/lib/mui/tab_page.rb +35 -0
  75. data/lib/mui/terminal_adapter/base.rb +92 -0
  76. data/lib/mui/terminal_adapter/curses.rb +162 -0
  77. data/lib/mui/terminal_adapter.rb +4 -0
  78. data/lib/mui/themes/default.rb +315 -0
  79. data/lib/mui/undo_manager.rb +83 -0
  80. data/lib/mui/undoable_action.rb +175 -0
  81. data/lib/mui/unicode_width.rb +100 -0
  82. data/lib/mui/version.rb +1 -1
  83. data/lib/mui/window.rb +158 -0
  84. data/lib/mui/window_manager.rb +249 -0
  85. data/lib/mui.rb +156 -2
  86. metadata +98 -3
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module KeyHandler
5
+ # Handles key inputs in Search mode (/ and ?)
6
+ class SearchMode < Base
7
+ attr_reader :completion_state
8
+
9
+ def initialize(mode_manager, buffer, search_input, search_state)
10
+ super(mode_manager, buffer)
11
+ @search_input = search_input
12
+ @search_state = search_state
13
+ @original_cursor_row = nil
14
+ @original_cursor_col = nil
15
+ @completion_state = CompletionState.new
16
+ @search_completer = SearchCompleter.new
17
+ end
18
+
19
+ def start_search
20
+ @original_cursor_row = cursor_row
21
+ @original_cursor_col = cursor_col
22
+ @completion_state.reset
23
+ end
24
+
25
+ def handle(key)
26
+ case key
27
+ when KeyCode::ESCAPE
28
+ handle_escape
29
+ when KeyCode::BACKSPACE, Curses::KEY_BACKSPACE
30
+ handle_backspace
31
+ when KeyCode::ENTER_CR, KeyCode::ENTER_LF, Curses::KEY_ENTER
32
+ handle_enter
33
+ when KeyCode::TAB
34
+ handle_tab
35
+ when Curses::KEY_BTAB
36
+ handle_shift_tab
37
+ else
38
+ handle_character_input(key)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def handle_tab
45
+ return result unless @completion_state.active?
46
+
47
+ @completion_state.select_next
48
+ apply_current_completion
49
+ result
50
+ end
51
+
52
+ def handle_shift_tab
53
+ return result unless @completion_state.active?
54
+
55
+ @completion_state.select_previous
56
+ apply_current_completion
57
+ result
58
+ end
59
+
60
+ def apply_current_completion
61
+ candidate = @completion_state.current_candidate
62
+ return unless candidate
63
+
64
+ @search_input.clear
65
+ candidate.each_char { |c| @search_input.input(c) }
66
+ update_incremental_search
67
+ end
68
+
69
+ def handle_escape
70
+ @search_input.clear
71
+ @search_state.clear
72
+ @completion_state.reset
73
+ # Restore original cursor position
74
+ restore_cursor_position
75
+ result(mode: Mode::NORMAL, cancelled: true)
76
+ end
77
+
78
+ def restore_cursor_position
79
+ return unless @original_cursor_row && @original_cursor_col
80
+
81
+ window.cursor_row = @original_cursor_row
82
+ window.cursor_col = @original_cursor_col
83
+ end
84
+
85
+ def handle_backspace
86
+ if @search_input.empty?
87
+ @search_state.clear
88
+ @completion_state.reset
89
+ restore_cursor_position
90
+ result(mode: Mode::NORMAL, cancelled: true)
91
+ else
92
+ @search_input.backspace
93
+ update_incremental_search
94
+ update_completion
95
+ result
96
+ end
97
+ end
98
+
99
+ def handle_enter
100
+ @completion_state.reset
101
+ execute_search
102
+ end
103
+
104
+ def handle_character_input(key)
105
+ char = extract_printable_char(key)
106
+ if char
107
+ @search_input.input(char)
108
+ update_incremental_search
109
+ update_completion
110
+ end
111
+ result
112
+ end
113
+
114
+ def update_completion
115
+ prefix = @search_input.pattern
116
+ if prefix.empty?
117
+ @completion_state.reset
118
+ return
119
+ end
120
+
121
+ candidates = @search_completer.complete(buffer, prefix)
122
+ if candidates.empty?
123
+ @completion_state.reset
124
+ else
125
+ @completion_state.start(candidates, prefix, :search)
126
+ end
127
+ end
128
+
129
+ def update_incremental_search
130
+ pattern = @search_input.pattern
131
+ if pattern.empty?
132
+ @search_state.clear
133
+ restore_cursor_position
134
+ return
135
+ end
136
+
137
+ direction = @search_input.prompt == "/" ? :forward : :backward
138
+ @search_state.set_pattern(pattern, direction)
139
+ @search_state.find_all_matches(buffer)
140
+
141
+ # Move cursor to first match from original position
142
+ return if @search_state.matches.empty?
143
+
144
+ # Use original position if set, otherwise use current cursor position
145
+ search_row = @original_cursor_row || cursor_row
146
+ search_col = @original_cursor_col || cursor_col
147
+
148
+ match = if direction == :forward
149
+ @search_state.find_next(search_row, search_col)
150
+ else
151
+ @search_state.find_previous(search_row, search_col)
152
+ end
153
+
154
+ return unless match
155
+
156
+ window.cursor_row = match[:row]
157
+ window.cursor_col = match[:col]
158
+ end
159
+
160
+ def execute_search
161
+ pattern = @search_input.pattern
162
+ return result(mode: Mode::NORMAL, cancelled: true) if pattern.empty?
163
+
164
+ direction = @search_input.prompt == "/" ? :forward : :backward
165
+ @search_state.set_pattern(pattern, direction)
166
+ @search_state.find_all_matches(@buffer)
167
+
168
+ match = if direction == :forward
169
+ @search_state.find_next(cursor_row, cursor_col)
170
+ else
171
+ @search_state.find_previous(cursor_row, cursor_col)
172
+ end
173
+
174
+ if match
175
+ window.cursor_row = match[:row]
176
+ window.cursor_col = match[:col]
177
+ result(mode: Mode::NORMAL)
178
+ else
179
+ result(mode: Mode::NORMAL, message: "Pattern not found: #{pattern}")
180
+ end
181
+ end
182
+
183
+ def result(mode: nil, message: nil, quit: false, cancelled: false)
184
+ HandlerResult::SearchModeResult.new(mode:, message:, quit:, cancelled:)
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module KeyHandler
5
+ # Handler for line-wise visual mode (V)
6
+ class VisualLineMode < VisualMode
7
+ private
8
+
9
+ def handle_v_key
10
+ # v in visual line mode switches to visual mode
11
+ result(mode: Mode::VISUAL, toggle_line_mode: true)
12
+ end
13
+
14
+ def handle_upper_v_key
15
+ # V in visual line mode exits to normal mode
16
+ result(mode: Mode::NORMAL, clear_selection: true)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,397 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module KeyHandler
5
+ # Handler for character-wise visual mode (v)
6
+ class VisualMode < Base
7
+ include Motions::MotionHandler
8
+
9
+ attr_reader :selection
10
+
11
+ def initialize(mode_manager, buffer, selection, register = nil, undo_manager: nil)
12
+ super(mode_manager, buffer)
13
+ @selection = selection
14
+ @register = register || Register.new
15
+ @undo_manager = undo_manager
16
+ @pending_motion = nil
17
+ @pending_register = nil
18
+ end
19
+
20
+ def handle(key)
21
+ if @pending_motion
22
+ handle_pending_motion(key)
23
+ else
24
+ handle_visual_key(key)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def handle_visual_key(key)
31
+ case key
32
+ when KeyCode::ESCAPE
33
+ result(mode: Mode::NORMAL, clear_selection: true)
34
+ when "v"
35
+ handle_v_key
36
+ when "V"
37
+ handle_upper_v_key
38
+ when "h", Curses::KEY_LEFT
39
+ handle_move_left
40
+ when "j", Curses::KEY_DOWN
41
+ handle_move_down
42
+ when "k", Curses::KEY_UP
43
+ handle_move_up
44
+ when "l", Curses::KEY_RIGHT
45
+ handle_move_right
46
+ when "w"
47
+ handle_word_forward
48
+ when "b"
49
+ handle_word_backward
50
+ when "e"
51
+ handle_word_end
52
+ when "0"
53
+ handle_line_start
54
+ when "^"
55
+ handle_first_non_blank
56
+ when "$"
57
+ handle_line_end
58
+ when "g"
59
+ @pending_motion = :g
60
+ result
61
+ when "G"
62
+ handle_file_end
63
+ when "f"
64
+ @pending_motion = :f
65
+ result
66
+ when "F"
67
+ @pending_motion = :F
68
+ result
69
+ when "t"
70
+ @pending_motion = :t
71
+ result
72
+ when "T"
73
+ @pending_motion = :T
74
+ result
75
+ when "d"
76
+ handle_delete
77
+ when "c"
78
+ handle_change
79
+ when "y"
80
+ handle_yank
81
+ when ">"
82
+ handle_indent(:right)
83
+ when "<"
84
+ handle_indent(:left)
85
+ when '"'
86
+ @pending_motion = :register_select
87
+ result
88
+ when "*"
89
+ handle_search_selection(:forward)
90
+ when "#"
91
+ handle_search_selection(:backward)
92
+ else
93
+ result
94
+ end
95
+ end
96
+
97
+ def handle_v_key
98
+ # v in visual mode exits to normal mode
99
+ result(mode: Mode::NORMAL, clear_selection: true)
100
+ end
101
+
102
+ def handle_upper_v_key
103
+ # V in visual mode switches to visual line mode
104
+ result(mode: Mode::VISUAL_LINE, toggle_line_mode: true)
105
+ end
106
+
107
+ def handle_delete
108
+ range = @selection.normalized_range
109
+ if @selection.line_mode
110
+ delete_lines(range)
111
+ else
112
+ delete_range(range)
113
+ end
114
+ @pending_register = nil
115
+ result(mode: Mode::NORMAL, clear_selection: true)
116
+ end
117
+
118
+ def handle_change
119
+ range = @selection.normalized_range
120
+ if @selection.line_mode
121
+ change_lines(range)
122
+ else
123
+ @undo_manager&.begin_group
124
+ change_range(range)
125
+ end
126
+ @pending_register = nil
127
+ result(mode: Mode::INSERT, clear_selection: true, group_started: true)
128
+ end
129
+
130
+ def handle_yank
131
+ range = @selection.normalized_range
132
+ if @selection.line_mode
133
+ yank_lines(range)
134
+ else
135
+ yank_range(range)
136
+ end
137
+ @pending_register = nil
138
+ self.cursor_row = range[:start_row]
139
+ self.cursor_col = range[:start_col]
140
+ result(mode: Mode::NORMAL, clear_selection: true)
141
+ end
142
+
143
+ def handle_search_selection(direction)
144
+ range = @selection.normalized_range
145
+ text = if @selection.line_mode
146
+ # For line mode, use the full line content (trimmed)
147
+ buffer.line(range[:start_row]).strip
148
+ else
149
+ extract_selection_text(range)
150
+ end
151
+
152
+ return result(mode: Mode::NORMAL, clear_selection: true) if text.empty?
153
+
154
+ # Escape special regex characters for literal search
155
+ escaped_pattern = Regexp.escape(text)
156
+
157
+ # Set search state and find matches
158
+ search_state = @mode_manager.search_state
159
+ search_state.set_pattern(escaped_pattern, direction)
160
+ search_state.find_all_matches(buffer)
161
+
162
+ # Find next/previous match from current position
163
+ match = if direction == :forward
164
+ search_state.find_next(cursor_row, cursor_col)
165
+ else
166
+ search_state.find_previous(cursor_row, cursor_col)
167
+ end
168
+
169
+ if match
170
+ window.cursor_row = match[:row]
171
+ window.cursor_col = match[:col]
172
+ result(mode: Mode::NORMAL, clear_selection: true)
173
+ else
174
+ result(mode: Mode::NORMAL, clear_selection: true, message: "Pattern not found: #{text}")
175
+ end
176
+ end
177
+
178
+ def yank_lines(range)
179
+ lines = (range[:start_row]..range[:end_row]).map { |r| buffer.line(r) }
180
+ @register.yank(lines.join("\n"), linewise: true, name: @pending_register)
181
+ end
182
+
183
+ def yank_range(range)
184
+ text = extract_selection_text(range)
185
+ @register.yank(text, linewise: false, name: @pending_register)
186
+ end
187
+
188
+ def extract_selection_text(range)
189
+ if range[:start_row] == range[:end_row]
190
+ buffer.line(range[:start_row])[range[:start_col]..range[:end_col]] || ""
191
+ else
192
+ lines = []
193
+ (range[:start_row]..range[:end_row]).each do |row|
194
+ line = buffer.line(row)
195
+ lines << if row == range[:start_row]
196
+ line[range[:start_col]..]
197
+ elsif row == range[:end_row]
198
+ line[0..range[:end_col]]
199
+ else
200
+ line
201
+ end
202
+ end
203
+ lines.join("\n")
204
+ end
205
+ end
206
+
207
+ def change_lines(range)
208
+ lines = (range[:start_row]..range[:end_row]).map { |r| buffer.line(r) }
209
+ @register.delete(lines.join("\n"), linewise: true, name: @pending_register)
210
+ @undo_manager&.begin_group
211
+ (range[:end_row] - range[:start_row] + 1).times do
212
+ buffer.delete_line(range[:start_row])
213
+ end
214
+ buffer.insert_line(range[:start_row])
215
+ # NOTE: group will be closed when leaving Insert mode
216
+ self.cursor_row = range[:start_row]
217
+ self.cursor_col = 0
218
+ end
219
+
220
+ def change_range(range)
221
+ text = extract_selection_text(range)
222
+ @register.delete(text, linewise: false, name: @pending_register)
223
+ buffer.delete_range(range[:start_row], range[:start_col], range[:end_row], range[:end_col])
224
+ self.cursor_row = range[:start_row]
225
+ self.cursor_col = range[:start_col]
226
+ window.clamp_cursor_to_line(buffer)
227
+ end
228
+
229
+ def delete_lines(range)
230
+ lines = (range[:start_row]..range[:end_row]).map { |r| buffer.line(r) }
231
+ @register.delete(lines.join("\n"), linewise: true, name: @pending_register)
232
+ @undo_manager&.begin_group unless @undo_manager&.in_group?
233
+ (range[:end_row] - range[:start_row] + 1).times do
234
+ buffer.delete_line(range[:start_row])
235
+ end
236
+ @undo_manager&.end_group
237
+ self.cursor_row = [range[:start_row], buffer.line_count - 1].min
238
+ self.cursor_col = 0
239
+ window.clamp_cursor_to_line(buffer)
240
+ end
241
+
242
+ def delete_range(range)
243
+ text = extract_selection_text(range)
244
+ @register.delete(text, linewise: false, name: @pending_register)
245
+ buffer.delete_range(range[:start_row], range[:start_col], range[:end_row], range[:end_col])
246
+ self.cursor_row = range[:start_row]
247
+ self.cursor_col = range[:start_col]
248
+ window.clamp_cursor_to_line(buffer)
249
+ end
250
+
251
+ def handle_indent(direction)
252
+ range = @selection.normalized_range
253
+ indent_lines(range[:start_row], range[:end_row], direction)
254
+
255
+ # Move cursor to the beginning of the first selected line (Vim behavior)
256
+ self.cursor_row = range[:start_row]
257
+ self.cursor_col = 0
258
+
259
+ if Mui.config.get(:reselect_after_indent)
260
+ # Keep selection for continuous indent adjustment
261
+ @selection.update_end(range[:end_row], buffer.line(range[:end_row]).length)
262
+ result
263
+ else
264
+ result(mode: Mode::NORMAL, clear_selection: true)
265
+ end
266
+ end
267
+
268
+ def indent_lines(start_row, end_row, direction)
269
+ indent_string = build_indent_string
270
+
271
+ @undo_manager&.begin_group unless @undo_manager&.in_group?
272
+
273
+ (start_row..end_row).each do |row|
274
+ if direction == :right
275
+ add_indent(row, indent_string)
276
+ else
277
+ remove_indent(row, Mui.config.get(:shiftwidth))
278
+ end
279
+ end
280
+
281
+ @undo_manager&.end_group
282
+ end
283
+
284
+ def build_indent_string
285
+ if Mui.config.get(:expandtab)
286
+ " " * Mui.config.get(:shiftwidth)
287
+ else
288
+ "\t"
289
+ end
290
+ end
291
+
292
+ def add_indent(row, indent_string)
293
+ return if buffer.line(row).empty? # Skip empty lines
294
+
295
+ indent_string.reverse.each_char do |char|
296
+ buffer.insert_char(row, 0, char)
297
+ end
298
+ end
299
+
300
+ def remove_indent(row, width)
301
+ line = buffer.line(row)
302
+ return if line.empty? # Skip empty lines
303
+
304
+ removed = 0
305
+
306
+ while removed < width && !line.empty?
307
+ char = line[0]
308
+ break unless [" ", "\t"].include?(char)
309
+
310
+ char_width = char == "\t" ? Mui.config.get(:tabstop) : 1
311
+ break if removed + char_width > width && char == "\t"
312
+
313
+ buffer.delete_char(row, 0)
314
+ removed += char_width
315
+ line = buffer.line(row)
316
+ end
317
+ end
318
+
319
+ def handle_pending_motion(key)
320
+ char = key_to_char(key)
321
+ return clear_pending unless char
322
+
323
+ return handle_register_select(char) if @pending_motion == :register_select
324
+
325
+ motion_result = execute_pending_motion(char)
326
+ apply_motion(motion_result) if motion_result
327
+ clear_pending
328
+ end
329
+
330
+ def handle_register_select(char)
331
+ if valid_register_name?(char)
332
+ @pending_register = char
333
+ @pending_motion = nil
334
+ result
335
+ else
336
+ clear_pending
337
+ end
338
+ end
339
+
340
+ def valid_register_name?(char)
341
+ Register::NAMED_REGISTERS.include?(char) ||
342
+ Register::DELETE_HISTORY_REGISTERS.include?(char) ||
343
+ [Register::YANK_REGISTER, Register::BLACK_HOLE_REGISTER, Register::UNNAMED_REGISTER].include?(char)
344
+ end
345
+
346
+ def clear_pending
347
+ @pending_motion = nil
348
+ @pending_register = nil
349
+ result
350
+ end
351
+
352
+ def handle_move_left
353
+ window.move_left
354
+ update_selection
355
+ result
356
+ end
357
+
358
+ def handle_move_down
359
+ window.move_down
360
+ update_selection
361
+ result
362
+ end
363
+
364
+ def handle_move_up
365
+ window.move_up
366
+ update_selection
367
+ result
368
+ end
369
+
370
+ def handle_move_right
371
+ window.move_right
372
+ update_selection
373
+ result
374
+ end
375
+
376
+ def apply_motion(motion_result)
377
+ super
378
+ update_selection if motion_result
379
+ end
380
+
381
+ def update_selection
382
+ @selection.update_end(cursor_row, cursor_col)
383
+ end
384
+
385
+ def result(mode: nil, message: nil, quit: false, clear_selection: false, toggle_line_mode: false, group_started: false)
386
+ HandlerResult::VisualModeResult.new(
387
+ mode:,
388
+ message:,
389
+ quit:,
390
+ clear_selection:,
391
+ toggle_line_mode:,
392
+ group_started:
393
+ )
394
+ end
395
+ end
396
+ end
397
+ end
@@ -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