Skip to content

Commit 5cd5067

Browse files
committed
Handle ctrl+c and ctrl+d (for exiting) better.
1 parent ec0230c commit 5cd5067

File tree

1 file changed

+47
-1
lines changed

1 file changed

+47
-1
lines changed

bpython/urwid.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import sys
3939
import os
4040
import locale
41+
import signal
4142
from types import ModuleType
4243
from optparse import Option
4344

@@ -309,17 +310,34 @@ def reprint_line(self, lineno, tokens):
309310
edit.set_edit_markup(list(format_tokens(tokens)))
310311

311312
def push(self, s, insert_into_history=True):
313+
# Restore the original SIGINT handler. This is needed to be able
314+
# to break out of infinite loops. If the interpreter itself
315+
# sees this it prints 'KeyboardInterrupt' and returns (good).
316+
orig_handler = signal.getsignal(signal.SIGINT)
317+
signal.signal(signal.SIGINT, signal.default_int_handler)
312318
# Pretty blindly adapted from bpython.cli
313319
try:
314320
return repl.Repl.push(self, s, insert_into_history)
315321
except SystemExit:
316322
raise urwid.ExitMainLoop()
323+
except KeyboardInterrupt:
324+
# KeyboardInterrupt happened between the except block around
325+
# user code execution and this code. This should be rare,
326+
# but make sure to not kill bpython here, so leaning on
327+
# ctrl+c to kill buggy code running inside bpython is safe.
328+
self.keyboard_interrupt()
329+
finally:
330+
signal.signal(signal.SIGINT, orig_handler)
317331

318332
def start(self):
319333
# Stolen from bpython.cli again
320334
self.push('from bpython._internal import _help as help\n', False)
321335
self.prompt(False)
322336

337+
def keyboard_interrupt(self):
338+
# Do we need to do more here? Break out of multiline input perhaps?
339+
self.echo('KeyboardInterrupt')
340+
323341
def prompt(self, more):
324342
# XXX what is s_hist?
325343
if not more:
@@ -382,8 +400,16 @@ def handle_input(self, event):
382400
# XXX what is this s_hist thing?
383401
self.stdout_hist += inp + '\n'
384402
self.edit = None
403+
# This may take a while, so force a redraw first:
404+
self.main_loop.draw_screen()
385405
more = self.push(inp)
386406
self.prompt(more)
407+
elif event == 'ctrl d':
408+
# ctrl+d on an empty line exits
409+
if self.edit is not None and not self.edit.get_edit_text():
410+
raise urwid.ExitMainLoop()
411+
#else:
412+
# self.echo(repr(event))
387413

388414

389415
def main(args=None, locals_=None, banner=None):
@@ -461,6 +487,13 @@ def main(args=None, locals_=None, banner=None):
461487
myrepl = URWIDRepl(loop, frame, listbox, overlay, tooltip,
462488
interpreter, statusbar, config)
463489

490+
if options.reactor:
491+
# Twisted sets a sigInt handler that stops the reactor unless
492+
# it sees a different custom signal handler.
493+
def sigint(*args):
494+
reactor.callFromThread(myrepl.keyboard_interrupt)
495+
signal.signal(signal.SIGINT, sigint)
496+
464497
# XXX HACK: circular dependency between the event loop and repl.
465498
# Fix by not using unhandled_input?
466499
loop._unhandled_input = myrepl.handle_input
@@ -515,7 +548,20 @@ def run_find_coroutine():
515548

516549
loop.set_alarm_in(0, start)
517550

518-
loop.run()
551+
while True:
552+
try:
553+
loop.run()
554+
except KeyboardInterrupt:
555+
# HACK: if we run under a twisted mainloop this should
556+
# never happen: we have a SIGINT handler set.
557+
# If we use the urwid select-based loop we just restart
558+
# that loop if interrupted, instead of trying to cook
559+
# up an equivalent to reactor.callFromThread (which
560+
# is what our Twisted sigint handler does)
561+
loop.set_alarm_in(0,
562+
lambda *args: myrepl.keyboard_interrupt())
563+
continue
564+
break
519565

520566
if config.hist_length:
521567
histfilename = os.path.expanduser(config.hist_file)

0 commit comments

Comments
 (0)