| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,6 +1,7 @@ | |||
| 1 | 1 | from __future__ import absolute_import | |
| 2 | 2 | ||
| 3 | 3 | import code | |
| 4 | + import collections | ||
| 4 | 5 | import io | |
| 5 | 6 | import logging | |
| 6 | 7 | import sys | |
@@ -91,10 +92,45 @@ def main(args=None, locals_=None, banner=None, welcome_message=None): | |||
| 91 | 92 | return extract_exit_value(exit_value) | |
| 92 | 93 | ||
| 93 | 94 | ||
| 95 | + def _combined_events(event_provider, paste_threshold): | ||
| 96 | + """Combines consecutive keypress events into paste events.""" | ||
| 97 | + timeout = yield 'nonsense_event' # so send can be used immediately | ||
| 98 | + queue = collections.deque() | ||
| 99 | + while True: | ||
| 100 | + e = event_provider.send(timeout) | ||
| 101 | + if isinstance(e, curtsies.events.Event): | ||
| 102 | + timeout = yield e | ||
| 103 | + continue | ||
| 104 | + elif e is None: | ||
| 105 | + timeout = yield None | ||
| 106 | + continue | ||
| 107 | + else: | ||
| 108 | + queue.append(e) | ||
| 109 | + e = event_provider.send(0) | ||
| 110 | + while not (e is None or isinstance(e, curtsies.events.Event)): | ||
| 111 | + queue.append(e) | ||
| 112 | + e = event_provider.send(0) | ||
| 113 | + if len(queue) >= paste_threshold: | ||
| 114 | + paste = curtsies.events.PasteEvent() | ||
| 115 | + paste.events.extend(queue) | ||
| 116 | + queue.clear() | ||
| 117 | + timeout = yield paste | ||
| 118 | + else: | ||
| 119 | + while len(queue): | ||
| 120 | + timeout = yield queue.popleft() | ||
| 121 | + | ||
| 122 | + | ||
| 123 | + def combined_events(event_provider, paste_threshold=3): | ||
| 124 | + g = _combined_events(event_provider, paste_threshold) | ||
| 125 | + next(g) | ||
| 126 | + return g | ||
| 127 | + | ||
| 128 | + | ||
| 94 | 129 | def mainloop(config, locals_, banner, interp=None, paste=None, | |
| 95 | 130 | interactive=True): | |
| 96 | - with curtsies.input.Input(keynames='curtsies', sigint_event=True) as \ | ||
| 97 | - input_generator: | ||
| 131 | + with curtsies.input.Input(keynames='curtsies', | ||
| 132 | + sigint_event=True, | ||
| 133 | + paste_threshold=None) as input_generator: | ||
| 98 | 134 | with curtsies.window.CursorAwareWindow( | |
| 99 | 135 | sys.stdout, | |
| 100 | 136 | sys.stdin, | |
@@ -180,12 +216,13 @@ def process_event(e): | |||
| 180 | 216 | ||
| 181 | 217 | # do a display before waiting for first event | |
| 182 | 218 | process_event(None) | |
| 219 | + inputs = combined_events(input_generator) | ||
| 183 | 220 | for unused in find_iterator: | |
| 184 | - e = input_generator.send(0) | ||
| 221 | + e = inputs.send(0) | ||
| 185 | 222 | if e is not None: | |
| 186 | 223 | process_event(e) | |
| 187 | 224 | ||
| 188 | - for e in input_generator: | ||
| 225 | + for e in inputs: | ||
| 189 | 226 | process_event(e) | |
| 190 | 227 | ||
| 191 | 228 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -84,6 +84,9 @@ | |||
| 84 | 84 | Press {config.edit_config_key} to edit this config file. | |
| 85 | 85 | """ | |
| 86 | 86 | EXAMPLE_CONFIG_URL = 'https://raw.githubusercontent.com/bpython/bpython/master/bpython/sample-config' | |
| 87 | + MAX_EVENTS_POSSIBLY_NOT_PASTE = 20 # more than this many events will be assumed to | ||
| 88 | + # be a true paste event, i.e. control characters | ||
| 89 | + # like '<Ctrl-a>' will be stripped | ||
| 87 | 90 | ||
| 88 | 91 | # This is needed for is_nop and should be removed once is_nop is fixed. | |
| 89 | 92 | if py3: | |
@@ -545,16 +548,25 @@ def process_control_event(self, e): | |||
| 545 | 548 | ctrl_char = compress_paste_event(e) | |
| 546 | 549 | if ctrl_char is not None: | |
| 547 | 550 | return self.process_event(ctrl_char) | |
| 548 | - simple_events = just_simple_events(e.events) | ||
| 549 | - source = preprocess(''.join(simple_events), | ||
| 550 | - self.interp.compile) | ||
| 551 | - | ||
| 552 | 551 | with self.in_paste_mode(): | |
| 553 | - for ee in source: | ||
| 554 | - if self.stdin.has_focus: | ||
| 555 | - self.stdin.process_event(ee) | ||
| 556 | - else: | ||
| 557 | - self.process_simple_keypress(ee) | ||
| 552 | + # Might not really be a paste, UI might just be lagging | ||
| 553 | + if (len(e.events) <= MAX_EVENTS_POSSIBLY_NOT_PASTE and | ||
| 554 | + any(not is_simple_event(ee) for ee in e.events)): | ||
| 555 | + for ee in e.events: | ||
| 556 | + if self.stdin.has_focus: | ||
| 557 | + self.stdin.process_event(ee) | ||
| 558 | + else: | ||
| 559 | + self.process_event(ee) | ||
| 560 | + else: | ||
| 561 | + simple_events = just_simple_events(e.events) | ||
| 562 | + source = preprocess(''.join(simple_events), | ||
| 563 | + self.interp.compile) | ||
| 564 | + for ee in source: | ||
| 565 | + if self.stdin.has_focus: | ||
| 566 | + self.stdin.process_event(ee) | ||
| 567 | + else: | ||
| 568 | + self.process_simple_keypress(ee) | ||
| 569 | + | ||
| 558 | 570 | ||
| 559 | 571 | elif isinstance(e, bpythonevents.RunStartupFileEvent): | |
| 560 | 572 | try: | |
@@ -1619,11 +1631,24 @@ def just_simple_events(event_list): | |||
| 1619 | 1631 | pass # ignore events | |
| 1620 | 1632 | elif e == '<SPACE>': | |
| 1621 | 1633 | simple_events.append(' ') | |
| 1634 | + elif len(e) > 1: | ||
| 1635 | + pass # get rid of <Ctrl-a> etc. | ||
| 1622 | 1636 | else: | |
| 1623 | 1637 | simple_events.append(e) | |
| 1624 | 1638 | return simple_events | |
| 1625 | 1639 | ||
| 1626 | 1640 | ||
| 1641 | + def is_simple_event(e): | ||
| 1642 | + if isinstance(e, events.Event): | ||
| 1643 | + return False | ||
| 1644 | + if e in ("<Ctrl-j>", "<Ctrl-m>", "<PADENTER>", "\n", "\r", "<SPACE>"): | ||
| 1645 | + return True | ||
| 1646 | + if len(e) > 1: | ||
| 1647 | + return False | ||
| 1648 | + else: | ||
| 1649 | + return True | ||
| 1650 | + | ||
| 1651 | + | ||
| 1627 | 1652 | # TODO this needs some work to function again and be useful for embedding | |
| 1628 | 1653 | def simple_repl(): | |
| 1629 | 1654 | refreshes = [] | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,91 @@ | |||
| 1 | + # coding: utf-8 | ||
| 2 | + from __future__ import unicode_literals | ||
| 3 | + | ||
| 4 | + from collections import namedtuple | ||
| 5 | + | ||
| 6 | + from bpython.curtsies import combined_events | ||
| 7 | + from bpython.test import (FixLanguageTestCase as TestCase, unittest) | ||
| 8 | + | ||
| 9 | + import curtsies.events | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + ScheduledEvent = namedtuple('ScheduledEvent', ['when', 'event']) | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + class EventGenerator(object): | ||
| 16 | + def __init__(self, initial_events=(), scheduled_events=()): | ||
| 17 | + self._events = [] | ||
| 18 | + self._current_tick = 0 | ||
| 19 | + for e in initial_events: | ||
| 20 | + self.schedule_event(e, 0) | ||
| 21 | + for e, w in scheduled_events: | ||
| 22 | + self.schedule_event(e, w) | ||
| 23 | + | ||
| 24 | + def schedule_event(self, event, when): | ||
| 25 | + self._events.append(ScheduledEvent(when, event)) | ||
| 26 | + self._events.sort() | ||
| 27 | + | ||
| 28 | + def send(self, timeout=None): | ||
| 29 | + if timeout not in [None, 0]: | ||
| 30 | + raise ValueError('timeout value %r not supported' % timeout) | ||
| 31 | + if not self._events: | ||
| 32 | + return None | ||
| 33 | + if self._events[0].when <= self._current_tick: | ||
| 34 | + return self._events.pop(0).event | ||
| 35 | + | ||
| 36 | + if timeout == 0: | ||
| 37 | + return None | ||
| 38 | + elif timeout is None: | ||
| 39 | + e = self._events.pop(0) | ||
| 40 | + self._current_tick = e.when | ||
| 41 | + return e.event | ||
| 42 | + else: | ||
| 43 | + raise ValueError('timeout value %r not supported' % timeout) | ||
| 44 | + | ||
| 45 | + def tick(self, dt=1): | ||
| 46 | + self._current_tick += dt | ||
| 47 | + return self._current_tick | ||
| 48 | + | ||
| 49 | + | ||
| 50 | + class TestCurtsiesPasteDetection(TestCase): | ||
| 51 | + def test_paste_threshold(self): | ||
| 52 | + eg = EventGenerator(list('abc')) | ||
| 53 | + cb = combined_events(eg, paste_threshold=3) | ||
| 54 | + e = next(cb) | ||
| 55 | + self.assertIsInstance(e, curtsies.events.PasteEvent) | ||
| 56 | + self.assertEqual(e.events, list('abc')) | ||
| 57 | + self.assertEqual(next(cb), None) | ||
| 58 | + | ||
| 59 | + eg = EventGenerator(list('abc')) | ||
| 60 | + cb = combined_events(eg, paste_threshold=4) | ||
| 61 | + self.assertEqual(next(cb), 'a') | ||
| 62 | + self.assertEqual(next(cb), 'b') | ||
| 63 | + self.assertEqual(next(cb), 'c') | ||
| 64 | + self.assertEqual(next(cb), None) | ||
| 65 | + | ||
| 66 | + def test_set_timeout(self): | ||
| 67 | + eg = EventGenerator('a', zip('bcdefg', [1, 2, 3, 3, 3, 4])) | ||
| 68 | + eg.schedule_event(curtsies.events.SigIntEvent(), 5) | ||
| 69 | + eg.schedule_event('h', 6) | ||
| 70 | + cb = combined_events(eg, paste_threshold=3) | ||
| 71 | + self.assertEqual(next(cb), 'a') | ||
| 72 | + self.assertEqual(cb.send(0), None) | ||
| 73 | + self.assertEqual(next(cb), 'b') | ||
| 74 | + self.assertEqual(cb.send(0), None) | ||
| 75 | + eg.tick() | ||
| 76 | + self.assertEqual(cb.send(0), 'c') | ||
| 77 | + self.assertEqual(cb.send(0), None) | ||
| 78 | + eg.tick() | ||
| 79 | + self.assertIsInstance(cb.send(0), curtsies.events.PasteEvent) | ||
| 80 | + self.assertEqual(cb.send(0), None) | ||
| 81 | + self.assertEqual(cb.send(None), 'g') | ||
| 82 | + self.assertEqual(cb.send(0), None) | ||
| 83 | + eg.tick(1) | ||
| 84 | + self.assertIsInstance(cb.send(0), curtsies.events.SigIntEvent) | ||
| 85 | + self.assertEqual(cb.send(0), None) | ||
| 86 | + self.assertEqual(cb.send(None), 'h') | ||
| 87 | + self.assertEqual(cb.send(None), None) | ||
| 88 | + | ||
| 89 | + | ||
| 90 | + if __name__ == '__main__': | ||
| 91 | + unittest.main() | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -20,6 +20,8 @@ | |||
| 20 | 20 | from bpython.test import (FixLanguageTestCase as TestCase, MagicIterMock, mock, | |
| 21 | 21 | builtin_target, unittest) | |
| 22 | 22 | ||
| 23 | + from curtsies import events | ||
| 24 | + | ||
| 23 | 25 | if py3: | |
| 24 | 26 | from importlib import invalidate_caches | |
| 25 | 27 | else: | |
@@ -422,5 +424,29 @@ def test_startup_event_latin1(self): | |||
| 422 | 424 | self.assertIn('a', self.repl.interp.locals) | |
| 423 | 425 | ||
| 424 | 426 | ||
| 427 | + class TestCurtsiesPasteEvents(TestCase): | ||
| 428 | + | ||
| 429 | + def setUp(self): | ||
| 430 | + self.repl = create_repl() | ||
| 431 | + | ||
| 432 | + def test_control_events_in_small_paste(self): | ||
| 433 | + self.assertGreaterEqual(curtsiesrepl.MAX_EVENTS_POSSIBLY_NOT_PASTE, 6, | ||
| 434 | + 'test assumes UI lag could cause 6 events') | ||
| 435 | + p = events.PasteEvent() | ||
| 436 | + p.events = ['a', 'b', 'c', 'd', '<Ctrl-a>', 'e'] | ||
| 437 | + self.repl.process_event(p) | ||
| 438 | + self.assertEqual(self.repl.current_line, 'eabcd') | ||
| 439 | + | ||
| 440 | + | ||
| 441 | + def test_control_events_in_large_paste(self): | ||
| 442 | + """Large paste events should ignore control characters""" | ||
| 443 | + p = events.PasteEvent() | ||
| 444 | + p.events = (['a', '<Ctrl-a>'] + | ||
| 445 | + ['e'] * curtsiesrepl.MAX_EVENTS_POSSIBLY_NOT_PASTE) | ||
| 446 | + self.repl.process_event(p) | ||
| 447 | + self.assertEqual(self.repl.current_line, | ||
| 448 | + 'a' + 'e'*curtsiesrepl.MAX_EVENTS_POSSIBLY_NOT_PASTE) | ||
| 449 | + | ||
| 450 | + | ||
| 425 | 451 | if __name__ == '__main__': | |
| 426 | 452 | unittest.main() | |
| Back | FazBrowse Home | New Git URL |
0 commit comments