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
|
|
|
|