thor-interactive 0.1.0.pre.5 → 0.1.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/README.md +143 -399
- data/docs/assets/thor-interactive-wide.png +0 -0
- data/docs/assets/thor-interactive.png +0 -0
- data/examples/tui_demo.rb +94 -0
- data/lib/thor/interactive/command.rb +12 -1
- data/lib/thor/interactive/command_dispatch.rb +410 -0
- data/lib/thor/interactive/shell.rb +41 -517
- data/lib/thor/interactive/tui/output_buffer.rb +87 -0
- data/lib/thor/interactive/tui/ratatui_shell.rb +606 -0
- data/lib/thor/interactive/tui/spinner.rb +107 -0
- data/lib/thor/interactive/tui/status_bar.rb +58 -0
- data/lib/thor/interactive/tui/text_input.rb +218 -0
- data/lib/thor/interactive/tui/theme.rb +158 -0
- data/lib/thor/interactive/tui.rb +14 -0
- data/lib/thor/interactive/version_constant.rb +1 -1
- metadata +14 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# TUI Demo - demonstrates the ratatui_ruby-powered TUI mode
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# ruby examples/tui_demo.rb interactive
|
|
8
|
+
# ruby examples/tui_demo.rb interactive --theme dark
|
|
9
|
+
#
|
|
10
|
+
# Key bindings:
|
|
11
|
+
# Enter - Submit input
|
|
12
|
+
# Shift+Enter - New line (requires Kitty keyboard protocol)
|
|
13
|
+
# Alt+Enter - New line (alternative)
|
|
14
|
+
# Ctrl+N - Toggle multi-line mode (fallback for older terminals)
|
|
15
|
+
# Ctrl+J - Always submit (even in multi-line mode)
|
|
16
|
+
# Tab - Auto-complete commands
|
|
17
|
+
# Ctrl+C - Clear input / double-tap to exit
|
|
18
|
+
# Ctrl+D - Exit
|
|
19
|
+
# Escape - Clear input / exit multi-line mode
|
|
20
|
+
#
|
|
21
|
+
# Requires: gem install ratatui_ruby
|
|
22
|
+
|
|
23
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
24
|
+
|
|
25
|
+
require "thor"
|
|
26
|
+
require "thor/interactive"
|
|
27
|
+
|
|
28
|
+
class TuiDemo < Thor
|
|
29
|
+
include Thor::Interactive::Command
|
|
30
|
+
|
|
31
|
+
configure_interactive(
|
|
32
|
+
ui_mode: :tui,
|
|
33
|
+
prompt: "demo> ",
|
|
34
|
+
# theme: :dark, # Try: :default, :dark, :light, :minimal
|
|
35
|
+
status_bar: {
|
|
36
|
+
left: ->(instance) { " TUI Demo" },
|
|
37
|
+
right: ->(instance) { " commands: #{instance.class.tasks.count} " }
|
|
38
|
+
},
|
|
39
|
+
# Custom spinner messages (optional - defaults are fun too)
|
|
40
|
+
spinner_messages: ["Thinking", "Brewing", "Crunching", "Vibing", "Noodling"]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
desc "hello", "Say hello"
|
|
44
|
+
def hello
|
|
45
|
+
puts "Hello from TUI mode!"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
desc "greet NAME", "Greet someone by name"
|
|
49
|
+
def greet(name)
|
|
50
|
+
puts "Hello, #{name}! Welcome to the TUI demo."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "count N", "Count from 1 to N"
|
|
54
|
+
def count(n)
|
|
55
|
+
n.to_i.times do |i|
|
|
56
|
+
puts "#{i + 1}..."
|
|
57
|
+
sleep(0.1) # Slow enough to see the spinner
|
|
58
|
+
end
|
|
59
|
+
puts "Done counting!"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc "status", "Show current status"
|
|
63
|
+
def status
|
|
64
|
+
puts "TUI Demo Status:"
|
|
65
|
+
puts " Session: active"
|
|
66
|
+
puts " Mode: TUI (ratatui_ruby)"
|
|
67
|
+
puts " Theme: #{self.class.interactive_options[:theme] || :default}"
|
|
68
|
+
puts " Commands available: #{self.class.tasks.keys.join(", ")}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
desc "error_demo", "Demonstrate error handling"
|
|
72
|
+
def error_demo
|
|
73
|
+
puts "About to raise an error..."
|
|
74
|
+
raise "This is a demo error to show error handling"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
desc "slow", "Demonstrate spinner with a slow command"
|
|
78
|
+
def slow
|
|
79
|
+
puts "Starting slow operation..."
|
|
80
|
+
sleep(3)
|
|
81
|
+
puts "Slow operation complete!"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
desc "multiline", "Print multiple lines of output"
|
|
85
|
+
def multiline
|
|
86
|
+
puts "Line 1: This is a multi-line output demo"
|
|
87
|
+
puts "Line 2: Each line appears in the output buffer"
|
|
88
|
+
puts "Line 3: You can scroll up/down with PageUp/PageDown"
|
|
89
|
+
puts "Line 4: The output persists between commands"
|
|
90
|
+
puts "Line 5: End of demo output"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
TuiDemo.start(ARGV)
|
|
@@ -25,7 +25,18 @@ class Thor
|
|
|
25
25
|
opts[:prompt] = options[:prompt] || options["prompt"] if options[:prompt] || options["prompt"]
|
|
26
26
|
opts[:history_file] = options[:history_file] || options["history_file"] if options[:history_file] || options["history_file"]
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
if opts[:ui_mode] == :tui
|
|
29
|
+
require_relative "tui"
|
|
30
|
+
if Thor::Interactive::TUI.available?
|
|
31
|
+
require_relative "tui/ratatui_shell"
|
|
32
|
+
Thor::Interactive::TUI::RatatuiShell.new(self.class, opts).start
|
|
33
|
+
else
|
|
34
|
+
warn "ratatui_ruby gem not found, falling back to standard shell"
|
|
35
|
+
Thor::Interactive::Shell.new(self.class, opts).start
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
Thor::Interactive::Shell.new(self.class, opts).start
|
|
39
|
+
end
|
|
29
40
|
end
|
|
30
41
|
end
|
|
31
42
|
end
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require "thor"
|
|
5
|
+
|
|
6
|
+
class Thor
|
|
7
|
+
module Interactive
|
|
8
|
+
# Shared command dispatch logic used by both the Reline-based Shell
|
|
9
|
+
# and the ratatui_ruby-based TUI shell.
|
|
10
|
+
#
|
|
11
|
+
# Includers must provide:
|
|
12
|
+
# @thor_class - the Thor class
|
|
13
|
+
# @thor_instance - a persistent Thor instance
|
|
14
|
+
# @default_handler - optional proc for natural language input
|
|
15
|
+
# @merged_options - merged configuration options
|
|
16
|
+
module CommandDispatch
|
|
17
|
+
EXIT_COMMANDS = %w[exit quit q].freeze
|
|
18
|
+
|
|
19
|
+
def process_input(input)
|
|
20
|
+
return if input.nil? || input.strip.empty?
|
|
21
|
+
|
|
22
|
+
if input.strip.start_with?("/")
|
|
23
|
+
handle_slash_command(input.strip[1..-1])
|
|
24
|
+
elsif is_help_request?(input)
|
|
25
|
+
if input.strip.split.length == 1
|
|
26
|
+
show_help
|
|
27
|
+
else
|
|
28
|
+
command_part = input.strip.split[1]
|
|
29
|
+
show_help(command_part)
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
command_word = input.strip.split(/\s+/, 2).first
|
|
33
|
+
|
|
34
|
+
if thor_command?(command_word)
|
|
35
|
+
handle_command(input.strip)
|
|
36
|
+
elsif @default_handler
|
|
37
|
+
begin
|
|
38
|
+
@default_handler.call(input, @thor_instance)
|
|
39
|
+
rescue => e
|
|
40
|
+
puts "Error in default handler: #{e.message}"
|
|
41
|
+
puts "Input was: #{input}"
|
|
42
|
+
puts "Try using /commands or type '/help' for available commands."
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
puts "No default handler configured. Use /command for commands, or type '/help' for available commands."
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def handle_slash_command(command_input)
|
|
51
|
+
return if command_input.empty?
|
|
52
|
+
handle_command(command_input)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def handle_command(command_input)
|
|
56
|
+
command_word = command_input.split(/\s+/, 2).first
|
|
57
|
+
|
|
58
|
+
if thor_command?(command_word)
|
|
59
|
+
task = @thor_class.tasks[command_word]
|
|
60
|
+
|
|
61
|
+
if task && single_text_command?(task) && !task.options.any?
|
|
62
|
+
text_part = command_input.sub(/^#{Regexp.escape(command_word)}\s*/, "")
|
|
63
|
+
if text_part.empty?
|
|
64
|
+
invoke_thor_command(command_word, [])
|
|
65
|
+
else
|
|
66
|
+
invoke_thor_command(command_word, [text_part])
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
args = safe_parse_input(command_input)
|
|
70
|
+
if args && !args.empty?
|
|
71
|
+
command = args.shift
|
|
72
|
+
invoke_thor_command(command, args)
|
|
73
|
+
else
|
|
74
|
+
parts = command_input.split(/\s+/)
|
|
75
|
+
command = parts.shift
|
|
76
|
+
invoke_thor_command(command, parts)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
puts "Unknown command: '#{command_word}'. Type '/help' for available commands."
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def safe_parse_input(input)
|
|
85
|
+
Shellwords.split(input)
|
|
86
|
+
rescue ArgumentError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def parse_input(input)
|
|
91
|
+
safe_parse_input(input) || []
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_unparseable_command(input, command_word)
|
|
95
|
+
text_part = input.strip.sub(/^#{Regexp.escape(command_word)}\s*/, "")
|
|
96
|
+
if text_part.empty?
|
|
97
|
+
invoke_thor_command(command_word, [])
|
|
98
|
+
else
|
|
99
|
+
invoke_thor_command(command_word, [text_part])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def single_text_command?(task)
|
|
104
|
+
return false unless task
|
|
105
|
+
|
|
106
|
+
method_name = task.name.to_sym
|
|
107
|
+
if @thor_instance.respond_to?(method_name)
|
|
108
|
+
method_obj = @thor_instance.method(method_name)
|
|
109
|
+
param_count = method_obj.parameters.count { |type, _| type == :req }
|
|
110
|
+
param_count == 1
|
|
111
|
+
else
|
|
112
|
+
false
|
|
113
|
+
end
|
|
114
|
+
rescue
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def is_help_request?(input)
|
|
119
|
+
stripped = input.strip.downcase
|
|
120
|
+
stripped == "help" || stripped.start_with?("help ")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def thor_command?(command)
|
|
124
|
+
@thor_class.tasks.key?(command) ||
|
|
125
|
+
@thor_class.subcommand_classes.key?(command) ||
|
|
126
|
+
command == "help"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def invoke_thor_command(command, args)
|
|
130
|
+
if command == "help"
|
|
131
|
+
show_help(args.first)
|
|
132
|
+
else
|
|
133
|
+
task = @thor_class.tasks[command]
|
|
134
|
+
|
|
135
|
+
if task && task.options && !task.options.empty?
|
|
136
|
+
result = parse_thor_options(args, task)
|
|
137
|
+
return unless result
|
|
138
|
+
|
|
139
|
+
parsed_args, parsed_options = result
|
|
140
|
+
|
|
141
|
+
@thor_instance.options = Thor::CoreExt::HashWithIndifferentAccess.new(parsed_options)
|
|
142
|
+
|
|
143
|
+
if @thor_instance.respond_to?(command)
|
|
144
|
+
@thor_instance.send(command, *parsed_args)
|
|
145
|
+
else
|
|
146
|
+
@thor_instance.send(command, *parsed_args)
|
|
147
|
+
end
|
|
148
|
+
else
|
|
149
|
+
if @thor_instance.respond_to?(command)
|
|
150
|
+
@thor_instance.send(command, *args)
|
|
151
|
+
else
|
|
152
|
+
@thor_instance.send(command, *args)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
rescue SystemExit => e
|
|
157
|
+
if e.status == 0
|
|
158
|
+
puts "Command completed successfully (would have exited with code 0 in CLI mode)"
|
|
159
|
+
else
|
|
160
|
+
puts "Command failed with exit code #{e.status}"
|
|
161
|
+
end
|
|
162
|
+
puts "(Use 'exit' or Ctrl+D to exit the interactive session)" if ENV["DEBUG"]
|
|
163
|
+
rescue Thor::Error => e
|
|
164
|
+
puts "Thor Error: #{e.message}"
|
|
165
|
+
rescue ArgumentError => e
|
|
166
|
+
puts "Thor Error: #{e.message}"
|
|
167
|
+
puts "Try: help #{command}" if thor_command?(command)
|
|
168
|
+
rescue StandardError => e
|
|
169
|
+
puts "Error: #{e.message}"
|
|
170
|
+
puts "Command: #{command}, Args: #{args.inspect}" if ENV["DEBUG"]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def parse_thor_options(args, task)
|
|
174
|
+
remaining_args = []
|
|
175
|
+
parsed_options = {}
|
|
176
|
+
|
|
177
|
+
begin
|
|
178
|
+
parser = Thor::Options.new(task.options)
|
|
179
|
+
|
|
180
|
+
if args.is_a?(Array)
|
|
181
|
+
parsed_options = parser.parse(args)
|
|
182
|
+
remaining_args = parser.remaining
|
|
183
|
+
else
|
|
184
|
+
split_args = safe_parse_input(args) || args.split(/\s+/)
|
|
185
|
+
parsed_options = parser.parse(split_args)
|
|
186
|
+
remaining_args = parser.remaining
|
|
187
|
+
end
|
|
188
|
+
rescue Thor::Error => e
|
|
189
|
+
puts "Option error: #{e.message}"
|
|
190
|
+
return nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
unknown = remaining_args.select { |a| a.start_with?("--") || (a.start_with?("-") && a.length > 1 && !a.match?(/^-\d/)) }
|
|
194
|
+
unless unknown.empty?
|
|
195
|
+
puts "Unknown option#{"s" if unknown.length > 1}: #{unknown.join(", ")}"
|
|
196
|
+
puts "Run '/help #{task.name}' to see available options."
|
|
197
|
+
return nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
[remaining_args, parsed_options]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def show_help(command = nil)
|
|
204
|
+
if command && @thor_class.subcommand_classes.key?(command)
|
|
205
|
+
subcommand_class = @thor_class.subcommand_classes[command]
|
|
206
|
+
puts "Commands for '#{command}':"
|
|
207
|
+
subcommand_class.tasks.each do |name, task|
|
|
208
|
+
puts " /#{command} #{name.ljust(15)} #{task.description}"
|
|
209
|
+
end
|
|
210
|
+
puts
|
|
211
|
+
elsif command && @thor_class.tasks.key?(command)
|
|
212
|
+
@thor_class.command_help(Thor::Base.shell.new, command)
|
|
213
|
+
else
|
|
214
|
+
puts "Available commands (prefix with /):"
|
|
215
|
+
@thor_class.tasks.each do |name, task|
|
|
216
|
+
puts " /#{name.ljust(19)} #{task.description}"
|
|
217
|
+
end
|
|
218
|
+
puts
|
|
219
|
+
puts "Special commands:"
|
|
220
|
+
puts " /help [COMMAND] Show help for command"
|
|
221
|
+
puts " /exit, /quit, /q Exit the REPL"
|
|
222
|
+
puts
|
|
223
|
+
if @default_handler
|
|
224
|
+
puts "Natural language mode:"
|
|
225
|
+
puts " Type anything without / to use default handler"
|
|
226
|
+
else
|
|
227
|
+
puts "Use /command syntax for all commands"
|
|
228
|
+
end
|
|
229
|
+
puts
|
|
230
|
+
if ENV["DEBUG"]
|
|
231
|
+
puts "Debug info:"
|
|
232
|
+
puts " Thor class: #{@thor_class.name}"
|
|
233
|
+
puts " Available tasks: #{@thor_class.tasks.keys.sort}"
|
|
234
|
+
puts " Instance methods: #{@thor_instance.methods.grep(/^[a-z]/).sort}" if @thor_instance
|
|
235
|
+
puts
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def should_exit?(line)
|
|
241
|
+
return true if line.nil?
|
|
242
|
+
|
|
243
|
+
stripped = line.strip.downcase
|
|
244
|
+
EXIT_COMMANDS.include?(stripped) || EXIT_COMMANDS.include?(stripped.sub(/^\//, ""))
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Completion methods
|
|
248
|
+
|
|
249
|
+
def complete_input(text, preposing)
|
|
250
|
+
full_line = preposing + text
|
|
251
|
+
|
|
252
|
+
if full_line.start_with?("/")
|
|
253
|
+
if preposing.strip == "/" || preposing.strip.empty?
|
|
254
|
+
command_completions = complete_commands(text.sub(/^\//, ""))
|
|
255
|
+
command_completions.map { |cmd| "/#{cmd}" }
|
|
256
|
+
else
|
|
257
|
+
complete_command_options(text, preposing)
|
|
258
|
+
end
|
|
259
|
+
else
|
|
260
|
+
[]
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def complete_commands(text)
|
|
265
|
+
return [] if text.nil?
|
|
266
|
+
|
|
267
|
+
command_names = @thor_class.tasks.keys + EXIT_COMMANDS + ["help"]
|
|
268
|
+
command_names.select { |cmd| cmd.start_with?(text) }.sort
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def complete_command_options(text, preposing)
|
|
272
|
+
parts = preposing.split(/\s+/)
|
|
273
|
+
command = parts[0].sub(/^\//, "") if parts[0]
|
|
274
|
+
|
|
275
|
+
subcommand_class = @thor_class.subcommand_classes[command] if command
|
|
276
|
+
if subcommand_class
|
|
277
|
+
return complete_subcommand_args(subcommand_class, text, parts)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
task = @thor_class.tasks[command] if command
|
|
281
|
+
|
|
282
|
+
if path_like?(text) || after_path_option?(preposing)
|
|
283
|
+
complete_path(text)
|
|
284
|
+
elsif text.start_with?("--") || text.start_with?("-")
|
|
285
|
+
complete_option_names(task, text)
|
|
286
|
+
else
|
|
287
|
+
complete_path(text)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def complete_subcommand_args(subcommand_class, text, parts)
|
|
292
|
+
if parts.length <= 1
|
|
293
|
+
complete_subcommands(subcommand_class, text)
|
|
294
|
+
else
|
|
295
|
+
sub_cmd_name = parts[1]
|
|
296
|
+
sub_task = subcommand_class.tasks[sub_cmd_name]
|
|
297
|
+
|
|
298
|
+
if text.start_with?("--") || text.start_with?("-")
|
|
299
|
+
complete_option_names(sub_task, text)
|
|
300
|
+
else
|
|
301
|
+
if parts.length == 2 && !text.empty?
|
|
302
|
+
complete_subcommands(subcommand_class, text)
|
|
303
|
+
else
|
|
304
|
+
complete_path(text)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def complete_subcommands(subcommand_class, text)
|
|
311
|
+
return [] if text.nil?
|
|
312
|
+
|
|
313
|
+
command_names = subcommand_class.tasks.keys
|
|
314
|
+
command_names.select { |cmd| cmd.start_with?(text) }.sort
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def path_like?(text)
|
|
318
|
+
text.match?(%r{^[~./]|/}) || text.match?(/\.(txt|rb|md|json|xml|yaml|yml|csv|log|html|css|js)$/i)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def after_path_option?(preposing)
|
|
322
|
+
preposing.match?(/(?:--file|--output|--input|--path|--dir|--directory|-f|-o|-i|-d)\s*$/)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def complete_path(text)
|
|
326
|
+
return [] if text.nil?
|
|
327
|
+
|
|
328
|
+
if text.empty?
|
|
329
|
+
matches = Dir.glob("*", File::FNM_DOTMATCH).select do |path|
|
|
330
|
+
basename = File.basename(path)
|
|
331
|
+
basename != "." && basename != ".."
|
|
332
|
+
end
|
|
333
|
+
return format_path_completions(matches, text)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
expanded = text.start_with?("~") ? File.expand_path(text) : text
|
|
337
|
+
|
|
338
|
+
if text.end_with?("/")
|
|
339
|
+
dir = expanded
|
|
340
|
+
prefix = ""
|
|
341
|
+
elsif File.directory?(expanded) && !text.end_with?("/")
|
|
342
|
+
dir = File.dirname(expanded)
|
|
343
|
+
prefix = File.basename(expanded)
|
|
344
|
+
else
|
|
345
|
+
dir = File.dirname(expanded)
|
|
346
|
+
prefix = File.basename(expanded)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
pattern = File.join(dir, "#{prefix}*")
|
|
350
|
+
matches = Dir.glob(pattern, File::FNM_DOTMATCH).select do |path|
|
|
351
|
+
basename = File.basename(path)
|
|
352
|
+
basename != "." && basename != ".."
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
format_path_completions(matches, text)
|
|
356
|
+
rescue => e
|
|
357
|
+
[]
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def format_path_completions(matches, original_text)
|
|
361
|
+
matches.map do |path|
|
|
362
|
+
display_path = File.directory?(path) && !path.end_with?("/") ? "#{path}/" : path
|
|
363
|
+
display_path = display_path.gsub(" ", '\ ')
|
|
364
|
+
|
|
365
|
+
if original_text.start_with?("~")
|
|
366
|
+
home = ENV["HOME"]
|
|
367
|
+
if display_path.start_with?(home)
|
|
368
|
+
"~#{display_path[home.length..-1]}"
|
|
369
|
+
else
|
|
370
|
+
display_path.sub(ENV["HOME"], "~")
|
|
371
|
+
end
|
|
372
|
+
elsif original_text.start_with?("./")
|
|
373
|
+
if display_path.start_with?(Dir.pwd)
|
|
374
|
+
rel_path = display_path.sub(/^#{Regexp.escape(Dir.pwd)}\//, "")
|
|
375
|
+
"./#{rel_path}"
|
|
376
|
+
else
|
|
377
|
+
display_path.start_with?("./") ? display_path : "./#{File.basename(display_path)}"
|
|
378
|
+
end
|
|
379
|
+
elsif original_text.start_with?("/")
|
|
380
|
+
display_path
|
|
381
|
+
else
|
|
382
|
+
dir = File.dirname(display_path)
|
|
383
|
+
if dir == "." || display_path.start_with?("./")
|
|
384
|
+
basename = File.basename(display_path)
|
|
385
|
+
basename += "/" if File.directory?(display_path.gsub('\ ', " ")) && !basename.end_with?("/")
|
|
386
|
+
basename
|
|
387
|
+
else
|
|
388
|
+
display_path.sub(/^#{Regexp.escape(Dir.pwd)}\//, "")
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end.sort
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def complete_option_names(task, text)
|
|
395
|
+
return [] unless task && task.options
|
|
396
|
+
|
|
397
|
+
options = []
|
|
398
|
+
task.options.each do |name, option|
|
|
399
|
+
options << "--#{name}"
|
|
400
|
+
if option.aliases
|
|
401
|
+
aliases = option.aliases.is_a?(Array) ? option.aliases : [option.aliases]
|
|
402
|
+
aliases.each { |a| options << a if a.start_with?("-") }
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
options.select { |opt| opt.start_with?(text) }.sort
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|