thor-interactive 0.1.0.pre.2 → 0.1.0.pre.4
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/examples/edge_case_test.rb +107 -0
- data/examples/options_demo.rb +235 -0
- data/examples/signal_demo.rb +203 -0
- data/lib/thor/interactive/shell.rb +112 -20
- data/lib/thor/interactive/version.rb +2 -1
- data/lib/thor/interactive/version_constant.rb +8 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c927c297d565d278a09334334217dca98c5e05e72277f7fec4f5d07568f19c30
|
4
|
+
data.tar.gz: 353b4acde28a2a780a2b7a1ce85d7fb6e510f20d2862dd3dc4b2505140e2c014
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77f42909ede7bb049ea1675bc2ccc4f486437be713bc2dd9c16cb5bfdd98b8723b77cacb87147335cb85d439f79c57b0ae18f0bbb0d874f80dd56b27a39bd5c9
|
7
|
+
data.tar.gz: 11f5c9a01fa32839b669d968c7cb154827bfd4cbe910c272094403280193c5115bef46fa4eb430ad476f9437d75fe13a515f12eadaa1d91080e48d4724c4866d
|
@@ -0,0 +1,107 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "thor/interactive"
|
6
|
+
|
7
|
+
class EdgeCaseTest < Thor
|
8
|
+
include Thor::Interactive::Command
|
9
|
+
|
10
|
+
configure_interactive(
|
11
|
+
prompt: "test> ",
|
12
|
+
default_handler: ->(input, instance) {
|
13
|
+
puts "DEFAULT HANDLER: '#{input}'"
|
14
|
+
}
|
15
|
+
)
|
16
|
+
|
17
|
+
desc "topics [FILTER]", "List topics"
|
18
|
+
option :summarize, type: :boolean, desc: "Summarize topics"
|
19
|
+
option :format, type: :string, desc: "Output format"
|
20
|
+
def topics(filter = nil)
|
21
|
+
puts "TOPICS COMMAND:"
|
22
|
+
puts " Filter: #{filter.inspect}"
|
23
|
+
puts " Options: #{options.to_h.inspect}"
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "echo TEXT", "Echo text"
|
27
|
+
def echo(text)
|
28
|
+
puts "ECHO: '#{text}'"
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "test", "Run edge case tests"
|
32
|
+
def test
|
33
|
+
puts "\n=== Edge Case Tests ==="
|
34
|
+
|
35
|
+
puts "\n1. Unknown option:"
|
36
|
+
puts " Input: /topics --unknown-option"
|
37
|
+
process_input("/topics --unknown-option")
|
38
|
+
|
39
|
+
puts "\n2. Unknown option with value:"
|
40
|
+
puts " Input: /topics --unknown-option value"
|
41
|
+
process_input("/topics --unknown-option value")
|
42
|
+
|
43
|
+
puts "\n3. Mixed text and option-like strings:"
|
44
|
+
puts " Input: /topics The start of a string --option the rest"
|
45
|
+
process_input("/topics The start of a string --option the rest")
|
46
|
+
|
47
|
+
puts "\n4. Valid option mixed with text:"
|
48
|
+
puts " Input: /topics Some text --summarize more text"
|
49
|
+
process_input("/topics Some text --summarize more text")
|
50
|
+
|
51
|
+
puts "\n5. Option-like text in echo command (no options defined):"
|
52
|
+
puts " Input: /echo This has --what-looks-like an option"
|
53
|
+
process_input("/echo This has --what-looks-like an option")
|
54
|
+
|
55
|
+
puts "\n6. Real option after text:"
|
56
|
+
puts " Input: /topics AI and ML --format json"
|
57
|
+
process_input("/topics AI and ML --format json")
|
58
|
+
|
59
|
+
puts "\n=== End Tests ==="
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def process_input(input)
|
65
|
+
# Simulate what the shell does
|
66
|
+
if input.start_with?('/')
|
67
|
+
send(:handle_slash_command, input[1..-1])
|
68
|
+
else
|
69
|
+
@default_handler.call(input, self)
|
70
|
+
end
|
71
|
+
rescue => e
|
72
|
+
puts " ERROR: #{e.message}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_slash_command(command_input)
|
76
|
+
parts = command_input.split(/\s+/, 2)
|
77
|
+
command = parts[0]
|
78
|
+
args = parts[1] || ""
|
79
|
+
|
80
|
+
if command == "topics"
|
81
|
+
# Parse with shellwords
|
82
|
+
require 'shellwords'
|
83
|
+
parsed = Shellwords.split(args) rescue args.split(/\s+/)
|
84
|
+
invoke("topics", parsed)
|
85
|
+
elsif command == "echo"
|
86
|
+
echo(args)
|
87
|
+
end
|
88
|
+
rescue => e
|
89
|
+
puts " ERROR: #{e.message}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
if __FILE__ == $0
|
94
|
+
puts "Edge Case Testing"
|
95
|
+
puts "================="
|
96
|
+
|
97
|
+
# Create instance and run tests
|
98
|
+
app = EdgeCaseTest.new
|
99
|
+
app.test
|
100
|
+
|
101
|
+
puts "\nInteractive mode - try these:"
|
102
|
+
puts " /topics --unknown-option"
|
103
|
+
puts " /topics The start --option the rest"
|
104
|
+
puts
|
105
|
+
|
106
|
+
app.interactive
|
107
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "thor/interactive"
|
6
|
+
|
7
|
+
class OptionsDemo < Thor
|
8
|
+
include Thor::Interactive::Command
|
9
|
+
|
10
|
+
configure_interactive(
|
11
|
+
prompt: "opts> "
|
12
|
+
)
|
13
|
+
|
14
|
+
desc "process FILE", "Process a file with various options"
|
15
|
+
option :verbose, type: :boolean, aliases: "-v", desc: "Enable verbose output"
|
16
|
+
option :format, type: :string, default: "json", enum: ["json", "xml", "yaml", "csv"], desc: "Output format"
|
17
|
+
option :output, type: :string, aliases: "-o", desc: "Output file"
|
18
|
+
option :limit, type: :numeric, aliases: "-l", desc: "Limit number of results"
|
19
|
+
option :skip, type: :numeric, default: 0, desc: "Skip N results"
|
20
|
+
option :tags, type: :array, desc: "Tags to filter by"
|
21
|
+
option :config, type: :hash, desc: "Additional configuration"
|
22
|
+
option :dry_run, type: :boolean, desc: "Don't actually process, just show what would happen"
|
23
|
+
def process(file)
|
24
|
+
if options[:dry_run]
|
25
|
+
puts "DRY RUN MODE - No actual processing"
|
26
|
+
end
|
27
|
+
|
28
|
+
puts "Processing file: #{file}"
|
29
|
+
puts "=" * 50
|
30
|
+
|
31
|
+
if options[:verbose]
|
32
|
+
puts "Verbose mode enabled"
|
33
|
+
puts "All options:"
|
34
|
+
options.each do |key, value|
|
35
|
+
puts " #{key}: #{value.inspect}"
|
36
|
+
end
|
37
|
+
puts
|
38
|
+
end
|
39
|
+
|
40
|
+
puts "Format: #{options[:format]}"
|
41
|
+
puts "Output: #{options[:output] || 'stdout'}"
|
42
|
+
|
43
|
+
if options[:limit]
|
44
|
+
puts "Limiting to #{options[:limit]} results"
|
45
|
+
puts "Skipping first #{options[:skip]} results" if options[:skip] > 0
|
46
|
+
end
|
47
|
+
|
48
|
+
if options[:tags] && !options[:tags].empty?
|
49
|
+
puts "Filtering by tags: #{options[:tags].join(', ')}"
|
50
|
+
end
|
51
|
+
|
52
|
+
if options[:config] && !options[:config].empty?
|
53
|
+
puts "Configuration:"
|
54
|
+
options[:config].each do |key, value|
|
55
|
+
puts " #{key}: #{value}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
unless options[:dry_run]
|
60
|
+
puts "\n[Simulating processing...]"
|
61
|
+
sleep(1)
|
62
|
+
puts "✓ Processing complete!"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "search QUERY", "Search with options"
|
67
|
+
option :case_sensitive, type: :boolean, aliases: "-c", desc: "Case sensitive search"
|
68
|
+
option :regex, type: :boolean, aliases: "-r", desc: "Use regex"
|
69
|
+
option :files, type: :array, aliases: "-f", desc: "Files to search in"
|
70
|
+
option :max_results, type: :numeric, default: 10, desc: "Maximum results"
|
71
|
+
def search(query)
|
72
|
+
puts "Searching for: #{query}"
|
73
|
+
puts "Options:"
|
74
|
+
puts " Case sensitive: #{options[:case_sensitive] ? 'Yes' : 'No'}"
|
75
|
+
puts " Regex mode: #{options[:regex] ? 'Yes' : 'No'}"
|
76
|
+
puts " Max results: #{options[:max_results]}"
|
77
|
+
|
78
|
+
if options[:files]
|
79
|
+
puts " Searching in files: #{options[:files].join(', ')}"
|
80
|
+
else
|
81
|
+
puts " Searching in all files"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Simulate search
|
85
|
+
results = [
|
86
|
+
"result_1.txt:10: #{query} found here",
|
87
|
+
"result_2.txt:25: another #{query} match",
|
88
|
+
"result_3.txt:40: #{query} appears again"
|
89
|
+
]
|
90
|
+
|
91
|
+
puts "\nResults:"
|
92
|
+
results.take(options[:max_results]).each do |result|
|
93
|
+
puts " #{result}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
desc "convert INPUT OUTPUT", "Convert file from one format to another"
|
98
|
+
option :from, type: :string, required: true, desc: "Input format"
|
99
|
+
option :to, type: :string, required: true, desc: "Output format"
|
100
|
+
option :preserve_metadata, type: :boolean, desc: "Preserve file metadata"
|
101
|
+
option :compression, type: :string, enum: ["none", "gzip", "bzip2", "xz"], default: "none"
|
102
|
+
def convert(input, output)
|
103
|
+
puts "Converting: #{input} → #{output}"
|
104
|
+
puts "Format: #{options[:from]} → #{options[:to]}"
|
105
|
+
puts "Compression: #{options[:compression]}"
|
106
|
+
puts "Preserve metadata: #{options[:preserve_metadata] ? 'Yes' : 'No'}"
|
107
|
+
|
108
|
+
# Validation
|
109
|
+
unless File.exist?(input)
|
110
|
+
puts "Error: Input file '#{input}' not found"
|
111
|
+
return
|
112
|
+
end
|
113
|
+
|
114
|
+
puts "\n[Simulating conversion...]"
|
115
|
+
puts "✓ Conversion complete!"
|
116
|
+
end
|
117
|
+
|
118
|
+
desc "test", "Test various option formats"
|
119
|
+
def test
|
120
|
+
puts "\n=== Option Parsing Test Cases ==="
|
121
|
+
puts "\nTry these commands to test option parsing:\n"
|
122
|
+
|
123
|
+
examples = [
|
124
|
+
"/process file.txt --verbose",
|
125
|
+
"/process file.txt -v",
|
126
|
+
"/process data.json --format xml --output result.xml",
|
127
|
+
"/process data.json --format=yaml --limit=100",
|
128
|
+
"/process file.txt --tags important urgent todo",
|
129
|
+
"/process file.txt --config env:production db:postgres",
|
130
|
+
"/process file.txt -v --format csv -o output.csv --limit 50",
|
131
|
+
"/process file.txt --dry-run --verbose",
|
132
|
+
"",
|
133
|
+
"/search 'hello world' --case-sensitive",
|
134
|
+
"/search pattern -r -f file1.txt file2.txt file3.txt",
|
135
|
+
"/search query --max-results 5",
|
136
|
+
"",
|
137
|
+
"/convert input.json output.yaml --from json --to yaml",
|
138
|
+
"/convert data.csv data.json --from=csv --to=json --compression=gzip"
|
139
|
+
]
|
140
|
+
|
141
|
+
examples.each do |example|
|
142
|
+
puts example.empty? ? "" : " #{example}"
|
143
|
+
end
|
144
|
+
|
145
|
+
puts "\n=== Features Demonstrated ==="
|
146
|
+
puts "✓ Boolean options (--verbose, -v)"
|
147
|
+
puts "✓ String options (--format xml, --format=xml)"
|
148
|
+
puts "✓ Numeric options (--limit 100)"
|
149
|
+
puts "✓ Array options (--tags tag1 tag2 tag3)"
|
150
|
+
puts "✓ Hash options (--config key1:val1 key2:val2)"
|
151
|
+
puts "✓ Required options (--from, --to in convert)"
|
152
|
+
puts "✓ Default values (format: json, skip: 0)"
|
153
|
+
puts "✓ Enum validation (format must be json/xml/yaml/csv)"
|
154
|
+
puts "✓ Short aliases (-v for --verbose, -o for --output)"
|
155
|
+
puts "✓ Multiple options in one command"
|
156
|
+
end
|
157
|
+
|
158
|
+
desc "help_options", "Explain how options work in interactive mode"
|
159
|
+
def help_options
|
160
|
+
puts <<~HELP
|
161
|
+
|
162
|
+
=== Option Parsing in thor-interactive ===
|
163
|
+
|
164
|
+
Thor-interactive now fully supports Thor's option parsing!
|
165
|
+
|
166
|
+
BASIC USAGE:
|
167
|
+
/command arg1 arg2 --option value --flag
|
168
|
+
|
169
|
+
OPTION TYPES:
|
170
|
+
Boolean: --verbose or -v (no value needed)
|
171
|
+
String: --format json or --format=json
|
172
|
+
Numeric: --limit 100 or --limit=100
|
173
|
+
Array: --tags tag1 tag2 tag3
|
174
|
+
Hash: --config key1:val1 key2:val2
|
175
|
+
|
176
|
+
FEATURES:
|
177
|
+
• Long form: --option-name value
|
178
|
+
• Short form: -o value
|
179
|
+
• Equals syntax: --option=value
|
180
|
+
• Multiple options: --opt1 val1 --opt2 val2
|
181
|
+
• Default values: Defined in Thor command
|
182
|
+
• Required options: Must be provided
|
183
|
+
• Enum validation: Limited to specific values
|
184
|
+
|
185
|
+
BACKWARD COMPATIBILITY:
|
186
|
+
• Commands without options work as before
|
187
|
+
• Natural language still works for text commands
|
188
|
+
• Single-text commands preserve their behavior
|
189
|
+
• Default handler unaffected
|
190
|
+
|
191
|
+
EXAMPLES:
|
192
|
+
# Boolean flag
|
193
|
+
/process file.txt --verbose
|
194
|
+
|
195
|
+
# String option with equals
|
196
|
+
/process file.txt --format=xml
|
197
|
+
|
198
|
+
# Multiple options
|
199
|
+
/process file.txt -v --format csv --limit 10
|
200
|
+
|
201
|
+
# Array option
|
202
|
+
/search term --files file1.txt file2.txt
|
203
|
+
|
204
|
+
# Hash option
|
205
|
+
/deploy --config env:prod region:us-west
|
206
|
+
|
207
|
+
HOW IT WORKS:
|
208
|
+
1. Thor-interactive detects if command has options defined
|
209
|
+
2. Uses Thor's option parser to parse the arguments
|
210
|
+
3. Separates options from regular arguments
|
211
|
+
4. Sets options hash on Thor instance
|
212
|
+
5. Calls command with remaining arguments
|
213
|
+
6. Falls back to original behavior if parsing fails
|
214
|
+
|
215
|
+
NATURAL LANGUAGE:
|
216
|
+
Natural language input still works! Options are only
|
217
|
+
parsed for Thor commands that define them. Text sent
|
218
|
+
to default handlers is unchanged.
|
219
|
+
|
220
|
+
HELP
|
221
|
+
end
|
222
|
+
|
223
|
+
default_task :test
|
224
|
+
end
|
225
|
+
|
226
|
+
if __FILE__ == $0
|
227
|
+
puts "Thor Options Demo"
|
228
|
+
puts "=================="
|
229
|
+
puts
|
230
|
+
puts "This demo shows Thor option parsing in interactive mode."
|
231
|
+
puts "Type '/test' to see examples or '/help_options' for details."
|
232
|
+
puts
|
233
|
+
|
234
|
+
OptionsDemo.new.interactive
|
235
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "thor/interactive"
|
6
|
+
|
7
|
+
class SignalDemo < Thor
|
8
|
+
include Thor::Interactive::Command
|
9
|
+
|
10
|
+
configure_interactive(
|
11
|
+
prompt: "signal> ",
|
12
|
+
ctrl_c_behavior: :clear_prompt, # Default
|
13
|
+
double_ctrl_c_timeout: 0.5 # 500ms window for double Ctrl-C
|
14
|
+
)
|
15
|
+
|
16
|
+
desc "slow", "Simulate a slow command"
|
17
|
+
def slow
|
18
|
+
puts "Starting slow operation..."
|
19
|
+
5.times do |i|
|
20
|
+
puts "Step #{i + 1}/5"
|
21
|
+
sleep(1)
|
22
|
+
end
|
23
|
+
puts "Done!"
|
24
|
+
rescue Interrupt
|
25
|
+
puts "\nOperation cancelled!"
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "loop", "Run an infinite loop (test Ctrl-C)"
|
29
|
+
def loop
|
30
|
+
puts "Starting infinite loop (press Ctrl-C to stop)..."
|
31
|
+
counter = 0
|
32
|
+
while true
|
33
|
+
print "\rCounter: #{counter}"
|
34
|
+
counter += 1
|
35
|
+
sleep(0.1)
|
36
|
+
end
|
37
|
+
rescue Interrupt
|
38
|
+
puts "\nLoop stopped at #{counter}"
|
39
|
+
end
|
40
|
+
|
41
|
+
desc "input", "Test input with special text"
|
42
|
+
def input
|
43
|
+
puts "Type something with Ctrl chars:"
|
44
|
+
puts " - Ctrl-C to clear and start over"
|
45
|
+
puts " - Ctrl-D to cancel"
|
46
|
+
puts " - Enter to submit"
|
47
|
+
|
48
|
+
print "input> "
|
49
|
+
begin
|
50
|
+
text = $stdin.gets
|
51
|
+
if text
|
52
|
+
puts "You entered: #{text.inspect}"
|
53
|
+
else
|
54
|
+
puts "Cancelled with Ctrl-D"
|
55
|
+
end
|
56
|
+
rescue Interrupt
|
57
|
+
puts "\nInterrupted - input cancelled"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "behaviors", "Demo different Ctrl-C behaviors"
|
62
|
+
def behaviors
|
63
|
+
puts "\n=== Ctrl-C Behavior Options ==="
|
64
|
+
puts
|
65
|
+
puts "1. :clear_prompt (default)"
|
66
|
+
puts " - Shows ^C and hint message"
|
67
|
+
puts " - Clear and friendly"
|
68
|
+
|
69
|
+
puts "\n2. :show_help"
|
70
|
+
puts " - Shows help reminder"
|
71
|
+
puts " - Good for new users"
|
72
|
+
|
73
|
+
puts "\n3. :silent"
|
74
|
+
puts " - Just clears the line"
|
75
|
+
puts " - Minimal interruption"
|
76
|
+
|
77
|
+
puts "\nYou can configure with:"
|
78
|
+
puts " configure_interactive(ctrl_c_behavior: :show_help)"
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "test_clear", "Test with clear_prompt behavior"
|
82
|
+
def test_clear
|
83
|
+
puts "Starting new shell with :clear_prompt behavior"
|
84
|
+
puts "Try pressing Ctrl-C..."
|
85
|
+
puts
|
86
|
+
|
87
|
+
SignalDemo.new.interactive
|
88
|
+
end
|
89
|
+
|
90
|
+
desc "test_help", "Test with show_help behavior"
|
91
|
+
def test_help
|
92
|
+
puts "Starting new shell with :show_help behavior"
|
93
|
+
puts "Try pressing Ctrl-C..."
|
94
|
+
puts
|
95
|
+
|
96
|
+
test_app = Class.new(Thor) do
|
97
|
+
include Thor::Interactive::Command
|
98
|
+
configure_interactive(
|
99
|
+
prompt: "help> ",
|
100
|
+
ctrl_c_behavior: :show_help
|
101
|
+
)
|
102
|
+
|
103
|
+
desc "test", "Test command"
|
104
|
+
def test
|
105
|
+
puts "Test executed"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
test_app.new.interactive
|
110
|
+
end
|
111
|
+
|
112
|
+
desc "test_silent", "Test with silent behavior"
|
113
|
+
def test_silent
|
114
|
+
puts "Starting new shell with :silent behavior"
|
115
|
+
puts "Try pressing Ctrl-C..."
|
116
|
+
puts
|
117
|
+
|
118
|
+
test_app = Class.new(Thor) do
|
119
|
+
include Thor::Interactive::Command
|
120
|
+
configure_interactive(
|
121
|
+
prompt: "silent> ",
|
122
|
+
ctrl_c_behavior: :silent
|
123
|
+
)
|
124
|
+
|
125
|
+
desc "test", "Test command"
|
126
|
+
def test
|
127
|
+
puts "Test executed"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
test_app.new.interactive
|
132
|
+
end
|
133
|
+
|
134
|
+
desc "help_signals", "Explain signal handling"
|
135
|
+
def help_signals
|
136
|
+
puts <<~HELP
|
137
|
+
|
138
|
+
=== Signal Handling in thor-interactive ===
|
139
|
+
|
140
|
+
CTRL-C (SIGINT):
|
141
|
+
Single Press:
|
142
|
+
- Clears current input line
|
143
|
+
- Shows hint about double Ctrl-C
|
144
|
+
- Returns to fresh prompt
|
145
|
+
|
146
|
+
Double Press (within 500ms):
|
147
|
+
- Exits the interactive shell
|
148
|
+
- Same as typing 'exit'
|
149
|
+
|
150
|
+
CTRL-D (EOF):
|
151
|
+
- Exits immediately
|
152
|
+
- Standard Unix EOF behavior
|
153
|
+
- Same as typing 'exit'
|
154
|
+
|
155
|
+
EXIT COMMANDS:
|
156
|
+
- exit
|
157
|
+
- quit
|
158
|
+
- q
|
159
|
+
- /exit, /quit, /q (with slash)
|
160
|
+
|
161
|
+
CONFIGURATION:
|
162
|
+
configure_interactive(
|
163
|
+
ctrl_c_behavior: :clear_prompt, # or :show_help, :silent
|
164
|
+
double_ctrl_c_timeout: 0.5 # seconds
|
165
|
+
)
|
166
|
+
|
167
|
+
BEHAVIOR OPTIONS:
|
168
|
+
:clear_prompt (default)
|
169
|
+
Shows "^C" and hint message
|
170
|
+
|
171
|
+
:show_help
|
172
|
+
Shows help reminder on Ctrl-C
|
173
|
+
|
174
|
+
:silent
|
175
|
+
Just clears the line, no message
|
176
|
+
|
177
|
+
WHY THIS DESIGN?
|
178
|
+
- Matches behavior of Python, Node.js REPLs
|
179
|
+
- Prevents accidental exit
|
180
|
+
- Clear feedback to user
|
181
|
+
- Configurable for different preferences
|
182
|
+
|
183
|
+
HELP
|
184
|
+
end
|
185
|
+
|
186
|
+
default_task :help_signals
|
187
|
+
end
|
188
|
+
|
189
|
+
if __FILE__ == $0
|
190
|
+
puts "Signal Handling Demo"
|
191
|
+
puts "==================="
|
192
|
+
puts
|
193
|
+
puts "Try these:"
|
194
|
+
puts " 1. Press Ctrl-C once (clears prompt)"
|
195
|
+
puts " 2. Press Ctrl-C twice quickly (exits)"
|
196
|
+
puts " 3. Press Ctrl-D (exits immediately)"
|
197
|
+
puts " 4. Type 'exit', 'quit', or 'q' (exits)"
|
198
|
+
puts
|
199
|
+
puts "Starting interactive shell..."
|
200
|
+
puts
|
201
|
+
|
202
|
+
SignalDemo.new.interactive
|
203
|
+
end
|
@@ -29,6 +29,12 @@ class Thor
|
|
29
29
|
@prompt = merged_options[:prompt] || DEFAULT_PROMPT
|
30
30
|
@history_file = File.expand_path(merged_options[:history_file] || DEFAULT_HISTORY_FILE)
|
31
31
|
|
32
|
+
# Ctrl-C handling configuration
|
33
|
+
@ctrl_c_behavior = merged_options[:ctrl_c_behavior] || :clear_prompt
|
34
|
+
@double_ctrl_c_timeout = merged_options.key?(:double_ctrl_c_timeout) ?
|
35
|
+
merged_options[:double_ctrl_c_timeout] : 0.5
|
36
|
+
@last_interrupt_time = nil
|
37
|
+
|
32
38
|
setup_completion
|
33
39
|
load_history
|
34
40
|
end
|
@@ -56,27 +62,36 @@ class Thor
|
|
56
62
|
puts "(Debug: Entering main loop)" if ENV["DEBUG"]
|
57
63
|
|
58
64
|
loop do
|
59
|
-
line = Reline.readline(display_prompt, true)
|
60
|
-
puts "(Debug: Got input: #{line.inspect})" if ENV["DEBUG"]
|
61
|
-
|
62
|
-
if should_exit?(line)
|
63
|
-
puts "(Debug: Exit condition met)" if ENV["DEBUG"]
|
64
|
-
break
|
65
|
-
end
|
66
|
-
|
67
|
-
next if line.nil? || line.strip.empty?
|
68
|
-
|
69
65
|
begin
|
66
|
+
line = Reline.readline(display_prompt, true)
|
67
|
+
puts "(Debug: Got input: #{line.inspect})" if ENV["DEBUG"]
|
68
|
+
|
69
|
+
# Reset interrupt tracking on successful input
|
70
|
+
@last_interrupt_time = nil if line
|
71
|
+
|
72
|
+
if should_exit?(line)
|
73
|
+
puts "(Debug: Exit condition met)" if ENV["DEBUG"]
|
74
|
+
break
|
75
|
+
end
|
76
|
+
|
77
|
+
next if line.nil? || line.strip.empty?
|
78
|
+
|
70
79
|
puts "(Debug: Processing input: #{line.strip})" if ENV["DEBUG"]
|
71
80
|
process_input(line.strip)
|
72
81
|
puts "(Debug: Input processed successfully)" if ENV["DEBUG"]
|
82
|
+
|
73
83
|
rescue Interrupt
|
74
|
-
|
84
|
+
# Handle Ctrl-C
|
85
|
+
if handle_interrupt
|
86
|
+
break # Exit on double Ctrl-C
|
87
|
+
end
|
88
|
+
next # Continue on single Ctrl-C
|
89
|
+
|
75
90
|
rescue SystemExit => e
|
76
91
|
puts "A command tried to exit with code #{e.status}. Staying in interactive mode."
|
77
92
|
puts "(Debug: SystemExit caught in main loop)" if ENV["DEBUG"]
|
78
93
|
rescue => e
|
79
|
-
puts "Error
|
94
|
+
puts "Error: #{e.message}"
|
80
95
|
puts e.backtrace.first(5) if ENV["DEBUG"]
|
81
96
|
puts "(Debug: Error handled, continuing loop)" if ENV["DEBUG"]
|
82
97
|
# Continue the loop - don't let errors break the session
|
@@ -190,8 +205,8 @@ class Thor
|
|
190
205
|
if thor_command?(command_word)
|
191
206
|
task = @thor_class.tasks[command_word]
|
192
207
|
|
193
|
-
if task && single_text_command?(task)
|
194
|
-
# Single text command - pass everything after command as one argument
|
208
|
+
if task && single_text_command?(task) && !task.options.any?
|
209
|
+
# Single text command without options - pass everything after command as one argument
|
195
210
|
text_part = command_input.sub(/^#{Regexp.escape(command_word)}\s*/, '')
|
196
211
|
if text_part.empty?
|
197
212
|
invoke_thor_command(command_word, [])
|
@@ -280,13 +295,29 @@ class Thor
|
|
280
295
|
if command == "help"
|
281
296
|
show_help(args.first)
|
282
297
|
else
|
283
|
-
#
|
284
|
-
|
285
|
-
|
286
|
-
|
298
|
+
# Get the Thor task/command definition
|
299
|
+
task = @thor_class.tasks[command]
|
300
|
+
|
301
|
+
if task && task.options && !task.options.empty?
|
302
|
+
# Parse options if the command has them defined
|
303
|
+
parsed_args, parsed_options = parse_thor_options(args, task)
|
304
|
+
|
305
|
+
# Set options on the Thor instance
|
306
|
+
@thor_instance.options = Thor::CoreExt::HashWithIndifferentAccess.new(parsed_options)
|
307
|
+
|
308
|
+
# Call with parsed arguments only (options are in the options hash)
|
309
|
+
if @thor_instance.respond_to?(command)
|
310
|
+
@thor_instance.send(command, *parsed_args)
|
311
|
+
else
|
312
|
+
@thor_instance.send(command, *parsed_args)
|
313
|
+
end
|
287
314
|
else
|
288
|
-
#
|
289
|
-
@thor_instance.
|
315
|
+
# No options defined, use original behavior
|
316
|
+
if @thor_instance.respond_to?(command)
|
317
|
+
@thor_instance.send(command, *args)
|
318
|
+
else
|
319
|
+
@thor_instance.send(command, *args)
|
320
|
+
end
|
290
321
|
end
|
291
322
|
end
|
292
323
|
rescue SystemExit => e
|
@@ -305,6 +336,36 @@ class Thor
|
|
305
336
|
puts "Error: #{e.message}"
|
306
337
|
puts "Command: #{command}, Args: #{args.inspect}" if ENV["DEBUG"]
|
307
338
|
end
|
339
|
+
|
340
|
+
def parse_thor_options(args, task)
|
341
|
+
# Convert args array to a format Thor's option parser expects
|
342
|
+
remaining_args = []
|
343
|
+
parsed_options = {}
|
344
|
+
|
345
|
+
# Create a temporary parser using Thor's options
|
346
|
+
parser = Thor::Options.new(task.options)
|
347
|
+
|
348
|
+
# Parse the arguments
|
349
|
+
begin
|
350
|
+
if args.is_a?(Array)
|
351
|
+
# Parse the options from the array
|
352
|
+
parsed_options = parser.parse(args)
|
353
|
+
remaining_args = parser.remaining
|
354
|
+
else
|
355
|
+
# Single string argument, split it first
|
356
|
+
split_args = safe_parse_input(args) || args.split(/\s+/)
|
357
|
+
parsed_options = parser.parse(split_args)
|
358
|
+
remaining_args = parser.remaining
|
359
|
+
end
|
360
|
+
rescue Thor::Error => e
|
361
|
+
# If parsing fails, treat everything as arguments (backward compatibility)
|
362
|
+
puts "Option parsing error: #{e.message}" if ENV["DEBUG"]
|
363
|
+
remaining_args = args.is_a?(Array) ? args : [args]
|
364
|
+
parsed_options = {}
|
365
|
+
end
|
366
|
+
|
367
|
+
[remaining_args, parsed_options]
|
368
|
+
end
|
308
369
|
|
309
370
|
def show_help(command = nil)
|
310
371
|
if command && @thor_class.tasks.key?(command)
|
@@ -343,6 +404,37 @@ class Thor
|
|
343
404
|
# Handle both /exit and exit for convenience
|
344
405
|
EXIT_COMMANDS.include?(stripped) || EXIT_COMMANDS.include?(stripped.sub(/^\//, ''))
|
345
406
|
end
|
407
|
+
|
408
|
+
def handle_interrupt
|
409
|
+
current_time = Time.now
|
410
|
+
|
411
|
+
# Check for double Ctrl-C
|
412
|
+
if @last_interrupt_time && @double_ctrl_c_timeout && (current_time - @last_interrupt_time) < @double_ctrl_c_timeout
|
413
|
+
puts "\n(Interrupted twice - exiting)"
|
414
|
+
@last_interrupt_time = nil # Reset for next time
|
415
|
+
return true # Signal to exit
|
416
|
+
end
|
417
|
+
|
418
|
+
@last_interrupt_time = current_time
|
419
|
+
|
420
|
+
# Single Ctrl-C behavior
|
421
|
+
case @ctrl_c_behavior
|
422
|
+
when :clear_prompt
|
423
|
+
puts "^C"
|
424
|
+
puts "(Press Ctrl-C again quickly or Ctrl-D to exit)"
|
425
|
+
when :show_help
|
426
|
+
puts "\n^C - Interrupt"
|
427
|
+
puts "Press Ctrl-C again to exit, or type 'help' for commands"
|
428
|
+
when :silent
|
429
|
+
# Just clear the line, no message
|
430
|
+
print "\r#{' ' * 80}\r"
|
431
|
+
else
|
432
|
+
# Default behavior
|
433
|
+
puts "^C"
|
434
|
+
end
|
435
|
+
|
436
|
+
false # Don't exit, just clear prompt
|
437
|
+
end
|
346
438
|
|
347
439
|
def show_welcome(nesting_level = 0)
|
348
440
|
if nesting_level > 0
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: thor-interactive
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.0.pre.
|
4
|
+
version: 0.1.0.pre.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Petersen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-09-
|
11
|
+
date: 2025-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -55,13 +55,17 @@ files:
|
|
55
55
|
- Rakefile
|
56
56
|
- examples/README.md
|
57
57
|
- examples/demo_session.rb
|
58
|
+
- examples/edge_case_test.rb
|
58
59
|
- examples/nested_example.rb
|
60
|
+
- examples/options_demo.rb
|
59
61
|
- examples/sample_app.rb
|
62
|
+
- examples/signal_demo.rb
|
60
63
|
- examples/test_interactive.rb
|
61
64
|
- lib/thor/interactive.rb
|
62
65
|
- lib/thor/interactive/command.rb
|
63
66
|
- lib/thor/interactive/shell.rb
|
64
67
|
- lib/thor/interactive/version.rb
|
68
|
+
- lib/thor/interactive/version_constant.rb
|
65
69
|
- sig/thor/interactive.rbs
|
66
70
|
homepage: https://github.com/scientist-labs/thor-interactive
|
67
71
|
licenses:
|