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,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Mui
6
+ # Manages asynchronous job execution and result collection
7
+ class JobManager
8
+ def initialize(autocmd: nil)
9
+ @autocmd = autocmd
10
+ @result_queue = Queue.new
11
+ @next_id = 0
12
+ @id_mutex = Mutex.new
13
+ @active_jobs = {}
14
+ @jobs_mutex = Mutex.new
15
+ end
16
+
17
+ def run_async(on_complete: nil, &)
18
+ job = create_job(&)
19
+
20
+ Thread.new do
21
+ job.run
22
+ @result_queue.push({ job:, callback: on_complete })
23
+ end
24
+
25
+ job
26
+ end
27
+
28
+ def run_command(cmd, on_complete: nil)
29
+ run_async(on_complete:) do
30
+ stdout, stderr, status = Open3.capture3(*Array(cmd))
31
+ {
32
+ stdout:,
33
+ stderr:,
34
+ exit_status: status.exitstatus,
35
+ success: status.success?
36
+ }
37
+ end
38
+ end
39
+
40
+ def poll
41
+ processed = []
42
+
43
+ loop do
44
+ entry = @result_queue.pop(true) # non-blocking
45
+ processed << entry
46
+ invoke_callback(entry)
47
+ remove_job(entry[:job].id)
48
+ rescue ThreadError
49
+ break # Queue is empty
50
+ end
51
+
52
+ processed
53
+ end
54
+
55
+ def job(id)
56
+ @jobs_mutex.synchronize { @active_jobs[id] }
57
+ end
58
+
59
+ def cancel(id)
60
+ job = job(id)
61
+ return false unless job
62
+
63
+ job.cancel
64
+ end
65
+
66
+ def active_count
67
+ @jobs_mutex.synchronize { @active_jobs.values.count { |j| !j.finished? } }
68
+ end
69
+
70
+ def busy?
71
+ active_count.positive?
72
+ end
73
+
74
+ private
75
+
76
+ def create_job(&)
77
+ id = generate_id
78
+ job = Job.new(id, &)
79
+ @jobs_mutex.synchronize { @active_jobs[id] = job }
80
+ job
81
+ end
82
+
83
+ def generate_id
84
+ @id_mutex.synchronize do
85
+ @next_id += 1
86
+ end
87
+ end
88
+
89
+ def remove_job(id)
90
+ @jobs_mutex.synchronize { @active_jobs.delete(id) }
91
+ end
92
+
93
+ def invoke_callback(entry)
94
+ entry[:callback]&.call(entry[:job])
95
+ trigger_autocmd_event(entry[:job])
96
+ rescue StandardError => e
97
+ warn "Job callback error: #{e.message}"
98
+ warn e.backtrace.first(5).join("\n")
99
+ end
100
+
101
+ def trigger_autocmd_event(job)
102
+ return unless @autocmd
103
+
104
+ event = case job.status
105
+ when Job::Status::COMPLETED then :JobCompleted
106
+ when Job::Status::FAILED then :JobFailed
107
+ when Job::Status::CANCELLED then :JobCancelled
108
+ end
109
+
110
+ @autocmd.trigger(event, job:) if event
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Key code constants for terminal input handling
5
+ module KeyCode
6
+ ESCAPE = 27
7
+ BACKSPACE = 127
8
+ ENTER_CR = 13
9
+ ENTER_LF = 10
10
+ TAB = 9
11
+ PRINTABLE_MIN = 32
12
+ # Extended to support Unicode characters (including CJK)
13
+ # 0x10FFFF is the maximum valid Unicode code point
14
+ PRINTABLE_MAX = 0x10FFFF
15
+
16
+ # Control key codes (Ctrl+letter)
17
+ CTRL_SPACE = 0 # Also Ctrl+@ (NUL)
18
+ CTRL_C = 3
19
+ CTRL_H = 8 # Also backspace in some terminals
20
+ CTRL_J = 10 # Also newline
21
+ CTRL_K = 11
22
+ CTRL_L = 12
23
+ CTRL_N = 14
24
+ CTRL_O = 15
25
+ CTRL_P = 16
26
+ CTRL_S = 19
27
+ CTRL_V = 22
28
+ CTRL_W = 23
29
+ end
30
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../error"
4
+
5
+ module Mui
6
+ module KeyHandler
7
+ class MethodNotOverriddenError < Mui::Error; end
8
+
9
+ # Base class for mode-specific key handlers
10
+ class Base
11
+ attr_accessor :mode_manager
12
+
13
+ def initialize(mode_manager, buffer)
14
+ @mode_manager = mode_manager
15
+ @buffer = buffer
16
+ end
17
+
18
+ def window
19
+ @mode_manager&.active_window
20
+ end
21
+
22
+ def buffer
23
+ window&.buffer || @buffer
24
+ end
25
+
26
+ def editor
27
+ @mode_manager&.editor
28
+ end
29
+
30
+ # Returns the current buffer's undo_manager for dynamic access
31
+ # This ensures undo/redo works correctly when buffer changes (e.g., via :e)
32
+ def undo_manager
33
+ buffer&.undo_manager
34
+ end
35
+
36
+ # Access to key sequence handler for multi-key mappings
37
+ def key_sequence_handler
38
+ @mode_manager&.key_sequence_handler
39
+ end
40
+
41
+ # Handle a key input
42
+ def handle(_key)
43
+ raise MethodNotOverriddenError, "Subclasses must orverride #handle"
44
+ end
45
+
46
+ # Check plugin keymap with multi-key sequence support
47
+ # @param key [Integer, String] Raw key input
48
+ # @param mode_symbol [Symbol] Mode symbol (:normal, :insert, :visual, :command)
49
+ # @return [HandlerResult, nil] Result if handled or pending, nil to continue with built-in
50
+ def check_plugin_keymap(key, mode_symbol)
51
+ return nil unless key_sequence_handler
52
+
53
+ type, data = key_sequence_handler.process(key, mode_symbol)
54
+
55
+ case type
56
+ when KeySequenceHandler::RESULT_HANDLED
57
+ execute_plugin_handler(data, mode_symbol)
58
+ when KeySequenceHandler::RESULT_PENDING
59
+ # Return a result that tells the handler to wait
60
+ # Use base class directly since subclasses may not support pending_sequence
61
+ HandlerResult::Base.new(pending_sequence: true)
62
+ when KeySequenceHandler::RESULT_PASSTHROUGH
63
+ # Check for single-key keymap (backward compatibility)
64
+ check_single_key_keymap(data, mode_symbol)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # Check single-key keymap for backward compatibility
71
+ def check_single_key_keymap(key, mode_symbol)
72
+ key_str = convert_key_to_string(key)
73
+ return nil unless key_str
74
+
75
+ plugin_handler = Mui.config.keymaps[mode_symbol]&.[](key_str)
76
+ return nil unless plugin_handler
77
+
78
+ execute_plugin_handler(plugin_handler, mode_symbol)
79
+ end
80
+
81
+ # Execute a plugin handler and wrap result
82
+ def execute_plugin_handler(handler, mode_symbol)
83
+ return nil unless @mode_manager&.editor
84
+
85
+ context = CommandContext.new(
86
+ editor: @mode_manager.editor,
87
+ buffer:,
88
+ window:
89
+ )
90
+ handler_result = handler.call(context)
91
+
92
+ return nil unless handler_result
93
+
94
+ wrap_handler_result(handler_result, mode_symbol)
95
+ end
96
+
97
+ # Wrap handler result in appropriate type
98
+ def wrap_handler_result(handler_result, _mode_symbol)
99
+ return handler_result if handler_result.is_a?(HandlerResult::Base)
100
+
101
+ result
102
+ end
103
+
104
+ # Convert key to string for keymap lookup
105
+ def convert_key_to_string(key)
106
+ return key if key.is_a?(String)
107
+
108
+ case key
109
+ when KeyCode::ENTER_CR, KeyCode::ENTER_LF
110
+ "\r"
111
+ when KeyCode::ESCAPE
112
+ "\e"
113
+ when KeyCode::TAB
114
+ "\t"
115
+ when KeyCode::BACKSPACE
116
+ "\x7f"
117
+ else
118
+ key.chr
119
+ end
120
+ rescue RangeError
121
+ nil
122
+ end
123
+
124
+ def cursor_row
125
+ window.cursor_row
126
+ end
127
+
128
+ def cursor_col
129
+ window.cursor_col
130
+ end
131
+
132
+ def cursor_row=(value)
133
+ window.cursor_row = value
134
+ end
135
+
136
+ def cursor_col=(value)
137
+ window.cursor_col = value
138
+ end
139
+
140
+ def current_line
141
+ buffer.line(cursor_row)
142
+ end
143
+
144
+ def current_line_length
145
+ current_line.length
146
+ end
147
+
148
+ def extract_printable_char(key)
149
+ if key.is_a?(String)
150
+ # Curses returns multibyte characters as String
151
+ key
152
+ elsif key.is_a?(Integer) && key >= KeyCode::PRINTABLE_MIN && key <= KeyCode::PRINTABLE_MAX
153
+ # Use UTF-8 encoding to support Unicode characters
154
+ key.chr(Encoding::UTF_8)
155
+ end
156
+ rescue RangeError
157
+ # Invalid Unicode code point
158
+ nil
159
+ end
160
+
161
+ def key_to_char(key)
162
+ key.is_a?(String) ? key : key.chr
163
+ rescue RangeError
164
+ nil
165
+ end
166
+
167
+ def execute_pending_motion(char)
168
+ case @pending_motion
169
+ when :g
170
+ char == "g" ? Motion.file_start(buffer, cursor_row, cursor_col) : nil
171
+ when :f
172
+ Motion.find_char_forward(buffer, cursor_row, cursor_col, char)
173
+ when :F
174
+ Motion.find_char_backward(buffer, cursor_row, cursor_col, char)
175
+ when :t
176
+ Motion.till_char_forward(buffer, cursor_row, cursor_col, char)
177
+ when :T
178
+ Motion.till_char_backward(buffer, cursor_row, cursor_col, char)
179
+ end
180
+ end
181
+
182
+ def result(mode: nil, message: nil, quit: false)
183
+ HandlerResult::Base.new(mode:, message:, quit:)
184
+ end
185
+ end
186
+ end
187
+ end