NvimTk.run()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 7

Duplication

Lines 7
Ratio 100 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
c 2
b 0
f 0
dl 7
loc 7
rs 9.4285
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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