rubish-gem 0.0.1
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 +7 -0
- data/.dockerignore +23 -0
- data/Dockerfile +54 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +12 -0
- data/lib/rubish/arithmetic.rb +140 -0
- data/lib/rubish/ast.rb +168 -0
- data/lib/rubish/builtins/arithmetic.rb +129 -0
- data/lib/rubish/builtins/bind_readline.rb +834 -0
- data/lib/rubish/builtins/directory_stack.rb +182 -0
- data/lib/rubish/builtins/echo_printf.rb +510 -0
- data/lib/rubish/builtins/hash_directories.rb +260 -0
- data/lib/rubish/builtins/read.rb +299 -0
- data/lib/rubish/builtins/trap.rb +324 -0
- data/lib/rubish/codegen.rb +1273 -0
- data/lib/rubish/completion.rb +840 -0
- data/lib/rubish/completions/bash_helpers.rb +530 -0
- data/lib/rubish/completions/git.rb +431 -0
- data/lib/rubish/completions/help_parser.rb +453 -0
- data/lib/rubish/completions/ssh.rb +114 -0
- data/lib/rubish/config.rb +267 -0
- data/lib/rubish/data/builtin_help.rb +716 -0
- data/lib/rubish/data/completion_data.rb +53 -0
- data/lib/rubish/data/readline_config.rb +47 -0
- data/lib/rubish/data/shell_options.rb +251 -0
- data/lib/rubish/data_define.rb +65 -0
- data/lib/rubish/execution_context.rb +1124 -0
- data/lib/rubish/expansion.rb +988 -0
- data/lib/rubish/history.rb +663 -0
- data/lib/rubish/lazy_loader.rb +127 -0
- data/lib/rubish/lexer.rb +1194 -0
- data/lib/rubish/parser.rb +1167 -0
- data/lib/rubish/prompt.rb +766 -0
- data/lib/rubish/repl.rb +2267 -0
- data/lib/rubish/runtime/builtins.rb +7222 -0
- data/lib/rubish/runtime/command.rb +1153 -0
- data/lib/rubish/runtime/job.rb +153 -0
- data/lib/rubish/runtime.rb +1169 -0
- data/lib/rubish/shell_state.rb +241 -0
- data/lib/rubish/startup_profiler.rb +67 -0
- data/lib/rubish/version.rb +5 -0
- data/lib/rubish.rb +60 -0
- data/sig/rubish.rbs +4 -0
- metadata +85 -0
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubish
|
|
4
|
+
module Builtins
|
|
5
|
+
# READLINE_LINE - contents of the readline buffer during bind -x execution
|
|
6
|
+
def readline_line
|
|
7
|
+
@state.readline_line_getter&.call || ''
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def readline_line=(value)
|
|
11
|
+
@state.readline_line_setter&.call(value.to_s)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# READLINE_POINT - cursor position (index) in READLINE_LINE during bind -x execution
|
|
15
|
+
def readline_point
|
|
16
|
+
@state.readline_point_getter&.call || 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def readline_point=(value)
|
|
20
|
+
@state.readline_point_setter&.call(value.to_i)
|
|
21
|
+
@state.readline_point_modified = true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# READLINE_MARK - mark position in READLINE_LINE during bind -x execution
|
|
25
|
+
def readline_mark
|
|
26
|
+
@state.readline_mark_getter&.call || 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def readline_mark=(value)
|
|
30
|
+
@state.readline_mark_setter&.call(value.to_i)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Track if READLINE_POINT was explicitly modified during bind -x
|
|
34
|
+
def readline_point_modified
|
|
35
|
+
@state.readline_point_modified
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def readline_point_modified=(value)
|
|
39
|
+
@state.readline_point_modified = value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Executor for bind -x commands
|
|
43
|
+
def bind_x_executor
|
|
44
|
+
@state.bind_x_executor
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def bind_x_executor=(value)
|
|
48
|
+
@state.bind_x_executor = value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def bind(args)
|
|
52
|
+
# bind [-m keymap] [-lpsvPSVX]
|
|
53
|
+
# bind [-m keymap] [-q function] [-u function] [-r keyseq]
|
|
54
|
+
# bind [-m keymap] -f filename
|
|
55
|
+
# bind [-m keymap] -x keyseq:shell-command
|
|
56
|
+
# bind [-m keymap] keyseq:function-name
|
|
57
|
+
# bind "set variable value"
|
|
58
|
+
|
|
59
|
+
keymap = 'emacs' # default keymap
|
|
60
|
+
list_functions = false
|
|
61
|
+
print_bindings = false
|
|
62
|
+
print_bindings_readable = false
|
|
63
|
+
print_macros = false
|
|
64
|
+
print_macros_readable = false
|
|
65
|
+
print_variables = false
|
|
66
|
+
print_variables_readable = false
|
|
67
|
+
print_shell_bindings = false
|
|
68
|
+
query_function = nil
|
|
69
|
+
unbind_function = nil
|
|
70
|
+
remove_keyseq = nil
|
|
71
|
+
read_file = nil
|
|
72
|
+
shell_command_binding = nil
|
|
73
|
+
bindings_to_add = []
|
|
74
|
+
variable_settings = []
|
|
75
|
+
|
|
76
|
+
i = 0
|
|
77
|
+
while i < args.length
|
|
78
|
+
arg = args[i]
|
|
79
|
+
|
|
80
|
+
if arg.start_with?('-') && !arg.include?(':')
|
|
81
|
+
case arg
|
|
82
|
+
when '-m'
|
|
83
|
+
i += 1
|
|
84
|
+
keymap = args[i] if args[i]
|
|
85
|
+
when '-l'
|
|
86
|
+
list_functions = true
|
|
87
|
+
when '-p'
|
|
88
|
+
print_bindings_readable = true
|
|
89
|
+
when '-P'
|
|
90
|
+
print_bindings = true
|
|
91
|
+
when '-s'
|
|
92
|
+
print_macros_readable = true
|
|
93
|
+
when '-S'
|
|
94
|
+
print_macros = true
|
|
95
|
+
when '-v'
|
|
96
|
+
print_variables_readable = true
|
|
97
|
+
when '-V'
|
|
98
|
+
print_variables = true
|
|
99
|
+
when '-X'
|
|
100
|
+
print_shell_bindings = true
|
|
101
|
+
when '-q'
|
|
102
|
+
i += 1
|
|
103
|
+
query_function = args[i]
|
|
104
|
+
when '-u'
|
|
105
|
+
i += 1
|
|
106
|
+
unbind_function = args[i]
|
|
107
|
+
when '-r'
|
|
108
|
+
i += 1
|
|
109
|
+
remove_keyseq = args[i]
|
|
110
|
+
when '-f'
|
|
111
|
+
i += 1
|
|
112
|
+
read_file = args[i]
|
|
113
|
+
when '-x'
|
|
114
|
+
i += 1
|
|
115
|
+
shell_command_binding = args[i]
|
|
116
|
+
else
|
|
117
|
+
# Handle combined flags
|
|
118
|
+
arg[1..].each_char do |c|
|
|
119
|
+
case c
|
|
120
|
+
when 'l' then list_functions = true
|
|
121
|
+
when 'p' then print_bindings_readable = true
|
|
122
|
+
when 'P' then print_bindings = true
|
|
123
|
+
when 's' then print_macros_readable = true
|
|
124
|
+
when 'S' then print_macros = true
|
|
125
|
+
when 'v' then print_variables_readable = true
|
|
126
|
+
when 'V' then print_variables = true
|
|
127
|
+
when 'X' then print_shell_bindings = true
|
|
128
|
+
else
|
|
129
|
+
puts "bind: -#{c}: invalid option"
|
|
130
|
+
return false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
elsif arg.start_with?('set ')
|
|
135
|
+
# Variable setting: set variable value
|
|
136
|
+
variable_settings << arg
|
|
137
|
+
elsif arg.include?(':')
|
|
138
|
+
# keyseq:function-name or keyseq:macro
|
|
139
|
+
bindings_to_add << arg
|
|
140
|
+
else
|
|
141
|
+
puts "bind: #{arg}: invalid key binding"
|
|
142
|
+
return false
|
|
143
|
+
end
|
|
144
|
+
i += 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# List all readline function names
|
|
148
|
+
if list_functions
|
|
149
|
+
READLINE_FUNCTIONS.each { |f| puts f }
|
|
150
|
+
return true
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Print key bindings in reusable format
|
|
154
|
+
if print_bindings_readable
|
|
155
|
+
# First show Reline's actual key bindings
|
|
156
|
+
get_reline_key_bindings.each do |keyseq, action|
|
|
157
|
+
puts "\"#{escape_keyseq(keyseq)}\": #{action}"
|
|
158
|
+
end
|
|
159
|
+
# Then show any additional bindings from @key_bindings
|
|
160
|
+
@state.key_bindings.each do |keyseq, binding|
|
|
161
|
+
next if binding[:type] == :macro || binding[:type] == :command
|
|
162
|
+
|
|
163
|
+
puts "\"#{escape_keyseq(keyseq)}\": #{binding[:value]}"
|
|
164
|
+
end
|
|
165
|
+
return true
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Print key bindings with function names
|
|
169
|
+
if print_bindings
|
|
170
|
+
# First show Reline's actual key bindings
|
|
171
|
+
get_reline_key_bindings.each do |keyseq, action|
|
|
172
|
+
puts "#{escape_keyseq(keyseq)} can be found in #{action}."
|
|
173
|
+
end
|
|
174
|
+
# Then show any additional bindings from @key_bindings
|
|
175
|
+
@state.key_bindings.each do |keyseq, binding|
|
|
176
|
+
next if binding[:type] == :macro || binding[:type] == :command
|
|
177
|
+
|
|
178
|
+
puts "#{escape_keyseq(keyseq)} can be found in #{binding[:value]}."
|
|
179
|
+
end
|
|
180
|
+
return true
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Print macros in reusable format
|
|
184
|
+
if print_macros_readable
|
|
185
|
+
@state.key_bindings.each do |keyseq, binding|
|
|
186
|
+
next unless binding[:type] == :macro
|
|
187
|
+
|
|
188
|
+
puts "\"#{escape_keyseq(keyseq)}\": \"#{binding[:value]}\""
|
|
189
|
+
end
|
|
190
|
+
return true
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Print macros
|
|
194
|
+
if print_macros
|
|
195
|
+
@state.key_bindings.each do |keyseq, binding|
|
|
196
|
+
next unless binding[:type] == :macro
|
|
197
|
+
|
|
198
|
+
puts "#{escape_keyseq(keyseq)} outputs #{binding[:value]}"
|
|
199
|
+
end
|
|
200
|
+
return true
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Print readline variables in reusable format
|
|
204
|
+
if print_variables_readable
|
|
205
|
+
READLINE_VARIABLES_LIST.each do |var|
|
|
206
|
+
value = get_readline_variable(var) || 'off'
|
|
207
|
+
puts "set #{var} #{value}"
|
|
208
|
+
end
|
|
209
|
+
return true
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Print readline variables
|
|
213
|
+
if print_variables
|
|
214
|
+
READLINE_VARIABLES_LIST.each do |var|
|
|
215
|
+
value = get_readline_variable(var) || 'off'
|
|
216
|
+
puts "#{var} is set to `#{value}'"
|
|
217
|
+
end
|
|
218
|
+
return true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Print shell command bindings
|
|
222
|
+
if print_shell_bindings
|
|
223
|
+
@state.key_bindings.each do |keyseq, binding|
|
|
224
|
+
next unless binding[:type] == :command
|
|
225
|
+
|
|
226
|
+
puts "\"#{escape_keyseq(keyseq)}\": \"#{binding[:value]}\""
|
|
227
|
+
end
|
|
228
|
+
return true
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Query which keys invoke a function
|
|
232
|
+
if query_function
|
|
233
|
+
found = false
|
|
234
|
+
@state.key_bindings.each do |keyseq, binding|
|
|
235
|
+
if binding[:value] == query_function && binding[:type] == :function
|
|
236
|
+
puts "#{query_function} can be invoked via \"#{escape_keyseq(keyseq)}\"."
|
|
237
|
+
found = true
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
puts "#{query_function} is not bound to any keys." unless found
|
|
241
|
+
return true
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Unbind all keys for a function
|
|
245
|
+
if unbind_function
|
|
246
|
+
@state.key_bindings.delete_if { |_, binding| binding[:value] == unbind_function }
|
|
247
|
+
return true
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Remove binding for keyseq
|
|
251
|
+
if remove_keyseq
|
|
252
|
+
@state.key_bindings.delete(remove_keyseq)
|
|
253
|
+
return true
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Read bindings from file
|
|
257
|
+
if read_file
|
|
258
|
+
unless File.exist?(read_file)
|
|
259
|
+
puts "bind: #{read_file}: cannot read: No such file or directory"
|
|
260
|
+
return false
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
File.readlines(read_file).each do |line|
|
|
264
|
+
line = line.strip
|
|
265
|
+
next if line.empty? || line.start_with?('#')
|
|
266
|
+
|
|
267
|
+
# Skip conditional directives ($if, $else, $endif, $include)
|
|
268
|
+
next if line.start_with?('$')
|
|
269
|
+
|
|
270
|
+
if line.start_with?('set ')
|
|
271
|
+
# Variable setting: set variable value
|
|
272
|
+
parts = line.split(/\s+/, 3)
|
|
273
|
+
if parts.length >= 3
|
|
274
|
+
apply_readline_variable(parts[1], parts[2])
|
|
275
|
+
end
|
|
276
|
+
elsif line.include?(':')
|
|
277
|
+
parse_and_add_binding(line, keymap)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
return true
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Add shell command binding
|
|
284
|
+
if shell_command_binding
|
|
285
|
+
if shell_command_binding.include?(':')
|
|
286
|
+
keyseq, command = shell_command_binding.split(':', 2)
|
|
287
|
+
keyseq = unescape_keyseq(keyseq.delete('"'))
|
|
288
|
+
command = command.delete('"').strip
|
|
289
|
+
@state.key_bindings[keyseq] = {type: :command, value: command, keymap: keymap}
|
|
290
|
+
# Register with Reline for actual execution
|
|
291
|
+
register_bind_x_with_reline(keyseq, command, keymap)
|
|
292
|
+
else
|
|
293
|
+
puts "bind: #{shell_command_binding}: invalid key binding"
|
|
294
|
+
return false
|
|
295
|
+
end
|
|
296
|
+
return true
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Add bindings from arguments
|
|
300
|
+
bindings_to_add.each do |binding|
|
|
301
|
+
parse_and_add_binding(binding, keymap)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Process variable settings: "set variable value"
|
|
305
|
+
variable_settings.each do |setting|
|
|
306
|
+
parts = setting.split(/\s+/, 3)
|
|
307
|
+
if parts.length >= 3 && parts[0] == 'set'
|
|
308
|
+
apply_readline_variable(parts[1], parts[2])
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
true
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def parse_and_add_binding(binding_str, keymap = 'emacs')
|
|
316
|
+
keyseq, value = binding_str.split(':', 2)
|
|
317
|
+
return unless keyseq && value
|
|
318
|
+
|
|
319
|
+
keyseq = unescape_keyseq(keyseq.delete('"').strip)
|
|
320
|
+
value = value.strip
|
|
321
|
+
|
|
322
|
+
# Determine if it's a function or macro
|
|
323
|
+
if value.start_with?('"') && value.end_with?('"')
|
|
324
|
+
# Macro
|
|
325
|
+
@state.key_bindings[keyseq] = {type: :macro, value: value[1..-2], keymap: keymap}
|
|
326
|
+
else
|
|
327
|
+
# Function
|
|
328
|
+
@state.key_bindings[keyseq] = {type: :function, value: value, keymap: keymap}
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Get Reline's actual key bindings as a hash of keyseq string => action symbol
|
|
333
|
+
# Combines bindings from both @additional_key_bindings (higher priority, from inputrc/custom)
|
|
334
|
+
# and @default_key_bindings (lower priority, built-in defaults)
|
|
335
|
+
def get_reline_key_bindings
|
|
336
|
+
result = {}
|
|
337
|
+
begin
|
|
338
|
+
config = Reline.core.config
|
|
339
|
+
editing_mode = config.instance_variable_get(:@editing_mode_label) || :emacs
|
|
340
|
+
|
|
341
|
+
# First, get default key bindings (lowest priority)
|
|
342
|
+
default_bindings = config.instance_variable_get(:@default_key_bindings)
|
|
343
|
+
if default_bindings && default_bindings[editing_mode]
|
|
344
|
+
bindings = default_bindings[editing_mode].instance_variable_get(:@key_bindings)
|
|
345
|
+
bindings&.each do |seq, action|
|
|
346
|
+
next if action == :ed_insert || action == :ed_digit
|
|
347
|
+
keyseq = seq.pack('C*')
|
|
348
|
+
result[keyseq] = action
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Then, get additional key bindings (higher priority, will override defaults)
|
|
353
|
+
additional_bindings = config.instance_variable_get(:@additional_key_bindings)
|
|
354
|
+
if additional_bindings && additional_bindings[editing_mode]
|
|
355
|
+
bindings = additional_bindings[editing_mode].instance_variable_get(:@key_bindings)
|
|
356
|
+
bindings&.each do |seq, action|
|
|
357
|
+
next if action == :ed_insert || action == :ed_digit
|
|
358
|
+
keyseq = seq.pack('C*')
|
|
359
|
+
result[keyseq] = action
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
rescue StandardError
|
|
363
|
+
# Reline not available or error accessing bindings
|
|
364
|
+
end
|
|
365
|
+
result
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def escape_keyseq(keyseq)
|
|
369
|
+
result = +''
|
|
370
|
+
keyseq.each_char do |c|
|
|
371
|
+
case c.ord
|
|
372
|
+
when 0x00..0x1F
|
|
373
|
+
if c == "\t"
|
|
374
|
+
result << '\\t'
|
|
375
|
+
elsif c == "\n"
|
|
376
|
+
result << '\\n'
|
|
377
|
+
elsif c == "\r"
|
|
378
|
+
result << '\\r'
|
|
379
|
+
elsif c == "\e"
|
|
380
|
+
result << '\\e'
|
|
381
|
+
else
|
|
382
|
+
# Control character: display as \C-x
|
|
383
|
+
result << "\\C-#{(c.ord + 'a'.ord - 1).chr}"
|
|
384
|
+
end
|
|
385
|
+
when 0x7F
|
|
386
|
+
result << '\\C-?'
|
|
387
|
+
when 0x80..0x9F
|
|
388
|
+
# Meta control character
|
|
389
|
+
result << "\\M-\\C-#{(c.ord - 0x80 + 'a'.ord - 1).chr}"
|
|
390
|
+
when 0xA0..0xFF
|
|
391
|
+
# Meta character
|
|
392
|
+
result << "\\M-#{(c.ord - 0x80).chr}"
|
|
393
|
+
else
|
|
394
|
+
result << c
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
result
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def unescape_keyseq(keyseq)
|
|
401
|
+
result = keyseq.dup
|
|
402
|
+
|
|
403
|
+
# Handle meta escape sequences first (\M-x)
|
|
404
|
+
result.gsub!(/\\M-\\C-([a-zA-Z@\[\]\\^_?])/) do |_|
|
|
405
|
+
char = ::Regexp.last_match(1)
|
|
406
|
+
if char == '?'
|
|
407
|
+
(0x80 | 0x7F).chr # Meta-DEL
|
|
408
|
+
else
|
|
409
|
+
(0x80 | (char.upcase.ord & 0x1F)).chr
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
result.gsub!(/\\M-([^\s])/) do |_|
|
|
414
|
+
char = ::Regexp.last_match(1)
|
|
415
|
+
(0x80 | char.ord).chr
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Handle escape sequences
|
|
419
|
+
result.gsub!('\\e', "\e")
|
|
420
|
+
result.gsub!('\\E', "\e") # Both \e and \E mean escape
|
|
421
|
+
result.gsub!('\\t', "\t")
|
|
422
|
+
result.gsub!('\\n', "\n")
|
|
423
|
+
result.gsub!('\\r', "\r")
|
|
424
|
+
result.gsub!('\\a', "\a") # Bell
|
|
425
|
+
result.gsub!('\\b', "\b") # Backspace
|
|
426
|
+
result.gsub!('\\f', "\f") # Form feed
|
|
427
|
+
result.gsub!('\\v', "\v") # Vertical tab
|
|
428
|
+
result.gsub!('\\\\', '\\') # Literal backslash
|
|
429
|
+
result.gsub!('\\"', '"') # Literal quote
|
|
430
|
+
result.gsub!("\\'", "'") # Literal single quote
|
|
431
|
+
|
|
432
|
+
# Handle octal escape sequences \nnn
|
|
433
|
+
result.gsub!(/\\([0-7]{1,3})/) do |_|
|
|
434
|
+
::Regexp.last_match(1).to_i(8).chr
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Handle hex escape sequences \xNN
|
|
438
|
+
result.gsub!(/\\x([0-9a-fA-F]{1,2})/) do |_|
|
|
439
|
+
::Regexp.last_match(1).to_i(16).chr
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Handle control characters \C-x format
|
|
443
|
+
result.gsub!(/\\C-([a-zA-Z@\[\]\\^_])/) do |_|
|
|
444
|
+
char = ::Regexp.last_match(1)
|
|
445
|
+
(char.upcase.ord & 0x1F).chr
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Handle control characters \C-? format for DEL
|
|
449
|
+
result.gsub!('\\C-?', "\x7F")
|
|
450
|
+
|
|
451
|
+
# Handle ^x control character format
|
|
452
|
+
result.gsub!(/\^([a-zA-Z@\[\]\\^_?])/) do |_|
|
|
453
|
+
char = ::Regexp.last_match(1)
|
|
454
|
+
if char == '?'
|
|
455
|
+
"\x7F" # DEL
|
|
456
|
+
else
|
|
457
|
+
(char.upcase.ord & 0x1F).chr
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
result
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def get_key_binding(keyseq)
|
|
465
|
+
@state.key_bindings[keyseq]
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def clear_key_bindings
|
|
469
|
+
@state.key_bindings.clear
|
|
470
|
+
@state.readline_variables.clear
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Register a bind -x shell command with Reline for actual execution
|
|
474
|
+
def register_bind_x_with_reline(keyseq, command, keymap)
|
|
475
|
+
return unless defined?(Reline)
|
|
476
|
+
|
|
477
|
+
# Generate a unique method name for this binding
|
|
478
|
+
method_name = :"__rubish_bind_x_#{@state.bind_x_counter}"
|
|
479
|
+
@state.bind_x_counter += 1
|
|
480
|
+
|
|
481
|
+
# Store the command in the binding for lookup
|
|
482
|
+
@state.key_bindings[keyseq][:method_name] = method_name
|
|
483
|
+
|
|
484
|
+
# Define the method on Reline::LineEditor
|
|
485
|
+
# We need to capture 'command' and 'self' (Builtins) in the closure
|
|
486
|
+
builtins = self
|
|
487
|
+
Reline::LineEditor.define_method(method_name) do |key = nil, **kwargs|
|
|
488
|
+
# Get current line and cursor position from the line editor
|
|
489
|
+
current_line = whole_buffer
|
|
490
|
+
current_point = byte_pointer
|
|
491
|
+
|
|
492
|
+
# Set READLINE_LINE, READLINE_POINT, READLINE_MARK
|
|
493
|
+
builtins.readline_line = current_line
|
|
494
|
+
builtins.readline_point = current_point
|
|
495
|
+
builtins.readline_mark = 0
|
|
496
|
+
|
|
497
|
+
# Track if READLINE_POINT is explicitly modified
|
|
498
|
+
builtins.readline_point_modified = false
|
|
499
|
+
|
|
500
|
+
# Execute the shell command
|
|
501
|
+
if builtins.bind_x_executor
|
|
502
|
+
begin
|
|
503
|
+
builtins.bind_x_executor.call(command)
|
|
504
|
+
rescue => e
|
|
505
|
+
$stderr.puts "bind -x: #{e.message}" if ENV['RUBISH_DEBUG']
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Check if READLINE_LINE was modified
|
|
510
|
+
new_line = builtins.readline_line || current_line
|
|
511
|
+
new_point = builtins.readline_point
|
|
512
|
+
point_was_modified = builtins.readline_point_modified
|
|
513
|
+
|
|
514
|
+
# Update the line buffer if it changed
|
|
515
|
+
if new_line != current_line
|
|
516
|
+
# Clear and replace the buffer
|
|
517
|
+
@buffer_of_lines = new_line.split("\n", -1)
|
|
518
|
+
@buffer_of_lines = [''] if @buffer_of_lines.empty?
|
|
519
|
+
@line_index = @buffer_of_lines.length - 1
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Update cursor position
|
|
523
|
+
if point_was_modified
|
|
524
|
+
# READLINE_POINT was explicitly set - use it
|
|
525
|
+
self.byte_pointer = [new_point, new_line.bytesize].min
|
|
526
|
+
elsif new_line != current_line
|
|
527
|
+
# Line changed but point not explicitly set - move to end
|
|
528
|
+
self.byte_pointer = new_line.bytesize
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Convert keymap name to Reline keymap symbol
|
|
533
|
+
reline_keymap = case keymap
|
|
534
|
+
when 'vi', 'vi-command' then :vi_command
|
|
535
|
+
when 'vi-insert' then :vi_insert
|
|
536
|
+
else :emacs
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Register the key binding with Reline
|
|
540
|
+
keystroke = keyseq.bytes.to_a
|
|
541
|
+
Reline.core.config.add_default_key_binding_by_keymap(reline_keymap, keystroke, method_name)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Apply a readline variable to Reline (if applicable)
|
|
545
|
+
def apply_readline_variable(var, value)
|
|
546
|
+
@state.readline_variables[var] = value
|
|
547
|
+
|
|
548
|
+
# Sync with Reline where possible
|
|
549
|
+
begin
|
|
550
|
+
case var
|
|
551
|
+
when 'editing-mode'
|
|
552
|
+
if value == 'vi'
|
|
553
|
+
Reline.vi_editing_mode if defined?(Reline)
|
|
554
|
+
else
|
|
555
|
+
Reline.emacs_editing_mode if defined?(Reline)
|
|
556
|
+
end
|
|
557
|
+
when 'completion-ignore-case'
|
|
558
|
+
if defined?(Reline)
|
|
559
|
+
Reline.completion_case_fold = (value == 'on')
|
|
560
|
+
end
|
|
561
|
+
when 'horizontal-scroll-mode'
|
|
562
|
+
# Reline doesn't support this, but we store it
|
|
563
|
+
when 'mark-directories'
|
|
564
|
+
# Reline doesn't directly support, but completion can check this
|
|
565
|
+
when 'show-all-if-ambiguous'
|
|
566
|
+
# Could be implemented in completion_proc
|
|
567
|
+
when 'bell-style'
|
|
568
|
+
# Reline doesn't expose bell control
|
|
569
|
+
end
|
|
570
|
+
rescue => e
|
|
571
|
+
$stderr.puts "bind: warning: #{e.message}" if ENV['RUBISH_DEBUG']
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Get a readline variable value
|
|
576
|
+
def get_readline_variable(var)
|
|
577
|
+
# Check Reline state first for live values
|
|
578
|
+
begin
|
|
579
|
+
case var
|
|
580
|
+
when 'editing-mode'
|
|
581
|
+
if defined?(Reline)
|
|
582
|
+
return Reline.vi_editing_mode? ? 'vi' : 'emacs'
|
|
583
|
+
end
|
|
584
|
+
when 'completion-ignore-case'
|
|
585
|
+
if defined?(Reline)
|
|
586
|
+
return Reline.completion_case_fold ? 'on' : 'off'
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
rescue
|
|
590
|
+
# Fall through to stored value
|
|
591
|
+
end
|
|
592
|
+
@state.readline_variables[var]
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def bindkey(args)
|
|
596
|
+
keymap = nil
|
|
597
|
+
list_keymaps = false
|
|
598
|
+
remove_key = nil
|
|
599
|
+
macro_binding = false
|
|
600
|
+
i = 0
|
|
601
|
+
|
|
602
|
+
while i < args.length
|
|
603
|
+
arg = args[i]
|
|
604
|
+
|
|
605
|
+
if arg.start_with?('-')
|
|
606
|
+
case arg
|
|
607
|
+
when '-l'
|
|
608
|
+
list_keymaps = true
|
|
609
|
+
when '-L'
|
|
610
|
+
# List in bindkey command format (same as no args for now)
|
|
611
|
+
return list_bindkey_bindings(keymap)
|
|
612
|
+
when '-M'
|
|
613
|
+
i += 1
|
|
614
|
+
keymap = args[i] if args[i]
|
|
615
|
+
when '-e'
|
|
616
|
+
# Select emacs keymap
|
|
617
|
+
select_keymap('emacs')
|
|
618
|
+
return true
|
|
619
|
+
when '-v'
|
|
620
|
+
# Select viins keymap
|
|
621
|
+
select_keymap('viins')
|
|
622
|
+
return true
|
|
623
|
+
when '-a'
|
|
624
|
+
# Select vicmd keymap
|
|
625
|
+
select_keymap('vicmd')
|
|
626
|
+
return true
|
|
627
|
+
when '-r'
|
|
628
|
+
i += 1
|
|
629
|
+
remove_key = args[i] if args[i]
|
|
630
|
+
when '-s'
|
|
631
|
+
macro_binding = true
|
|
632
|
+
else
|
|
633
|
+
$stderr.puts "bindkey: bad option: #{arg}"
|
|
634
|
+
return false
|
|
635
|
+
end
|
|
636
|
+
else
|
|
637
|
+
# Non-option argument
|
|
638
|
+
break
|
|
639
|
+
end
|
|
640
|
+
i += 1
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# List keymap names
|
|
644
|
+
if list_keymaps
|
|
645
|
+
puts 'emacs'
|
|
646
|
+
puts 'viins'
|
|
647
|
+
puts 'vicmd'
|
|
648
|
+
puts 'visual'
|
|
649
|
+
puts 'isearch'
|
|
650
|
+
puts 'command'
|
|
651
|
+
puts 'main'
|
|
652
|
+
return true
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# Remove binding
|
|
656
|
+
if remove_key
|
|
657
|
+
keyseq = parse_bindkey_keyseq(remove_key)
|
|
658
|
+
@state.key_bindings.delete(keyseq)
|
|
659
|
+
return true
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
remaining_args = args[i..]
|
|
663
|
+
|
|
664
|
+
# No more args - list all bindings
|
|
665
|
+
if remaining_args.empty?
|
|
666
|
+
return list_bindkey_bindings(keymap)
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# One arg - show binding for that key
|
|
670
|
+
if remaining_args.length == 1
|
|
671
|
+
keyseq = parse_bindkey_keyseq(remaining_args[0])
|
|
672
|
+
binding = @state.key_bindings[keyseq]
|
|
673
|
+
if binding
|
|
674
|
+
puts "\"#{format_bindkey_keyseq(keyseq)}\" #{binding[:value]}"
|
|
675
|
+
else
|
|
676
|
+
puts "\"#{format_bindkey_keyseq(keyseq)}\" undefined-key"
|
|
677
|
+
end
|
|
678
|
+
return true
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Two args - bind key to widget/macro
|
|
682
|
+
keyseq = parse_bindkey_keyseq(remaining_args[0])
|
|
683
|
+
value = remaining_args[1]
|
|
684
|
+
|
|
685
|
+
if macro_binding
|
|
686
|
+
# -s: bind to macro (string output)
|
|
687
|
+
@state.key_bindings[keyseq] = {type: :macro, value: parse_bindkey_keyseq(value), keymap: keymap || 'main'}
|
|
688
|
+
else
|
|
689
|
+
# Bind to widget (function)
|
|
690
|
+
@state.key_bindings[keyseq] = {type: :function, value: value, keymap: keymap || 'main'}
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
true
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Parse zsh-style key sequence (e.g., "^A", "^[a", "\e[A")
|
|
697
|
+
def parse_bindkey_keyseq(str)
|
|
698
|
+
return str if str.nil? || str.empty?
|
|
699
|
+
|
|
700
|
+
result = +''
|
|
701
|
+
i = 0
|
|
702
|
+
|
|
703
|
+
# Remove surrounding quotes if present
|
|
704
|
+
if (str.start_with?('"') && str.end_with?('"')) ||
|
|
705
|
+
(str.start_with?("'") && str.end_with?("'"))
|
|
706
|
+
str = str[1...-1]
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
while i < str.length
|
|
710
|
+
char = str[i]
|
|
711
|
+
|
|
712
|
+
if char == '^' && i + 1 < str.length
|
|
713
|
+
# ^X -> Ctrl-X
|
|
714
|
+
next_char = str[i + 1]
|
|
715
|
+
if next_char == '?'
|
|
716
|
+
result << "\x7F" # DEL
|
|
717
|
+
elsif next_char == '['
|
|
718
|
+
result << "\e" # ESC
|
|
719
|
+
else
|
|
720
|
+
# Convert to control character
|
|
721
|
+
result << (next_char.upcase.ord & 0x1F).chr
|
|
722
|
+
end
|
|
723
|
+
i += 2
|
|
724
|
+
elsif char == '\\' && i + 1 < str.length
|
|
725
|
+
next_char = str[i + 1]
|
|
726
|
+
case next_char
|
|
727
|
+
when 'e', 'E'
|
|
728
|
+
result << "\e"
|
|
729
|
+
i += 2
|
|
730
|
+
when 'n'
|
|
731
|
+
result << "\n"
|
|
732
|
+
i += 2
|
|
733
|
+
when 'r'
|
|
734
|
+
result << "\r"
|
|
735
|
+
i += 2
|
|
736
|
+
when 't'
|
|
737
|
+
result << "\t"
|
|
738
|
+
i += 2
|
|
739
|
+
when '\\'
|
|
740
|
+
result << '\\'
|
|
741
|
+
i += 2
|
|
742
|
+
when 'C'
|
|
743
|
+
# \C-x -> Ctrl-x
|
|
744
|
+
if i + 3 < str.length && str[i + 2] == '-'
|
|
745
|
+
ctrl_char = str[i + 3]
|
|
746
|
+
result << (ctrl_char.upcase.ord & 0x1F).chr
|
|
747
|
+
i += 4
|
|
748
|
+
else
|
|
749
|
+
result << char
|
|
750
|
+
i += 1
|
|
751
|
+
end
|
|
752
|
+
when 'M'
|
|
753
|
+
# \M-x -> Meta-x (ESC + x)
|
|
754
|
+
if i + 3 < str.length && str[i + 2] == '-'
|
|
755
|
+
result << "\e" << str[i + 3]
|
|
756
|
+
i += 4
|
|
757
|
+
else
|
|
758
|
+
result << char
|
|
759
|
+
i += 1
|
|
760
|
+
end
|
|
761
|
+
else
|
|
762
|
+
result << next_char
|
|
763
|
+
i += 2
|
|
764
|
+
end
|
|
765
|
+
else
|
|
766
|
+
result << char
|
|
767
|
+
i += 1
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
result
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# Format key sequence for display
|
|
775
|
+
def format_bindkey_keyseq(keyseq)
|
|
776
|
+
return '' if keyseq.nil? || keyseq.empty?
|
|
777
|
+
|
|
778
|
+
result = +''
|
|
779
|
+
keyseq.each_char do |char|
|
|
780
|
+
ord = char.ord
|
|
781
|
+
if ord < 32
|
|
782
|
+
if ord == 27
|
|
783
|
+
result << '^['
|
|
784
|
+
else
|
|
785
|
+
result << '^' << (ord + 64).chr
|
|
786
|
+
end
|
|
787
|
+
elsif ord == 127
|
|
788
|
+
result << '^?'
|
|
789
|
+
else
|
|
790
|
+
result << char
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
result
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# List all key bindings in bindkey format
|
|
797
|
+
def list_bindkey_bindings(keymap = nil)
|
|
798
|
+
if @state.key_bindings.empty?
|
|
799
|
+
# Show some default bindings
|
|
800
|
+
puts '"^A" beginning-of-line'
|
|
801
|
+
puts '"^E" end-of-line'
|
|
802
|
+
puts '"^K" kill-line'
|
|
803
|
+
puts '"^U" unix-line-discard'
|
|
804
|
+
puts '"^W" backward-kill-word'
|
|
805
|
+
return true
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
@state.key_bindings.each do |keyseq, binding|
|
|
809
|
+
next if keymap && binding[:keymap] != keymap
|
|
810
|
+
|
|
811
|
+
formatted_key = format_bindkey_keyseq(keyseq)
|
|
812
|
+
case binding[:type]
|
|
813
|
+
when :function
|
|
814
|
+
puts "\"#{formatted_key}\" #{binding[:value]}"
|
|
815
|
+
when :macro
|
|
816
|
+
puts "\"#{formatted_key}\" \"#{format_bindkey_keyseq(binding[:value])}\""
|
|
817
|
+
when :command
|
|
818
|
+
puts "\"#{formatted_key}\" \"#{binding[:value]}\""
|
|
819
|
+
end
|
|
820
|
+
end
|
|
821
|
+
true
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Select a keymap (emacs or vi)
|
|
825
|
+
def select_keymap(keymap)
|
|
826
|
+
case keymap
|
|
827
|
+
when 'emacs', 'main'
|
|
828
|
+
apply_readline_variable('editing-mode', 'emacs')
|
|
829
|
+
when 'viins', 'vicmd', 'vi'
|
|
830
|
+
apply_readline_variable('editing-mode', 'vi')
|
|
831
|
+
end
|
|
832
|
+
end
|
|
833
|
+
end
|
|
834
|
+
end
|