Skip to content

Commit cf4cc6d

Browse files
authored
Merge pull request #4 from scientist-labs/ctrl-c
Ctrl c
2 parents 10419ee + 946ebae commit cf4cc6d

File tree

4 files changed

+600
-14
lines changed

4 files changed

+600
-14
lines changed

examples/signal_demo.rb

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/thor/interactive/shell.rb

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ def initialize(thor_class, options = {})
2929
@prompt = merged_options[:prompt] || DEFAULT_PROMPT
3030
@history_file = File.expand_path(merged_options[:history_file] || DEFAULT_HISTORY_FILE)
3131

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+
3238
setup_completion
3339
load_history
3440
end
@@ -56,27 +62,36 @@ def start
5662
puts "(Debug: Entering main loop)" if ENV["DEBUG"]
5763

5864
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-
6965
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+
7079
puts "(Debug: Processing input: #{line.strip})" if ENV["DEBUG"]
7180
process_input(line.strip)
7281
puts "(Debug: Input processed successfully)" if ENV["DEBUG"]
82+
7383
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+
7590
rescue SystemExit => e
7691
puts "A command tried to exit with code #{e.status}. Staying in interactive mode."
7792
puts "(Debug: SystemExit caught in main loop)" if ENV["DEBUG"]
7893
rescue => e
79-
puts "Error in main loop: #{e.message}"
94+
puts "Error: #{e.message}"
8095
puts e.backtrace.first(5) if ENV["DEBUG"]
8196
puts "(Debug: Error handled, continuing loop)" if ENV["DEBUG"]
8297
# Continue the loop - don't let errors break the session
@@ -343,6 +358,37 @@ def should_exit?(line)
343358
# Handle both /exit and exit for convenience
344359
EXIT_COMMANDS.include?(stripped) || EXIT_COMMANDS.include?(stripped.sub(/^\//, ''))
345360
end
361+
362+
def handle_interrupt
363+
current_time = Time.now
364+
365+
# Check for double Ctrl-C
366+
if @last_interrupt_time && @double_ctrl_c_timeout && (current_time - @last_interrupt_time) < @double_ctrl_c_timeout
367+
puts "\n(Interrupted twice - exiting)"
368+
@last_interrupt_time = nil # Reset for next time
369+
return true # Signal to exit
370+
end
371+
372+
@last_interrupt_time = current_time
373+
374+
# Single Ctrl-C behavior
375+
case @ctrl_c_behavior
376+
when :clear_prompt
377+
puts "^C"
378+
puts "(Press Ctrl-C again quickly or Ctrl-D to exit)"
379+
when :show_help
380+
puts "\n^C - Interrupt"
381+
puts "Press Ctrl-C again to exit, or type 'help' for commands"
382+
when :silent
383+
# Just clear the line, no message
384+
print "\r#{' ' * 80}\r"
385+
else
386+
# Default behavior
387+
puts "^C"
388+
end
389+
390+
false # Don't exit, just clear prompt
391+
end
346392

347393
def show_welcome(nesting_level = 0)
348394
if nesting_level > 0

lib/thor/interactive/version_constant.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
# This file is separate to avoid circular dependencies during gem installation
55

66
module ThorInteractive
7-
VERSION = "0.1.0.pre.2"
8-
end
7+
VERSION = "0.1.0.pre.3"
8+
end

0 commit comments

Comments
 (0)