|
1
|
|
|
"""Neovim TKinter UI.""" |
|
2
|
|
|
import sys |
|
3
|
|
|
from Tkinter import Canvas, Tk |
|
4
|
|
|
from collections import deque |
|
5
|
|
|
from threading import Thread |
|
6
|
|
|
# import StringIO, cProfile, pstats |
|
7
|
|
|
|
|
8
|
|
|
from neovim import attach |
|
9
|
|
|
|
|
10
|
|
|
from tkFont import Font |
|
11
|
|
|
|
|
12
|
|
|
SPECIAL_KEYS = { |
|
13
|
|
|
'Escape': 'Esc', |
|
14
|
|
|
'Return': 'CR', |
|
15
|
|
|
'BackSpace': 'BS', |
|
16
|
|
|
'Prior': 'PageUp', |
|
17
|
|
|
'Next': 'PageDown', |
|
18
|
|
|
'Delete': 'Del', |
|
19
|
|
|
} |
|
20
|
|
|
|
|
21
|
|
|
|
|
22
|
|
|
if sys.version_info < (3, 0): |
|
23
|
|
|
range = xrange |
|
24
|
|
|
|
|
25
|
|
|
|
|
26
|
|
View Code Duplication |
class NvimTk(object): |
|
|
|
|
|
|
27
|
|
|
|
|
28
|
|
|
"""Wraps all nvim/tk event handling.""" |
|
29
|
|
|
|
|
30
|
|
|
def __init__(self, nvim): |
|
31
|
|
|
"""Initialize with a Nvim instance.""" |
|
32
|
|
|
self._nvim = nvim |
|
33
|
|
|
self._attrs = {} |
|
34
|
|
|
self._nvim_updates = deque() |
|
35
|
|
|
self._canvas = None |
|
36
|
|
|
self._fg = '#000000' |
|
37
|
|
|
self._bg = '#ffffff' |
|
38
|
|
|
|
|
39
|
|
|
def run(self): |
|
40
|
|
|
"""Start the UI.""" |
|
41
|
|
|
self._tk_setup() |
|
42
|
|
|
t = Thread(target=self._nvim_event_loop) |
|
43
|
|
|
t.daemon = True |
|
44
|
|
|
t.start() |
|
45
|
|
|
self._root.mainloop() |
|
46
|
|
|
|
|
47
|
|
|
def _tk_setup(self): |
|
48
|
|
|
self._root = Tk() |
|
49
|
|
|
self._root.bind('<<nvim_redraw>>', self._tk_nvim_redraw) |
|
50
|
|
|
self._root.bind('<<nvim_detach>>', self._tk_nvim_detach) |
|
51
|
|
|
self._root.bind('<Key>', self._tk_key) |
|
52
|
|
|
|
|
53
|
|
|
def _tk_nvim_redraw(self, *args): |
|
54
|
|
|
update = self._nvim_updates.popleft() |
|
55
|
|
|
for update in update: |
|
56
|
|
|
handler = getattr(self, '_tk_nvim_' + update[0]) |
|
57
|
|
|
for args in update[1:]: |
|
58
|
|
|
handler(*args) |
|
59
|
|
|
|
|
60
|
|
|
def _tk_nvim_detach(self, *args): |
|
61
|
|
|
self._root.destroy() |
|
62
|
|
|
|
|
63
|
|
|
def _tk_nvim_resize(self, width, height): |
|
64
|
|
|
self._tk_redraw_canvas(width, height) |
|
65
|
|
|
|
|
66
|
|
|
def _tk_nvim_clear(self): |
|
67
|
|
|
self._tk_clear_region(0, self._height - 1, 0, self._width - 1) |
|
68
|
|
|
|
|
69
|
|
|
def _tk_nvim_eol_clear(self): |
|
70
|
|
|
row, col = (self._cursor_row, self._cursor_col,) |
|
71
|
|
|
self._tk_clear_region(row, row, col, self._scroll_right) |
|
72
|
|
|
|
|
73
|
|
|
def _tk_nvim_cursor_goto(self, row, col): |
|
74
|
|
|
self._cursor_row = row |
|
75
|
|
|
self._cursor_col = col |
|
76
|
|
|
|
|
77
|
|
|
def _tk_nvim_cursor_on(self): |
|
78
|
|
|
pass |
|
79
|
|
|
|
|
80
|
|
|
def _tk_nvim_cursor_off(self): |
|
81
|
|
|
pass |
|
82
|
|
|
|
|
83
|
|
|
def _tk_nvim_mouse_on(self): |
|
84
|
|
|
pass |
|
85
|
|
|
|
|
86
|
|
|
def _tk_nvim_mouse_off(self): |
|
87
|
|
|
pass |
|
88
|
|
|
|
|
89
|
|
|
def _tk_nvim_insert_mode(self): |
|
90
|
|
|
pass |
|
91
|
|
|
|
|
92
|
|
|
def _tk_nvim_normal_mode(self): |
|
93
|
|
|
pass |
|
94
|
|
|
|
|
95
|
|
|
def _tk_nvim_set_scroll_region(self, top, bot, left, right): |
|
96
|
|
|
self._scroll_top = top |
|
97
|
|
|
self._scroll_bot = bot |
|
98
|
|
|
self._scroll_left = left |
|
99
|
|
|
self._scroll_right = right |
|
100
|
|
|
|
|
101
|
|
|
def _tk_nvim_scroll(self, count): |
|
102
|
|
|
top, bot = (self._scroll_top, self._scroll_bot,) |
|
103
|
|
|
left, right = (self._scroll_left, self._scroll_right,) |
|
104
|
|
|
|
|
105
|
|
|
if count > 0: |
|
106
|
|
|
destroy_top = top |
|
107
|
|
|
destroy_bot = top + count - 1 |
|
108
|
|
|
move_top = destroy_bot + 1 |
|
109
|
|
|
move_bot = bot |
|
110
|
|
|
fill_top = move_bot + 1 |
|
111
|
|
|
fill_bot = fill_top + count - 1 |
|
112
|
|
|
else: |
|
113
|
|
|
destroy_top = bot + count + 1 |
|
114
|
|
|
destroy_bot = bot |
|
115
|
|
|
move_top = top |
|
116
|
|
|
move_bot = destroy_top - 1 |
|
117
|
|
|
fill_bot = move_top - 1 |
|
118
|
|
|
fill_top = fill_bot + count + 1 |
|
119
|
|
|
|
|
120
|
|
|
# destroy items that would be moved outside the scroll region after |
|
121
|
|
|
# scrolling |
|
122
|
|
|
# self._tk_clear_region(destroy_top, destroy_bot, left, right) |
|
123
|
|
|
# self._tk_clear_region(move_top, move_bot, left, right) |
|
124
|
|
|
self._tk_destroy_region(destroy_top, destroy_bot, left, right) |
|
125
|
|
|
self._tk_tag_region('move', move_top, move_bot, left, right) |
|
126
|
|
|
self._canvas.move('move', 0, -count * self._rowsize) |
|
127
|
|
|
self._canvas.dtag('move', 'move') |
|
128
|
|
|
# self._tk_fill_region(fill_top, fill_bot, left, right) |
|
129
|
|
|
|
|
130
|
|
|
def _tk_nvim_highlight_set(self, attrs): |
|
131
|
|
|
self._attrs = attrs |
|
132
|
|
|
|
|
133
|
|
|
def _tk_nvim_put(self, data): |
|
134
|
|
|
# choose a Font instance |
|
135
|
|
|
font = self._fnormal |
|
136
|
|
|
if self._attrs.get('bold', False): |
|
137
|
|
|
font = self._fbold |
|
138
|
|
|
if self._attrs.get('italic', False): |
|
139
|
|
|
font = self._fbolditalic if font == self._fbold else self._fitalic |
|
140
|
|
|
# colors |
|
141
|
|
|
fg = "#{0:0{1}x}".format(self._attrs.get('foreground', self._fg), 6) |
|
142
|
|
|
bg = "#{0:0{1}x}".format(self._attrs.get('background', self._bg), 6) |
|
143
|
|
|
# get the "text" and "rect" which correspond to the current cell |
|
144
|
|
|
x, y = self._tk_get_coords(self._cursor_row, self._cursor_col) |
|
145
|
|
|
items = self._canvas.find_overlapping(x, y, x + 1, y + 1) |
|
146
|
|
|
if len(items) != 2: |
|
147
|
|
|
# caught part the double-width character in the cell to the left, |
|
148
|
|
|
# filter items which dont have the same horizontal coordinate as |
|
149
|
|
|
# "x" |
|
150
|
|
|
predicate = lambda item: self._canvas.coords(item)[0] == x |
|
151
|
|
|
items = filter(predicate, items) |
|
152
|
|
|
# rect has lower id than text, sort to unpack correctly |
|
153
|
|
|
rect, text = sorted(items) |
|
154
|
|
|
self._canvas.itemconfig(text, fill=fg, font=font, text=data or ' ') |
|
155
|
|
|
self._canvas.itemconfig(rect, fill=bg) |
|
156
|
|
|
self._tk_nvim_cursor_goto(self._cursor_row, self._cursor_col + 1) |
|
157
|
|
|
|
|
158
|
|
|
def _tk_nvim_bell(self): |
|
159
|
|
|
self._root.bell() |
|
160
|
|
|
|
|
161
|
|
|
def _tk_nvim_update_fg(self, fg): |
|
162
|
|
|
self._fg = "#{0:0{1}x}".format(fg, 6) |
|
163
|
|
|
|
|
164
|
|
|
def _tk_nvim_update_bg(self, bg): |
|
165
|
|
|
self._bg = "#{0:0{1}x}".format(bg, 6) |
|
166
|
|
|
|
|
167
|
|
|
def _tk_redraw_canvas(self, width, height): |
|
168
|
|
|
if self._canvas: |
|
169
|
|
|
self._canvas.destroy() |
|
170
|
|
|
self._fnormal = Font(family='Monospace', size=13) |
|
171
|
|
|
self._fbold = Font(family='Monospace', weight='bold', size=13) |
|
172
|
|
|
self._fitalic = Font(family='Monospace', slant='italic', size=13) |
|
173
|
|
|
self._fbolditalic = Font(family='Monospace', weight='bold', |
|
174
|
|
|
slant='italic', size=13) |
|
175
|
|
|
self._colsize = self._fnormal.measure('A') |
|
176
|
|
|
self._rowsize = self._fnormal.metrics('linespace') |
|
177
|
|
|
self._canvas = Canvas(self._root, width=self._colsize * width, |
|
178
|
|
|
height=self._rowsize * height) |
|
179
|
|
|
self._tk_fill_region(0, height - 1, 0, width - 1) |
|
180
|
|
|
self._cursor_row = 0 |
|
181
|
|
|
self._cursor_col = 0 |
|
182
|
|
|
self._scroll_top = 0 |
|
183
|
|
|
self._scroll_bot = height - 1 |
|
184
|
|
|
self._scroll_left = 0 |
|
185
|
|
|
self._scroll_right = width - 1 |
|
186
|
|
|
self._width, self._height = (width, height,) |
|
187
|
|
|
self._canvas.pack() |
|
188
|
|
|
|
|
189
|
|
|
def _tk_fill_region(self, top, bot, left, right): |
|
190
|
|
|
# create columns from right to left so the left columns have a |
|
191
|
|
|
# higher z-index than the right columns. This is required to |
|
192
|
|
|
# properly display characters that cross cell boundary |
|
193
|
|
|
for rownum in range(bot, top - 1, -1): |
|
194
|
|
|
for colnum in range(right, left - 1, -1): |
|
195
|
|
|
x1 = colnum * self._colsize |
|
196
|
|
|
y1 = rownum * self._rowsize |
|
197
|
|
|
x2 = (colnum + 1) * self._colsize |
|
198
|
|
|
y2 = (rownum + 1) * self._rowsize |
|
199
|
|
|
# for each cell, create two items: The rectangle is used for |
|
200
|
|
|
# filling background and the text is for cell contents. |
|
201
|
|
|
self._canvas.create_rectangle(x1, y1, x2, y2, |
|
202
|
|
|
fill=self._background, width=0) |
|
203
|
|
|
self._canvas.create_text(x1, y1, anchor='nw', |
|
204
|
|
|
font=self._fnormal, width=1, |
|
205
|
|
|
fill=self._foreground, text=' ') |
|
206
|
|
|
|
|
207
|
|
|
def _tk_clear_region(self, top, bot, left, right): |
|
208
|
|
|
self._tk_tag_region('clear', top, bot, left, right) |
|
209
|
|
|
self._canvas.itemconfig('clear', fill=self._bg) |
|
210
|
|
|
self._canvas.dtag('clear', 'clear') |
|
211
|
|
|
|
|
212
|
|
|
def _tk_destroy_region(self, top, bot, left, right): |
|
213
|
|
|
self._tk_tag_region('destroy', top, bot, left, right) |
|
214
|
|
|
self._canvas.delete('destroy') |
|
215
|
|
|
self._canvas.dtag('destroy', 'destroy') |
|
216
|
|
|
|
|
217
|
|
|
def _tk_tag_region(self, tag, top, bot, left, right): |
|
218
|
|
|
x1, y1 = self._tk_get_coords(top, left) |
|
219
|
|
|
x2, y2 = self._tk_get_coords(bot, right) |
|
220
|
|
|
self._canvas.addtag_overlapping(tag, x1, y1, x2 + 1, y2 + 1) |
|
221
|
|
|
|
|
222
|
|
|
def _tk_get_coords(self, row, col): |
|
223
|
|
|
x = col * self._colsize |
|
224
|
|
|
y = row * self._rowsize |
|
225
|
|
|
return x, y |
|
226
|
|
|
|
|
227
|
|
|
def _tk_key(self, event): |
|
228
|
|
|
if 0xffe1 <= event.keysym_num <= 0xffee: |
|
229
|
|
|
# this is a modifier key, ignore. Source: |
|
230
|
|
|
# https://www.tcl.tk/man/tcl8.4/TkCmd/keysyms.htm |
|
231
|
|
|
return |
|
232
|
|
|
# Translate to Nvim representation of keys |
|
233
|
|
|
send = [] |
|
234
|
|
|
if event.state & 0x1: |
|
235
|
|
|
send.append('S') |
|
236
|
|
|
if event.state & 0x4: |
|
237
|
|
|
send.append('C') |
|
238
|
|
|
if event.state & (0x8 | 0x80): |
|
239
|
|
|
send.append('A') |
|
240
|
|
|
special = len(send) > 0 |
|
241
|
|
|
key = event.char |
|
242
|
|
|
if _is_invalid_key(key): |
|
243
|
|
|
special = True |
|
244
|
|
|
key = event.keysym |
|
245
|
|
|
send.append(SPECIAL_KEYS.get(key, key)) |
|
246
|
|
|
send = '-'.join(send) |
|
247
|
|
|
if special: |
|
248
|
|
|
send = '<' + send + '>' |
|
249
|
|
|
nvim = self._nvim |
|
250
|
|
|
nvim.session.threadsafe_call(lambda: nvim.input(send)) |
|
251
|
|
|
|
|
252
|
|
|
def _nvim_event_loop(self): |
|
253
|
|
|
self._nvim.session.run(self._nvim_request, |
|
254
|
|
|
self._nvim_notification, |
|
255
|
|
|
lambda: self._nvim.attach_ui(80, 24)) |
|
256
|
|
|
self._root.event_generate('<<nvim_detach>>', when='tail') |
|
257
|
|
|
|
|
258
|
|
|
def _nvim_request(self, method, args): |
|
259
|
|
|
raise Exception('This UI does not implement any methods') |
|
260
|
|
|
|
|
261
|
|
|
def _nvim_notification(self, method, args): |
|
262
|
|
|
if method == 'redraw': |
|
263
|
|
|
self._nvim_updates.append(args) |
|
264
|
|
|
self._root.event_generate('<<nvim_redraw>>', when='tail') |
|
265
|
|
|
|
|
266
|
|
|
|
|
267
|
|
|
def _is_invalid_key(c): |
|
268
|
|
|
try: |
|
269
|
|
|
return len(c.decode('utf-8')) != 1 or ord(c[0]) < 0x20 |
|
270
|
|
|
except UnicodeDecodeError: |
|
271
|
|
|
return True |
|
272
|
|
|
|
|
273
|
|
|
|
|
274
|
|
|
nvim = attach('child', argv=['../neovim/build/bin/nvim', '--embed']) |
|
275
|
|
|
ui = NvimTk(nvim) |
|
276
|
|
|
|
|
277
|
|
|
# pr = cProfile.Profile() |
|
278
|
|
|
# pr.enable() |
|
279
|
|
|
ui.run() |
|
280
|
|
|
# pr.disable() |
|
281
|
|
|
# s = StringIO.StringIO() |
|
282
|
|
|
# ps = pstats.Stats(pr, stream=s) |
|
283
|
|
|
# ps.strip_dirs().sort_stats('ncalls').print_stats(15) |
|
284
|
|
|
# print s.getvalue() |
|
285
|
|
|
|