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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04a0318e910fee2ac10a134b2a9514ceef72921c2b1525d29face4c46a306ad8
4
- data.tar.gz: 1376543ed73e25721cb220893cb62fbe0fb6bc18d94ea38ecf00df60878fd4d1
3
+ metadata.gz: c927c297d565d278a09334334217dca98c5e05e72277f7fec4f5d07568f19c30
4
+ data.tar.gz: 353b4acde28a2a780a2b7a1ce85d7fb6e510f20d2862dd3dc4b2505140e2c014
5
5
  SHA512:
6
- metadata.gz: '05909f8f5c99a783831eec436ee94236b279b883c2cda0c62379081c28cc7258d8ab8317dfa4d3aba36c942f763767eed1dc08d45427ee05c00b7513f8488af9'
7
- data.tar.gz: 16a55871f6754affc2450703f3de24ecfee977ca372566b93dc44537cd3599bf305ecd988ec7f06349c786df9f0f7ab2e5bfed7d608390e53fedc6bdeb05f1cb
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
- puts "\n(Interrupted - press Ctrl+D or type 'exit' to quit)"
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 in main loop: #{e.message}"
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
- # Always use direct method calls to avoid Thor's invoke deduplication
284
- # Thor's invoke method silently fails on subsequent calls to the same method
285
- if @thor_instance.respond_to?(command)
286
- @thor_instance.send(command, *args)
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
- # If method doesn't exist, this will raise a proper error
289
- @thor_instance.send(command, *args)
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
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
+ require_relative "version_constant"
4
5
 
5
6
  class Thor
6
7
  module Interactive
7
- VERSION = "0.1.0.pre.2"
8
+ VERSION = ThorInteractive::VERSION
8
9
  end
9
10
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Version constant for thor-interactive gem
4
+ # This file is separate to avoid circular dependencies during gem installation
5
+
6
+ module ThorInteractive
7
+ VERSION = "0.1.0.pre.4"
8
+ end
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.2
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-07 00:00:00.000000000 Z
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: