thor-interactive 0.1.0.pre.5 → 0.1.0.pre.6

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.
@@ -1,54 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "reline"
4
- require "shellwords"
5
- require "thor"
4
+ require_relative "command_dispatch"
6
5
 
7
6
  class Thor
8
7
  module Interactive
9
8
  class Shell
10
9
  DEFAULT_PROMPT = "> "
11
10
  DEFAULT_HISTORY_FILE = "~/.thor_interactive_history"
12
- EXIT_COMMANDS = %w[exit quit q].freeze
13
11
 
14
12
  attr_reader :thor_class, :thor_instance, :prompt
15
13
 
14
+ include CommandDispatch
15
+
16
16
  def initialize(thor_class, options = {})
17
17
  @thor_class = thor_class
18
18
  @thor_instance = thor_class.new
19
-
19
+
20
20
  # Merge class-level interactive options if available
21
21
  merged_options = {}
22
22
  if thor_class.respond_to?(:interactive_options)
23
23
  merged_options.merge!(thor_class.interactive_options)
24
24
  end
25
25
  merged_options.merge!(options)
26
-
26
+
27
27
  @merged_options = merged_options
28
28
  @default_handler = merged_options[:default_handler]
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
32
  # Ctrl-C handling configuration
33
33
  @ctrl_c_behavior = merged_options[:ctrl_c_behavior] || :clear_prompt
34
- @double_ctrl_c_timeout = merged_options.key?(:double_ctrl_c_timeout) ?
34
+ @double_ctrl_c_timeout = merged_options.key?(:double_ctrl_c_timeout) ?
35
35
  merged_options[:double_ctrl_c_timeout] : 0.5
36
36
  @last_interrupt_time = nil
37
-
37
+
38
38
  setup_completion
39
39
  load_history
40
40
  end
41
41
 
42
42
  def start
43
43
  # Track that we're in an interactive session
44
- was_in_session = ENV['THOR_INTERACTIVE_SESSION']
45
- nesting_level = ENV['THOR_INTERACTIVE_LEVEL'].to_i
46
-
47
- ENV['THOR_INTERACTIVE_SESSION'] = 'true'
48
- ENV['THOR_INTERACTIVE_LEVEL'] = (nesting_level + 1).to_s
49
-
44
+ was_in_session = ENV["THOR_INTERACTIVE_SESSION"]
45
+ nesting_level = ENV["THOR_INTERACTIVE_LEVEL"].to_i
46
+
47
+ ENV["THOR_INTERACTIVE_SESSION"] = "true"
48
+ ENV["THOR_INTERACTIVE_LEVEL"] = (nesting_level + 1).to_s
49
+
50
50
  puts "(Debug: Interactive session started, level #{nesting_level + 1})" if ENV["DEBUG"]
51
-
51
+
52
52
  # Adjust prompt for nested sessions if configured
53
53
  display_prompt = @prompt
54
54
  if nesting_level > 0 && @merged_options[:nested_prompt_format]
@@ -56,37 +56,35 @@ class Thor
56
56
  elsif nesting_level > 0
57
57
  display_prompt = "(#{nesting_level + 1}) #{@prompt}"
58
58
  end
59
-
59
+
60
60
  show_welcome(nesting_level)
61
-
61
+
62
62
  puts "(Debug: Entering main loop)" if ENV["DEBUG"]
63
-
63
+
64
64
  loop do
65
65
  begin
66
66
  line = Reline.readline(display_prompt, true)
67
67
  puts "(Debug: Got input: #{line.inspect})" if ENV["DEBUG"]
68
-
68
+
69
69
  # Reset interrupt tracking on successful input
70
70
  @last_interrupt_time = nil if line
71
-
71
+
72
72
  if should_exit?(line)
73
73
  puts "(Debug: Exit condition met)" if ENV["DEBUG"]
74
74
  break
75
75
  end
76
-
76
+
77
77
  next if line.nil? || line.strip.empty?
78
-
78
+
79
79
  puts "(Debug: Processing input: #{line.strip})" if ENV["DEBUG"]
80
80
  process_input(line.strip)
81
81
  puts "(Debug: Input processed successfully)" if ENV["DEBUG"]
82
-
83
82
  rescue Interrupt
84
83
  # Handle Ctrl-C
85
84
  if handle_interrupt
86
- break # Exit on double Ctrl-C
85
+ break # Exit on double Ctrl-C
87
86
  end
88
- next # Continue on single Ctrl-C
89
-
87
+ next # Continue on single Ctrl-C
90
88
  rescue SystemExit => e
91
89
  puts "A command tried to exit with code #{e.status}. Staying in interactive mode."
92
90
  puts "(Debug: SystemExit caught in main loop)" if ENV["DEBUG"]
@@ -97,19 +95,18 @@ class Thor
97
95
  # Continue the loop - don't let errors break the session
98
96
  end
99
97
  end
100
-
98
+
101
99
  puts "(Debug: Exited main loop)" if ENV["DEBUG"]
102
100
  save_history
103
101
  puts nesting_level > 0 ? "Exiting nested session..." : "Goodbye!"
104
-
105
102
  ensure
106
103
  # Restore previous session state
107
104
  if was_in_session
108
- ENV['THOR_INTERACTIVE_SESSION'] = 'true'
109
- ENV['THOR_INTERACTIVE_LEVEL'] = nesting_level.to_s
105
+ ENV["THOR_INTERACTIVE_SESSION"] = "true"
106
+ ENV["THOR_INTERACTIVE_LEVEL"] = nesting_level.to_s
110
107
  else
111
- ENV.delete('THOR_INTERACTIVE_SESSION')
112
- ENV.delete('THOR_INTERACTIVE_LEVEL')
108
+ ENV.delete("THOR_INTERACTIVE_SESSION")
109
+ ENV.delete("THOR_INTERACTIVE_LEVEL")
113
110
  end
114
111
  end
115
112
 
@@ -121,491 +118,18 @@ class Thor
121
118
  end
122
119
  end
123
120
 
124
- def complete_input(text, preposing)
125
- # Handle completion for slash commands
126
- full_line = preposing + text
127
-
128
- if full_line.start_with?('/')
129
- # Command completion mode
130
- if preposing.strip == '/' || preposing.strip.empty?
131
- # Complete command names with / prefix
132
- command_completions = complete_commands(text.sub(/^\//, ''))
133
- command_completions.map { |cmd| "/#{cmd}" }
134
- else
135
- # Complete command arguments (basic implementation)
136
- complete_command_options(text, preposing)
137
- end
138
- else
139
- # Natural language mode - no completion for now
140
- []
141
- end
142
- end
143
-
144
- def complete_commands(text)
145
- return [] if text.nil?
146
-
147
- command_names = @thor_class.tasks.keys + EXIT_COMMANDS + ["help"]
148
- command_names.select { |cmd| cmd.start_with?(text) }.sort
149
- end
150
-
151
- def complete_command_options(text, preposing)
152
- # Parse the command and check what we're completing
153
- parts = preposing.split(/\s+/)
154
- command = parts[0].sub(/^\//, '') if parts[0]
155
-
156
- # Check if this is a subcommand
157
- subcommand_class = @thor_class.subcommand_classes[command] if command
158
- if subcommand_class
159
- return complete_subcommand_args(subcommand_class, text, parts)
160
- end
161
-
162
- # Get the Thor task if it exists
163
- task = @thor_class.tasks[command] if command
164
-
165
- # Check if we're likely completing a path
166
- if path_like?(text) || after_path_option?(preposing)
167
- complete_path(text)
168
- elsif text.start_with?('--') || text.start_with?('-')
169
- # Complete option names
170
- complete_option_names(task, text)
171
- else
172
- # Default to path completion for positional args that might be files
173
- # This helps with commands that take file arguments
174
- complete_path(text)
175
- end
176
- end
177
-
178
- def complete_subcommand_args(subcommand_class, text, parts)
179
- # parts[0] = "/db" or "db", parts[1..] = subcommand args
180
- if parts.length <= 1
181
- # No subcommand yet typed, complete subcommand names
182
- # e.g. "/db <TAB>"
183
- complete_subcommands(subcommand_class, text)
184
- else
185
- # A subcommand name has been typed, check for option completion
186
- sub_cmd_name = parts[1]
187
- sub_task = subcommand_class.tasks[sub_cmd_name]
188
-
189
- if text.start_with?('--') || text.start_with?('-')
190
- complete_option_names(sub_task, text)
191
- else
192
- # Could be completing a subcommand name or a positional arg
193
- if parts.length == 2 && !text.empty?
194
- # Still typing the subcommand name, e.g. "/db cr<TAB>"
195
- # But only if 'text' is part of a subcommand name being typed
196
- # (parts[1] is the preposing word, text is what's being completed)
197
- complete_subcommands(subcommand_class, text)
198
- else
199
- complete_path(text)
200
- end
201
- end
202
- end
203
- end
204
-
205
- def complete_subcommands(subcommand_class, text)
206
- return [] if text.nil?
207
-
208
- command_names = subcommand_class.tasks.keys
209
- command_names.select { |cmd| cmd.start_with?(text) }.sort
210
- end
211
-
212
- def path_like?(text)
213
- # Check if text looks like a path
214
- text.match?(%r{^[~./]|/}) || text.match?(/\.(txt|rb|md|json|xml|yaml|yml|csv|log|html|css|js)$/i)
215
- end
216
-
217
- def after_path_option?(preposing)
218
- # Check if we're after a common file/path option
219
- preposing.match?(/(?:--file|--output|--input|--path|--dir|--directory|-f|-o|-i|-d)\s*$/)
220
- end
221
-
222
- def complete_path(text)
223
- return [] if text.nil?
224
-
225
- # Special case for empty text - show files in current directory
226
- if text.empty?
227
- matches = Dir.glob("*", File::FNM_DOTMATCH).select do |path|
228
- basename = File.basename(path)
229
- basename != '.' && basename != '..'
230
- end
231
- return format_path_completions(matches, text)
232
- end
233
-
234
- # Expand ~ to home directory for matching
235
- expanded = text.start_with?('~') ? File.expand_path(text) : text
236
-
237
- # Determine directory and prefix for matching
238
- if text.end_with?('/')
239
- # User typed a directory with trailing slash - show its contents
240
- dir = expanded
241
- prefix = ''
242
- elsif File.directory?(expanded) && !text.end_with?('/')
243
- # It's a directory without trailing slash - complete the directory name
244
- dir = File.dirname(expanded)
245
- prefix = File.basename(expanded)
246
- else
247
- # Completing a partial filename
248
- dir = File.dirname(expanded)
249
- prefix = File.basename(expanded)
250
- end
251
-
252
- # Get matching files/dirs
253
- pattern = File.join(dir, "#{prefix}*")
254
- matches = Dir.glob(pattern, File::FNM_DOTMATCH).select do |path|
255
- # Filter out . and .. entries
256
- basename = File.basename(path)
257
- basename != '.' && basename != '..'
258
- end
259
-
260
- format_path_completions(matches, text)
261
- rescue => e
262
- # If path completion fails, return empty array
263
- []
264
- end
265
-
266
- def format_path_completions(matches, original_text)
267
- # Format the completions based on how the user typed the path
268
- matches.map do |path|
269
- # Add trailing / for directories
270
- display_path = File.directory?(path) && !path.end_with?('/') ? "#{path}/" : path
271
-
272
- # Handle paths with spaces by escaping them
273
- display_path = display_path.gsub(' ', '\ ')
274
-
275
- # Return path as user would type it
276
- if original_text.start_with?('~')
277
- # Replace home directory with ~
278
- home = ENV['HOME']
279
- if display_path.start_with?(home)
280
- "~#{display_path[home.length..-1]}"
281
- else
282
- display_path.sub(ENV['HOME'], '~')
283
- end
284
- elsif original_text.start_with?('./')
285
- # Keep ./ prefix and make path relative
286
- if display_path.start_with?(Dir.pwd)
287
- rel_path = display_path.sub(/^#{Regexp.escape(Dir.pwd)}\//, '')
288
- "./#{rel_path}"
289
- else
290
- # Already relative, just ensure ./ prefix
291
- display_path.start_with?('./') ? display_path : "./#{File.basename(display_path)}"
292
- end
293
- elsif original_text.start_with?('/')
294
- # Absolute path - return as is
295
- display_path
296
- else
297
- # Relative path without ./ prefix
298
- # If the matched path is in current dir, just return the basename
299
- dir = File.dirname(display_path)
300
- if dir == '.' || display_path.start_with?('./')
301
- basename = File.basename(display_path)
302
- basename += '/' if File.directory?(display_path.gsub('\ ', ' ')) && !basename.end_with?('/')
303
- basename
304
- else
305
- display_path.sub(/^#{Regexp.escape(Dir.pwd)}\//, '')
306
- end
307
- end
308
- end.sort
309
- end
310
-
311
- def complete_option_names(task, text)
312
- return [] unless task && task.options
313
-
314
- # Get all option names (long and short forms)
315
- options = []
316
- task.options.each do |name, option|
317
- options << "--#{name}"
318
- if option.aliases
319
- # Aliases can be string or array
320
- aliases = option.aliases.is_a?(Array) ? option.aliases : [option.aliases]
321
- aliases.each { |a| options << a if a.start_with?('-') }
322
- end
323
- end
324
-
325
- # Filter by what user has typed
326
- options.select { |opt| opt.start_with?(text) }.sort
327
- end
328
-
329
- def process_input(input)
330
- # Handle completely empty input
331
- return if input.nil? || input.strip.empty?
332
-
333
- # Check if input starts with / for explicit command mode
334
- if input.strip.start_with?('/')
335
- # Explicit command mode: /command args
336
- handle_slash_command(input.strip[1..-1])
337
- elsif is_help_request?(input)
338
- # Special case: treat bare "help" as /help for convenience
339
- if input.strip.split.length == 1
340
- show_help
341
- else
342
- command_part = input.strip.split[1]
343
- show_help(command_part)
344
- end
345
- else
346
- # Determine if this looks like a command or natural language
347
- command_word = input.strip.split(/\s+/, 2).first
348
-
349
- if thor_command?(command_word)
350
- # Looks like a command - handle it as a command (backward compatibility)
351
- handle_command(input.strip)
352
- elsif @default_handler
353
- # Natural language mode: send whole input to default handler
354
- begin
355
- @default_handler.call(input, @thor_instance)
356
- rescue => e
357
- puts "Error in default handler: #{e.message}"
358
- puts "Input was: #{input}"
359
- puts "Try using /commands or type '/help' for available commands."
360
- end
361
- else
362
- # No default handler, suggest using command mode
363
- puts "No default handler configured. Use /command for commands, or type '/help' for available commands."
364
- end
365
- end
366
- end
367
-
368
- def handle_slash_command(command_input)
369
- return if command_input.empty?
370
- handle_command(command_input)
371
- end
372
-
373
- def handle_command(command_input)
374
- # Extract command and check if it's a single-text command
375
- command_word = command_input.split(/\s+/, 2).first
376
-
377
- if thor_command?(command_word)
378
- task = @thor_class.tasks[command_word]
379
-
380
- if task && single_text_command?(task) && !task.options.any?
381
- # Single text command without options - pass everything after command as one argument
382
- text_part = command_input.sub(/^#{Regexp.escape(command_word)}\s*/, '')
383
- if text_part.empty?
384
- invoke_thor_command(command_word, [])
385
- else
386
- invoke_thor_command(command_word, [text_part])
387
- end
388
- else
389
- # Multi-argument command, use proper parsing
390
- args = safe_parse_input(command_input)
391
- if args && !args.empty?
392
- command = args.shift
393
- invoke_thor_command(command, args)
394
- else
395
- # Parsing failed, try simple split
396
- parts = command_input.split(/\s+/)
397
- command = parts.shift
398
- invoke_thor_command(command, parts)
399
- end
400
- end
401
- else
402
- puts "Unknown command: '#{command_word}'. Type '/help' for available commands."
403
- end
404
- end
405
-
406
- def safe_parse_input(input)
407
- # Try proper shell parsing first
408
- Shellwords.split(input)
409
- rescue ArgumentError
410
- # If parsing fails, return nil so caller can handle it
411
- nil
412
- end
413
-
414
- def parse_input(input)
415
- # Legacy method - kept for backward compatibility
416
- safe_parse_input(input) || []
417
- end
418
-
419
- def handle_unparseable_command(input, command_word)
420
- # For commands that failed shell parsing, try intelligent handling
421
- task = @thor_class.tasks[command_word]
422
-
423
- # Always try single text approach first for better natural language support
424
- text_part = input.strip.sub(/^#{Regexp.escape(command_word)}\s*/, '')
425
- if text_part.empty?
426
- invoke_thor_command(command_word, [])
427
- else
428
- invoke_thor_command(command_word, [text_part])
429
- end
430
- end
431
-
432
- def single_text_command?(task)
433
- # Heuristic: determine if this is likely a single text command
434
- return false unless task
435
-
436
- # Check the method signature to see how many parameters it expects
437
- method_name = task.name.to_sym
438
- if @thor_instance.respond_to?(method_name)
439
- method_obj = @thor_instance.method(method_name)
440
- param_count = method_obj.parameters.count { |type, _| type == :req }
441
-
442
- # Only single required parameter = likely text command
443
- param_count == 1
444
- else
445
- # Fallback for introspection issues
446
- false
447
- end
448
- rescue
449
- # If introspection fails, default to false (safer)
450
- false
451
- end
452
-
453
- def is_help_request?(input)
454
- # Check if input is a help request (help, ?, etc.)
455
- stripped = input.strip.downcase
456
- stripped == "help" || stripped.start_with?("help ")
457
- end
458
-
459
- def thor_command?(command)
460
- @thor_class.tasks.key?(command) ||
461
- @thor_class.subcommand_classes.key?(command) ||
462
- command == "help"
463
- end
464
-
465
- def invoke_thor_command(command, args)
466
- # Use the persistent instance to maintain state
467
- if command == "help"
468
- show_help(args.first)
469
- else
470
- # Get the Thor task/command definition
471
- task = @thor_class.tasks[command]
472
-
473
- if task && task.options && !task.options.empty?
474
- # Parse options if the command has them defined
475
- result = parse_thor_options(args, task)
476
- return unless result # Parse failed, error already shown
477
-
478
- parsed_args, parsed_options = result
479
-
480
- # Set options on the Thor instance
481
- @thor_instance.options = Thor::CoreExt::HashWithIndifferentAccess.new(parsed_options)
482
-
483
- # Call with parsed arguments only (options are in the options hash)
484
- if @thor_instance.respond_to?(command)
485
- @thor_instance.send(command, *parsed_args)
486
- else
487
- @thor_instance.send(command, *parsed_args)
488
- end
489
- else
490
- # No options defined, use original behavior
491
- if @thor_instance.respond_to?(command)
492
- @thor_instance.send(command, *args)
493
- else
494
- @thor_instance.send(command, *args)
495
- end
496
- end
497
- end
498
- rescue SystemExit => e
499
- if e.status == 0
500
- puts "Command completed successfully (would have exited with code 0 in CLI mode)"
501
- else
502
- puts "Command failed with exit code #{e.status}"
503
- end
504
- puts "(Use 'exit' or Ctrl+D to exit the interactive session)" if ENV["DEBUG"]
505
- rescue Thor::Error => e
506
- puts "Thor Error: #{e.message}"
507
- rescue ArgumentError => e
508
- puts "Thor Error: #{e.message}"
509
- puts "Try: help #{command}" if thor_command?(command)
510
- rescue StandardError => e
511
- puts "Error: #{e.message}"
512
- puts "Command: #{command}, Args: #{args.inspect}" if ENV["DEBUG"]
513
- end
514
-
515
- def parse_thor_options(args, task)
516
- # Convert args array to a format Thor's option parser expects
517
- remaining_args = []
518
- parsed_options = {}
519
-
520
- begin
521
- # Create a temporary parser using Thor's options
522
- parser = Thor::Options.new(task.options)
523
-
524
- if args.is_a?(Array)
525
- # Parse the options from the array
526
- parsed_options = parser.parse(args)
527
- remaining_args = parser.remaining
528
- else
529
- # Single string argument, split it first
530
- split_args = safe_parse_input(args) || args.split(/\s+/)
531
- parsed_options = parser.parse(split_args)
532
- remaining_args = parser.remaining
533
- end
534
- rescue Thor::Error => e
535
- # Show user-friendly error for option parsing failures (e.g. invalid numeric value)
536
- puts "Option error: #{e.message}"
537
- return nil
538
- end
539
-
540
- # Check for unknown options left in remaining args
541
- unknown = remaining_args.select { |a| a.start_with?('--') || (a.start_with?('-') && a.length > 1 && !a.match?(/^-\d/)) }
542
- unless unknown.empty?
543
- puts "Unknown option#{'s' if unknown.length > 1}: #{unknown.join(', ')}"
544
- puts "Run '/help #{task.name}' to see available options."
545
- return nil
546
- end
547
-
548
- [remaining_args, parsed_options]
549
- end
550
-
551
- def show_help(command = nil)
552
- if command && @thor_class.subcommand_classes.key?(command)
553
- # Show help for a subcommand — list its available commands
554
- subcommand_class = @thor_class.subcommand_classes[command]
555
- puts "Commands for '#{command}':"
556
- subcommand_class.tasks.each do |name, task|
557
- puts " /#{command} #{name.ljust(15)} #{task.description}"
558
- end
559
- puts
560
- elsif command && @thor_class.tasks.key?(command)
561
- @thor_class.command_help(Thor::Base.shell.new, command)
562
- else
563
- puts "Available commands (prefix with /):"
564
- @thor_class.tasks.each do |name, task|
565
- puts " /#{name.ljust(19)} #{task.description}"
566
- end
567
- puts
568
- puts "Special commands:"
569
- puts " /help [COMMAND] Show help for command"
570
- puts " /exit, /quit, /q Exit the REPL"
571
- puts
572
- if @default_handler
573
- puts "Natural language mode:"
574
- puts " Type anything without / to use default handler"
575
- else
576
- puts "Use /command syntax for all commands"
577
- end
578
- puts
579
- if ENV["DEBUG"]
580
- puts "Debug info:"
581
- puts " Thor class: #{@thor_class.name}"
582
- puts " Available tasks: #{@thor_class.tasks.keys.sort}"
583
- puts " Instance methods: #{@thor_instance.methods.grep(/^[a-z]/).sort}" if @thor_instance
584
- puts
585
- end
586
- end
587
- end
588
-
589
- def should_exit?(line)
590
- return true if line.nil? # Ctrl+D
591
-
592
- stripped = line.strip.downcase
593
- # Handle both /exit and exit for convenience
594
- EXIT_COMMANDS.include?(stripped) || EXIT_COMMANDS.include?(stripped.sub(/^\//, ''))
595
- end
596
-
597
121
  def handle_interrupt
598
122
  current_time = Time.now
599
-
123
+
600
124
  # Check for double Ctrl-C
601
125
  if @last_interrupt_time && @double_ctrl_c_timeout && (current_time - @last_interrupt_time) < @double_ctrl_c_timeout
602
126
  puts "\n(Interrupted twice - exiting)"
603
- @last_interrupt_time = nil # Reset for next time
604
- return true # Signal to exit
127
+ @last_interrupt_time = nil # Reset for next time
128
+ return true # Signal to exit
605
129
  end
606
-
130
+
607
131
  @last_interrupt_time = current_time
608
-
132
+
609
133
  # Single Ctrl-C behavior
610
134
  case @ctrl_c_behavior
611
135
  when :clear_prompt
@@ -616,13 +140,13 @@ class Thor
616
140
  puts "Press Ctrl-C again to exit, or type 'help' for commands"
617
141
  when :silent
618
142
  # Just clear the line, no message
619
- print "\r#{' ' * 80}\r"
143
+ print "\r#{" " * 80}\r"
620
144
  else
621
145
  # Default behavior
622
146
  puts "^C"
623
147
  end
624
-
625
- false # Don't exit, just clear prompt
148
+
149
+ false # Don't exit, just clear prompt
626
150
  end
627
151
 
628
152
  def show_welcome(nesting_level = 0)
@@ -638,7 +162,7 @@ class Thor
638
162
 
639
163
  def load_history
640
164
  return unless File.exist?(@history_file)
641
-
165
+
642
166
  File.readlines(@history_file, chomp: true).each do |line|
643
167
  Reline::HISTORY << line
644
168
  end
@@ -648,11 +172,11 @@ class Thor
648
172
 
649
173
  def save_history
650
174
  return unless Reline::HISTORY.size > 0
651
-
175
+
652
176
  File.write(@history_file, Reline::HISTORY.to_a.join("\n"))
653
177
  rescue => e
654
- # Ignore history saving errors
178
+ # Ignore history saving errors
655
179
  end
656
180
  end
657
181
  end
658
- end
182
+ end