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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +163 -0
- data/CHANGELOG.md +448 -0
- data/README.md +309 -6
- data/docs/_config.yml +56 -0
- data/docs/configuration.md +301 -0
- data/docs/getting-started.md +140 -0
- data/docs/index.md +55 -0
- data/docs/jobs.md +297 -0
- data/docs/keybindings.md +229 -0
- data/docs/plugins.md +285 -0
- data/docs/syntax-highlighting.md +149 -0
- data/exe/mui +1 -2
- data/lib/mui/autocmd.rb +66 -0
- data/lib/mui/buffer.rb +275 -0
- data/lib/mui/buffer_word_cache.rb +131 -0
- data/lib/mui/buffer_word_completer.rb +77 -0
- data/lib/mui/color_manager.rb +136 -0
- data/lib/mui/color_scheme.rb +63 -0
- data/lib/mui/command_completer.rb +30 -0
- data/lib/mui/command_context.rb +90 -0
- data/lib/mui/command_history.rb +89 -0
- data/lib/mui/command_line.rb +167 -0
- data/lib/mui/command_registry.rb +44 -0
- data/lib/mui/completion_renderer.rb +84 -0
- data/lib/mui/completion_state.rb +58 -0
- data/lib/mui/config.rb +58 -0
- data/lib/mui/editor.rb +395 -0
- data/lib/mui/error.rb +29 -0
- data/lib/mui/file_completer.rb +51 -0
- data/lib/mui/floating_window.rb +161 -0
- data/lib/mui/handler_result.rb +107 -0
- data/lib/mui/highlight.rb +22 -0
- data/lib/mui/highlighters/base.rb +23 -0
- data/lib/mui/highlighters/search_highlighter.rb +27 -0
- data/lib/mui/highlighters/selection_highlighter.rb +48 -0
- data/lib/mui/highlighters/syntax_highlighter.rb +107 -0
- data/lib/mui/input.rb +17 -0
- data/lib/mui/insert_completion_renderer.rb +92 -0
- data/lib/mui/insert_completion_state.rb +77 -0
- data/lib/mui/job.rb +81 -0
- data/lib/mui/job_manager.rb +113 -0
- data/lib/mui/key_code.rb +30 -0
- data/lib/mui/key_handler/base.rb +187 -0
- data/lib/mui/key_handler/command_mode.rb +511 -0
- data/lib/mui/key_handler/insert_mode.rb +323 -0
- data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
- data/lib/mui/key_handler/normal_mode.rb +552 -0
- data/lib/mui/key_handler/operators/base_operator.rb +134 -0
- data/lib/mui/key_handler/operators/change_operator.rb +179 -0
- data/lib/mui/key_handler/operators/delete_operator.rb +176 -0
- data/lib/mui/key_handler/operators/paste_operator.rb +119 -0
- data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
- data/lib/mui/key_handler/search_mode.rb +191 -0
- data/lib/mui/key_handler/visual_line_mode.rb +20 -0
- data/lib/mui/key_handler/visual_mode.rb +402 -0
- data/lib/mui/key_handler/window_command.rb +112 -0
- data/lib/mui/key_handler.rb +16 -0
- data/lib/mui/key_notation_parser.rb +152 -0
- data/lib/mui/key_sequence.rb +67 -0
- data/lib/mui/key_sequence_buffer.rb +85 -0
- data/lib/mui/key_sequence_handler.rb +163 -0
- data/lib/mui/key_sequence_matcher.rb +79 -0
- data/lib/mui/layout/calculator.rb +15 -0
- data/lib/mui/layout/leaf_node.rb +33 -0
- data/lib/mui/layout/node.rb +29 -0
- data/lib/mui/layout/split_node.rb +132 -0
- data/lib/mui/line_renderer.rb +173 -0
- data/lib/mui/mode.rb +13 -0
- data/lib/mui/mode_manager.rb +186 -0
- data/lib/mui/motion.rb +139 -0
- data/lib/mui/plugin.rb +35 -0
- data/lib/mui/plugin_manager.rb +106 -0
- data/lib/mui/register.rb +110 -0
- data/lib/mui/screen.rb +103 -0
- data/lib/mui/search_completer.rb +50 -0
- data/lib/mui/search_input.rb +40 -0
- data/lib/mui/search_state.rb +121 -0
- data/lib/mui/selection.rb +55 -0
- data/lib/mui/status_line_renderer.rb +40 -0
- data/lib/mui/syntax/language_detector.rb +106 -0
- data/lib/mui/syntax/lexer_base.rb +106 -0
- data/lib/mui/syntax/lexers/c_lexer.rb +127 -0
- data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
- data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
- data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
- data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
- data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
- data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
- data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
- data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
- data/lib/mui/syntax/token.rb +42 -0
- data/lib/mui/syntax/token_cache.rb +91 -0
- data/lib/mui/tab_bar_renderer.rb +87 -0
- data/lib/mui/tab_manager.rb +96 -0
- data/lib/mui/tab_page.rb +35 -0
- data/lib/mui/terminal_adapter/base.rb +92 -0
- data/lib/mui/terminal_adapter/curses.rb +164 -0
- data/lib/mui/terminal_adapter.rb +4 -0
- data/lib/mui/themes/default.rb +315 -0
- data/lib/mui/undo_manager.rb +83 -0
- data/lib/mui/undoable_action.rb +175 -0
- data/lib/mui/unicode_width.rb +100 -0
- data/lib/mui/version.rb +1 -1
- data/lib/mui/window.rb +201 -0
- data/lib/mui/window_manager.rb +256 -0
- data/lib/mui/wrap_cache.rb +40 -0
- data/lib/mui/wrap_helper.rb +84 -0
- data/lib/mui.rb +171 -2
- 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
|
data/lib/mui/key_code.rb
ADDED
|
@@ -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
|