X Tutup
Skip to content

Commit f409901

Browse files
add scroll frontend
--HG-- branch : scroll-frontend extra : rebase_source : 455c110865f3559e66917824ffbcf77660404c97
1 parent b1b3fbb commit f409901

File tree

12 files changed

+1028
-1
lines changed

12 files changed

+1028
-1
lines changed

bpython/scroll.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from fmtstr.terminal import Terminal
2+
from fmtstr.terminalcontrol import TerminalController
3+
4+
from bpython.scrollfrontend.repl import Repl
5+
6+
def main():
7+
with TerminalController() as tc:
8+
with Terminal(tc) as term:
9+
with Repl() as repl:
10+
rows, columns = tc.get_screen_size()
11+
repl.width = columns
12+
repl.height = rows
13+
while True:
14+
try:
15+
repl.process_event(tc.get_event())
16+
except SystemExit:
17+
array, cursor_pos = repl.paint(about_to_exit=True)
18+
term.render_to_terminal(array, cursor_pos)
19+
raise
20+
else:
21+
array, cursor_pos = repl.paint()
22+
scrolled = term.render_to_terminal(array, cursor_pos)
23+
repl.scroll_offset += scrolled
24+
25+
if __name__ == '__main__':
26+
main()

bpython/scrollfrontend/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
ABBR = {
2+
'improt' : 'import',
3+
'imprt' : 'import',
4+
'impotr' : 'import',
5+
'form' : 'from',
6+
}
7+
8+
import re
9+
10+
def substitute_abbreviations(cursor_offset, line):
11+
"""This should be much better"""
12+
new_line = ''.join([ABBR.get(word, word) for word in re.split(r'(\w+)', line)])
13+
cursor_offset = cursor_offset + len(new_line) - len(line)
14+
return cursor_offset, new_line
15+
16+
if __name__ == '__main__':
17+
print(substitute_abbreviations(0, 'improt asdf'))
18+
print(substitute_abbreviations(0, 'foo(x, y() - 2.3242) + "asdf"'))

bpython/scrollfrontend/friendly.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class NotImplementedError(NotImplementedError):
2+
def __init__(self, msg=None):
3+
if msg is None:
4+
super(NotImplementedError, self).__init__("Implement it and submit a pull request!")
5+
else:
6+
super(NotImplementedError, self).__init__(msg)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import Queue
2+
import time
3+
4+
from bpython.repl import Interaction as BpythonInteraction
5+
6+
from bpython.scrollfrontend.manual_readline import char_sequences as rl_char_sequences
7+
8+
class StatusBar(BpythonInteraction):
9+
"""StatusBar and Interaction for Repl
10+
11+
Passing of control back and forth between calls that use interact api
12+
(notify, confirm, file_prompt) like bpython.Repl.write2file and events
13+
on the main thread happens via those calls and self.wait_for_request_or_notify.
14+
15+
Calling one of these three is required for the main thread to regain control!
16+
17+
This is probably a terrible idea, and better would be rewriting this
18+
functionality in a evented or callback style, but trying to integrate
19+
bpython.Repl code.
20+
"""
21+
#TODO Remove dependence on bpython.Repl, it's more complicated than it's worth!
22+
def __init__(self, initial_message='', permanent_text=""):
23+
self._current_line = ''
24+
self.cursor_offset_in_line = 0
25+
self.in_prompt = False
26+
self.in_confirm = False
27+
self.prompt = ''
28+
self._message = initial_message
29+
self.message_start_time = time.time()
30+
self.message_time = 3
31+
self.permanent_text = permanent_text
32+
self.response_queue = Queue.Queue(maxsize=1)
33+
self.request_or_notify_queue = Queue.Queue()
34+
35+
@property
36+
def has_focus(self):
37+
return self.in_prompt or self.in_confirm
38+
39+
def message(self, msg):
40+
self.message_start_time = time.time()
41+
self._message = msg
42+
43+
def _check_for_expired_message(self):
44+
if self._message and time.time() > self.message_start_time + self.message_time:
45+
self._message = ''
46+
47+
def process_event(self, e):
48+
"""Returns True if shutting down"""
49+
assert self.in_prompt or self.in_confirm
50+
if e in rl_char_sequences:
51+
self.cursor_offset_in_line, self._current_line = rl_char_sequences[e](self.cursor_offset_in_line, self._current_line)
52+
elif e == "":
53+
raise KeyboardInterrupt()
54+
elif e == "":
55+
raise SystemExit()
56+
elif self.in_prompt and e in ("\n", "\r"):
57+
self.response_queue.put(self._current_line)
58+
self.escape()
59+
elif self.in_confirm:
60+
if e in ('y', 'Y'):
61+
self.response_queue.put(True)
62+
else:
63+
self.response_queue.put(False)
64+
self.escape()
65+
elif e == "\x1b":
66+
self.response_queue.put(False)
67+
self.escape()
68+
else: # add normal character
69+
#TODO factor this out, same in both process_event methods
70+
self._current_line = (self._current_line[:self.cursor_offset_in_line] +
71+
e +
72+
self._current_line[self.cursor_offset_in_line:])
73+
self.cursor_offset_in_line += 1
74+
75+
def escape(self):
76+
"""unfocus from statusbar, clear prompt state, wait for notify call"""
77+
self.wait_for_request_or_notify()
78+
self.in_prompt = False
79+
self.in_confirm = False
80+
self.prompt = ''
81+
self._current_line = ''
82+
83+
@property
84+
def current_line(self):
85+
self._check_for_expired_message()
86+
if self.in_prompt:
87+
return self.prompt + self._current_line
88+
if self.in_confirm:
89+
return self.prompt
90+
if self._message:
91+
return self._message
92+
return self.permanent_text
93+
94+
def wait_for_request_or_notify(self):
95+
try:
96+
r = self.request_or_notify_queue.get(True, 1)
97+
except Queue.Empty:
98+
raise Exception('Main thread blocked because task thread not calling back')
99+
return r
100+
101+
# interaction interface - should be called from other threads
102+
def notify(self, msg, n=3):
103+
self.message_time = n
104+
self.message(msg)
105+
self.request_or_notify_queue.put(msg)
106+
# below Really ought to be called from threads other than the mainloop because they block
107+
def confirm(self, q):
108+
"""Expected to return True or False, given question prompt q"""
109+
self.prompt = q
110+
self.in_confirm = True
111+
self.request_or_notify_queue.put(q)
112+
return self.response_queue.get()
113+
def file_prompt(self, s):
114+
"""Expected to return a file name, given """
115+
self.prompt = s
116+
self.in_prompt = True
117+
self.request_or_notify_queue.put(s)
118+
r = self.response_queue.get()
119+
return r
120+
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""implementations of simple readline control sequences
2+
3+
just the ones that fit the model of transforming the current line
4+
and the cursor location
5+
in the order of description at http://www.bigsmoke.us/readline/shortcuts"""
6+
7+
from bpython.scrollfrontend.friendly import NotImplementedError
8+
import re
9+
char_sequences = {}
10+
11+
#TODO fix this - should use value in repl.
12+
# Sadly, this breaks the pure function aspect of backspace!
13+
INDENT = 4
14+
15+
#TODO make an object out of this so instances can have keybindings via config
16+
17+
def on(seq):
18+
def add_to_char_sequences(func):
19+
char_sequences[seq] = func
20+
return func
21+
return add_to_char_sequences
22+
23+
@on('')
24+
@on('')
25+
@on(chr(2))
26+
@on('KEY_LEFT')
27+
def left_arrow(cursor_offset, line):
28+
return max(0, cursor_offset - 1), line
29+
30+
@on('')
31+
@on('')
32+
@on(chr(6))
33+
@on('KEY_RIGHT')
34+
def right_arrow(cursor_offset, line):
35+
return min(len(line), cursor_offset + 1), line
36+
37+
@on('')
38+
@on('KEY_HOME')
39+
def beginning_of_line(cursor_offset, line):
40+
return 0, line
41+
42+
@on('')
43+
@on('KEY_END')
44+
def end_of_line(cursor_offset, line):
45+
return len(line), line
46+
47+
@on('f')
48+
@on('\x1bOC')
49+
def forward_word(cursor_offset, line):
50+
patt = r"\S\s"
51+
match = re.search(patt, line[cursor_offset:]+' ')
52+
delta = match.end() - 1 if match else 0
53+
return (cursor_offset + delta, line)
54+
55+
@on('b')
56+
@on('\x1bOD')
57+
def back_word(cursor_offset, line):
58+
return (last_word_pos(line[:cursor_offset]), line)
59+
60+
def last_word_pos(string):
61+
"""returns the start index of the last word of given string"""
62+
patt = r'\S\s'
63+
match = re.search(patt, string[::-1])
64+
index = match and len(string) - match.end() + 1
65+
return index or 0
66+
67+
@on('[3~')
68+
@on('KEY_DC')
69+
def delete(cursor_offset, line):
70+
return (cursor_offset,
71+
line[:cursor_offset] + line[cursor_offset+1:])
72+
73+
@on('')
74+
@on('')
75+
@on('KEY_BACKSPACE')
76+
def backspace(cursor_offset, line):
77+
if cursor_offset == 0:
78+
return cursor_offset, line
79+
if not line[:cursor_offset].strip(): #if just whitespace left of cursor
80+
front_white = len(line[:cursor_offset]) - len(line[:cursor_offset].lstrip())
81+
to_delete = ((front_white - 1) % INDENT) + 1
82+
return cursor_offset - to_delete, line[:to_delete]
83+
return (cursor_offset - 1,
84+
line[:cursor_offset - 1] + line[cursor_offset:])
85+
86+
@on('')
87+
def delete_from_cursor_back(cursor_offset, line):
88+
return 0, line[cursor_offset:]
89+
90+
@on(' ')
91+
def delete_from_cursor_forward(cursor_offset, line):
92+
return cursor_offset, line[:cursor_offset]
93+
94+
@on('d')
95+
def delete_rest_of_word(cursor_offset, line):
96+
raise NotImplementedError()
97+
98+
@on('')
99+
def delete_word_to_cursor(cursor_offset, line):
100+
raise NotImplementedError()
101+
102+
@on('y')
103+
def yank_prev_prev_killed_text(cursor_offset, line):
104+
raise NotImplementedError()
105+
106+
@on('')
107+
def transpose_character_before_cursor(cursor_offset, line):
108+
raise NotImplementedError()
109+
110+
@on('t')
111+
def transpose_word_before_cursor(cursor_offset, line):
112+
raise NotImplementedError()
113+
114+
# bonus functions (not part of readline)
115+
116+
@on('\x1b\x7f')
117+
@on('\xff')
118+
def delete_word_from_cursor_back(cursor_offset, line):
119+
raise NotImplementedError()
120+
121+
def get_updated_char_sequences(key_dispatch, config):
122+
updated_char_sequences = dict(char_sequences)
123+
updated_char_sequences[key_dispatch[config.delete_key]] = backspace
124+
updated_char_sequences[key_dispatch[config.clear_word_key]] = delete_word_to_cursor
125+
updated_char_sequences[key_dispatch[config.clear_line_key]] = delete_from_cursor_back
126+
return updated_char_sequences
127+
128+
if __name__ == '__main__':
129+
import doctest; doctest.testmod()
130+
from pprint import pprint
131+
pprint(char_sequences)
132+
133+

0 commit comments

Comments
 (0)
X Tutup